PS:欢迎转载,但请注明出处,谢谢配合。
Shiro具体使用详解
一、前言
基于springboot2.x,前后端分离
二、具体步骤
1、加入shiro依赖包
修改pom.xml
<!-- 引入 shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
2、自定义认证过滤器FormAuthenticationFilter
目的:将原来登录校验不通过的重定向改为返回Json数据
做法:继承 authc 的默认认证过滤器(FormAuthenticationFilter)
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
1)重写onAccessDenied 方法
针对shiro配置认证规则为“authc”的那些url,当校验出用户没有登录的话,执行该方法
/**
* 当shiro校验用户未登录时,返回JSON数据(代替原有的跳转到登录界面)
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
this.log.error("用户未登录,拒绝访问");
response.setContentType("application/json") ;
response.setCharacterEncoding("UTF-8") ;
PrintWriter out = response.getWriter() ;
String resultJson = JSON.toJSONString(ResultUtil.error("IMS0000","登录已失效,请重新登录!")) ;
out.write(resultJson) ;
out.flush() ;
out.close() ;
return false ;
}
3、自定义授权过滤器RolesAuthorizationFilter
目的:将原来权限校验不通过的重定向改为返回Json数据
做法:继承 roles 的默认授权过滤器(RolesAuthorizationFilter)
public class MyRolesAuthorizationFilter extends RolesAuthorizationFilter{
1)重写 onAccessDenied 方法
针对shiro配置授权规则为“roles”的那些url,当校验出用户没有对应权限的话,执行该方法。
返回json数据格式,由前端决定授权拒绝的后续逻辑。
写法参考上面的自定义认证过滤器MyFormAuthenticationFilter。
2)重写 isAccessAllowed 方法(可选)
若role配置多个,默认情况下,是必须多个role同时具备,才有访问权限。但一般实际情况,是只要拥有配置中的某个权限,即可访问。因此可以重写isAccessAllowed 方法,将父类中的“and”逻辑改为“or”逻辑。
// 原逻辑,多个role必须同时满足,才有权限访问
// return subject.hasAllRoles(roles);
// 修改后逻辑 -- begin (多个role之间是“或”关系,只有一个满足即可)
for(String role: roles) {
if(subject.hasRole(role)) {
return true ;
}
}
return false ;
// 修改后逻辑 --end
4、编写shiro配置类
1)编写一个shiro的配置类
@Configuration
public class ShiroConfig {
2)配置类中,定义shiro的过滤器工厂bean
在该过滤器工厂中,可以 设置自定义过滤器、配置认证规则、配置授权规则
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
// 创建 ShiroFilterFactoryBean
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 1、创建过滤器Map,用来装自定义过滤器
LinkedHashMap<String, Filter> map = new LinkedHashMap<>();
// 2、将自定义过滤器放入map中,如果实现了自定义授权过滤器,那就必须在这里注册,否则Shiro不会使用自定义的授权过滤器
map.put("authc", new MyFormAuthenticationFilter()); // 认证过滤器
map.put("roles", new MyRolesAuthorizationFilter()); // 授权过滤器
// 3、将过滤器绑定到shiroFilterFactoryBean上
shiroFilterFactoryBean.setFilters(map);
// 4.配置认证规则 // <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/login/**", "anon"); // 类似/login/xxx请求,可以直接访问
filterChainDefinitionMap.put("/jnl/**", "anon"); // 类似/jnl/xxx请求,可以直接访问
// 5.配置授权规则
filterChainDefinitionMap.put("/admin/**", "roles[admin]"); // 类似/admin/**请求,需要admin角色才可访问(可以写多个role,默认是多个role同时满足,可以自定义改成或关系)
//注意:下面这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截,都需要认证(因为filterChainDefinitions 配置过滤规则,是从上到下的顺序匹配)
filterChainDefinitionMap.put("/**", "authc"); // 剩下的其他请求,需要登录后才可访问
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
3)配置类中,定义权限管理bean
主要包装我们编写的业务Realm,该bean会注给上面的过滤器工厂。shiro过滤器拦截的请求,会通过SecurityManager,走到业务Realm的相关方法。
@Bean
public SecurityManager securityManager(UserRealm myShiroRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm);
return securityManager;
}
4)配置类中,定义业务Realm bean
下面代码中的两个参数,是我项目中的数据库操作Service,大家根据自己的情况实现即可。或者先不用数据库操作,直接代码中模拟数据库的返回值,先试试Shiro效果,则不用注入下面两个参数。
@Bean
public UserRealm myShiroRealm(ShiroUserService shiroUserServiceImpl, ShiroUserRoleService shiroUserRoleServiceImpl) {
UserRealm userRealm = new UserRealm();
userRealm.setShiroUserService(shiroUserServiceImpl) ;
userRealm.setShiroUserRoleService(shiroUserRoleServiceImpl) ;
return userRealm;
}
5、编写Realm类
目的:操作数据层,实现用户的认证和授权
做法:继承Realm父类 AuthorizingRealm
public class UserRealm extends AuthorizingRealm {
1)实现 doGetAuthenticationInfo(认证方法)
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String userName = upToken.getPrincipal().toString() ;
// 1.根据用户名,查询用户信息
Map userMap = this.shiroUserService.queryUser(userName) ;
// 2.判断用户名是否存在
if(null == userMap || userMap.isEmpty()) {
return null ; // 这里返回null后,后面逻辑会报出对应异常(账号不存在)
}
// 3.根据salt,将前端上送的密码,加盐加密后,重新放入原token,以便后续逻辑比对两个加密后的密码是否一致【我项目中数据库存储的用户密码是MD5加盐加密后的,明文存储的则不需要这步】
String salt = (String) userMap.get("salt") ; // 密码加密用的盐值
char[] oldPassword = upToken.getPassword() ;
upToken.setPassword(this.md5withSalt(String.valueOf(oldPassword), salt).toCharArray());
String password = (String) userMap.get("password") ; //DB中的密码(加密后的)
// 4.根据查询出的用户名/密码,构建SimpleAuthenticationInfo认证对象(第一个参数: 用户名 ;第二个参数:DB中查到的密码 ; 第三个参数:当前Realm名字)
return new SimpleAuthenticationInfo(userName, password, this.getClass().getName()) ; //shiro后续会根据该对象,与原上送参数token对象,比对两者的密码是否一致,不一致则抛出IncorrectCredentialsException异常
}
登录交易中,会调用Subject.login(token),最终会执行到Realm 的 doGetAuthenticationInfo 方法。
2)实现 doGetAuthorizationInfo(授权方法)
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 1. 获取授权的用户
String userName = principals.getPrimaryPrincipal().toString() ;
// 2. 根据用户名,从DB中查询拥有的角色(另:实际可进行缓存优化,不应每次都查库)
Set<String> roles = this.shiroUserRoleService.queryUserRoles(userName) ;
// 3. 根据角色集合Set<String>,构建SimpleAuthorizationInfo授权对象
return new SimpleAuthorizationInfo(roles);
}
shiro过滤器拦截请求后,若对应了role规则配置的路径,则会判断是否具有role角色权限,通过RolesAuthorizationFilter的 isAccessAllowed方法,最终会执行到Realm 的 doGetAuthorizationInfo 方法。
6、编写登录Controller
登录交易中,调用shiro相关类进行认证,主要包括如下内容:
Subject subject = SecurityUtils.getSubject() ; // 获取Subject-用户主体(会把操作交给SecurityManager,最后到Realm)
AuthenticationToken token = new UsernamePasswordToken(userName, passWord) ; // 将从获取的用户名和密码设置到一个token中
subject.login(token); // 通过捕获该方法抛出的异常,返回对应报错信息给前端(UnknownAccountException-用户名错误,IncorrectCredentialsException-密码错误)