• SpringBoot+Shiro+JWT前后端分离实现用户权限和接口权限控制


    1. 引入需要的依赖

    我使用的是原生jwt的依赖包,在maven仓库中有好多衍生的jwt依赖包,可自己在maven仓库中选择,实现大同小异。

        <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-web</artifactId>
                <version>${shiro.version}</version>
                <scope>compile</scope>
            </dependency>
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring</artifactId>
                <version>${shiro.version}</version>
            </dependency>
            <dependency>
                <groupId>org.crazycake</groupId>
                <artifactId>shiro-redis</artifactId>
                <version>${shiro-redis.version}</version>
                <exclusions>
                    <exclusion>
                        <artifactId>shiro-core</artifactId>
                        <groupId>org.apache.shiro</groupId>
                    </exclusion>
                </exclusions>
            </dependency>
            <!--JWT依赖-->
            <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>${jwt.version}</version>
            </dependency>

    2. 配置shiro信息

    2.1. 配置文件增加属性值配置

    # shiro 配置
    shiro:
      filter-chain-map:
        # 用户登录
        '[/login/**]': origin
        # 获取api token
        '[/api/token/**]': anon
        # api接口权限配置
        '[/api/**]': api
        # 用户权限控制
        '[/**]': origin, jwt
      # 设置权限缓存时间
      cache-timeout: 60

    2.2. shiro 配置类

    package com.example.shiro.configuration;
    
    import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
    import org.apache.shiro.mgt.DefaultSubjectDAO;
    import org.apache.shiro.mgt.SessionStorageEvaluator;
    import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.apache.shiro.web.mgt.DefaultWebSessionStorageEvaluator;
    import org.crazycake.shiro.RedisCacheManager;
    import org.crazycake.shiro.RedisManager;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.autoconfigure.AutoConfigureAfter;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Lazy;
    
    import javax.servlet.Filter;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * shiro配置文件
     *
     * @author xsshu
     * @date 2020-07-18 16:22
     */
    @Configuration
    @AutoConfigureAfter(ShiroProperties.class)
    public class ShiroConfig {
    
        @Autowired
        private ShiroProperties shiroProperties;
    
        @Value("${spring.redis.host}:${spring.redis.port}")
        private String host;
        @Value("${spring.redis.password}")
        private String password;
        @Value("${spring.redis.database}")
        private int database;
        /**
         * shiroFilter
         *
         * @param securityManager
         * @return
         */
        @Bean("shiroFilter")
        public ShiroFilterFactoryBean factory(@Qualifier("webSecurityManager") @Lazy DefaultWebSecurityManager securityManager) {
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            // 必须设置 SecurityManager
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            shiroFilterFactoryBean.setFilterChainDefinitionMap(shiroProperties.getFilterChainMap());
            Map<String, Filter> filters = new HashMap<>(3);
            // 跨域拦截
            OriginFilter originFilter = new OriginFilter();
            filters.put("origin", originFilter);
            // 用户请求拦截
            filters.put("jwt", new UserJwtFilter());
            // API请求拦截
            filters.put("api", new AppJwtFilter());
            shiroFilterFactoryBean.setFilters(filters);
            return shiroFilterFactoryBean;
        }
    
        /**
         * redis配置
         *
         * @return
         */
        public RedisManager redisManager() {
            RedisManager redisManager = new RedisManager();
            redisManager.setHost(host);
            redisManager.setPassword(password);
            redisManager.setDatabase(database);
            return redisManager;
        }
    
        @Bean("redisCacheManager")
        public RedisCacheManager cacheManager() {
            RedisCacheManager cacheManager = new RedisCacheManager();
            cacheManager.setRedisManager(redisManager());
            // redis key默认 = shiro:cache:com.unionticketing.auth.interceptor.realm.MyRealm.authorizationCache:用户ID
            // redis key = shiro:cache:com.unionticketing.auth.interceptor.realm.MyRealm.authorizationCache:token值
            cacheManager.setPrincipalIdFieldName("token");
            cacheManager.setExpire(shiroProperties.getCacheTimeout());
            return cacheManager;
        }
    
        /**
         * 禁用session, 不保存用户登录状态。保证每次请求都重新认证。
         * 需要注意的是,如果用户代码里调用Subject.getSession()还是可以用session
         */
        @Bean
        protected SessionStorageEvaluator sessionStorageEvaluator(){
            DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
            sessionStorageEvaluator.setSessionStorageEnabled(false);
            return sessionStorageEvaluator;
        }
        /**
         * 配置webSecurityManager
         *
         * @param
         * @return
         **/
        @Bean("webSecurityManager")
        public DefaultWebSecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(myRealm());
            DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
            DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
            defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
            subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
            securityManager.setSubjectDAO(subjectDAO);
            securityManager.setCacheManager(cacheManager());
            return securityManager;
        }
    
        @Bean("myRealm")
        public MyRealm myRealm() {
            return new MyRealm();
        }
    
        /**
         * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
         *
         * @param securityManager
         * @return
         */
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("webSecurityManager") @Lazy
                                                                                               DefaultWebSecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
            advisor.setSecurityManager(securityManager);
            return advisor;
        }
    }

    2.3. MyRealm

    package com.example.shiro.configuration;
    
    import com.example.shiro.service.TokenService;
    import lombok.NoArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authc.SimpleAuthenticationInfo;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.authz.SimpleAuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    import org.apache.shiro.subject.SimplePrincipalCollection;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Lazy;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 自定义Realm
     *
     * @author xsshu
     * @date 2020-07-18 16:33:12
     */
    @Slf4j
    @NoArgsConstructor
    public class MyRealm extends AuthorizingRealm {
    
        /**
         * 增加@Lazy注解 是TokenService为低优先级注入的bean,为防止项目启动时报Bean 'xxx' of type [com.xx.xxx.xxxx.xxxxx$$EnhancerBySpringCGLIB$$babebd0] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
         */
        @Autowired
        @Lazy
        private TokenService tokenService;
    
        /**
         * 定义自己的认证匹配方式
         *
         * @param jwtCredentialsMatcher
         */
        public MyRealm(JwtCredentialsMatcher jwtCredentialsMatcher) {
            super(jwtCredentialsMatcher);
        }
    
        /**
         * 添加支持自定义token
         *
         * @param token token
         * @return 是否支持
         */
        @Override
        public boolean supports(AuthenticationToken token) {
            if (token instanceof JwtToken) {
                return true;
            }
            return super.supports(token);
        }
    
        /**
         * 清除权限缓存
         *
         * @param principals
         */
        @Override
        protected void clearCachedAuthenticationInfo(PrincipalCollection principals) {
            super.clearCachedAuthenticationInfo(new SimplePrincipalCollection(principals, getName()));
        }
    
        /**
         * 授权
         *
         * @param authenticationToken 请求的token
         * @return
         * @throws AuthenticationException
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            if (authenticationToken instanceof JwtToken) {
                JwtToken jwtToken = (JwtToken) authenticationToken;
                // 用户TOKEN 授权
                String tokenType = jwtToken.getTokenType();
                try {
                    if (JwtToken.USER_TYPE.equals(tokenType) || JwtToken.API_TYPE.equals(tokenType)) {
                        tokenService.validateToken(jwtToken);
                    } else {
                        log.error("不合法的token");
                        throw new AuthenticationException("不合法的token");
                    }
                } catch (Exception e) {
                    log.error("tokenType:{} 校验异常:{}", tokenType, e.getMessage());
                    throw new AuthenticationException("token校验失败", e);
                }
                return new SimpleAuthenticationInfo(jwtToken, authenticationToken, getName());
            }
            throw new AuthenticationException("token不合法.");
        }
    
        /**
         * 设置权限信息
         *
         * @param principals
         * @return 设置权限
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            Object primaryPrincipal = principals.getPrimaryPrincipal();
            List<String> permissionList = new ArrayList<>();
            if (primaryPrincipal instanceof JwtToken) {
                JwtToken jwtToken = (JwtToken) primaryPrincipal;
                // TOKEN 授权
                String token = jwtToken.getToken();
                String tokenType = jwtToken.getTokenType();
                if (tokenType.equals(JwtToken.USER_TYPE)) {
                    // 根据token解析用户信息查询用户所拥有的的权限列表,这里只是测试数据
                    permissionList.add("demo:user:query");
                } else {
                    // 获取接口的权限列表,这里只是测试数据
                    permissionList.add("demo:api:test:add");
                    permissionList.add("demo:api:test:delete");
                }
            }
            SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo();
            authInfo.addStringPermissions(permissionList);
            return authInfo;
        }
    
    }

    说明

    1. MyReam类中用到了@Lazy注解,该注解的作用是:增加@Lazy注解 是TokenService为低优先级注入的bean,为防止项目启动时报Bean 'xxx' of type [com.xx.xxx.xxxx.xxxxx$$EnhancerBySpringCGLIB$$babebd0] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
    2. 关于权限的设置,如果权限存在包含关系,那么配置了大范围的权限后,即使没有配置范围小的权限,也是可以访问的。比如:父菜单-用户权限管理,对应权限编码为:demo:user:auth;功能菜单用户管理查询对应权限编码为:demo:user:auth:query;那么配置了demo:user:auth后,即使没有配置demo:user:auth:query,去访问相应的带权限查询接口的时候依然可以访问到。避免上述问题的解决方案具体操作如下:
      1. 父菜单-用户权限管理,对应权限编码为:demo:user:auth:manager;子菜单-用户管理查询,对应权限编码为:demo:user:auth:query
      2. 查询菜单的时候排除掉 父菜单-用户权限管理这种非功能性菜单权限

    3. 测试

    3.1. 登录测试

    POST http://localhost:6666/login
    Content-Type: application/json;utf-8
    
    Body
    {"account": "admin", "password": "admin"}
    
    返回数据
    {"ret":0,"code":0,"msg":"success","data":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoidXNlciIsIm5iZiI6MTU5NTIyNzg5MywiaXNzIjoiYWRtaW4iLCJpYXQiOjE1OTUyMjc4OTMsImFjY291bnQiOiJhZG1pbiIsImp0aSI6ImExYjEyYjkyLWM1YjQtNGRmZC05ZjI5LWNjNTRiOGNkZjU4YyJ9.H4SGEHKo6f9SwrRYEYacKKJfR9GxKhYFO3zGmCv_f5k"}

    3.2. 用户查询(权限编码:demo:user:query)

    GET http://localhost:6666/user/query
    Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoidXNlciIsIm5iZiI6MTU5NTIyNzg5MywiaXNzIjoiYWRtaW4iLCJpYXQiOjE1OTUyMjc4OTMsImFjY291bnQiOiJhZG1pbiIsImp0aSI6ImExYjEyYjkyLWM1YjQtNGRmZC05ZjI5LWNjNTRiOGNkZjU4YyJ9.H4SGEHKo6f9SwrRYEYacKKJfR9GxKhYFO3zGmCv_f5k
    
    返回数据
    {"ret":0,"code":0,"msg":"success","data":"hell query"}

    demo地址:https://gitee.com/xsshu/shiro-demo.git

    如果有什么表述不对的地方,还请各位大佬纠正。

  • 相关阅读:
    使用littleTools简化docker/kubectl的命令
    (上)python3 selenium3 从框架实现学习selenium让你事半功倍
    一篇文教你使用python Turtle库画出“精美碎花小清新风格树”快来拿代码!
    VxLAN协议详解
    深入理解大数据之——事务及其ACID特性
    深入理解大数据架构之——Lambda架构
    JQCloud: 一个前端生成美化标签云的简单JQuery插件
    详解Java中的final关键字
    OpenDaylight虚拟租户网络(VTN)详解及开发环境搭建
    使用Pelican在Github(国外线路访问)和Coding(国内线路访问)同步托管博客
  • 原文地址:https://www.cnblogs.com/xsshu/p/13336760.html
Copyright © 2020-2023  润新知