我的项目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=8081server.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=falsespring.datasource.username=ENC(sKYSfpOJ1eQ/GAHhi266/99zGjyWvdaVXar4vpKtLZIjQmb7ZiGn/BuStoWIsPDd)spring.datasource.password=ENC(L97dG07OE0nuqkBm2cQxiOBHSwDd3yrnMPEOU1Ntwaoc8KMHlqe1xycNQZYD6DE7x7y4pmtS9X8NzePxq4toNg==)# redis数据库spring.redis.host=81.68.216.209spring.redis.database=0spring.redis.port=6379spring.redis.password=ENC(2QRDHOpEQS4c7XGivDuFEsisfC/LbLbAfEFlC3CCH5s1MYr2CPYS+tEJJEsSnMdkm+GeFndZqPSsCx1o3zp5iQ==)spring.redis.timeout=300spring.redis.jedis.pool.max-active=8spring.redis.jedis.pool.max-wait=-1msspring.redis.jedis.pool.max-idle=8spring.redis.jedis.pool.min-idle=0mybatis-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@Componentpublic 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过滤器 */@Slf4jpublic 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@Componentpublic 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保留读取 */@Componentpublic 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: */@Componentpublic 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@EnableCachingpublic 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@RestControllerAdvicepublic 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@Configurationpublic 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@RestControllerpublic 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...