乐趣区

关于springboot:springboot-shiro-jwt-redis-实现登录授权

我的项目 github 地址:https://github.com/liboshuai0…
我的项目 gitee 地址:https://gitee.com/liboshuai01…

背景

公司用的我的项目是基于 shiro + cookie/session 的,然而当初微服务架构的背景下都是采纳 token 机制进行认证和受权的。于是决定先本人搭建一个 spring+shiro+jwt 的我的项目,用来不便替换公司的技术栈。

Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,能够记录会话信息。而 Token 是令牌,拜访资源接口(API)时所须要的资源凭证。Token 使服务端无状态化,不会存储会话信息。

Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个申请都有签名还能避免监听以及重放攻打,而 Session 就必须依赖链路层来保障通信平安了。如果你须要实现有状态的会话,依然能够减少 Session 来在服务器端保留一些状态。

所谓 Session 认证只是简略的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是平安的。而 Token,如果指的是 OAuth Token 或相似的机制的话,提供的是 认证 和 受权,认证是针对用户,受权是针对 App。其目标是让某 App 有权力拜访某用户的信息。这里的 Token 是惟一的。不能够转移到其它 App 上,也不能够转到其它用户上。Session 只提供一种简略的认证,即只有有此 SessionID,即认为有此 User 的全副权力。是须要严格窃密的,这个数据应该只保留在站方,不应该共享给其它网站或者第三方 App。所以简略来说:如果你的用户数据可能须要和第三方共享,或者容许第三方调用 API 接口,用 Token。如果永远只是本人的网站,本人的 App,用什么就无所谓了。

