如果要想在项目之中去使用JWT技术,那么就必须结合到已有的模块之中,最佳的做法就是将JWT的相关的处理
操作做为一个自动的starter组件进行接入
1、【microcloud项目】既然要开发一个starter组件,最佳的做法就是开发一个新的模块,模块名称:“yootk-starter.jwt ”
2、【microcloud 项目】需要为“yootk-starter-jwt”模块配置所需要的依赖库,这些依赖库包括
implementation group: 'org.springframework.boot', name: 'spring-boot-configuration-processor', version: '2.5.5'
compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1'
implementation group: 'commons-codec', name: 'commons-codec', version: '1.15'
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1'
implementation group: 'com.sun.xml.bind', name: 'jaxb-impl', version: '2.3.0'
implementation group: 'com.sun.xml.bind', name: 'jaxb-core', version: '2.3.0'
3、【microcloud项目】既然已经确定了所需要的项目依赖库,随后就可以修改“dependencies.gradle”配置文件,定义所依赖模块的配置。
ext.versions = [ // 定义全部的依赖库版本号
servlet : '4.0.1', // Servlet的依赖库
commonsCodec : '1.15', // codec依赖库
jjwt : '0.9.1', // jwt依赖库
jaxb : '2.3.0', // JAXB依赖库 JDK11需要加的
]
ext.libraries = [
// 以下的配置为JWT的服务整合
'servlet-api' : "javax.servlet:javax.servlet-api:${versions.servlet}",
'commons-codec' : "commons-codec:commons-codec:${versions.commonsCodec}",
'jjwt' : "io.jsonwebtoken:jjwt:${versions.jjwt}",
'jaxb-api' : "javax.xml.bind:jaxb-api:${versions.jaxb}",
'jaxb-impl' : "com.sun.xml.bind:jaxb-impl:${versions.jaxb}",
'jaxb-core' : "com.sun.xml.bind:jaxb-core:${versions.jaxb}",
]
4、【microcloud项目】修改build.gradle配置文件,添加相关的依赖
project(":yootk-starter-jwt") { // JWT的实现组件
dependencies {
annotationProcessor('org.springframework.boot:spring-boot-configuration-processor')
implementation(libraries.'servlet-api')
implementation(libraries.'commons-codec')
// 以下的组件会被其他的模块继续引用,所以必须将其的编译范围配置为compile
compile(libraries.'jjwt')
compile(libraries.'jaxb-api')
compile(libraries.'jaxb-impl')
compile(libraries.'jaxb-core')
}
}
5、【yootk-starter-jwt子模块】由于该模块最终需要进行编译处理,所以此时要修改build.gradle配置文件,进行任务配置。
jar { enabled = true} // 允许打包为jar文件
bootJar { enabled = false } // 不允许打包为Boot执行文件
javadocJar { enabled = false } // 不需要打包为jar文件
javadocTask { enabled = false } // 不需要打包为doc文件
6、【yootk-starter-jwt子模块】为了便于用户的信息的相应,创建一个JWT响应代码枚举类。
package com.yootk.jwt.code;
import javax.servlet.http.HttpServletResponse;
public enum JWTResponseCode { // 定义为一个枚举类
SUCCESS_CODE(HttpServletResponse.SC_OK, "Token数据正确,服务正常访问!"),
TOKEN_TIMEOUT_CODE(HttpServletResponse.SC_BAD_REQUEST, "Token信息已经失效,需要重新申请!"),
NO_AUTH_CODE(HttpServletResponse.SC_NOT_FOUND, "没有找到匹配的Token信息,无法进行服务访问!");
private int code; // 响应的代码
private String message; // 响应信息
private JWTResponseCode(int code, String message) {
this.code = code;
this.message = message;
}
public String toString() { // 直接将数据以JSON的形式返回
return "{\"code\":" + this.code + ",\"message\":" + this.message + "}";
}
}
7、 【yootk-starter-jwt】此时的yootk-starter-jwt模块最终是一个自动装配的组件,那么既然是组件就需要通过一个配置类来读取引用该模块时所添加的配置信息,那么创建一个JWTConfigProperties 配置类。
package com.yootk.jwt.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data // Lombok直接生成的所有代码
@ConfigurationProperties(prefix = "yootk.security.config.jwt") // 配置项的前缀
public class JWTConfigProperties { // JWT配置类
private String sign; // 保存签名信息
private String issuer; // 证书签发者
private String secret; // 加密的密钥
private long expire; // 失效时间
}
8、【yootk-starter-jwt子模块】创建ITokenService服务处理接口,专门实现JWT数据的相关处理。
package com.yootk.jwt.service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import javax.crypto.SecretKey;
import java.util.Map;
public interface ITokenService { // 创建一个JWT的操作接口
public SecretKey generalKey(); // 获取当前JWT数据的加密KEY
// 创建Token的数据内容,同时要求保存用户的id以及所需要的附加数据
public String createToken(String id, Map<String, Object> subject);
public Jws<Claims> parseToken(String token) throws JwtException; // 解析Token数据
public boolean verifyToken(String token); // 验证Token有效性
public String refreshToken(String token); // 刷新Token内容
}
9.【yootk-starter-jwt子模块】创建TokenServicelmpl实现子类,很多的数据需要通过JSON实现传递。
package com.yootk.jwt.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yootk.jwt.config.JWTConfigProperties;
import com.yootk.jwt.service.ITokenService;
import io.jsonwebtoken.*;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
// 此时的组件中的代码需要被其他的模块去引用,所以未必会与扫描包相同
public class TokenServiceImpl implements ITokenService {
@Autowired // SpringBoot容器启动时会自动提供Jackson 实例
private ObjectMapper objectMapper; // Jackson的数据处理类对象
@Autowired
private JWTConfigProperties jwtConfigProperties; // 获取JWT的相关配置属性
@Value("${spring.application.name}") // 通过SpEL进行配置注入
private String applicationName; // 应用名称
private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 签名算法
@Override
public SecretKey generalKey() {
byte [] encodeKey = Base64.decodeBase64(Base64.encodeBase64(this.jwtConfigProperties.getSecret().getBytes()));
SecretKey key = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES"); // 获取加密KEY
return key;
}
@Override
public String createToken(String id, Map<String, Object> subject) {
// 使用JWT数据结构进行开发,目的之一就是不需要进行JWT数据的分布式存储,所以所谓的缓存组件、数据库都用不到
// 所有的Token都存在有保存时效的问题,所以就需要通过当前时间来进行计算
Date nowDate = new Date(); // 获取当前的日期时间
Date expireDate = new Date(nowDate.getTime() + this.jwtConfigProperties.getExpire() * 1000); // 证书过期时间
Map<String, Object> cliams = new HashMap<>(); // 保存所有附加数据
cliams.put("site", "www.yootk.com"); // 视频下载地址,顶部有一个下载资源
cliams.put("msg", "世界上爆可爱的老师 —— 爆可爱的小李老师"); // 随便添加内容
cliams.put("nice", "Good Good Good");
Map<String, Object> headers = new HashMap<>(); // 保存头信息
headers.put("author", "李兴华"); // 作者,也可以通过配置处理
// 后续由于很多的模块都会引用此组件,所以为了后续的安全,最佳的做法就是设置一个模块名称的信息
headers.put("module", this.applicationName);
JwtBuilder builder = null;
try {
builder = Jwts.builder() // 进行JWTBuilder对象实例化
.setClaims(cliams) // 保存附加的数据内容
.setHeader(headers) // 保存头信息
.setId(id)// 保存ID信息
.setIssuedAt(nowDate) // 签发时间
.setIssuer(this.jwtConfigProperties.getIssuer()) // 设置签发者
.setSubject(this.objectMapper.writeValueAsString(subject)) // 所要传递的数据转为JSON
.signWith(this.signatureAlgorithm, this.generalKey()) // 获取签名算法
.setExpiration(expireDate); // 配置失效时间
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return builder.compact(); // 创建Token
}
@Override
public Jws<Claims> parseToken(String token) throws JwtException {
if (this.verifyToken(token)) { // 只有正确的时候再进行Token解析
Jws<Claims> claims = Jwts.parser().setSigningKey(this.generalKey()).parseClaimsJws(token);
return claims;
}
return null; // 解析失败返回null
}
@Override
public boolean verifyToken(String token) {
try {
Jwts.parser().setSigningKey(this.generalKey()).parseClaimsJws(token).getBody();
return true; // 没有异常就返回true
} catch (Exception e) {}
return false;
}
@Override
public String refreshToken(String token) {
if (this.verifyToken(token)) {
Jws<Claims> jws = this.parseToken(token); // 解析Token数据
return this.createToken(jws.getBody().getId(), this.objectMapper.readValue(jws.getBody().getSubject(), Map.class));
}
return null;
}
}
10、【yootk-starter-jwt子模块】定义一个加密的属性配置
package com.yootk.jwt.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "yootk.security.config.password.encrypt") // 配置前缀
public class EncryptConfigProperties { // 加密配置属性
private Integer repeat; // 定义重复的次数
private String salt; // 加密的盐值
}
11、【yootk-starter-jwt子模块】既然所有的用户的信息都要保存在数据表里面,那么就需要进行密码的加密处理。
package com.yootk.jwt.service;
public interface IEncryptService { // 密码加密
public String getEncryptPassword(String password); // 得到一个加密后的密码
}
12、【yootk-starter-jwt子模块】定义具体的实现子类
package com.yootk.jwt.service.impl;
import com.yootk.jwt.config.EncryptConfigProperties;
import com.yootk.jwt.service.IEncryptService;
import org.springframework.beans.factory.annotation.Autowired;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class EncryptServiceImpl implements IEncryptService {
@Autowired
private EncryptConfigProperties encryptConfigProperties; // 属性配置
private static MessageDigest MD5_DIGEST; // MD5加密处理
private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder(); // 加密器
static { // 初始化操作
try {
MD5_DIGEST = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
@Override
public String getEncryptPassword(String password) {
String saltPassword = "{" + this.encryptConfigProperties.getSalt() + "}" + password;
for (int x = 0 ; x < this.encryptConfigProperties.getRepeat(); x ++) {
saltPassword = BASE64_ENCODER.encodeToString(MD5_DIGEST.digest(saltPassword.getBytes()));
}
return saltPassword;
}
}
13、【yootk-starter-jwt子模块】创建JWT自动配置类
package com.yootk.jwt.autoconfig;
import com.yootk.jwt.config.EncryptConfigProperties;
import com.yootk.jwt.config.JWTConfigProperties;
import com.yootk.jwt.service.IEncryptService;
import com.yootk.jwt.service.ITokenService;
import com.yootk.jwt.service.impl.EncryptServiceImpl;
import com.yootk.jwt.service.impl.TokenServiceImpl;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties({JWTConfigProperties.class, EncryptConfigProperties.class}) // 配置注入属性
public class JWTAutoConfiguration {
@Bean("tokenService")
public ITokenService getTokenServiceBean() {
return new TokenServiceImpl();
}
@Bean("encryptService")
public IEncryptService getEncryptServiceBean() {
return new EncryptServiceImpl();
}
}
14、【yootk-starter-jwt子模块】在“src/main/resources”目录之中创建“META-INF/spring.factories”配置文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration= com.yootk.jwt.autoconfig.JWTAutoConfiguration
15、【yootk-starter-jwt子模块】模块开发完成之后来进行编译: gradle build
16、【yootk-starter-jwt子模块】既然已经成功的实现了模块的编译处理,随后就需要进行一些环境上的测试,创建SpringBoot的配置文件: application.yml
yootk:
security:
config:
jwt:
sign: muyan
issuer: Muyan
secret: yootk
expire: 100 # 单位:秒
password:
encrypt:
repeat: 5
salt: yootk
spring:
application:
name: JWT-TEST
17、【yootk-starter-jwt子模块】创建一个程序启动的主类,主要是进行测试用的
package com.yootk.jwt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StartJWTConfiguration {
public static void main(String[] args) {
SpringApplication.run(StartJWTConfiguration.class, args);
}
}
18、【yootk-starter-jwt子模块】编写测试程序进行TokenService测试
package com.yootk.test;
import com.yootk.jwt.StartJWTConfiguration;
import com.yootk.jwt.service.ITokenService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwsHeader;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@SpringBootTest(classes = StartJWTConfiguration.class) // 随便写的测试类
public class TestTokenService { // 代码测试
@Autowired
private ITokenService tokenService;
private String jwt = "eyJhdXRob3IiOiLmnY7lhbTljY4iLCJtb2R1bGUiOiJKV1QtVEVTVCIsImFsZyI6IkhTMjU2In0.eyJtc2ciOiLkuJbnlYzkuIrniIblj6_niLHnmoTogIHluIgg4oCU4oCUIOeIhuWPr-eIseeahOWwj-adjuiAgeW4iCIsInN1YiI6IntcInJpZHNcIjpcIlVTRVI7QURNSU47REVQVDtFTVA7Uk9MRVwiLFwibmFtZVwiOlwi5rKQ6KiA56eR5oqAIOKAlOKAlCDmnY7lhbTljY5cIixcIm1pZFwiOlwibXV5YW5cIn0iLCJzaXRlIjoid3d3Lnlvb3RrLmNvbSIsImlzcyI6Ik11eWFuWW9vdGsiLCJleHAiOjE2MzM2NzE3NjcsImlhdCI6MTYzMzU3MTc2NywibmljZSI6Ikdvb2QgR29vZCBHb29kIiwianRpIjoieW9vdGstMDgwNGI3NDQtNTBjZC00NjI2LTgzNmEtNjA1MmFiZWMyYzQ4In0.O71QGGPtWYwL7Tyhx8iOLQFAWc1DmVlAS4i0N99OJJk"; // 测试解析使用的
@Test
public void testCreateToken() {
Map<String, Object> map = new HashMap<>(); // 测试生成
map.put("mid", "muyan");
map.put("name", "沐言科技 —— 李兴华");
map.put("rids", "USER;ADMIN;DEPT;EMP;ROLE"); // 用户角色信息
String id = "yootk-" + UUID.randomUUID(); // 随意生成一个JWT-ID数据
System.out.println(this.tokenService.createToken(id, map));
}
@Test
public void testParseToken() { // 解析Token数据内容
Jws<Claims> jws = this.tokenService.parseToken(jwt);
System.out.println("JWT签名数据:" + jws.getSignature()); // 获取签名数据
JwsHeader headers = jws.getHeader(); // 获取头信息
headers.forEach((headerName, headerValue) -> {
System.out.println("【JWT头信息】" + headerName + " = " + headerValue);
});
Claims claims = jws.getBody();
claims.forEach((bodyName, bodyValue) -> {
System.out.println("【JWT数据】" + bodyName + " = " + bodyValue);
});
}
@Test
public void testVerifyJWT() {
System.out.println("【JWT数据验证】" + this.tokenService.verifyToken(jwt));
}
@Test
public void testRefreshJWT() {
System.out.println("【JWT数据刷新】" + this.tokenService.refreshToken(jwt));
}
}
19、【yootk-starter-jwt子模块】随后进行密码加密的测试
package com.yootk.test;
import com.yootk.jwt.StartJWTConfiguration;
import com.yootk.jwt.service.IEncryptService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@SpringBootTest(classes = StartJWTConfiguration.class) // 随便写的测试类
public class TestEncryptService {
@Autowired
private IEncryptService encryptService;
@Test
public void testCreatePassword() {
System.out.println(this.encryptService.getEncryptPassword("hello"));
}
}
此时已经成功的开发出了一套完整的与JWT相关的应用组件模块,使用的时候直接导入依赖库即可应用。