目录
Shiro实现用户认证
在 shiro 中,用户需要提供principals (身份)和credentials(凭证)给shiro,从而应用能验证用户身份。身份即帐号/凭证即密码
认证流程
1、创建token令牌,token中有用户提交的认证信息即帐号和密码
2、执行Subject.login(token),Subject实例通常是DelegatingSubject类(或子类)的实例对象;在认证开始时,通过SecurityManager实例来调用securityManager.login(token)方法
3、SecurityManager接受到token(令牌)信息后委托Authenticator实例进行认证;Authenticator通过实现类ModularRealmAuthenticator来调用anthenticator.authenticate(token)方法;ModularRealmAuthenticator在认证过程中会对一个或多个Realm实例进行适配(可插拔)
4、如果配置了多个Realm,ModularRealmAuthenticator会根据配置的AuthenticationStrategy(认证策略)来进行多Realm的认证过程;在Realm被调用后,AuthenticationStrategy将对每一个Realm的结果做出响应
注意: 如果只有一个Realm,Realm将直接调用而无需再配置认证策略5、判断每一个Realm是否都支持提交的token,如果支持,Realm调用getAuthenticationInfo(token),该方法就是实际的认证处理,我们通过覆盖Realm的doGetAuthenticationInfo方法来编写我们**自定义的认证处理
6、shiro中有三种认证策略的具体实现:**
**AtleastOneSuccessfulStrategy:**只要有一个realm验证成功,则成功
**FirstSuccessfulStrategy:**第一个realm验证成功,则成功,后续realm将被忽
**AllSuccessfulStrategy:**所有realm成功,验证才成功认证失败后抛出的一些异常:
- UnknownAccountException帐号不存在
- IncorrectCredentialsException密码错误
- DisabledAccountException帐号被禁用
- LockedAccountException帐号被锁定
- ExcessiveAttemptsException登录失败次数过多
- ExpiredCredentialsException凭证过期
导入Shrio与Spring及springMVC相关依赖
<!-- shiro核心包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- 添加shiro web支持 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- 添加shiro spring支持 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
配置Shiro过滤器(web.xml)
<!-- shiro过滤器定义 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<!-- 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理 -->
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
创建自定义Realm
shiro自带的Realm接口,CachingRealm负责缓存处理,AuthenticationRealm负责认证,AuthorizingRealm负责授权等,但是通常情况下,正确的用户信息都是从数据库中取出,所以需要自定义realm,通常自定义的realm继承AuthorizingRealm,认证是重写**doGetAuthenticationInfo(AuthenticationToken token)方法,授权是重写doGetAuthorizationInfo(PrincipalCollection principals)**方法
package com.zking.ssm.book.shiro;
import com.zking.ssm.book.mapper.SysUserMapper;
import com.zking.ssm.book.model.SysUser;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Set;
/**
* 自定义Realm的安全数据源,采用数据库的方式
*/
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private SysUserMapper sysUserMapper;
/**
*授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
}
/**
* 认证
* @param authenticationToken 传入的账号密码令牌Token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取账号
String username = authenticationToken.getPrincipal().toString();
//获取密码
String password = authenticationToken.getCredentials().toString();
//根据账号查询数据库中的用户对象信息
SysUser sysUser = sysUserMapper.userLogin(username);
//判断账号是否存在
if(sysUser ==null){
throw new UnknownAccountException("账号不存在!!!");
}
//创建SimpleAuthenticationInfo,传入正确的账号和密码(来自于数据库)
SimpleAuthenticationInfo simple=new SimpleAuthenticationInfo(
sysUser.getUsername(),
sysUser.getPassword(),
ByteSource.Util.bytes(sysUser.getSalt()),
this.getName()
);
return simple;
}
}
配置spring与shiro的配置文件 spring-shiro.xml
在这里配置了盐加密
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--1、注册自定义的Realm 采用数据库方式-->
<bean id="shiroRealm" class="com.zking.ssm.book.shiro.ShiroRealm">
<!--配置Shiro明文密码如何进行加密-->
<!--注意:重要的事情说三次~~~~~~此处加密方式要与用户注册时的算法一致 -->
<!--注意:重要的事情说三次~~~~~~此处加密方式要与用户注册时的算法一致 -->
<!--注意:重要的事情说三次~~~~~~此处加密方式要与用户注册时的算法一致 -->
<!--以下三个配置告诉shiro将如何对用户传来的明文密码进行加密-->
<property name="credentialsMatcher">
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--指定hash算法为MD5-->
<property name="hashAlgorithmName" value="md5"/>
<!--指定散列次数为1024次-->
<property name="hashIterations" value="1024"/>
<!--true指定Hash散列值使用Hex加密存. false表明hash散列值用用Base64-encoded存储-->
<property name="storedCredentialsHexEncoded" value="true"/>
</bean>
</property>
</bean>
<!--2、创建安全管理器,并更换Realm-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--更换Realm 使用数据库方式-->
<property name="realm" ref="shiroRealm" />
</bean>
<!--3、配置Shiro核心过滤器-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口,这个属性是必须的 -->
<property name="securityManager" ref="securityManager" />
<!-- 身份验证失败,跳转到登录页面 -->
<property name="loginUrl" value="/home/index.html"/>
<!-- 身份验证成功,跳转到指定页面 -->
<!-- <property name="successUrl" value="/index.jsp"/>
<!– 权限验证失败,跳转到指定页面 –>
<property name="unauthorizedUrl" value="/noauthorizeUrl.jsp"/>-->
<!-- Shiro连接约束配置,即过滤链的定义 -->
<property name="filterChainDefinitions">
<value>
<!--anon 表示匿名访问,不需要认证以及授权-->
<!--authc表示需要认证 没有进行身份认证是不能进行访问的-->
/user/userLogin=anon
/book/**=authc
<!-- /css/** = anon
/images/** = anon
/js/** = anon
/ = anon
/user/logout = logout
/user/** = anon
/userInfo/** = authc
/dict/** = authc
/console/** = roles[admin]
/** = anon-->
</value>
</property>
</bean>
<!--4、配置Shiro的生命周期-->
<bean id="lifecycleBeanPostProcessor"
class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
</beans>
与spring集成
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--引入mybatis与spring的集成配置-->
<import resource="spring-mybatis.xml"/>
<!--引入shiro与spring的集成配置-->
<import resource="spring-shiro.xml"/>
</beans>
Controller层
/**
* 跳转到登录页面
* @return
@RequestMapping("tologin")
public String toLogin(){
return "login";
}
*/
/**
* 登录方法
* @return
*/
@RequestMapping("/userLogin")
public String userLogin(SysUser sysuser, Model model){
//获取主体
Subject subject = SecurityUtils.getSubject();
//创建账号密码令牌
UsernamePasswordToken token = new UsernamePasswordToken(
sysuser.getUsername(),
sysuser.getPassword()
);
//定义错误信息
String msg=null;
try {
subject.login(token);
}catch (UnknownAccountException e) {
msg="账号错误";
e.printStackTrace();
}catch (IncorrectCredentialsException e) {
msg="密码错误";
e.printStackTrace();
}catch (AuthenticationException e) {
msg="账号或密码错误";
e.printStackTrace();
}
//判断msg
if(msg == null){
return "redirect:/toIndex";
}else{
model.addAttribute("msg",msg);
// return "login";
return "forward:/home/index.html";//转发绕开视图解析器
}
}
/**
* 安全退出
* @return
*/
@RequestMapping("/userLogout")
public String userLogout(){
//获取主体
Subject subject = SecurityUtils.getSubject();
subject.logout();;
return "redirect:/home/index.html";
}
测试登录
盐加密
简单的介绍一下盐加密,其实很简单就是就是在数据库中多添加了一个字段,存随机数的,相当于盐salt。
盐加密把相同的明文密码转换成不同的密文密码,而在转换成密文密码的同时用了一套盐进行加密,而在解密密文密码时需要在这个明文密码转换成密文密码的那套盐,才能进行解密。
注册时
用户 输入用户名和密码,这个时候生成salt,然后将salt和密码进行拼接,存到数据库中去,一个密码对应一个salt
登录时
用户输入用户名和密码,后台根据用户名去数据库中找到用户信息,然后将用户输入的密码和对应的盐salt进行解密,去跟存到数据库中的密码进行对比,如果一致登录成功,否则失败
上述中的shiro登陆中用到的MD5+散列1024+Hex/Base64加密方式
在这里加密还需要更改spring-shiro中的自定义Realm配置Shiro明文密码如何进行加密、解密
<!--配置Shiro明文密码如何进行加密-->
<!--注意:重要的事情说三次~~~~~~此处加密方式要与用户注册时的算法一致 -->
<!--注意:重要的事情说三次~~~~~~此处加密方式要与用户注册时的算法一致 -->
<!--注意:重要的事情说三次~~~~~~此处加密方式要与用户注册时的算法一致 -->
<!--以下三个配置告诉shiro将如何对用户传来的明文密码进行加密-->
<property name="credentialsMatcher">
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--指定hash算法为MD5-->
<property name="hashAlgorithmName" value="md5"/>
<!--指定散列次数为1024次-->
<property name="hashIterations" value="1024"/>
<!--true指定Hash散列值使用Hex加密存. false表明hash散列值用用Base64-encoded存储-->
<property name="storedCredentialsHexEncoded" value="true"/>
</bean>
</property>
加密密码PasswordHelper类(盐加密)
package com.zking.ssm.book.util;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
/**
* 用于shiro权限认证的密码工具类
*/
public class PasswordHelper {
/**d
* 随机数生成器
*/
private static RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
/**
* 指定hash算法为MD5
*/
private static final String hashAlgorithmName = "md5";
/**
* 指定散列次数为1024次,即加密1024次
*/
private static final int hashIterations = 1024;
/**
* true指定Hash散列值使用Hex加密存. false表明hash散列值用用Base64-encoded存储
*/
private static final boolean storedCredentialsHexEncoded = true;
/**
* 获得加密用的盐
*
* @return
*/
public static String createSalt() {
return randomNumberGenerator.nextBytes().toHex();
}
/**
* 获得加密后的凭证
*
* @param credentials 凭证(即密码)
* @param salt 盐
* @return
*/
public static String createCredentials(String credentials, String salt) {
SimpleHash simpleHash = new SimpleHash(hashAlgorithmName, credentials,
salt, hashIterations);
return storedCredentialsHexEncoded ? simpleHash.toHex() : simpleHash.toBase64();
}
/**
* 进行密码验证
*
* @param credentials 未加密的密码
* @param salt 盐
* @param encryptCredentials 加密后的密码
* @return
*/
public static boolean checkCredentials(String credentials, String salt, String encryptCredentials) {
return encryptCredentials.equals(createCredentials(credentials, salt));
}
public static void main(String[] args) {
//盐
String salt = createSalt();
System.out.println(salt);
System.out.println(salt.length());
//凭证+盐加密后得到的密码
String credentials = createCredentials("123", salt);
System.out.println(credentials);
System.out.println(credentials.length());
boolean b = checkCredentials("123", salt, credentials);
System.out.println(b);
}
}
测试
存储到数据库
一个密码对应一个salt
Shiro授权流程
==============================================================================================================================================================================================================================================================================================================
1、调用授权验证方法(**Subject.isPermitted()或Subject.hasRole()**等)
2、Subject实例通常是DelegatingSubject类(或子类)的实例对象;在认证开始时,通过SecurityManager实例来调用**securityManager.isPermitted(string)**方法/**security.hasRole(string)**方法
3、SecurityManager委托Authorizer的实例(默认是ModularRealmAuthorizer类的实例,同样支持多个realm)调用相应的授权方法
4、每一个Realm将检查是否实现了相同的Authorizer接口,然后调用Realm自己的相应的授权验证方法
5、使用多个Realm时,不同于认证策略处理方式,授权处理过程中:
当调用Realm出现异常时,立即抛出,结束授权验证
只要一个Realm验证成功,则认为授权成功,立即返回,结束验证
在mapper层定义两个方法
这里返回Set集合是因为在授权时,参数必须为Set集合
在自定义Realm中重写授权方法
/**
*授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取登录账号
String username = principalCollection.getPrimaryPrincipal().toString();
//根据username获取用户对应角色
Set<String> roles = sysUserMapper.findRoles(username);
//根据username获取对应的角色权限
Set<String> permissions = sysUserMapper.findPermissions(username);
//创建SimpleAuthorizationInfo
SimpleAuthorizationInfo simple=new SimpleAuthorizationInfo();
//填充用户的角色和权限
simple.setRoles(roles);
simple.setStringPermissions(permissions);
return simple;
}
在spring-shiro.xml中的安全管理器中配置shiro的核心过滤器和并设置权限,和验证失败页面
<!--2、创建安全管理器,并更换Realm-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="shiroRealm" />
<!--配置会话管理器-->
<property name="sessionManager" ref="sessionManager"/>
<!--设置缓存管理器-->
<property name="cacheManager" ref="cacheManager"/>
</bean>
<!--3、配置Shiro核心过滤器-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口,这个属性是必须的 -->
<property name="securityManager" ref="securityManager" />
<!-- 身份验证失败,跳转到登录页面 -->
<property name="loginUrl" value="/home/index.html"/>
<!-- 身份验证成功,跳转到指定页面 -->
<!-- <property name="successUrl" value="/index.jsp"/>
<!– 权限验证失败,跳转到指定页面 –>
<property name="unauthorizedUrl" value="/noauthorizeUrl.jsp"/>-->
<!-- Shiro连接约束配置,即过滤链的定义 -->
<property name="filterChainDefinitions">
<value>
<!--anon 表示匿名访问,不需要认证以及授权-->
<!--authc表示需要认证 没有进行身份认证是不能进行访问的-->
/user/userLogin=anon
/book/**=authc
<!-- /css/** = anon
/images/** = anon
/js/** = anon
/ = anon
/user/logout = logout
/user/** = anon
/userInfo/** = authc
/dict/** = authc
/console/** = roles[admin]
/** = anon-->
</value>
</property>
</bean>
测试
在未登录下访问book模块下
自动被权限拦截并返回登录页面
权限标签
导入shiro标签
验证身份是否是管理员
同时还有角色权限
注解式开发
1、在spring-mvc中配置shiro注解式开发
<!--9、配置Shiro注解式权限-->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true"/>
</bean>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
2.在控制层方法上加上注解
@RequiresRoles(value={“管理员”,“高级用户”},logical= Logical.OR)
在此方法上使用此权限注解,判断使用该方法是否是管理员或者高级用户
OR该文AND 为同时满足两个角色
@RequiresRoles(“管理员”)
判断使用该方法是否是管理员
/**
* 返回List泛型格式的JSON数据
* @param book
* @param request
* @return
*/
@RequiresRoles(value={"管理员","高级用户"},logical= Logical.AND)
@RequestMapping("/queryListBooks")
@ResponseBody
public List<Book> queryListBooks(Book book,HttpServletRequest request){
PageBean pageBean=new PageBean();
pageBean.setRequest(request);
List<Book> books = bookService.queryBookPager(book, pageBean);
return books;
};
@RequiresPermissions(“bookmanager📖edit”)
此注解表示使用此方法需要具有此权限
/**
* 返回Map类型的JSON数据
* @param bookId
* @return
*/
@RequiresPermissions("bookmanager:book:edit")
@RequestMapping("/querySingleMap")
@ResponseBody
public Map<String, Object> querySingleMap(Integer bookId){
return bookService.querySingleMap(bookId);
}
测试
在登录普通用户下访问
至此,Shiro与SSM集成实现用户认证和授权介绍完毕,由于作者水平有限难免有疏漏,欢迎留言纠错。