Shiro学习(三):shiro整合springboot
一、Shiro整合到Springboot步骤
1、准备SpringBoot 环境,这一步省略
2、引入Shiro 依赖
因为是Web 项目,所以需要引入web 相关依赖 shiro-spring-boot-web-starter,如下所示:
3、准备Realm
因为实例化 ShiroFilterFactoryBean 时需要注入 SecurityManager 的bean,而
SecurityManager 实例化时需要绑定Realm。
在真正工作中,我们一般需要从数据库中查询用户信息、角色信息和用户权限信息,
即一般自定义Relam,自定义Realm如下:
@Component
public class CustomRealm extends AuthorizingRealm {@Autowiredprivate UserService userService;@Autowiredprivate RoleService roleService;@Autowiredprivate PermissionService permissionService;{//用于密码加密和比对HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();matcher.setHashAlgorithmName("MD5");matcher.setHashIterations(1024);this.setCredentialsMatcher(matcher);}/*** 授权* todo 注意:* 1、授权是在认证之后的操作,授权方法需要用到认证方法返回的 AuthenticationInfo 中的用户信息* 2、该方法是在父类 AuthorizingRealm.getAuthorizationInfo() 方法中调用的,在 getAuthorizationInfo()* 方法中,回先从缓存中查询权限信息,若缓存中数据不存在,再执行改当前方法从数据库中查询权限数据* 针对这个逻辑,我们可以扩展Shiro 把数据放到缓存中(一般放到redis 中)*** @param principals 即 doGetAuthenticationInfo 方法返回的 AuthenticationInfo 中的用户信息(这里是User )* @return*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {System.out.println("查询数据库,根据用户信息查询角色和权限信息~~~~~~~~~~~~~~~~~~~~~~~~");//0、判断是否完成认证Subject subject = SecurityUtils.getSubject();if(subject == null || subject.isAuthenticated())return null;//1. 获取认证用户的信息User user = (User) principals.getPrimaryPrincipal();//2. 基于用户信息获取当前用户拥有的角色。Set<Role> roleSet = roleService.findRolesByUid(user.getId());Set<Integer> roleIdSet = new HashSet<>();Set<String> roleNameSet = new HashSet<>();for (Role role : roleSet) {roleIdSet.add(role.getId());roleNameSet.add(role.getRoleName());}//3. 基于用户拥有的角色查询权限信息Set<Permission> permSet = permissionService.findPermsByRoleSet(roleIdSet);Set<String> permNameSet = new HashSet<>();for (Permission permission : permSet) {permNameSet.add(permission.getPermName());}//4. 声明AuthorizationInfo对象作为返回值,传入角色信息和权限信息SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();info.setRoles(roleNameSet);info.setStringPermissions(permNameSet);//5. 返回return info;}/*** 认证 用户执行认证操作传入的用户名和密码* 只需要完成用户名校验即可,密码校验由Shiro内部完成** @param token* @return* @throws AuthenticationException*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {//1、获取用户名称String userName = (String) token.getPrincipal();//2、判断用户名称是否为空if(StringUtils.isEmpty(userName)){// 返回null,会默认抛出一个异常,org.apache.shiro.authc.UnknownAccountExceptionreturn null;}//4、如果用户名称不为空,则基于用户名称去查询用户信息//这一步一般是自己的UserService 服务//模拟查询用户信息User user = userService.findByUsername(userName);if(user == null){return null;}//5、构建 AuthenticationInfo 对象,并填充用户信息/*** todo 注意:* SimpleAuthenticationInfo 第一个参数是用户信息,第二个参数是用户密码,第三个参数是Realm名称(这个参数没有意义)*/SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),"CustomRealm!!!");//设置盐info.setCredentialsSalt(ByteSource.Util.bytes(user.getSalt()));//返回 AuthenticationInfo 对象return info;}}
4、准备Shiro相关的配置文件
定义Shiro 配置文件,用于实例化 SecurityManager 对象 与 配置拦截器链。
虽然SpringBoot 自动装配机制会自动装配 SecurityManager,但自动装载 SecurityManager 时
只会注入 Shiro 自身默认提供的Relam,这里需要把自定义的Realm注入到 SecurityManager
,所以需要我们手动装载 SecurityManager 去覆盖Springboot 自动装载的 SecurityManager。
另外在 shiro-spring-boot-web-starter 包下的文件 spring.factores 中的配置类
ShiroWebAutoConfiguration中 springboot自动装配的拦截器链中只配置了一种请求,
如图所示:
所以我们也需要在自定义的shiro配置文件中,手动配置我们需要的拦截器链。
shiro配置文件如下:
/***************************************************** Shiro 配置类* 由前边 Shiro 与Spring Web 整合可以发现,Shiro与Spring 整合的核心是向spring中注入* ShiroFilterFactoryBean;但在spring boot 中,spring boot 会自动将 ShiroFilterFactoryBean* 注入到spring 中(在 AbstractShiroWebFilterConfiguration 中完成的)* 在 AbstractShiroWebFilterConfiguration 中发现,实例化 ShiroFilterFactoryBean 时需要* 提供 SecurityManager(使用的是 DefaultWebSecurityManager) 和 ShiroFilterChainDefinition(拦截器链)* SecurityManager 实例化需要提供 Realm ,** 定义 Shiro 配置类,用于实例化 DefaultWebSecurityManager 和 ShiroFilterChainDefinition** @author lbf* @date ****************************************************/
@Configuration
public class ShiroConfig {/*** 注入* 实例化 WebSecurityManager 时需要用到Releam bean,所以在这之前 Releam 一定要存在* @param realm* @return*/@Beanpublic DefaultWebSecurityManager securityManager(CustomRealm realm, SessionManager sessionManager, RedisCacheManager cacheManager){DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm);return securityManager;}/*** 添加拦截器链* @return*/@Beanpublic ShiroFilterChainDefinition filterChainDefinition(){DefaultShiroFilterChainDefinition filterChainDefinition = new DefaultShiroFilterChainDefinition();//添加拦截器信息Map<String, String> filterChainDefinitionMap = new LinkedHashMap();/*** anon 和 authc 都是Shiro 自带的过滤器* Shiro 自带的过滤器可以在枚举 DefaultFilter 中查看*///anon: 放行filterChainDefinitionMap.put("/login.html","anon");filterChainDefinitionMap.put("/user/**","anon");//authc:认证之后放行filterChainDefinitionMap.put("/**","authc");filterChainDefinition.addPathDefinitions(filterChainDefinitionMap);return filterChainDefinition;}}
5、测试
二、Shiro 授权方式
Shiro 常用的授权方式有2种,即
1)基于连接器链的权限角色校验,就是上边配置拦截器链的方式;将需要校验的请求
配置到拦截器链 ShiroFilterChainDefinition 种,如下图所示:
2)基于注解的权限角色校验
2、基于注解的权限角色校验
Shiro 提供了基于注解的方式来简化权限和角色的校验,可以在类或方法上直接声明所需要
的角色或权限;
注解进行权限或角色校验时,是基于对Controller类进行代理,在前置增强中对请求进行权限
校验,是在拦截器链方式的后边执行。
Shiro 提供了如下几个注解用于权限和角色校验
1)@RequiresAuthentication
要求当前 Subject 已经通过认证(即用户已登录)
2)@RequiresUser
要求当前 Subject 是一个应用程序用户(已认证或通过记住我功能登录)
3)@RequiresGuest
要求当前 Subject 是一个"访客",即未认证或未通过记住我登录
4)@RequiresRoles
要求当前 Subject 拥有指定的角色,即角色校验,常用
5)@RequiresPermissions
要求当前 Subject 拥有指定的权限,即权限校验,常用
示例代码如下:
@RestController
@RequestMapping("/item")
public class ItemController {/*** 基于过滤器链的角色、权限校验* @return*/@GetMapping("/select")public String select(){return "item Select!!!";}@GetMapping("/delete")public String delete(){return "item Delete!!!";}/*** 基于注解的角色校验* @return*/@GetMapping("/update")//默认多个角色是and 的关系@RequiresRoles(value = {"超级管理员","运营"})public String update(){return "item Update!!!";}@GetMapping("/insert")@RequiresRoles(value = {"超级管理员","运营"},logical = Logical.OR)public String insert(){return "item Update!!!";}/*** 基于注解的权限校验* logical=用于指定多个权限是同时满足,还是满足其中一个,默认是and* Logical.OR含义是:只有用于 admin 权限或del权限的用户才能执行删除操作* @return*/@GetMapping("/update")@RequiresPermissions(value = {"item:admin","item:del"},logical = Logical.OR)public String del(){return "item del !!!";}
}
3、基于注解方式的权限角色校验要注意的点
1)基于注解的方式与基于配置链的方式是可以配合使用的,并不冲突,基于配置的方式在
拦截器链之后执行。
2)注解只能用在 Spring 管理的 Bean 上(如 Controller、Service 等),对于静态方法
或非 Spring 管理的类,注解不会生效
3)当权限校验失败时,Shiro 会抛出相应的异常,我们需要自己配置异常处理器处理这些异常
,如:通过 @RestControllerAdvice,@ControllerAdvice
示例代码如下:
@RestControllerAdvice(basePackages = "com.msb.controller") //指定要处理异常的包路径
public class AuthExceptionHandler {//处理授权异常@ExceptionHandler(AuthorizationException.class)public ResponseEntity<String> handleAuthorizationException(AuthorizationException e) {return ResponseEntity.status(HttpStatus.FORBIDDEN).body("无权访问: " + e.getMessage());}//处理认证异常@ExceptionHandler(AuthenticationException.class)public ResponseEntity<String> handleAuthenticationException(AuthenticationException e) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("认证失败: " + e.getMessage());}
}
三、RolesAuthorizationFilter 分析
以默认的角色拦截器 RolesAuthorizationFilter 分析下 Shiro 种是如何进行角色认证的
1)角色校验方法 RolesAuthorizationFilter.isAccessAllowed
//mappedValue: 需要校验的角色列表,即在连接器链种指定的角色列表
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {//获取请求主体 Subject,即用户Subject subject = this.getSubject(request, response);//需要校验的角色列表String[] rolesArray = (String[])((String[])mappedValue);if (rolesArray != null && rolesArray.length != 0) {Set<String> roles = CollectionUtils.asSet(rolesArray);//调用 Subject.hasAllRoles 校验用户是否具有所有的 mappedValue 角色return subject.hasAllRoles(roles);} else {return true;}}
2)AuthorizingRealm.hasAllRoles
从Subject.hasAllRoles 方法一直点进去 ,如下所示:
Subject—>DelegatingSubject.hasAllRoles —> Authorizer.hasAllRoles
—> AuthorizingRealm.hasAllRoles
一直到 AuthorizingRealm.hasAllRoles 方法,在该方法种调用了getAuthorizationInfo
方法来获取 AuthorizationInfo,如下图所示:
3)AuthorizingRealm.getAuthorizationInfo 方法
在 getAuthorizationInfo 方法种,我们重点看下 doGetAuthorizationInfo 方法;
到这里是不是有点眼熟,doGetAuthorizationInfo是一个抽象方法,它有多个实现,
其中有一个实现就是上边我们自定义的CustomRealm中的方法
如下图所示:
四、自定义拦截器
在工作中Shiro 默认提供的校验拦截器往往不能满足我们实际的需要,这就需要我们自定义
Shiro拦截器,如:上边RolesAuthorizationFilter 是校验用户具有角色列表中的所有角色才
校验通过,而在工作中常常有 “存在一个角色在角色列表中就行” 的场景;
1、自定义Shiro拦截器解决 “存在一个角色在角色列表中就行” 的场景
自定义过滤器需要继承类 AuthorizationFilter,并重写 isAccessAllowed 方法
示例代码如下:
public class RolesOrAuthorizationFilter extends AuthorizationFilter {/*** 用户角色校验* @param request* @param response* @param mappedValue 指定的角色列表,* @return* @throws Exception*/@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {//获取校验主题,可以认为就是用户信息Subject subject = this.getSubject(request, response);/*** 获取用户指定的角色列表,就是在 初始化 ShiroFilterChainDefinition过程中指定的角色列表* (如:roles[超级管理员,运营],[]中的内容)*/String[] rolesArray = (String[])((String[])mappedValue);if(rolesArray != null && rolesArray.length > 0){for (String role:rolesArray){//有一个角色校验通过,则表示校验通过,返回trueif(subject.hasRole(role)){return true;}}}else {return true;}return false;}
}
2、将自定义Filter 交给Shiro 管理
自定义的角色校验器(过滤器)如何交给Shiro管理?
由 配置类 ShiroWebFilterConfiguration 初始化 ShiroFilterFactoryBean 时可以发现,过滤器
是在 ShiroFilterFactoryBean 实例化时交给 ShiroFilterFactoryBean 管理的;到这里就明白
了,我们可以手动初始化 ShiroFilterFactoryBean 来覆盖springboot 的自动初始化
ShiroFilterFactoryBean,并把自定义的 RolesOrAuthorizationFilter 过滤器交给
ShiroFilterFactoryBean 管理。
在配置类ShiroConfig 中手动注入ShiroFilterFactoryBean 代码如下:
//手动注入 ShiroFilterFactoryBean,覆盖 Springboot 自动装载ShiroFilterFactoryBean;/*** 初始化一些url* ShiroFilterFactoryBean 实例化需要的url,这些url可以在配置文件中配置*///登录url@Value("#{ @environment['shiro.loginUrl'] ?: '/login.jsp' }")protected String loginUrl;//登录成功后跳转url@Value("#{ @environment['shiro.successUrl'] ?: '/' }")protected String successUrl;//校验失败的url@Value("#{ @environment['shiro.unauthorizedUrl'] ?: null }")protected String unauthorizedUrl;/*** 手动注入 ShiroFilterFactoryBean,覆盖 Springboot 自动装载ShiroFilterFactoryBean;* 用于把自定义的过滤器交给 Shiro 管理* @param securityManager* @param filterChainDefinition 过滤器链* @return*/@Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition filterChainDefinition){//创建 ShiroFilterFactoryBeanShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();//设置大量的urlfilterFactoryBean.setLoginUrl(this.loginUrl);filterFactoryBean.setSuccessUrl(this.successUrl);filterFactoryBean.setUnauthorizedUrl(this.unauthorizedUrl);//设置安全管理器filterFactoryBean.setSecurityManager(securityManager);//设置过滤器链filterFactoryBean.setFilterChainDefinitionMap(filterChainDefinition.getFilterChainMap());//设置自定义过滤器 ,// todo 注意:这里一定要手动的new出来这个自定义过滤器,如果使用Spring自动注入自定义过滤器,// 会造成无法获取到Subject// 因为spring 自动注入 自定义过滤器 RolesOrAuthorizationFilter 初始化太早了,而RolesOrAuthorizationFilter// 初始化时需要做一些Shiro处理后,RolesOrAuthorizationFilter 实例才能拿到SubjectfilterFactoryBean.getFilters().put("roleOr",new RolesOrAuthorizationFilter());return filterFactoryBean;}
3、在拦截器链 ShiroFilterChainDefinition 配置使用自定义过滤器
由上边可以知道 自定义Shiro 过滤器 RolesOrAuthorizationFilter 的名称是 “roleOr”;
在拦截器链 ShiroFilterChainDefinition 中的配置如下:
@Beanpublic ShiroFilterChainDefinition filterChainDefinition(){DefaultShiroFilterChainDefinition filterChainDefinition = new DefaultShiroFilterChainDefinition();//添加拦截器信息,LinkedHashMap有序Map<String, String> filterChainDefinitionMap = new LinkedHashMap();/*** anon 和 authc 都是Shiro 自带的过滤器* Shiro 自带的过滤器可以在枚举 DefaultFilter 中查看*///anon: 放行filterChainDefinitionMap.put("/login.html","anon");filterChainDefinitionMap.put("/user/**","anon");//使用自定义的过滤器,有一个角色在[超级管理员,运营] 中就校验通过filterChainDefinitionMap.put("/item/select","rolesOr[超级管理员,运营]");//filterChainDefinitionMap.put("/item/select","roles[超级管理员,运营]");filterChainDefinitionMap.put("/item/delete","perms[item:delete,item:insert]");//authc:认证之后放行filterChainDefinitionMap.put("/**","authc");filterChainDefinition.addPathDefinitions(filterChainDefinitionMap);return filterChainDefinition;}