共计 14741 个字符,预计需要花费 37 分钟才能阅读完成。
JWT 简介
什么是 JWT
JWT 是 JSON Web Token 的缩写,是为了在网络应用环境间传递申明而执行的一种基于 JSON
的凋谢规范((RFC 7519)。定义了一种简洁的,自蕴含的办法用于通信单方之间以 JSON
对象的模式平安的传递信息。因为数字签名的存在,这些信息是可信的,JWT 能够应用 HMAC
算法或者是 RSA
的公私秘钥对进行签名。
JWT 申请流程
- 用户应用账号和明码发动 POST 申请;
- 服务器应用私钥创立一个 JWT;
- 服务器返回这个 JWT 给浏览器;
- 浏览器将该 JWT 串在申请头中像服务器发送申请;
- 服务器验证该 JWT;
- 返回响应的资源给浏览器。
JWT 的次要利用场景
身份认证在这种场景下,一旦用户实现了登录,在接下来的每个申请中蕴含 JWT,能够用来验证用户身份以及对路由,服务和资源的拜访权限进行验证。因为它的开销十分小,能够轻松的在不同域名的零碎中传递,所有目前在单点登录(SSO)中比拟宽泛的应用了该技术。信息替换在通信的单方之间应用 JWT 对数据进行编码是一种十分平安的形式,因为它的信息是通过签名的,能够确保发送者发送的信息是没有通过伪造的。
JWT 数据结构
JWT 是由三段信息形成的,将这三段信息文本用 .
连贯一起就形成了 JWT 字符串。
JWT 的三个局部顺次为头部:Header,负载:Payload 和签名:Signature。
Header
Header 局部是一个 JSON 对象,形容 JWT 的元数据,通常是上面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
下面代码中,alg
属性示意签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ
属性示意这个令牌(token)的类型(type),JWT 令牌对立写为 JWT
。
最初,将下面的 JSON 对象应用 Base64URL 算法转成字符串。
Payload
Payload 局部也是一个 JSON 对象,用来寄存理论须要传递的无效信息。无效信息蕴含三个局部:
- 规范中注册的申明
- 公共的申明
- 公有的申明
规范中注册的申明 (倡议但不强制应用):
- iss (issuer):签发人
- exp (expiration time):过期工夫,必须要大于签发工夫
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):失效工夫
- iat (Issued At):签发工夫
- jti (JWT ID):编号,JWT 的惟一身份标识,次要用来作为一次性
token
,从而回避重放攻打。
公共的申明:
公共的申明能够增加任何的信息,个别增加用户的相干信息或其余业务须要的必要信息。但不倡议增加敏感信息,因为该局部在客户端可解密。
公有的申明:
公有申明是提供者和消费者所独特定义的申明,个别不倡议寄存敏感信息,因为 base64
是对称解码的,意味着该局部信息能够归类为明文信息。
这个 JSON 对象也要应用 Base64URL 算法转成字符串。
Signature
Signature 局部是对前两局部的签名,避免数据篡改。
首先,须要指定一个密钥(secret)。这个密钥只有服务器才晓得,不能泄露给用户。而后,应用 Header 外面指定的签名算法(默认是 HMAC SHA256),依照上面的公式产生签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
算出签名当前,把 Header、Payload、Signature 三个局部拼成一个字符串,每个局部之间用 ” 点 ”(.
)分隔,就能够返回给用户。
Base64URL
后面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法根本相似,但有一些小的不同。
JWT 作为一个令牌(token),有些场合可能会放到 URL(比方 api.example.com/?token=xxx
)。Base64 有三个字符 +
、/
和 =
,在 URL 外面有非凡含意,所以要被替换掉:=
被省略、+
替换成 -
,/
替换成 _
。这就是 Base64URL 算法。
JWT 的应用形式
客户端收到服务器返回的 JWT 之后须要在本地做保留。尔后,客户端每次与服务器通信,都要带上这个 JWT。个别的的做法是放在 HTTP 申请的头信息 Authorization
字段外面。
Authorization: Bearer <token>
这样每个申请中,服务端就能够在申请头中拿到 JWT 进行解析与认证。
JWT 的个性
- JWT 默认是不加密,但也是能够加密的。生成原始 Token 当前,能够用密钥再加密一次。
- JWT 不加密的状况下,不能将机密数据写入 JWT。
- JWT 不仅能够用于认证,也能够用于替换信息。无效应用 JWT,能够升高服务器查询数据库的次数。
- JWT 的最大毛病是,因为服务器不保留 session 状态,因而无奈在应用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终无效,除非服务器部署额定的逻辑。
- JWT 自身蕴含了认证信息,一旦泄露,任何人都能够取得该令牌的所有权限。为了缩小盗用,JWT 的有效期应该设置得比拟短。对于一些比拟重要的权限,应用时应该再次对用户进行认证。
- 为了缩小盗用,JWT 不应该应用 HTTP 协定明码传输,要应用 HTTPS 协定传输。
基于 nimbus-jose-jwt 简略封装
nimbus-jose-jwt 是最受欢迎的 JWT 开源库,基于 Apache 2.0 开源协定,反对所有规范的签名 (JWS) 和加密 (JWE) 算法。nimbus-jose-jwt 反对应用对称加密(HMAC)和非对称加密(RSA)两种算法来生成和解析 JWT 令牌。
上面咱们对 nimbus-jose-jwt 进行简略的封装,提供以下性能的反对:
- 反对应用 HMAC 和 RSA 算法生成和解析 JWT 令牌
- 反对公有信息间接作为 Payload,以及规范信息 + 公有信息作为 Payload。内置反对后者。
- 提供工具类及可扩大接口,不便自定义扩大开发。
pom 中增加依赖
首先咱们在 pom.xml 中引入 nimbus-jose-jwt 的依赖。
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.20</version>
</dependency>
JwtConfig
这个类用于对立治理相干的参数配置。
public class JwtConfig {
// JWT 在 HTTP HEADER 中默认的 KEY
private String tokenName = JwtUtils.DEFAULT_TOKEN_NAME;
// HMAC 密钥,用于反对 HMAC 算法
private String hmacKey;
// JKS 密钥门路,用于反对 RSA 算法
private String jksFileName;
// JKS 密钥明码,用于反对 RSA 算法
private String jksPassword;
// 证书明码,用于反对 RSA 算法
private String certPassword;
// JWT 规范信息:签发人 - iss
private String issuer;
// JWT 规范信息:主题 - sub
private String subject;
// JWT 规范信息:受众 - aud
private String audience;
// JWT 规范信息:失效工夫 - nbf,将来多长时间内失效
private long notBeforeIn;
// JWT 规范信息:失效工夫 - nbf,具体哪个工夫失效
private long notBeforeAt;
// JWT 规范信息:过期工夫 - exp,将来多长时间内过期
private long expiredIn;
// JWT 规范信息:过期工夫 - exp,具体哪个工夫过期
private long expiredAt;
}
hmacKey
字段用于反对 HMAC 算法,只有该字段不为空,则应用该值作为 HMAC 的密钥对 JWT 进行签名与验证。
jksFileName
、jksPassword
、certPassword
三个字段用于反对 RSA 算法,程序将读取证书文件作为 RSA 密钥对 JWT 进行签名与验证。
其余几个字段用于设置 Payload 中须要携带的规范信息。
JwtService
JwtService 是提供 JWT 签名与验证的接口,内置了 HMACJwtServiceImpl 提供 HMAC 算法的实现和 RSAJwtServiceImpl 提供 RSA 算法的实现。两种算法在获取密钥的形式上是有差异的,这里也提出来成了接口办法。后续如果要自定义实现,只须要再写一个具体实现类。
public interface JwtService {
/**
* 获取 key
*
* @return
*/
Object genKey();
/**
* 对信息进行签名
*
* @param payload
* @return
*/
String sign(String payload);
/**
* 验证并返回信息
*
* @param token
* @return
*/
String verify(String token);
}
public class HMACJwtServiceImpl implements JwtService {
private JwtConfig jwtConfig;
public HMACJwtServiceImpl(JwtConfig jwtConfig) {this.jwtConfig = jwtConfig;}
@Override
public String genKey() {String key = jwtConfig.getHmacKey();
if (JwtUtils.isEmpty(key)) {throw new KeyGenerateException(JwtUtils.KEY_GEN_ERROR, new NullPointerException("HMAC need a key"));
}
return key;
}
@Override
public String sign(String info) {return JwtUtils.signClaimByHMAC(info, genKey(), jwtConfig);
}
@Override
public String verify(String token) {return JwtUtils.verifyClaimByHMAC(token, genKey(), jwtConfig);
}
}
public class RSAJwtServiceImpl implements JwtService {
private JwtConfig jwtConfig;
private RSAKey rsaKey;
public RSAJwtServiceImpl(JwtConfig jwtConfig) {this.jwtConfig = jwtConfig;}
private InputStream getCertInputStream() throws IOException {
// 读取配置文件中的证书门路
String jksFile = jwtConfig.getJksFileName();
if (jksFile.contains("://")) {
// 从本地文件读取
return new FileInputStream(new File(jksFile));
} else {
// 从 classpath 读取
return getClass().getClassLoader().getResourceAsStream(jwtConfig.getJksFileName());
}
}
@Override
public RSAKey genKey() {if (rsaKey != null) {return rsaKey;}
InputStream is = null;
try {KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
is = getCertInputStream();
keyStore.load(is, jwtConfig.getJksPassword().toCharArray());
Enumeration<String> aliases = keyStore.aliases();
String alias = null;
while (aliases.hasMoreElements()) {alias = aliases.nextElement();
}
RSAPrivateKey privateKey = (RSAPrivateKey) keyStore.getKey(alias, jwtConfig.getCertPassword().toCharArray());
Certificate certificate = keyStore.getCertificate(alias);
RSAPublicKey publicKey = (RSAPublicKey) certificate.getPublicKey();
rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).build();
return rsaKey;
} catch (IOException | CertificateException | UnrecoverableKeyException
| NoSuchAlgorithmException | KeyStoreException e) {e.printStackTrace();
throw new KeyGenerateException(JwtUtils.KEY_GEN_ERROR, e);
} finally {if (is != null) {
try {is.close();
} catch (IOException e) {e.printStackTrace();
}
}
}
}
@Override
public String sign(String payload) {return JwtUtils.signClaimByRSA(payload, genKey(), jwtConfig);
}
@Override
public String verify(String token) {return JwtUtils.verifyClaimByRSA(token, genKey(), jwtConfig);
}
}
JwtUtils
JwtService 的实现类中比拟简洁,因为次要的办法都在 JwtUtils 中提供了。如下是 Payload 中只蕴含公有信息时,两种算法的签名与验证实现。能够应用这些办法不便的实现本人的扩大。
/**
* 应用 HMAC 算法签名信息(Payload 中只蕴含公有信息)*
* @param info
* @param key
* @return
*/
public static String signDirectByHMAC(String info, String key) {
try {JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256)
.type(JOSEObjectType.JWT)
.build();
// 建设一个载荷 Payload
Payload payload = new Payload(info);
// 将头部和载荷联合在一起
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
// 建设一个密匙
JWSSigner jwsSigner = new MACSigner(key);
// 签名
jwsObject.sign(jwsSigner);
// 生成 token
return jwsObject.serialize();} catch (JOSEException e) {e.printStackTrace();
throw new PayloadSignException(JwtUtils.PAYLOAD_SIGN_ERROR, e);
}
}
/**
* 应用 RSA 算法签名信息(Payload 中只蕴含公有信息)*
* @param info
* @param rsaKey
* @return
*/
public static String signDirectByRSA(String info, RSAKey rsaKey) {
try {JWSSigner signer = new RSASSASigner(rsaKey);
JWSObject jwsObject = new JWSObject(new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaKey.getKeyID()).build(),
new Payload(info)
);
// 进行加密
jwsObject.sign(signer);
return jwsObject.serialize();} catch (JOSEException e) {e.printStackTrace();
throw new PayloadSignException(JwtUtils.PAYLOAD_SIGN_ERROR, e);
}
}
/**
* 应用 HMAC 算法验证 token(Payload 中只蕴含公有信息)*
* @param token
* @param key
* @return
*/
public static String verifyDirectByHMAC(String token, String key) {
try {JWSObject jwsObject = JWSObject.parse(token);
// 建设一个解锁密匙
JWSVerifier jwsVerifier = new MACVerifier(key);
if (jwsObject.verify(jwsVerifier)) {return jwsObject.getPayload().toString();}
throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, new NullPointerException("Payload can not be null"));
} catch (JOSEException | ParseException e) {e.printStackTrace();
throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, e);
}
}
/**
* 应用 RSA 算法验证 token(Payload 中只蕴含公有信息)*
* @param token
* @param rsaKey
* @return
*/
public static String verifyDirectByRSA(String token, RSAKey rsaKey) {
try {RSAKey publicRSAKey = rsaKey.toPublicJWK();
JWSObject jwsObject = JWSObject.parse(token);
JWSVerifier jwsVerifier = new RSASSAVerifier(publicRSAKey);
// 验证数据
if (jwsObject.verify(jwsVerifier)) {return jwsObject.getPayload().toString();}
throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, new NullPointerException("Payload can not be null"));
} catch (JOSEException | ParseException e) {e.printStackTrace();
throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, e);
}
}
JwtException
定义对立的异样类,能够屏蔽 nimbus-jose-jwt 以及其余诸如加载证书谬误抛出的异样,并且在其余我的项目集成咱们封装好的库的时候,不便的进行异样解决。
在 JwtService 实现的不同阶段,咱们封装了不同的 JwtException 子类,来不便内部依据须要做对应的解决。如异样是 KeyGenerateException,则解决成服务器处理错误;如异样是 TokenVerifyException,则解决成 Token 验证失败,无权限。
JwtContext
JWT 用于用户认证,常常在 Token 验证实现后,程序中须要获取到以后登录的用户信息,JwtContext 中提供了通过线程局部变量保存信息的办法。
public class JwtContext {
private static final String KEY_TOKEN = "token";
private static final String KEY_PAYLOAD = "payload";
private static ThreadLocal<Map<Object, Object>> context = new ThreadLocal<>();
private JwtContext() {}
public static void set(Object key, Object value) {Map<Object, Object> locals = context.get();
if (locals == null) {locals = new HashMap<>();
context.set(locals);
}
locals.put(key, value);
}
public static Object get(Object key) {Map<Object, Object> locals = context.get();
if (locals != null) {return locals.get(key);
}
return null;
}
public static void remove(Object key) {Map<Object, Object> locals = context.get();
if (locals != null) {locals.remove(key);
if (locals.isEmpty()) {context.remove();
}
}
}
public static void removeAll() {Map<Object, Object> locals = context.get();
if (locals != null) {locals.clear();
}
context.remove();}
public static void setToken(String token) {set(KEY_TOKEN, token);
}
public static String getToken() {return (String) get(KEY_TOKEN);
}
public static void setPayload(Object payload) {set(KEY_PAYLOAD, payload);
}
public static Object getPayload() {return get(KEY_PAYLOAD);
}
}
@AuthRequired
在我的项目实战中,并不是所有 Controller 中的办法都必须传 Token,通过 @AuthRequired 注解来辨别办法是否须要校验 Token。
/**
* 利用于 Controller 中的办法,标识是否拦挡进行 JWT 验证
*/
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface AuthRequired {boolean required() default true;
}
Spring Boot 集成 JWT 实例
有了下面封装好的库,咱们在 SpringBoot 我的项目中集成 JWT。创立好 Spring Boot 我的项目后,咱们编写上面次要的类。
JwtDemoInterceptor
在 Spring Boot 我的项目中,通过自定义 HandlerInterceptor 的实现类能够对申请和响应进行拦挡,咱们新建 JwtDemoInterceptor 类进行拦挡。
public class JwtDemoInterceptor implements HandlerInterceptor {private static final Logger logger = LoggerFactory.getLogger(JwtDemoInterceptor.class);
private static final String PREFIX_BEARER = "Bearer";
@Autowired
private JwtConfig jwtConfig;
@Autowired
private JwtService jwtService;
/**
* 预处理回调办法,实现处理器的预处理(如查看登陆),第三个参数为响应的处理器,自定义 Controller
* 返回值:* true 示意持续流程(如调用下一个拦截器或处理器);* false 示意流程中断(如登录查看失败),不会持续调用其余的拦截器或处理器,此时咱们须要通过 response 来产生响应。*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 如果不是映射到办法间接通过
if(!(handler instanceof HandlerMethod)){return true;}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 查看是否有 @AuthRequired 注解,有且 required() 为 false 则跳过
if (method.isAnnotationPresent(AuthRequired.class)) {AuthRequired authRequired = method.getAnnotation(AuthRequired.class);
if (!authRequired.required()) {return true;}
}
String token = request.getHeader(jwtConfig.getTokenName());
logger.info("token: {}", token);
if (StringUtils.isEmpty(token) || token.trim().equals(PREFIX_BEARER.trim())) {return true;}
token = token.replace(PREFIX_BEARER, "");
String payload = jwtService.verify(token);
// 设置线程局部变量中的 token
JwtContext.setToken(token);
JwtContext.setPayload(payload);
return true;
}
/**
* 后处理回调办法,实现处理器的后处理(但在渲染视图之前),此时咱们能够通过 modelAndView(模型和视图对象)对模型数据进行解决或对视图进行解决,modelAndView 也可能为 null。*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { }
/**
* 整个申请处理完毕回调办法,即在视图渲染结束时回调,如性能监控中咱们能够在此记录完结工夫并输入耗费工夫,还能够进行一些资源清理,相似于 try-catch-finally 中的 finally
* 但仅调用处理器执行链中 preHandle 返回 true 的拦截器的 afterCompletion。*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {JwtContext.removeAll();
}
}
preHandle
、postHandle
、afterCompletion
三个办法的具体作用,能够看代码上的正文。
preHandle
中这段代码中的逻辑如下:
- 拦挡被 @AuthRequired 注解的办法,只有不是
required = false
都会进行 Token 的校验。 - 从申请中解析出 Token,对 Token 进行验证。如果验证异样,会在办法中抛出异样。
- Token 验证通过,会在线程局部变量中设置相干信息,以便后续程序获取解决。
afterCompletion
中这段代码对线程变量进行了清理。
InterceptorConfig
定义 InterceptorConfig,通过 @Configuration 注解,Spring 会加载该类,并实现拆卸。
addInterceptors
办法中设置拦截器,并拦挡所有申请。
jwtDemoConfig
办法中注入 JwtConfig,并设置了 HMACKey。
jwtDemoService
办法会依据注入的 JwtConfig 配置,生成具体的 JwtService,这里是 HMACJwtServiceImpl。
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(jwtDemoInterceptor()).addPathPatterns("/**");
}
@Bean
public JwtDemoInterceptor jwtDemoInterceptor() {return new JwtDemoInterceptor();
}
@Bean
public JwtConfig jwtDemoConfig() {JwtConfig jwtConfig = new JwtConfig();
jwtConfig.setHmacKey("cb9915297c8b43e820afd2a90a1e36cb");
return jwtConfig;
}
@Bean
public JwtService jwtDemoService() {return JwtUtils.obtainJwtService(jwtDemoConfig());
}
}
编写测试 Controller
@RestController
public class UserController {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private JwtService jwtService;
@GetMapping("/sign")
@AuthRequired(required = false)
public String sign() throws JsonProcessingException {UserDTO userDTO = new UserDTO();
userDTO.setName("fatfoo");
userDTO.setPassword("112233");
userDTO.setSex(0);
String payload = objectMapper.writeValueAsString(userDTO);
return jwtService.sign(payload);
}
@GetMapping("/verify")
public UserDTO verify() throws IOException {String payload = (String) JwtContext.getPayload();
return objectMapper.readValue(payload, UserDTO.class);
}
}
sign
办法对用户信息进行签名并返回 Token;因为 @AuthRequired(required = false)
拦截器将不会对其进行拦挡。
verify
办法在 Token 通过验证后,获取解析出的信息并返回。
用 Postman 进行测试
拜访 sign 接口,返回签名 Token。
在 Header 中增加 Token 信息,申请 verify 接口,返回用户信息。
测试 RSA 算法实现
下面咱们只设置了 JwtConfig 的 hmacKey 参数,应用的是 HMAC 算法进行签名和验证。本节咱们演示 RSA 算法进行签名和验证的实现。
生成签名文件
应用 Java 自带的 keytool 工具能够不便的生成证书文件。
➜ resources git:(master) ✗ keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
输出密钥库口令:
密钥库口令太短 - 至多必须为 6 个字符
输出密钥库口令: ronjwt
再次输出新口令: ronjwt
您的名字与姓氏是什么?
[Unknown]: ron
您的组织单位名称是什么?
[Unknown]: ron
您的组织名称是什么?
[Unknown]: ron
您所在的城市或区域名称是什么?
[Unknown]: Xiamen
您所在的省 / 市 / 自治区名称是什么?
[Unknown]: Fujian
该单位的双字母国家 / 地区代码是什么?
[Unknown]: CN
CN=ron, OU=ron, O=ron, L=Xiamen, ST=Fujian, C=CN 是否正确?
[否]: 是
输出 <jwt> 的密钥口令
(如果和密钥库口令雷同, 按回车):
Warning:
JKS 密钥库应用专用格局。倡议应用 "keytool -importkeystore -srckeystore jwt.jks -destkeystore jwt.jks -deststoretype pkcs12" 迁徙到行业标准格局 PKCS12。
文件生成后,复制到我的项目的 resource 目录下。
设置 JwtConfig 参数
批改上节 InterceptorConfig 中的 jwtDemoConfig
办法,这是 jksFileName、jksPassword、certPassword 3 个参数。
@Bean
public JwtConfig jwtDemoConfig() {JwtConfig jwtConfig = new JwtConfig();
// jwtConfig.setHmacKey("cb9915297c8b43e820afd2a90a1e36cb");
jwtConfig.setJksFileName("jwt.jks");
jwtConfig.setJksPassword("ronjwt");
jwtConfig.setCertPassword("ronjwt");
return jwtConfig;
}
不要设置 hmacKey 参数,否则会加载 HMACJwtServiceImpl。因为 JwtUtils#obtainJwtService
办法实现如下:
/**
* 获取内置 JwtService 的工厂办法。*
* 优先采纳 HMAC 算法实现
*
* @param jwtConfig
* @return
*/
public static JwtService obtainJwtService(JwtConfig jwtConfig) {if (!JwtUtils.isEmpty(jwtConfig.getHmacKey())) {return new HMACJwtServiceImpl(jwtConfig);
}
return new RSAJwtServiceImpl(jwtConfig);
}
这样就能够进行 RSA 算法签名与验证的测试了。运行程序并应用 Postman 测试,请自行查看区别。
– End –
本文只是 Spring Boot 集成 JWT 的第一篇,在后续咱们还将持续对这个库进行封装,构建 spring-boot-starter,自定义 @Enable 注解来不便在我的项目中引入。
请关注我的公众号:精进 Java(ID:craft4j),第一工夫获取常识动静。
如果你对我的项目的残缺源码感兴趣,能够在公众号中回复 jwt
来获取。