Loading... ## 为什么要用令牌技术? 这个问题其实问的就是Cookice、Session、Token(令牌)之间的区别了。 首先,存放的位置做一下比较,Cookice小饼干存放在客户端的浏览器当中,Session会话存放在服务器线程当中(本质上还是需要利用Cookice实现),而Token存放位置不固定,但是一般是服务端存放在Redis中,客户端一般存放在Storage中(这个主要看业务定义,并不是固定的)。 其次,安全性做一下比较,Cookice存放在客户端当中容易被窃取或者修改,首当其冲安全性是最低的,但是Session不是也是基于Cookice实现吗?Session只是利用Cookice技术记录了Session会话ID而已,并没有什么特殊的敏感信息,但是也是会有泄露的风险,但没有篡改的风险,而Token只是一种概念设计,可自定义加密的算法以及内容,三者之中安全性还是有一定的保障的。 那为什么现在大部分的认证鉴权啥的都用的令牌技术呢?首先第一点是安全性比其他两者都好,其次现在有些场景不支持Cookice,比如说小程序、移动应用、桌面应用等等,而Token可以放在头部信息当中发给服务端进行校验,应用场景比其他两者范围广泛。 总结下来就是以下的五点优势,使用 令牌技术 相较于传统的基于会话的认证机制,可以减少服务器存储开销和管理复杂性,实现跨域支持和水平扩展,并且更适应无状态和微服务架构。 1. 无需服务器存储状态: 传统的基于会话的认证机制需要服务器在会话中存储用户的状态信息,包括用户的登录状态、权限等。而使用 JWT,服务器无需存储任何会话状态信息,所有的认证和授权信息都可以包含在 Token 中,使得系统可以更容易地进行水平扩展。 2. 跨域支持: 由于 Token 包含了完整的认证和授权信息,因此可以轻松地在多个域之间进行传递和使用,实现跨域授权。 3. 适应微服务架构: 在微服务架构中,很多服务是独立部署并且可以横向扩展的,这就需要保证认证和授权的无状态性。使用 Token 可以满足这种需求,每次请求携带 Token 即可实现认证和授权。 4. 自包含: Token 可以包含认证和授权信息,以及其他自定义的声明,这些信息都被编码在 Token 中,在服务端解码后使用。Token 的自包含性减少了对服务端资源的依赖,并提供了统一的安全机制。 5. 扩展性: Token 可以被扩展和定制,可以按照需求添加自定义的声明和数据,灵活性更高。 ## 什么是JWT? JWT`(JSON Web Token)`是一种开放标准(RFC 7519),用于在网络上安全传输信息的简洁、自包含的方式,它通常被用于身份验证和授权机制。 JWT 由三部分组成:头部(Header) 、载荷(Payload) 、 签名(Signature)。 **头部(Header):** 包含了关于生成该 JWT 的信息以及所使用的算法类型。 **载荷(Payload):** 包含了要传递的数据,例如身份信息和其他附属数据。JWT 官方规定了 7 个字段,分别是:签发者`(Issuer)`、主题`(Subject)`、接收者`(Audience)`、过期时间`(Expiration time)`、生效时间`(Not Before)`、签发时间`(Issued At)`、编号`(JWT ID)`。 **签名(Signature):** 使用密钥对头部和载荷进行签名,以验证其完整性。 ## 生成非对称加密证书 ### Java keytool 工具生成密钥对 Java JDK安装好之后再,`/${Java_Home}/bin`目录下就会有一个可执行文件`kettool`,该工具可以帮助我们生成非对称加解密的 密钥文件(私钥) 和 证书文件(公钥)。 生成密钥文件命令:`keytool -genkey -alias IF010 -keyalg RSA -keysize 2048 -validity 365 -keystore if010-private.jks` 生成证书文件命令:`keytool -export -alias IF010 -file if010-public.cer -keystore if010-private.jks` > 命令选项解析:`IF010`是密钥的别名,`if010-private.jks`是密钥库文件的名称,`if010-public.cer`是导出的证书文件名称 > > * `keytool -genkey`:用于生成密钥对。 > * `-alias`:设置密钥的别名。 > * `-keyalg`:设置密钥算法,常用的是RSA。 > * `-keypass`:设置密钥密码。 > * `-storepass`:设置密钥库密码。 > * `-keystore`:设置密钥库文件路径和名称。 > * `-export`:导出证书到文件。 > * `-file`:设置导出的证书文件名称。 > * `-keysize`:证书加密长度,越长则越难破译。 > * `-validity`:证书有效期,以天为单位。 ```bash [root@localhost Desktop]# keytool -genkey -alias IF010 -keyalg RSA -keysize 2048 -validity 365 -keystore if010-private.jks 输入密钥库口令: 再次输入新口令: 输入唯一判别名。提供单个点 (.) 以将子组件留空,或按 ENTER 以使用大括号中的默认值。 您的名字与姓氏是什么? [Unknown]: IF010 您的组织单位名称是什么? [Unknown]: IF010 您的组织名称是什么? [Unknown]: IF010 您所在的城市或区域名称是什么? [Unknown]: GZ 您所在的省/市/自治区名称是什么? [Unknown]: GD 该单位的双字母国家/地区代码是什么? [Unknown]: CN CN=IF010, OU=IF010, O=IF010, L=GZ, ST=GD, C=CN是否正确? [否]: Y 正在为以下对象生成 2,048 位RSA密钥对和自签名证书 (SHA384withRSA) (有效期为 365 天): CN=IF010, OU=IF010, O=IF010, L=GZ, ST=GD, C=CN [root@localhost Desktop]# keytool -export -alias IF010 -file if010-public.cer -keystore if010-private.jks 输入密钥库口令: 存储在文件 <if010-public.cer> 中的证书 ``` ## 方案设计 > 这里只是提供一种设计方案,且建立在Spring Cloud微服务架构当中,在实际的开发场景当中还是需要结合业务本身 ![实现思路](https://resource.if010.com/JwtAsymmetricPassword_01.png) 1. 用户携带账号密码请求网关,网关转发请求到认证服务中心进行校验,校验通过则生成 `全局唯一的UUID Token` 和 `使用私钥生成的JWT Token` (这里注意的是生成JWT Token时,里面必须包含用户唯一身份标识),最后只将`UUID Token`交给客户端; 2. 用户拿到返回的`UUID Token`后,将携带该`Token`的请求发送给网关,网关进行校验或者网关不进行校验转发给服务进行校验(有些场景是交给网关进行校验后拿到唯一的身份用户标识传递给下面的微服务,降低微服务的计算压力,这种操作其实是非常不建议的,存在极大的安全风险,假设我知道了用户的唯一身份标识,内部人员在内网可以直接对微服务发起请求,当然也是可以配置白名单进行防范); 3. 网关获取到该请求携带的UUID Token后,可在Reids当中取得对应JWT Token; 4. 网关将取得的JWT Token使用公钥进行解密,解密失败则表示Token失效了,返回告知需重新认证登录,反之解析成功的话,网关将得到该用户的唯一身份标识,用唯一身份标识从Redis中取出用户最后一次登录所生成的UUID Token; 5. 将用户最后一次登录所生成的UUID Token 与该次携带的UUID Token进行比较,如果相同这代表该次请求正常,反之表明有新的客户端使用了该身份登录 > 注意:非对称加密场景当中,认证服务拿私钥加密,网关用公钥解密,认证服务存在于内网当中,私钥相对于比较安全,而公钥放在整个网络最边缘的网关当中泄露了也是无法伪造,这就是非对称加密为何会比较安全了 > 这里为什么非要UUID Token多次一举呢? > > 1、用于防止多客户端同时在线所带来的安全性问题,如果业务允许在不同的设备同时登陆在线,我们可以考虑别的方案。 > > 2、如果我们将Token放在请求头部,这数据长度不能超过最大长度4KB(4096字节),对我我们非对称加密的JWT Token而言非常的紧张。 ## 方案实现 ![方案实现代码结构](https://resource.if010.com/JwtAsymmetricPassword_02.png) 这里是将`Jwt Token的加解密`与`校验拦截` 做成了一个通用模块,可能会显得臃肿了些,但是这是为了可以更加清晰分明方便理解,在实际当中可以对某些地方加以改造和优化 > 约定: > > 1、以下有引用到[HuTool](https://hutool.cn/)工具包进行加解密 > > 2、以下的RedisService是自己制作的通用模块,参考了[若依](https://www.ruoyi.vip/)的微服务框架里的通用模块,有兴趣的可以连接了解一下 > > 3、拦截器部分添加了路径白名单的机制,定义了那些请求需要校验,那些不需要校验,这个可以根据业务需求进行调整定义 > > 4、配置属性、公钥和私钥存储的位置可以根据业务进行自定义的调整 ### 加解密部分 - 定义配置类 ```java package com.if010.common.security.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.core.io.Resource; /** * 【配置】Jwt配置类 * @Author Kim同学 */ @Data @ConfigurationProperties(prefix = "security.jwt") public class JwtProperties { /** * 加密模式 (对称加密: symmetry | 非对称加密: asymmetric) */ private String mode; /** * 私钥文件位置 */ private Resource privateKeyLocation; /** * 签名算法 * 对称签名: HS256(HmacSHA256)、HS384(HmacSHA384)、HS512(HmacSHA512) * 非对称签名: RS256(SHA256withRSA)、RS384(SHA384withRSA)、RS512(SHA512withRSA)、ES256(SHA256withECDSA)、ES384(SHA384withECDSA)、ES512(SHA512withECDSA) */ private String algorithm; /** * 私钥文件访问密码, 如果采用的是对称加密,那么这个用作 密钥配置 */ private String password; /** * 证书别名 */ private String alias; /** * 证书类型 */ private String type; /** * 证书格式 */ private String format; /** * token有效期 */ private Long expire; } ``` ### 加解密部分 - 定义配置注册类 ```java package com.if010.common.security.config; import com.alibaba.fastjson2.JSONObject; import com.if010.common.core.constant.TokenConstants; import com.if010.common.redis.service.RedisService; import com.if010.common.security.properties.JwtProperties; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.rsa.crypto.KeyStoreKeyFactory; import java.security.KeyPair; import java.security.PublicKey; /** * 安全配置注册类 * @author Kim同学 */ @Configuration @Slf4j @RequiredArgsConstructor @EnableConfigurationProperties(JwtProperties.class) public class SecurityConfig { /** * 注入Redis服务类 */ private final RedisService redisService; /** * 密码加密器 (加了点盐巴~) */ @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } /** * JWT 密钥工厂容器,通过 security.jwt.mode 配置项决定使用对称加密还是非对称加密,非对称则注入 */ @Bean @ConditionalOnProperty(name = "security.jwt.mode", havingValue = "asymmetric") public KeyPair keyPair(JwtProperties properties) { // 判断是否有配置密钥库文件,有则表示可以注入完整的公私密钥对,没有配置则只能从redis里读取公钥证书进行解密验证jwt if (properties.getPrivateKeyLocation() != null) { // 获取秘钥工厂 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory( properties.getPrivateKeyLocation(), properties.getPassword().toCharArray()); KeyPair keyPair = keyStoreKeyFactory.getKeyPair( properties.getAlias(), properties.getPassword().toCharArray()); // 缓存公钥 redisService.setCacheObject(TokenConstants.TOEKN_PUBLIC_KEY, keyPair.getPublic()); //返回钥匙对 return keyPair; } else { // 尝试读取公钥证书文件 try { PublicKey publicKey = JSONObject.parseObject(redisService.getCacheObject(TokenConstants.TOEKN_PUBLIC_KEY).toString(), PublicKey.class); return new KeyPair(publicKey, null); } catch (Exception e) { throw new RuntimeException("获取公钥证书失败, {}", e); } } } } ``` ### 加解密部分 - 服务接口 ```java package com.if010.common.security.service; import java.util.Map; /** * 【服务-接口】Jwt加解密服务接口 * @author Kim同学 */ public interface JwtCryptionService { /** * 创建令牌方法 */ String createToken(Map<String, Object> claims); /** * 解析令牌方法 */ Map<String, Object> parseToken(String token); } ``` #### 加解密部分 - (非对称加解密) 服务接口服务实现 ```java package com.if010.common.security.service.impl; import cn.hutool.core.exceptions.ValidateException; import cn.hutool.jwt.JWT; import cn.hutool.jwt.JWTValidator; import cn.hutool.jwt.signers.JWTSigner; import cn.hutool.jwt.signers.JWTSignerUtil; import com.if010.common.core.constant.HttpStatus; import com.if010.common.core.constant.TokenConstants; import com.if010.common.core.exception.UnauthorizedException; import com.if010.common.redis.service.RedisService; import com.if010.common.security.properties.JwtProperties; import com.if010.common.security.service.JwtCryptionService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.security.KeyPair; import java.util.Date; import java.util.Map; /** * 【服务-实现】Jwt(非对称对称加解密)服务接口实现类 * @author Kim同学 */ @Slf4j @Service @ConditionalOnProperty(name = "security.jwt.mode", havingValue = "asymmetric") public class JwtCryptionServiceImplByAsymmetric implements JwtCryptionService { // 初始化定义 JWTSigner private final JWTSigner jwtSigner; // 注入 Jwt 配置类 private final JwtProperties properties; // 注入 Redis 服务 private final RedisService redisService; @Autowired public JwtCryptionServiceImplByAsymmetric(KeyPair keyPair, JwtProperties properties, RedisService redisService) { this.properties = properties; this.jwtSigner = JWTSignerUtil.createSigner(properties.getAlgorithm(), keyPair); this.redisService = redisService; } /** * JWT 生成方法 */ @Override public String createToken(Map<String, Object> claims) { // 1.生成jws return JWT.create() .setPayload("claims", claims) .setExpiresAt( new Date(System.currentTimeMillis() + ( Long.parseLong(redisService.getCacheObject(TokenConstants.TOEKN_KEY_EXPIRE)) * 1000L) ) ) .setSigner(jwtSigner) .sign(); } /** * JWT 解密方法 */ @Override public Map<String, Object> parseToken(String token) { // 1.校验token是否为空 if (token == null) { throw new UnauthorizedException("未登录", HttpStatus.UNAUTHORIZED); } // 2.校验并解析jwt JWT jwt; try { jwt = JWT.of(token).setSigner(jwtSigner); } catch (Exception e) { throw new UnauthorizedException("无效的Token令牌", HttpStatus.UNAUTHORIZED); } // 3.校验是否过期 try { JWTValidator.of(jwt).validateDate(); } catch (ValidateException e) { throw new UnauthorizedException("Token令牌已经过期", HttpStatus.FORBIDDEN); } // 4.数据格式校验 Object claims = jwt.getPayload("claims"); if (claims == null) { // 数据为空 throw new UnauthorizedException("无效的Token令牌", HttpStatus.UNAUTHORIZED); } // 5.数据解析 try { return (Map<String, Object>) claims; } catch (RuntimeException e) { // 数据格式有误 throw new UnauthorizedException("无效的Token令牌", HttpStatus.UNAUTHORIZED); } } } ``` #### 加解密部分 - (对称加解密) 服务接口服务实现 ```java package com.if010.common.security.service.impl; import cn.hutool.core.exceptions.ValidateException; import cn.hutool.jwt.JWT; import cn.hutool.jwt.JWTValidator; import cn.hutool.jwt.signers.JWTSigner; import cn.hutool.jwt.signers.JWTSignerUtil; import com.if010.common.core.constant.HttpStatus; import com.if010.common.core.constant.TokenConstants; import com.if010.common.core.exception.UnauthorizedException; import com.if010.common.redis.service.RedisService; import com.if010.common.security.properties.JwtProperties; import com.if010.common.security.service.JwtCryptionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.Date; import java.util.Map; /** * 【服务-实现】Jwt(对称对称加解密)服务接口实现类 * @author Kim同学 */ @Service @ConditionalOnProperty(name = "security.jwt.mode", havingValue = "symmetry") public class JwtCryptionServiceImplBySymmetry implements JwtCryptionService { // 初始化定义 JWTSigner private final JWTSigner jwtSigner; // 注入 Jwt 配置类 private final JwtProperties properties; // 注入 Redis 服务 private final RedisService redisService; /** * 构造方法注入 Jwt 配置类, 初始化 JWTSigner */ @Autowired public JwtCryptionServiceImplBySymmetry(JwtProperties properties, RedisService redisService) { this.properties = properties; this.jwtSigner = JWTSignerUtil.createSigner(properties.getAlgorithm(), properties.getPassword().getBytes()); this.redisService = redisService; } /** * JWT 生成方法 */ @Override public String createToken(Map<String, Object> claims) { return JWT.create() .setPayload("claims", claims) .setExpiresAt( new Date(System.currentTimeMillis() + ( Long.parseLong(redisService.getCacheObject(TokenConstants.TOEKN_KEY_EXPIRE)) * 1000L) ) ) .setSigner(jwtSigner) .sign(); } /** * JWT 解析方法 */ @Override public Map<String, Object> parseToken(String token) { // 1.校验token是否为空 if (token == null) { throw new UnauthorizedException("未登录", HttpStatus.UNAUTHORIZED); } // 2.校验并解析jwt JWT jwt; try { jwt = JWT.of(token).setSigner(jwtSigner); } catch (Exception e) { throw new UnauthorizedException("无效的token", HttpStatus.UNAUTHORIZED); } // 2.校验jwt是否有效 if (!jwt.verify()) { // 验证失败 throw new UnauthorizedException("无效的Token令牌", HttpStatus.UNAUTHORIZED); } // 3.校验是否过期 try { JWTValidator.of(jwt).validateDate(); } catch (ValidateException e) { throw new UnauthorizedException("Token令牌已经过期", HttpStatus.FORBIDDEN); } // 4.数据格式校验 Object claims = jwt.getPayload("claims"); if (claims == null) { // 数据为空 throw new UnauthorizedException("无效的Token令牌", HttpStatus.UNAUTHORIZED); } // 5.数据解析 try { return (Map<String, Object>) claims; } catch (RuntimeException e) { // 数据格式有误 throw new UnauthorizedException("无效的Token令牌", HttpStatus.UNAUTHORIZED); } } } ``` ### 拦截校验部分 - 定义拦截过滤器 ```java package com.if010.common.security.interceptor; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.if010.common.core.constant.CacheConstants; import com.if010.common.core.constant.Constants; import com.if010.common.core.constant.HttpStatus; import com.if010.common.core.constant.TokenConstants; import com.if010.common.core.exception.UnauthorizedException; import com.if010.common.core.utils.UserContext; import com.if010.common.redis.service.RedisService; import com.if010.common.security.service.JwtCryptionService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.env.Environment; import org.springframework.util.AntPathMatcher; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Toekn 校验拦截器 * @author Kim同学 */ @Slf4j @RequiredArgsConstructor public class TokenAuthInterceptor implements HandlerInterceptor { // AntPathMatcher,用于请求比较 (Spring提供的一个工具类) private final AntPathMatcher antPathMatcher = new AntPathMatcher(); // 注入JWT加密解密工具类 private final JwtCryptionService jwtCryptionService; // 注入Redis服务类 private final RedisService redisService; // 注入 Spring Boot 环境变量 private final Environment environment; /** * Toekn 校验拦截器 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1、判断请求路径是否在白名单中 if (isAllowPath(request)) { return true; } // 1、通过请求头中的 authorization UUID Token 从 Redis 中取出 Jwt Token String uuidToken = request.getHeader(TokenConstants.AUTHENTICATION); String jwtToken = redisService.getCacheObject(TokenConstants.TOEKN_KEY_PREFIX+uuidToken); if (jwtToken == null) { throw new UnauthorizedException("未查询到有效的登录信息, 拒绝服务请求 !", HttpStatus.UNAUTHORIZED); } // 2、检验解析Token Map<String, Object> userTokenClaims = null; try { userTokenClaims = jwtCryptionService.parseToken(jwtToken); } catch (Exception e) { throw new UnauthorizedException("未知身份, 拒绝服务请求 !", HttpStatus.FORBIDDEN); } // 3、判断系统是否配置了不能多终端同时登录 Boolean isAllowDifferentDrvieLogin = Boolean.parseBoolean(redisService.getCacheObject(CacheConstants.SYS_CONFIG_SECURITY_PREFIX + "AllowDifferentDrvieLogin")); if (!isAllowDifferentDrvieLogin) { // 通过解析Token拿到userId,用userId获取最新一次登录所获取的UUID Token String lastUuidToken = redisService.getCacheObject(TokenConstants.TOEKN_KEY_PREFIX + userTokenClaims.get("userId").toString()).toString(); // 判断当前的UUID Token是否与最新颁发的Token一致,不一致则删除redis中的 旧 Token 存储信息,并抛出异常 if (!lastUuidToken.equals(uuidToken)) { redisService.deleteObject(TokenConstants.TOEKN_KEY_PREFIX + uuidToken); throw new UnauthorizedException("该账号已在其他设备登录, 请重新登录 !", HttpStatus.FORBIDDEN); } } //3、将 Jwt Token 解析成 User 信息存入线程当中 HashMap<String, Object> userTempText = new HashMap<>(); userTempText.put("userId", userTokenClaims.get("userId").toString()); userTempText.put("userToken", uuidToken); UserContext.setUserTempText(userTempText); // 4、放行 return true; } /** * 清除线程中的用户信息 */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 清除线程中的 User 信息, 防止内存泄露 UserContext.removeUserTempText(); } /** * 判断请求是否命中白名单 */ private boolean isAllowPath(HttpServletRequest request) { boolean flag = false; // 从 Redis 中取出 白名单 配置列表 String authExcludePathsStr = redisService.getCacheObject(CacheConstants.SYS_CONFIG_SECURITY_AUTH_EXCLUDE_PATH); List<String> authExcludePathsJsonArray = JSON.parseArray(authExcludePathsStr, String.class); log.info("isAllowPath: {}", authExcludePathsJsonArray.toString()); // 判断白名单是否为空 if (authExcludePathsJsonArray == null || authExcludePathsJsonArray.isEmpty()) { return flag; } // 1.获取当前路径 和 请求方法 String method = request.getMethod(); String path = request.getRequestURI(); String serviceName = environment.getProperty("spring.application.name"); // 2.要放行的路径 for (String excludePath : authExcludePathsJsonArray) { log.info("当前请求方法: {}, 当前请求路径: {}, 当前校验白名单条目: {}", method, Constants.API_PREFIX + serviceName + path, excludePath); boolean isMatch = antPathMatcher.match(excludePath, method + ":" + Constants.API_PREFIX + serviceName + path); if(isMatch){ flag = true; break; } } return flag; } } ``` ### 拦截检验部分 - 注册拦截过滤器 ```java package com.if010.common.security.config; import com.if010.common.redis.service.RedisService; import com.if010.common.security.interceptor.TokenAuthInterceptor; import com.if010.common.security.service.JwtCryptionService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * Mvc 注册配置类 * @author Kim同学 */ @Configuration @RequiredArgsConstructor public class MvcConfig implements WebMvcConfigurer { // 注入JWT加密解密工具类 private final JwtCryptionService jwtCryptionService; // 注入Redis服务类 private final RedisService redisService; // 注入 Spring Boot 环境变量 private final Environment environment; @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Token校验拦截器 registry.addInterceptor(new TokenAuthInterceptor(jwtCryptionService, redisService, environment)); } } ``` 到这里模块基本完成,微服务应用应用后并在配置文件当中加以必要项的配置即可食用,而认证方面这里就不做另外的讲解了,基本就是调用判定一下用户名密码是否正确,正确的话就调用一下`createToken`的方法传入一个Map集合即可,Map里面装什么根据业务需求定义即可~ 最后修改:2024 年 11 月 24 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 -