全网最新讲解shiro的@RequiresPermissions,@RequiresRoles的解析流程,绝对对你理解这两个验证流程大有脾益。
下面的代码肯定是我一点一点敲的,但直接拿去用也可能有问题,关于版本,关于一些小细节差异,这里只是提供一点思路,按照这个思路是肯定不会出问题的。直接用我的代码出问题了,解决问题也是一个很好的锻炼,有利于你对shiro的理解。
在引入相关依赖后:
<!--springboot版本,java用的是17-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.13</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--springboot对shiro集成-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.6.0</version>
</dependency>
配置:(在配置结束后再讲解)
Controller
然后开始controller层:
@GetMapping("/test")
// @RequiresPermissions("user:a,b:1")
@RequiresRoles({"user","system"})
public String test(){
return "hahhaha";
}
AuthenticationToken
这个只需要继承这个类,然后重写方法就行了
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String token){
this.token = token;
}
@Override
public Object getPrincipal() { //相当于用户名
return token;
}
@Override
public Object getCredentials() { //相当于密码
return token;
}
}
认证信息类
我也不知道怎么叫,这个类用于下面的realm验证传递的类,在realm的doGetAuthenticationInfo方法中不是认证成功了么,我们需要把认证成功的的信息,比如我这里认证过后,我把id放在里面了。然后你需要在认证成功后返回一个AuthenticationInfo,我这里new了一个SimpleAuthenticationInfo返回,这个类的第一个参数是认证的信息,相当于用户名(标识是谁),第二个参数是一个凭证,相当于密码,第三个参数是realm的名字。
@Data
public class AccountProfile implements Serializable {
private Integer id;
}
Realm
然后实现shiro的realm:
public class AccountRealm extends AuthorizingRealm {
@Autowired
UserService userService;
//每个realm需要起一个名字
@Override
public String getName() {
return "myRealm";
}
//这个需要重写,在你使用ModularRealmAuthenticator,也就是配置了不止一个realm,做多个realm的验证的时候,他会传入这个
//AuthenticationToken来判断是否支持这个token,不支持传入肯定是验证失败啊,就没必要再去验证了
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
//授权器
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
AccountProfile primaryPrincipal =(AccountProfile) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole("user"); //这里写死了,具体角色内容根据你的数据库之类的来写
info.addStringPermission("user:a,b:1,2"); //同上,写死了
return info;
}
//认证器
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//这里我写的不重要,你只需解析这个AuthenticationToken,获取你的关键信息,然后返回一个AuthenticationInfo就可以
//我这里用的是jwt+token,所以我下面解析token获取用户id,然后查询数据库,获取关键信息返回AuthenticationInfo
JwtToken jwtToken = (JwtToken) authenticationToken;
String jwt = (String) jwtToken.getCredentials();
Map<String, Claim> mes = JwtFactory.getMes(jwt);
Integer id = mes.get("id").asInt(); //获取用户id
User user = userService.getById(id); //通过id查询用户
if(Objects.isNull(user))throw new UnknownAccountException("用户不存在");
AccountProfile accountProfile =new AccountProfile();
BeanUtils.copyProperties(user,accountProfile);
return new SimpleAuthenticationInfo(accountProfile,jwtToken.getCredentials(),getName());
}
}
Jwt过滤器
这个过滤器继承AuthenticatingFilter这个类,至于为什么,因为它的父类是AuthenticationFilter,你按照字面意思叫“身份验证过滤器”,这个AuthenticatingFilter叫“正在身份验证过滤器”,在这个类里面实现了executeLogin方法,也就是不需要你去实现如何登录了,你只用关心什么情况需要登录验证,怎么验证,如何定义角色、权限。去实现这个类最合适了。
public class JwtFilter extends AuthenticatingFilter {
@Resource
RedisTemplate<Object,Object> template;
//在下面那个类执行executeLogin,执行后executeLogin会调用这个方法作为一个登录凭证,也就是一个token
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt =request.getHeader("Authorization");
return new JwtToken(jwt);
}
//访问拒绝的时候调用,反正就是如果这个接口需要访问权限,它就会拒绝你的访问,你在这里面进行登录操作
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt =request.getHeader("Authorization");
boolean access = true;
//这个try里面不用管,反正就是在做一件事:验证这个jwt是否合格
try{
if(StringUtils.isEmpty(jwt)){
access = false;
}else{
if(!JwtFactory.verify(jwt)){
throw new ExpiredCredentialsException("token已失效,请重新登录");
}
Map<String, Claim> mes = JwtFactory.getMes(jwt);
Integer id = mes.get("id").asInt();
String redisJwt = (String)template.opsForValue().get(id + "-jwt");
if("".equals(redisJwt) || !jwt.equals(redisJwt)){
throw new ExpiredCredentialsException("登录已退出,请重新登录");
}
}
}catch (ExpiredCredentialsException e){
access = false;
}
if(!access){
throw new AccessDeniedException("访问失败,权限不够!");
}
//执行登录
return executeLogin(servletRequest,servletResponse);
}
}
shiroConfig
shiro相关配置:
@Configuration
public class ShiroConfig {
@Bean("jwt-filter")
JwtFilter jwtFilter(){
return new JwtFilter();
}
<!-->我们写的这些都需要注入成bean才能被扫描到<-->
@Bean
public AccountRealm accountRealm(){
return new AccountRealm();
}
@Bean
public DefaultWebSecurityManager securityManager() {
AccountRealm accountRealm = accountRealm();
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(accountRealm);
// securityManager.setSessionManager(getDefaultWebSessionManager());
ThreadContext.bind(securityManager);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean ShiroFilterFactoryBean(@Autowired@Qualifier("jwt-filter")JwtFilter filter) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager());
Map<String,Filter> map = new LinkedHashMap<>();
map.put("filter",filter); //放入我们的过滤器
factoryBean.setFilters(map);
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); //为了保持顺序,使用这个
filterChainDefinitionMap.put("/user/**", "anon"); //注意后面的会覆盖前面的,我这里写的user/**,这里匹配路径就不会拦截,因为我们设置的是anno,
// anno啥意思可以自己去了解,它也对应一个过滤器,但是没做什么事儿
filterChainDefinitionMap.put("/**", "filter"); //设置我们自己的过滤器
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return factoryBean;
}
}
filter配置
我们继承使用的是AuthenticatingFilter,你往上找它的父类,它也是继承于javax.servlet.Filter,反正我试了,springboot会自动将这个过滤器注入成全局过滤器,所以不管我们的ShiroConfig怎么配置,所有请求都会被这个拦截,所以我们需要相关配置,这里我用的比较笨的方法,将过滤器注入,然后取消使用:
@Configuration
public class FilterConfig {
@Autowired
private JwtFilter myFilter;
@Bean
public FilterRegistrationBean<JwtFilter> filterRegistrationBean() {
FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(myFilter);
registrationBean.setName("jwt-filter");
registrationBean.setEnabled(false);
return registrationBean;
}
}
这样所有的相关代码工作大体就是这样。
@RequiresRoles执行流程
获取注解
首先在你的接口上打了这个方法后,在 RoleAnnotationHandler会获取这个注解,然后获取注解的参数。
调用验证角色方法
然后调用这个this.getSubject().checkRole(),然后这个方法会去检查是否本用户有对应的角色信息。在这个方法过后,来到关键的ModularRealmAuthorizer的checkRole方法
这是第一个checkRole,这里把集合转成数组
这是第二个checkRole方法,将数组遍历,单个去验证用户是否拥有这个角色
这是最后一个checkRole方法,去调用hasRole方法验证用户是否拥有此角色,如果用户缺少此角色,直接报错。
在这个hasRole方法里面,会遍历所有的realm,去调用每个realm的hasRole方法。只要有一个realm检查出符合条件,这里的hasRole方法就会返回true。
对比角色信息
这里就到了realm的角色信息比对了
获取用户的角色信息
我们已经在注解上获取到了需要的角色信息。现在我们需要获取到用户有什么角色信息。
上面的的第一个方法就是去获取用户的角色信息的方法,在这个方法里面
注意这里调用的doGetAuthorizationInfo就是我们实现的realm方法,有没有忘记啊,这个类是AuthorizingRealm,正是我们实现的realm:AccountRealm的父类。
在这个方法里面进行授权。
获取授权信息完成了,现在直接比对一下就好了
角色信息解析
现在只需要查看接口需要的角色信息用户是否拥有。
是不是就要回到上面的那个方法了啊(这里我再贴一下图)
调用的这个方法就是这个方法下面的那个方法
在我们写的那个授权方法里面我们不是直接将“user”添加进去了么,info.getRoles()就直接获取这个数组,然后查看传入的角色信息,用户是否拥有。
如果用户缺少需要的某个角色,就会报错。
比如我这里用户只是user角色,而这个接口需要user和system角色,就会验证失败!。
@RequiresPermissions执行流程
这个的执行流程和RequiresRoles有很多相似之处,但比RequiresRoles更多。
获取注解
PermissionAnnotationHandler类的assertAuthorized方法,调用subject的方法去检查是否符合权限
调用权限验证方法
来到DelegatingSubject的checkPermission方法,首先会去看你有没有认证成功。
然后subject这个主体会委托securityManager去做权限校验(这个subjectManager里面有授权器,上面的那个rolo也是如此)
同样,先检查有没有配置realm
再去看每个realm有没有这个权限。
解析权限
我们之前是不是获取注解后,就开始一顿委托,去调用我们配置的realm的授权方法,但是这里多了一个步骤,还记得我们这两个注解写的区别不,他们两个注解接受的参数都是字符串数组,但是关于权限的我们写的格式可以是“user:a,b:1”,这个代表是需要user:a:1和user:b:1,详细可以去网上搜,这样是不是就不能直接字符串比较验证了。所以这里需要解析
首先这里会获取这个权限解析器,调用解析器的解析方法去解析权限。默认是WildcardPermissionResolver这个,因为源码中实现下来也只有这个。你可以去实现这个类的父类去实现你自己的解析策略。
在这里解析,它会给你返回一个封装的对象
这个对象里面
进行我上面说的解析,这里的caseSensitive默认是false,也就是默认会给你统一转成小写,忽略大小写验证。
获取权限信息
在解析完成后,也就去获取用户的角色信息了,调用这个获取用户的信息,也是和之前一样(先是去看缓存,没有再去调用哪个授权方法,然后获取到了又把信息放入缓存,我这里就不贴图了)。
现在去执行这个this.isPermitted方法去比较,在这里还记得不,我们注解写成的那个格式会被解析成Permission对象,但是我们在给我们写的授权方法里也是这样写的,还没有被解析,这里调用的getPermission就会做这个处理
解析用户权限
在这里面分为三部分,第一部分,直接获取对象格式的Permission,这个在授权方法里面可以设置,这里我们授权方法设置的是字符串不是Permission对象。
第二部分,就是解析我们设置的字符串权限,resolvePermissions方法源码我放在这张图下面,总的来说,也只是和上面解析注解里面的权限字符串一样,这里只是变成了循环遍历去解析
第三部分,是去获取你的角色,然后调用角色权限解析器,根据你的角色信息转换成对应的权限,比如你是普通用户,我们可以设置普通用户本来就有某些权限,就不用每次所有的权限都需要在授权器里面设置了,这个默认是为null,也就是没有角色权限解析器,你可以自己去实现这个类RolePermissionResolver(这个resolveRolePermissions方法我放在第三张图)
这个获取到了是不是就只是上面简单的循环比较Permission了呢,这就完了。
总结
这两者都是拦截器拦截,获取注解信息,调用subject去认证,subject委托securityManager去认证,securityManager的授权器(默认是ModularRealmAuthorizer)去完成认证,其中权限校验涉及到权限解析。
制作不易,转载请@,有问题欢迎讨论。