shiro的@RequiresPermissions,@RequiresRoles解析流程

本文详细介绍了Shiro框架中的@RequiresPermissions和@RequiresRoles注解的使用,包括依赖配置、AuthenticationToken、AccountRealm的实现、JwtFilter的过滤逻辑以及权限验证流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    全网最新讲解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)去完成认证,其中权限校验涉及到权限解析。

制作不易,转载请@,有问题欢迎讨论。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值