疾速开始

  1. 搭建一个 springboot 我的项目 demo
  2. 我的项目 pom.xml 配置文件
    父工程 pom.xml 文件

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns="http://maven.apache.org/POM/4.0.0"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.1.3.RELEASE</version>
            <relativePath/>
            <!-- lookup parent from repository -->
        </parent>
    
        <groupId>com.liboshuai</groupId>
        <artifactId>mall-tiny</artifactId>
        <version>1.0-SNAPSHOT</version>
        <modules>
            <module>mall-tiny-01</module>
            <module>mall-tiny-00-api</module>
        </modules>
        <packaging>pom</packaging>
    
        <properties>
            <maven.compiler.source>8</maven.compiler.source>
            <maven.compiler.target>8</maven.compiler.target>
            <mybatis-plus-boot-starter-version>3.4.0</mybatis-plus-boot-starter-version>
            <druid-spring-boot-starte-version>1.2.11</druid-spring-boot-starte-version>
            <mysql-connector-java-version>8.0.15</mysql-connector-java-version>
            <lombok-version>1.18.10</lombok-version>
            <log4j-version>1.2.17</log4j-version>
            <springfox-swagger2-version>2.7.0</springfox-swagger2-version>
            <springfox-swagger-ui-version>2.7.0</springfox-swagger-ui-version>
            <jackson-databind-version>2.13.3</jackson-databind-version>
            <xxl-job-core-version>2.4.0-SNAPSHOT</xxl-job-core-version>
            <hutool-all-version>4.5.7</hutool-all-version>
            <jjwt-version>0.9.0</jjwt-version>
            <mybatis-plus-generator-version>3.5.1</mybatis-plus-generator-version>
            <velocity-engine-core-version>2.3</velocity-engine-core-version>
            <commons-io-version>2.4</commons-io-version>
            <shiro-version>1.4.0</shiro-version>
            <jwt-version>3.2.0</jwt-version>
            <fastjson.version>1.2.58</fastjson.version>
            <knife4j-swagger-version>2.0.4</knife4j-swagger-version>
        </properties>
    
    
    </project>

    子工程 pom.xml 文件

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns="http://maven.apache.org/POM/4.0.0"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>mall-tiny</artifactId>
            <groupId>com.liboshuai</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>mall-tiny-01</artifactId>
    
        <dependencies>
            <!--SpringBoot 通用依赖模块 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <!--redis 依赖配置 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <!-- mybatis-plus -->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis-plus-boot-starter-version}</version>
            </dependency>
            <!-- mybatis plus 主动代码生成 -->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-generator</artifactId>
                <version>${mybatis-plus-generator-version}</version>
            </dependency>
            <!--velocity 模板 -->
            <dependency>
                <groupId>org.apache.velocity</groupId>
                <artifactId>velocity-engine-core</artifactId>
                <version>2.3</version>
            </dependency>
            <!--freemarker 模板 -->
            <dependency>
                <groupId>org.freemarker</groupId>
                <artifactId>freemarker</artifactId>
            </dependency>
            <!-- 集成 druid 连接池 -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>${druid-spring-boot-starte-version}</version>
            </dependency>
            <!--Mysql 数据库驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql-connector-java-version}</version>
            </dependency>
            <!-- lombok -->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok-version}</version>
            </dependency>
            <!-- log4j -->
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>${log4j-version}</version>
            </dependency>
            <!--Swagger-UI API 文档生产工具 -->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
                <version>${springfox-swagger2-version}</version>
            </dependency>
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
                <version>${springfox-swagger-ui-version}</version>
            </dependency>
            <!-- xxl-job-core -->
            <!--<dependency>
                <groupId>com.xuxueli</groupId>
                <artifactId>xxl-job-core</artifactId>
                <version>${xxl-job-core-version}</version>
            </dependency>-->
            <!--Hutool Java 工具包 -->
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>${hutool-all-version}</version>
            </dependency>
            <!-- shiro -->
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring</artifactId>
                <version>${shiro-version}</version>
            </dependency>
            <!--Jwt-->
            <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>${jwt-version}</version>
            </dependency>
            <!-- Json-Path -->
            <dependency>
                <groupId>com.jayway.jsonpath</groupId>
                <artifactId>json-path</artifactId>
            </dependency>
            <!-- ali json -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>${fastjson.version}</version>
            </dependency>
            <!-- https://mvnrepository.com/artifact/junit/junit -->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.13.2</version>
                <scope>test</scope>
            </dependency>
            <!-- 解决通用文本问题 -->
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-text</artifactId>
                <version>1.1</version>
            </dependency>
            <!-- 通用 io 工具包 -->
            <dependency>
                <groupId>commons-io</groupId>
                <artifactId>commons-io</artifactId>
                <version>${commons-io-version}</version>
            </dependency>
            <!-- swagger 丑化加强 -->
            <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>knife4j-spring-boot-starter</artifactId>
                <version>${knife4j-swagger-version}</version>
            </dependency>
            <!-- 测试数据生成工具 -->
            <dependency>
                <groupId>com.github.binarywang</groupId>
                <artifactId>java-testdata-generator</artifactId>
                <version>1.1.2</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>
  3. 我的项目配置文件application.properties

    server.port=8081
    server.servlet.context-path = /mall-tiny
    
    # mysql 数据库
    spring.datasource.url=jdbc:mysql://81.68.182.114:3307/mall?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
    spring.datasource.username=ENC(sKYSfpOJ1eQ/GAHhi266/99zGjyWvdaVXar4vpKtLZIjQmb7ZiGn/BuStoWIsPDd)
    spring.datasource.password=ENC(L97dG07OE0nuqkBm2cQxiOBHSwDd3yrnMPEOU1Ntwaoc8KMHlqe1xycNQZYD6DE7x7y4pmtS9X8NzePxq4toNg==)
    
    # redis 数据库
    spring.redis.host=81.68.216.209
    spring.redis.database=0
    spring.redis.port=6379
    spring.redis.password=ENC(2QRDHOpEQS4c7XGivDuFEsisfC/LbLbAfEFlC3CCH5s1MYr2CPYS+tEJJEsSnMdkm+GeFndZqPSsCx1o3zp5iQ==)
    spring.redis.timeout=300
    spring.redis.jedis.pool.max-active=8
    spring.redis.jedis.pool.max-wait=-1ms
    spring.redis.jedis.pool.max-idle=8
    spring.redis.jedis.pool.min-idle=0
    
    mybatis-plus.mapper-locations= classpath:/mapper/*.xml
    
    # 手机号验证码 key 前缀
    redis.key.prefix.authCode="portal:authCode:" 
    # 手机验证码超时工夫
    redis.key.expire.authCode=60
    
    # logback 配置文件门路
    logging.config=classpath:logback-spring.xml
    
    # JWT 认证加密私钥(Base64 加密)
    config.encrypt-jwtKey= gHMzjdlP84njamo29YgoAjpH
    # AccessToken 过期工夫(秒)
    config.accessToken-expireTime= 600
    # RefreshToken 过期工夫(秒) 604800 秒 = 7 天
    config.refreshToken-expireTime= 604800
    # Shiro 缓存过期工夫(秒)(个别设置与 AccessToken 过期工夫统一)
    config.shiro-cache-expireTime= 600
    
    # 配置 mybatis plus 逻辑删除
    # 全局逻辑删除的实体字段名
    mybatis-plus.global-config.db-config.logic-delete-field=isDelete
    # 逻辑已删除值(默认为 1)
    mybatis-plus.global-config.db-config.logic-delete-value=1
    # 逻辑未删除值(默认为 0)
    mybatis-plus.global-config.db-config.logic-not-delete-value=0
  4. 增加 JwtToken 类,继承AuthenticationToken

    /**
     * @Author: liboshuai
     * @Date: 2022-09-08 00:53
     * @Description: JwtToken 类
     */
    public class JwtToken implements AuthenticationToken {
        private static final long serialVersionUID = -8523592214400915953L;
    
        private final String token;
    
        public JwtToken(String token) {this.token = token;}
    
        @Override
        public Object getPrincipal() {return token;}
    
        @Override
        public Object getCredentials() {return token;}
    }
  5. 增加 JwtUtil 工具类,用来了生成、验证、解析 jwt

    /**
     * @Author: liboshuai
     * @Date: 2022-09-09 12:10
     * @Description: Jwt 工具类
     */
    @Slf4j
    @Component
    public class JwtUtil {
    
        private static String ENCRYPT_JWT_KEY_STATIC;
        private static String ACCESS_TOKEN_EXPIRE_TIME_STATIC;
        @Value("${config.encrypt-jwtKey}")
        private String ENCRYPT_JWT_KEY;
        @Value("${config.accessToken-expireTime}")
        private String ACCESS_TOKEN_EXPIRE_TIME;
    
        /**
         * 效验 token 是否正确
         */
        public static boolean verify(String token) {
            try {String secret = Base64Util.decode(ENCRYPT_JWT_KEY_STATIC);
                Algorithm algorithm = Algorithm.HMAC256(secret);
                JWTVerifier jwtVerifier = JWT.require(algorithm).build();
                jwtVerifier.verify(token);
                return true;
            } catch (UnsupportedEncodingException e) {log.error("token 认证失败异样:{}", e.getMessage());
                e.printStackTrace();}
            return false;
        }
    
        /**
         * 获取 Jwt payload 的内容
         */
        public static String getClaim(String token, String claim) {
            try {
                // 只能输入 String 类型,如果是其余类型则返回 null
                return JWT.decode(token).getClaim(claim).asString();} catch (JWTDecodeException e) {log.error("解密 token 中的公共信息异样:{}" + e.getMessage());
                e.printStackTrace();}
            return null;
        }
    
        /**
         * 生成 Jwt
         */
        public static String generateJwt(String username, String currentTimeMillis) {
            try {
                // 获取 jwt 过期工夫(单位为毫秒)Date expireDate = new Date(System.currentTimeMillis() + Long.parseLong(ACCESS_TOKEN_EXPIRE_TIME_STATIC) * 1000);
                // 获取签名
                String secret = Base64Util.decode(ENCRYPT_JWT_KEY_STATIC);
                Algorithm algorithm = Algorithm.HMAC256(secret);
                // 生成 Jwt
                return JWT.create()
                        // 寄存 username
                        .withClaim(ShiroConstant.USERNAME, username)
                        // 寄存以后工夫戳
                        .withClaim(ShiroConstant.CURRENT_TIME_MILLIS, currentTimeMillis)
                        .withExpiresAt(expireDate)
                        .sign(algorithm);
            } catch (UnsupportedEncodingException e) {log.error("token 生成失败异样:{}", e.getMessage());
                e.printStackTrace();}
            return null;
        }
    
        @PostConstruct
        private void init() {
            ENCRYPT_JWT_KEY_STATIC = ENCRYPT_JWT_KEY;
            ACCESS_TOKEN_EXPIRE_TIME_STATIC = ACCESS_TOKEN_EXPIRE_TIME;
        }
    }
  6. 编写咱们自定义的 JwtFilter,用于退出前面的 shiro 中

    
    /**
     * @Author: liboshuai
     * @Date: 2022-09-09 22:49
     * @Description: jwt 过滤器
     */
    @Slf4j
    public class JwtFilter extends BasicHttpAuthenticationFilter {
        private static String serverServletContextPath;
        private static String refreshTokenExpireTime;
        private final AntPathMatcher pathMatcher = new AntPathMatcher();
        @Autowired
        private RedisClient redis;
    
        public JwtFilter() {ResourceBundle resource = ResourceBundle.getBundle("application");
            serverServletContextPath = resource.getString("server.servlet.context-path");
            refreshTokenExpireTime = resource.getString("config.refreshToken-expireTime");
        }
    
        /**
         * 登录认证
         */
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            // 增加免登录接口
            if (secretFree(httpServletRequest)) {return true;}
            // 判断用户是否想要登入
            if (this.isLoginAttempt(request, response)) {
                try {
                    // 进行 Shiro 的登录 UserRealm
                    this.executeLogin(request, response);
                } catch (Exception e) {
                    // 认证出现异常,传递错误信息 msg
                    String msg = e.getMessage();
                    // 获取利用异样 (该 Cause 是导致抛出此 throwable(异样) 的 throwable(异样))
                    Throwable throwable = e.getCause();
                    if (throwable instanceof SignatureVerificationException) {// 该异样为 JWT 的 AccessToken 认证失败(Token 或者密钥不正确)
                        msg = "token 或者密钥不正确(" + throwable.getMessage() + ")";
                    } else if (throwable instanceof TokenExpiredException) {
                        // 该异样为 JWT 的 AccessToken 已过期,判断 RefreshToken 未过期就进行 AccessToken 刷新
                        if (this.refreshToken(request, response)) {return true;} else {msg = "token 已过期(" + throwable.getMessage() + ")";
                        }
                    } else {
                        // 利用异样不为空
                        if (throwable != null) {
                            // 获取利用异样 msg
                            msg = throwable.getMessage();}
                    }
                    /**
                     * 谬误两种解决形式 1. 将非法申请转发到 /401 的 Controller 解决,抛出自定义无权拜访异样被全局捕获再返回 Response 信息 2.
                     * 无需转发,间接返回 Response 信息 个别应用第二种(更不便)
                     */
                    // 间接返回 Response 信息
                    this.response401(request, response, msg);
                    return false;
                }
            }
            return true;
        }
    
        /**
         * 增加免密登录门路
         */
        private boolean secretFree(HttpServletRequest httpServletRequest) {String[] anonUrl = {"/register", "/login", "/swagger-ui.html", "/doc.html",
                    "/webjars/**", "/swagger-resources", "/v2/api-docs", "/swagger-resources/**"};
            boolean match = false;
            String requestURI = httpServletRequest.getRequestURI();
            for (String u : anonUrl) {if (pathMatcher.match(serverServletContextPath + u, requestURI)) {match = true;}
            }
            return match;
        }
    
        /**
         * 这里咱们具体阐明下为什么重写 能够比照父类办法,只是将 executeLogin 办法调用去除了
         * 如果没有去除将会循环调用 doGetAuthenticationInfo 办法
         */
        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {this.sendChallenge(request, response);
            return false;
        }
    
        /**
         * 检测 Header 外面是否蕴含 Authorization 字段,有就进行 Token 登录认证受权
         */
        @Override
        protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {//        HttpServletRequest req = (HttpServletRequest) request;
    //        String authorization = req.getHeader("Authorization");
    //        return authorization != null;
            return true;
        }
    
        /**
         * 进行 AccessToken 登录认证受权
         */
        @Override
        protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {HttpServletRequest req = (HttpServletRequest) request;
            String authorization = req.getHeader(ShiroConstant.AUTHORIZATION);
            JwtToken token = new JwtToken(authorization);
            // 提交给 UserRealm 进行认证,如果谬误他会抛出异样并被捕捉
            this.getSubject(request, response).login(token);
            // 如果没有抛出异样则代表登入胜利,返回 true
            return true;
        }
    
        /**
         * 刷新 AccessToken,进行判断 RefreshToken 是否过期,未过期就返回新的 AccessToken 且持续失常拜访
         */
        private boolean refreshToken(ServletRequest request, ServletResponse response) {// 拿到以后 Header 中 Authorization 的 AccessToken(Shiro 中 getAuthzHeader 办法曾经实现)
            // String token = this.getAuthzHeader(request);
            HttpServletRequest req = (HttpServletRequest) request;
            String token = req.getHeader(ShiroConstant.AUTHORIZATION);
            // 获取以后 Token 的帐号信息
            String account = JwtUtil.getClaim(token, ShiroConstant.USERNAME);
            // 判断 Redis 中 RefreshToken 是否存在
            if (redis.hasKey(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account)) {
                // Redis 中 RefreshToken 还存在,获取 RefreshToken 的工夫戳
                String currentTimeMillisRedis = redis.get(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account).toString();
                // 获取以后 AccessToken 中的工夫戳,与 RefreshToken 的工夫戳比照,如果以后工夫戳统一,进行 AccessToken 刷新
                if (Objects.equals(JwtUtil.getClaim(token, ShiroConstant.CURRENT_TIME_MILLIS), currentTimeMillisRedis)) {
                    // 获取以后最新工夫戳
                    String currentTimeMillis = String.valueOf(System.currentTimeMillis());
                    // 设置 RefreshToken 中的工夫戳为以后最新工夫戳,且刷新过期工夫从新为 30 分钟过期(配置文件可配置 refreshTokenExpireTime 属性)
                    redis.set(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account, currentTimeMillis,
                            Integer.parseInt(refreshTokenExpireTime));
                    // 刷新 AccessToken,设置工夫戳为以后最新工夫戳
                    token = JwtUtil.generateJwt(account, currentTimeMillis);
                    // 将新刷新的 AccessToken 再次进行 Shiro 的登录
                    JwtToken jwtToken = new JwtToken(token);
                    // 提交给 UserRealm 进行认证,如果谬误他会抛出异样并被捕捉,如果没有抛出异样则代表登入胜利,返回 true
                    this.getSubject(request, response).login(jwtToken);
                    // 最初将刷新的 AccessToken 寄存在 Response 的 Header 中的 Authorization 字段返回
                    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
                    httpServletResponse.setHeader(ShiroConstant.AUTHORIZATION, token);
                    httpServletResponse.setHeader(ShiroConstant.ACCESS_CONTROL_EXPOSE_HEADERS, ShiroConstant.AUTHORIZATION);
                    return true;
                }
            }
            return false;
        }
    
        /**
         * 无需转发,间接返回 Response 信息
         */
        private void response401(ServletRequest req, ServletResponse resp, String msg) {HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
            httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
            httpServletResponse.setCharacterEncoding(CharsetUtil.UTF_8);
            httpServletResponse.setContentType(ShiroConstant.CONTENT_TYPE);
            PrintWriter out = null;
            try {out = httpServletResponse.getWriter();
                String data = JSONObject.toJSONString(ResponseResult.fail(ResponseCode.NOT_LOGIN_IN, msg));
                out.append(data);
            } catch (IOException e) {throw new CustomException("间接返回 Response 信息呈现 IOException 异样:" + e.getMessage());
            } finally {if (out != null) {out.close();
                }
            }
        }
    
        /**
         * 对跨域提供反对
         */
        @Override
        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
            httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
            httpServletResponse.setHeader("Access-Control-Allow-Headers",
                    httpServletRequest.getHeader("Access-Control-Request-Headers"));
            // 跨域时会首先发送一个 OPTIONS 申请,这里咱们给 OPTIONS 申请间接返回失常状态
            if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {httpServletResponse.setStatus(HttpStatus.OK.value());
                return false;
            }
            return super.preHandle(request, response);
        }
    }
  7. 编写自定义的 ShiroRealm-UserRealm

    /**
     * @Author: liboshuai
     * @Date: 2022-09-08 01:17
     * @Description: 自定义 shiroRealm
     */
    @Slf4j
    @Component
    public class UserRealm extends AuthorizingRealm {
    
        @Autowired
        private RedisClient redis;
    
        @Autowired
        private UmsAdminService umsAdminService;
    
        @Autowired
        private UmsRoleService umsRoleService;
    
        @Autowired
        private UmsPermissionService umsPermissionService;
    
    
        /**
         * 大坑!,必须重写此办法,不然 Shiro 会报错
         */
        @Override
        public boolean supports(AuthenticationToken token) {return token instanceof JwtToken;}
    
    
        /**
         * 受权认证
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            // 从 token 中获取 username
            String username = JwtUtil.getClaim(principalCollection.toString(), ShiroConstant.USERNAME);
            // 依据用户名称获取角色名称汇合
            List<UmsRoleDTO> umsRoleDTOList = umsRoleService.findRolesByUsername(username);
            Set<String> roleNameSet = umsRoleDTOList.stream().map(UmsRoleDTO::getName).collect(Collectors.toSet());
            // 依据角色 id 汇合获取权限值汇合
            List<Long> userIdList = umsRoleDTOList.stream().map(UmsRoleDTO::getId).collect(Collectors.toList());
            List<UmsPermissionDTO> permissionList = umsPermissionService.findPermissionsByRoleIds(userIdList);
            Set<String> permissionValueSet = permissionList.stream().map(UmsPermissionDTO::getValue).collect(Collectors.toSet());
            // 将角色名称汇合和权限值汇合放入到 shiro 认证信息中
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            simpleAuthorizationInfo.setRoles(roleNameSet);
            simpleAuthorizationInfo.setStringPermissions(permissionValueSet);
            return simpleAuthorizationInfo;
        }
    
        /**
         * 登录认证
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            // 获取 token 信息
            String token = (String) authenticationToken.getCredentials();
            if (StringUtils.isBlank(token)) {throw new AuthenticationException(ShiroConstant.TOKEN_CANNOT_BE_EMPTY);
            }
            // 应用 jwtUtil 解密获取 Username
            String username = JwtUtil.getClaim(token, ShiroConstant.USERNAME);
            if (StringUtils.isBlank(username)) {throw new AuthenticationException(ShiroConstant.TOKEN_INVALID);
            }
            Long userId = umsAdminService.findUserIdByUserName(username);
            if (Objects.isNull(userId)) {throw new AuthenticationException(ShiroConstant.USER_DIDNT_EXISTED);
            }
            // 开始认证,要 AccessToken 认证通过,且 Redis 中存在 RefreshToken,且两个 Token 工夫戳统一
            if (JwtUtil.verify(token) && redis.hasKey(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + username)) {
                // 获取 RefreshToken 的工夫戳
                String currentTimeMillisRedis = redis.get(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + username).toString();
                // 获取 AccessToken 工夫戳,与 RefreshToken 的工夫戳比照
                if (Objects.equals(JwtUtil.getClaim(token, ShiroConstant.CURRENT_TIME_MILLIS), currentTimeMillisRedis)) {return new SimpleAuthenticationInfo(token, token, ShiroConstant.REALM_NAME);
                }
            }
            throw new AuthenticationException(ShiroConstant.TOKEN_EXPIRED_OR_INCORRECT);
        }
    }
  8. 编写 Redis 相干代码,用于替换 shiro 自带的缓存
    CustomCache

    /**
     * @Author: liboshuai
     * @Date: 2022-09-12 19:20
     * @Description: 重写 Shiro 的 Cache 保留读取
     */
    @Component
    public class CustomCache<K, V> implements Cache<K, V> {@Value("${config.accessToken-expireTime}")
        private String ACCESS_TOKEN_EXPIRE_TIME;
    
        private final RedisTemplate<String, Object> redisTemplate;
    
        // todo: 如果 jwt 的缓存除了问题,可能须要去除这里的 @Autowired
        @Autowired
        public CustomCache(RedisTemplate redisTemplate) {
            // 应用 StringRedisSerializer 做序列化
            // redisTemplate.setValueSerializer(new StringRedisSerializer());
            this.redisTemplate = redisTemplate;
        }
    
        /**
         * 缓存的 key 名称获取为 shiro:cache:account
         *
         * @param key
         * @return java.lang.String
         * @author Wang926454
         * @date 2018/9/4 18:33
         */
        private String getKey(Object key) {return RedisConstant.PREFIX_SHIRO_CACHE + JwtUtil.getClaim(key.toString(), ShiroConstant.USERNAME);
        }
    
        /**
         * 获取缓存
         */
        @Override
        public Object get(Object key) throws CacheException {return redisTemplate.opsForValue().get(this.getKey(key));
        }
    
        /**
         * 保留缓存
         */
        @Override
        public Object put(Object key, Object value) throws CacheException {
            // 读取配置文件,获取 Redis 的 Shiro 缓存过期工夫
            // PropertiesUtil.readProperties("config.properties");
            // String shiroCacheExpireTime =
            // PropertiesUtil.getProperty("shiroCacheExpireTime");
            // 设置 Redis 的 Shiro 缓存
            try {redisTemplate.opsForValue().set(this.getKey(key), value, Integer.parseInt(ACCESS_TOKEN_EXPIRE_TIME), TimeUnit.SECONDS);
                return true;
            } catch (Exception e) {e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 移除缓存
         */
        @Override
        public Object remove(Object key) throws CacheException {redisTemplate.delete(this.getKey(key));
            return null;
        }
    
        /**
         * 清空所有缓存
         */
        @Override
        public void clear() throws CacheException {// TODO Auto-generated method stub}
    
        /**
         * 缓存的个数
         */
        @Override
        public Set<K> keys() {
            // TODO Auto-generated method stub
            return null;
        }
    
        /**
         * 获取所有的 key
         */
        @Override
        public int size() {
            // TODO Auto-generated method stub
            return 0;
        }
    
        /**
         * 获取所有的 value
         */
        @Override
        public Collection<V> values() {
            // TODO Auto-generated method stub
            return null;
        }
    }

    CustomCacheManager

    /**
     * @Author: liboshuai
     * @Date: 2022-09-12 19:27
     * @Description: 重写 Shiro 缓存管理器
     */
    public class CustomCacheManager implements CacheManager {
    
        private final RedisTemplate<String, Object> redisTemplate;
    
        public CustomCacheManager(RedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}
    
        @Override
        public <K, V> Cache<K, V> getCache(String s) throws CacheException {return new CustomCache<K, V>(redisTemplate);
        }
    }

    RedisClient

    /**
     * @Author: liboshuai
     * @Date: 2022-09-12 19:38
     * @Description:
     */
    @Component
    public class RedisClient {
    
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
    
        public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {this.redisTemplate = redisTemplate;}
    
        // =============================common============================
    
        /**
         * 指定缓存生效工夫
         *
         * @param key  键
         * @param time 工夫(秒)
         * @return
         */
        public boolean expire(String key, long time) {
            try {if (time > 0) {redisTemplate.expire(key, time, TimeUnit.SECONDS);
                }
                return true;
            } catch (Exception e) {e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 依据 key 获取过期工夫
         *
         * @param key 键 不能为 null
         * @return 工夫(秒) 返回 0 代表为永恒无效
         */
        public long getExpire(String key) {return redisTemplate.getExpire(key, TimeUnit.SECONDS);
        }
    
        /**
         * 判断 key 是否存在
         *
         * @param key 键
         * @return true 存在 false 不存在
         */
        public boolean hasKey(String key) {
            try {return redisTemplate.hasKey(key);
            } catch (Exception e) {e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 删除缓存
         *
         * @param key 能够传一个值 或多个
         */
        @SuppressWarnings("unchecked")
        public void del(String... key) {if (key != null && key.length > 0) {if (key.length == 1) {redisTemplate.delete(key[0]);
                } else {redisTemplate.delete(CollectionUtils.arrayToList(key));
                }
            }
        }
    
        // ============================String=============================
    
        /**
         * 一般缓存获取
         *
         * @param key 键
         * @return 值
         */
        public Object get(String key) {return key == null ? null : redisTemplate.opsForValue().get(key);
        }
    
        /**
         * 一般缓存放入
         *
         * @param key   键
         * @param value 值
         * @return true 胜利 false 失败
         */
        public boolean set(String key, Object value) {
            try {redisTemplate.opsForValue().set(key, value);
                return true;
            } catch (Exception e) {e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 一般缓存放入并设置工夫
         *
         * @param key   键
         * @param value 值
         * @param time  工夫(秒) time 要大于 0 如果 time 小于等于 0 将设置无限期
         * @return true 胜利 false 失败
         */
        public boolean set(String key, Object value, long time) {
            try {if (time > 0) {redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
                } else {set(key, value);
                }
                return true;
            } catch (Exception e) {e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 递增
         *
         * @param key 键
         * @return
         */
        public long incr(String key, long delta) {if (delta < 0) {throw new RuntimeException("递增因子必须大于 0");
            }
            return redisTemplate.opsForValue().increment(key, delta);
        }
    
        /**
         * 递加
         *
         * @param key 键
         * @return
         */
        public long decr(String key, long delta) {if (delta < 0) {throw new RuntimeException("递加因子必须大于 0");
            }
            return redisTemplate.opsForValue().increment(key, -delta);
        }
    
        // ================================Map=================================
    
        /**
         * HashGet
         *
         * @param key  键 不能为 null
         * @param item 项 不能为 null
         * @return 值
         */
        public Object hget(String key, String item) {return redisTemplate.opsForHash().get(key, item);
        }
    
        /**
         * 获取 hashKey 对应的所有键值
         *
         * @param key 键
         * @return 对应的多个键值
         */
        public Map<Object, Object> hmget(String key) {return redisTemplate.opsForHash().entries(key);
        }
    
        /**
         * HashSet
         *
         * @param key 键
         * @param map 对应多个键值
         * @return true 胜利 false 失败
         */
        public boolean hmset(String key, Map<String, Object> map) {
            try {redisTemplate.opsForHash().putAll(key, map);
                return true;
            } catch (Exception e) {e.printStackTrace();
                return false;
            }
        }
    
        /**
         * HashSet 并设置工夫
         *
         * @param key  键
         * @param map  对应多个键值
         * @param time 工夫(秒)
         * @return true 胜利 false 失败
         */
        public boolean hmset(String key, Map<String, Object> map, long time) {
            try {redisTemplate.opsForHash().putAll(key, map);
                if (time > 0) {expire(key, time);
                }
                return true;
            } catch (Exception e) {e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 向一张 hash 表中放入数据, 如果不存在将创立
         *
         * @param key   键
         * @param item  项
         * @param value 值
         * @return true 胜利 false 失败
         */
        public boolean hset(String key, String item, Object value) {
            try {redisTemplate.opsForHash().put(key, item, value);
                return true;
            } catch (Exception e) {e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 向一张 hash 表中放入数据, 如果不存在将创立
         *
         * @param key   键
         * @param item  项
         * @param value 值
         * @param time  工夫(秒) 留神: 如果已存在的 hash 表有工夫, 这里将会替换原有的工夫
         * @return true 胜利 false 失败
         */
        public boolean hset(String key, String item, Object value, long time) {
            try {redisTemplate.opsForHash().put(key, item, value);
                if (time > 0) {expire(key, time);
                }
                return true;
            } catch (Exception e) {e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 删除 hash 表中的值
         *
         * @param key  键 不能为 null
         * @param item 项 能够使多个 不能为 null
         */
        public void hdel(String key, Object... item) {redisTemplate.opsForHash().delete(key, item);
        }
    
        /**
         * 判断 hash 表中是否有该项的值
         *
         * @param key  键 不能为 null
         * @param item 项 不能为 null
         * @return true 存在 false 不存在
         */
        public boolean hHasKey(String key, String item) {return redisTemplate.opsForHash().hasKey(key, item);
        }
    
        /**
         * hash 递增 如果不存在, 就会创立一个 并把新增后的值返回
         *
         * @param key  键
         * @param item 项
         * @param by   要减少几(大于 0)
         * @return
         */
        public double hincr(String key, String item, double by) {return redisTemplate.opsForHash().increment(key, item, by);
        }
    
        /**
         * hash 递加
         *
         * @param key  键
         * @param item 项
         * @param by   要缩小记(小于 0)
         * @return
         */
        public double hdecr(String key, String item, double by) {return redisTemplate.opsForHash().increment(key, item, -by);
        }
    
        // ============================set=============================
    
        /**
         * 依据 key 获取 Set 中的所有值
         *
         * @param key 键
         * @return
         */
        public Set<Object> sGet(String key) {
            try {return redisTemplate.opsForSet().members(key);
            } catch (Exception e) {e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 依据 value 从一个 set 中查问, 是否存在
         *
         * @param key   键
         * @param value 值
         * @return true 存在 false 不存在
         */
        public boolean sHasKey(String key, Object value) {
            try {return redisTemplate.opsForSet().isMember(key, value);
            } catch (Exception e) {e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 将数据放入 set 缓存
         *
         * @param key    键
         * @param values 值 能够是多个
         * @return 胜利个数
         */
        public long sSet(String key, Object... values) {
            try {return redisTemplate.opsForSet().add(key, values);
            } catch (Exception e) {e.printStackTrace();
                return 0;
            }
        }
    
        /**
         * 将 set 数据放入缓存
         *
         * @param key    键
         * @param time   工夫(秒)
         * @param values 值 能够是多个
         * @return 胜利个数
         */
        public long sSetAndTime(String key, long time, Object... values) {
            try {Long count = redisTemplate.opsForSet().add(key, values);
                if (time > 0) {expire(key, time);
                }
                return count;
            } catch (Exception e) {e.printStackTrace();
                return 0;
            }
        }
    
        /**
         * 获取 set 缓存的长度
         *
         * @param key 键
         * @return
         */
        public long sGetSetSize(String key) {
            try {return redisTemplate.opsForSet().size(key);
            } catch (Exception e) {e.printStackTrace();
                return 0;
            }
        }
    
        /**
         * 移除值为 value 的
         *
         * @param key    键
         * @param values 值 能够是多个
         * @return 移除的个数
         */
        public long setRemove(String key, Object... values) {
            try {Long count = redisTemplate.opsForSet().remove(key, values);
                return count;
            } catch (Exception e) {e.printStackTrace();
                return 0;
            }
        }
        // ===============================list=================================
    
        /**
         * 获取 list 缓存的内容
         *
         * @param key   键
         * @param start 开始
         * @param end   完结 0 到 - 1 代表所有值
         * @return
         */
        public List<Object> lGet(String key, long start, long end) {
            try {return redisTemplate.opsForList().range(key, start, end);
            } catch (Exception e) {e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 获取 list 缓存的长度
         *
         * @param key 键
         * @return
         */
        public long lGetListSize(String key) {
            try {return redisTemplate.opsForList().size(key);
            } catch (Exception e) {e.printStackTrace();
                return 0;
            }
        }
    
        /**
         * 通过索引 获取 list 中的值
         *
         * @param key   键
         * @param index 索引 index>= 0 时,0 表头,1 第二个元素,顺次类推;index<0 时,-1,表尾,- 2 倒数第二个元素,顺次类推
         * @return
         */
        public Object lGetIndex(String key, long index) {
            try {return redisTemplate.opsForList().index(key, index);
            } catch (Exception e) {e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 将 list 放入缓存
         *
         * @param key   键
         * @param value 值
         * @return
         */
        public boolean lSet(String key, Object value) {
            try {redisTemplate.opsForList().rightPush(key, value);
                return true;
            } catch (Exception e) {e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 将 list 放入缓存
         *
         * @param key   键
         * @param value 值
         * @param time  工夫(秒)
         * @return
         */
        public boolean lSet(String key, Object value, long time) {
            try {redisTemplate.opsForList().rightPush(key, value);
                if (time > 0) {expire(key, time);
                }
                return true;
            } catch (Exception e) {e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 将 list 放入缓存
         *
         * @param key   键
         * @param value 值
         * @return
         */
        public boolean lSet(String key, List<Object> value) {
            try {redisTemplate.opsForList().rightPushAll(key, value);
                return true;
            } catch (Exception e) {e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 将 list 放入缓存
         *
         * @param key   键
         * @param value 值
         * @param time  工夫(秒)
         * @return
         */
        public boolean lSet(String key, List<Object> value, long time) {
            try {redisTemplate.opsForList().rightPushAll(key, value);
                if (time > 0) {expire(key, time);
                }
                return true;
            } catch (Exception e) {e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 依据索引批改 list 中的某条数据
         *
         * @param key   键
         * @param index 索引
         * @param value 值
         * @return
         */
        public boolean lUpdateIndex(String key, long index, Object value) {
            try {redisTemplate.opsForList().set(key, index, value);
                return true;
            } catch (Exception e) {e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 移除 N 个值为 value
         *
         * @param key   键
         * @param count 移除多少个
         * @param value 值
         * @return 移除的个数
         */
        public long lRemove(String key, long count, Object value) {
            try {Long remove = redisTemplate.opsForList().remove(key, count, value);
                return remove;
            } catch (Exception e) {e.printStackTrace();
                return 0;
            }
        }
    }

    RedisConfig

    /**
     * @Author: liboshuai
     * @Date: 2022-09-12 19:43
     * @Description: Redis 缓存配置
     */
    @Configuration
    @EnableCaching
    public class RedisConfig extends CachingConfigurerSupport {
    
        @Bean
        @Override
        public KeyGenerator keyGenerator() {return new KeyGenerator() {
                @Override
                public Object generate(Object target, Method method, Object... params) {StringBuilder sb = new StringBuilder();
                    sb.append(target.getClass().getName());
                    sb.append(method.getName());
                    if (params != null && params.length > 0 && params[0] != null) {for (Object obj : params) {sb.append(obj.toString());
                        }
                    }
                    return sb.toString();}
            };
        }
    
        /**
         * RedisTemplate
         */
        @Bean
        @SuppressWarnings("all")
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
            template.setConnectionFactory(factory);
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    
            jackson2JsonRedisSerializer.setObjectMapper(om);
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
    
            // key 采纳 String 的序列化形式
            template.setKeySerializer(stringRedisSerializer);
            // hash 的 key 也采纳 String 的序列化形式
            template.setHashKeySerializer(stringRedisSerializer);
            // value 序列化形式采纳 jackson
            template.setValueSerializer(jackson2JsonRedisSerializer);
            // hash 的 value 序列化形式采纳 jackson
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
            template.afterPropertiesSet();
    
            return template;
    
        }
    }
  9. 编写自定义异样类CustomException

    /**
     * @Author: liboshuai
     * @Date: 2022-09-10 00:30
     * @Description: 自定义异样类
     */
    public class CustomException extends RuntimeException {
    
        private static final long serialVersionUID = 781776451227176519L;
    
        public CustomException(String msg) {super(msg);
        }
    
        public CustomException() {super();
        }
    }
  10. 编写全局异样减少类ExceptionAdvice

    /**
     * @Author: liboshuai
     * @Date: 2022-09-10 00:34
     * @Description: 异样捕获加强类
     */
    @Slf4j
    @RestControllerAdvice
    public class ExceptionAdvice {
    
        /**
         * 捕获所有 shiro 异样
         */
        @ResponseStatus(HttpStatus.UNAUTHORIZED)
        @ExceptionHandler(ShiroException.class)
        public ResponseResult<?> handle401(ShiroException e) {return ResponseResult.fail(ResponseCode.UNAUTHORIZED, e.getMessage());
        }
    
        /**
         * 独自捕获 Shiro(UnauthorizedException)异样
         * 该异样为拜访有权限管控的申请而该用户没有所需权限所抛出的异样
         */
        @ResponseStatus(HttpStatus.UNAUTHORIZED)
        @ExceptionHandler(UnauthorizedException.class)
        public ResponseResult<?> handle401(UnauthorizedException e) {
            return ResponseResult.fail(ResponseCode.UNAUTHORIZED,
                    "无权拜访(Unauthorized): 以后 Subject 没有此申请所需权限(" + e.getMessage() + ")");
        }
    
        /**
         * 独自捕获 Shiro(UnauthenticatedException)异样
         * 该异样为以游客身份拜访有权限管控的申请无奈对匿名主体进行受权,而受权失败所抛出的异样
         */
        @ResponseStatus(HttpStatus.UNAUTHORIZED)
        @ExceptionHandler(UnauthenticatedException.class)
        public ResponseResult<?> handle401(UnauthenticatedException e) {
            return ResponseResult.fail(ResponseCode.UNAUTHORIZED,
                    "无权拜访(Unauthorized): 以后 Subject 是匿名 Subject,请先登录(This subject is anonymous.)");
        }
    
        /**
         * 获取效验错误信息
         */
        private Map<String, Object> getValidError(List<FieldError> fieldErrors) {Map<String, Object> map = new HashMap<>(16);
            List<String> errorList = new ArrayList<>();
            StringBuffer errorMsg = new StringBuffer("效验异样(ValidException):");
            for (FieldError error :
                    fieldErrors) {errorList.add(error.getField() + "-" + error.getDefaultMessage());
                errorMsg.append(error.getField() + "-" + error.getDefaultMessage() + "-");
            }
            map.put("errorList", errorList);
            map.put("errorMsg", errorMsg);
            return map;
        }
    }
  11. 编写 shiro 配置类ShiroConfig

    /**
     * @Author: liboshuai
     * @Date: 2022-09-09 17:41
     * @Description: shiro 配置类
     */
    @Slf4j
    @Configuration
    public class ShiroConfig {
    
        /**
         * 配置应用自定义 Realm
         */
        @Bean("securityManager")
        public DefaultWebSecurityManager securityManager(UserRealm userRealm, RedisTemplate<String, Object> template) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 应用自定义 Realm
            securityManager.setRealm(userRealm);
            // 敞开 Shiro 自带的 session(因为咱们采纳的是 Jwt token 的机制)DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
            DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
            defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
            defaultSubjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
            securityManager.setSubjectDAO(defaultSubjectDAO);
            // 设置自定义 Cache 缓存
            securityManager.setCacheManager(new CustomCacheManager(template));
            return securityManager;
        }
    
        /**
         * 配置自定义过滤器
         */
        @Bean("shiroFilter")
        public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager defaultWebSecurityManager) {ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
            // 增加本人的过滤器名为 jwtFilter
            Map<String, Filter> filterMap = new HashMap<>(16);
            filterMap.put("jwtFilter", jwtFilterBean());
            factoryBean.setFilters(filterMap);
            factoryBean.setSecurityManager(defaultWebSecurityManager);
            // 设置无权限时跳转的 url;
            factoryBean.setUnauthorizedUrl("/unauthorized/ 无权限");
            // 自定义 url 规定
            HashMap<String, String> filterRuleMap = new HashMap<>(16);
            // 所有申请通过咱们本人的 JwtFilter
            filterRuleMap.put("/**", "jwtFilter");
            factoryBean.setFilterChainDefinitionMap(filterRuleMap);
            return factoryBean;
        }
    
        /**
         * <pre>
         * 注入 bean,此处应留神:*
         * (1)代码程序,应搁置于 shiroFilter 前面,否则报错:*
         * (2)如不在此注册,在 filter 中将无奈失常注入 bean
         * </pre>
         */
        @Bean("jwtFilter")
        public JwtFilter jwtFilterBean() {return new JwtFilter();
        }
    
        /**
         * 增加注解反对
         */
        @Bean
        @DependsOn("lifecycleBeanPostProcessor")
        public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            // 强制应用 cglib,避免反复代理和可能引起代理出错的问题,https://zhuanlan.zhihu.com/p/29161098
            defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
            return defaultAdvisorAutoProxyCreator;
        }
    
        @Bean
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();
        }
    
        /**
         * || 启动 shiro 的 apo||
         * 使得咱们前面加在办法下面的权限管制注解能够失效。* 例如:@RequiresPermissions("/sys/bank/delete"), @RequiresRoles("admin")
         */
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
            advisor.setSecurityManager(defaultWebSecurityManager);
            return advisor;
        }
    
    }
  12. 用户注册、登录、退出接口LoginAdminController

    /**
     * @Author: liboshuai
     * @Date: 2022-09-10 01:27
     * @Description: 用户登录 controller
     */
    @Api(tags = "用户登录入口", value = "LoginAdminController")
    @Slf4j
    @RestController
    public class LoginAdminController {@Value("${config.refreshToken-expireTime}")
        private String refreshTokenExpireTime;
    
        @Autowired
        private RedisClient redis;
    
        @Autowired
        private HttpServletRequest request;
    
        @Autowired
        private UmsAdminService umsAdminService;
    
    
        /**
         * 用户注册
         */
        @ApiOperation(value = "注册", httpMethod = "POST")
        @PostMapping("/register")
        public ResponseResult<?> register(@RequestBody UmsAdminVo umsAdminVo) {UmsAdminDTO umsAdminDTO = new UmsAdminDTO();
            BeanUtils.copyProperties(umsAdminVo, umsAdminDTO);
            String username = umsAdminDTO.getUsername();
            String password = umsAdminDTO.getPassword();
            if (Objects.nonNull(password)) {
                int saltCount = ShiroConstant.HASH_INTERATIONS;
                String salt = ByteSource.Util.bytes(username).toString();
                String enPassword = new SimpleHash(ShiroConstant.ALGORITH_NAME, password,
                        salt, saltCount).toString();
                umsAdminDTO.setPassword(enPassword);
                umsAdminDTO.setSalt(salt);
                umsAdminDTO.setSaltCount(saltCount);
            }
            umsAdminDTO.setStatus(UserStatusEnum.Enable.getCode());
            UmsAdmin umsAdmin = new UmsAdmin();
            BeanUtils.copyProperties(umsAdminDTO, umsAdmin);
            umsAdminService.save(umsAdmin);
            return ResponseResult.success("注册胜利");
        }
    
    
        /**
         * 用户登录
         */
        @ApiOperation(value = "登录", httpMethod = "POST")
        @PostMapping("/login")
        public ResponseResult<?> login(@RequestParam String username, @RequestParam String password, HttpServletResponse response) {if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {return ResponseResult.fail(ResponseCode.USERNAME_PASSWORD_NULL);
            }
            UmsAdminDTO umsAdminDTO = umsAdminService.findByUserName(username);
            if (Objects.isNull(umsAdminDTO)) {return ResponseResult.fail(ResponseCode.INCORRECT_CREDENTIALS);
            }
            if (Objects.isNull(umsAdminDTO.getSalt()) || Objects.isNull(umsAdminDTO.getSaltCount())) {return ResponseResult.fail(ResponseCode.SALT_IS_NOT_EXISTED);
            }
            String enPassword = new SimpleHash(ShiroConstant.ALGORITH_NAME, password,
                    umsAdminDTO.getSalt(), umsAdminDTO.getSaltCount()).toString();
            if (!Objects.equals(umsAdminDTO.getPassword(), enPassword)) {return ResponseResult.fail(ResponseCode.INCORRECT_CREDENTIALS);
            }
            // 革除可能存在的 shiro 权限信息缓存
            if (redis.hasKey(RedisConstant.PREFIX_SHIRO_CACHE + username)) {redis.del(RedisConstant.PREFIX_SHIRO_CACHE + username);
            }
            // 设置 RefreshToken,工夫戳为以后工夫戳,间接设置即可(不必先删后设,会笼罩已有的 RefreshToken)
            String currentTimeMillis = String.valueOf(System.currentTimeMillis());
            redis.set(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + username, currentTimeMillis,
                    Integer.parseInt(refreshTokenExpireTime));
            // 从 Header 中 Authorization 返回 AccessToken,工夫戳为以后工夫戳
            String token = JwtUtil.generateJwt(username, currentTimeMillis);
            response.setHeader(ShiroConstant.AUTHORIZATION, token);
            response.setHeader(ShiroConstant.ACCESS_CONTROL_EXPOSE_HEADERS, ShiroConstant.AUTHORIZATION);
            // 更新登录工夫
            umsAdminDTO.setLoginTime(LocalDateTime.now());
            LambdaUpdateWrapper<UmsAdmin> umsAdminLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
            umsAdminLambdaUpdateWrapper.eq(UmsAdmin::getId, umsAdminDTO.getId());
            umsAdminLambdaUpdateWrapper.set(UmsAdmin::getLoginTime, umsAdminDTO.getLoginTime());
            umsAdminService.update(umsAdminLambdaUpdateWrapper);
            return ResponseResult.success("登录胜利");
    
    
        }
    
        /**
         * 退出
         */
        @ApiOperation(value = "退出", httpMethod = "POST")
        @PostMapping("/logout")
        public ResponseResult<?> logout() {
            try {
                String token = "";
                // 获取头部信息
                Enumeration<String> headerNames = request.getHeaderNames();
                while (headerNames.hasMoreElements()) {String key = headerNames.nextElement();
                    if (ShiroConstant.AUTHORIZATION.equalsIgnoreCase(key)) {token = request.getHeader(key);
                    }
                }
                // 效验 token
                if (StringUtils.isBlank(token)) {return ResponseResult.fail(ResponseCode.FAILED);
                }
                String username = JwtUtil.getClaim(token, ShiroConstant.USERNAME);
                if (StringUtils.isBlank(username)) {return ResponseResult.fail(ResponseCode.TOKEN_EXPIRE_OR_ERROR, ResponseCode.FAILED.getMessage());
                }
                // 革除 shiro 权限信息缓存
                if (redis.hasKey(RedisConstant.PREFIX_SHIRO_CACHE + username)) {redis.del(RedisConstant.PREFIX_SHIRO_CACHE + username);
                }
                // 革除 RefreshToken
                redis.del(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + username);
                return ResponseResult.success();} catch (Exception e) {e.printStackTrace();
                return ResponseResult.fail(ResponseCode.FAILED, e.getMessage());
            }
        }
    }

    文章参考:https://blog.csdn.net/hd24360…

退出移动版