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 {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private 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
*/
@Override
protected 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
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1、获取用户名称
String userName = (String) token.getPrincipal();
//2、判断用户名称是否为空
if(StringUtils.isEmpty(userName)){
// 返回null,会默认抛出一个异常,org.apache.shiro.authc.UnknownAccountException
return 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
*/
@Bean
public DefaultWebSecurityManager securityManager(CustomRealm realm, SessionManager sessionManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm);
return securityManager;
}
/**
* 添加拦截器链
* @return
*/
@Bean
public 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
*/
@Override
protected 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){
//有一个角色校验通过,则表示校验通过,返回true
if(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
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition filterChainDefinition){
//创建 ShiroFilterFactoryBean
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
//设置大量的url
filterFactoryBean.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 实例才能拿到Subject
filterFactoryBean.getFilters().put("roleOr",new RolesOrAuthorizationFilter());
return filterFactoryBean;
}
3、在拦截器链 ShiroFilterChainDefinition 配置使用自定义过滤器
由上边可以知道 自定义Shiro 过滤器 RolesOrAuthorizationFilter 的名称是 “roleOr”;
在拦截器链 ShiroFilterChainDefinition 中的配置如下:
@Bean
public 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;
}