项目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...

李博帅
23 声望5 粉丝