項目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,用什麼就無所謂了。
快速開始
- 搭建一個springboot項目demo
-
項目
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> -
項目配置文件
application.propertiesserver.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 -
添加
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; } } -
添加
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; } } -
編寫我們自定義的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); } } -
編寫自定義的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); } } -
編寫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; } } -
編寫自定義異常類
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(); } } -
編寫全局異常增加類
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; } } -
編寫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; } } -
用户註冊、登錄、退出接口
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...