为了保护项目之中的数据资源,那么一定就需要采用认证检测机制,于是SpringCloud进行认证处理,就可以使用SpringSecurity 来实现了,但是如果你真的去使用了SpringSecurity进行开发,因为维护的成本实在是太高了。
在后来的时候有很多的开发者开始尝试通过OAuth2统一认证来进行SpringCloud认证与授权服务,这种操作也属于较早期的实现了,这种实现最大的问题在于随着版本的更新会出现代码不稳定的情况,很多的开发者就开始尝试自己去独立的实现认证与授权的操作机制,于是就有了JWT开发技术,使用JWT最大的特点在于不需要维护所有的数据的状态。
使用JWT最大的特点在于不需要维护所有的数据的状态,同时可以自己包含有过期的时间,以及通过附加数据的形式传送所需要的额外的数据内容((可以保存认证以及授权信息)。
在WEB开发中需要维护Session的状态,所以如果用户访问量过大,那么必然会出现Session内容过多而导致服务器处理性能下降的惨剧,所以后面就需要引入WEB服务器的集群,但是为了便于服务器集群之中的Session管理,那么又需要进行分布式的Session存储,总之就一点:有状态的用户需要维护Session,Session维护成本很高。
对于JWT需要提供有一个专属的开发组件,这些组件可以实现JWT数据的生成以及各种检测操作机制,最重要的一点所有的认证可以直接通过网关来进行过滤,可以考虑通过网关来进行认证状态的检查,而后每一个具体的微服务实现权限的排查。
基于Token的单点登录技术可以有效的节约开发成本,但是这里面最重要的一点就是JWT数据的组成结构定什么?在实际的项目开发中,JWT主要是为了实现用户认证数据的处理,所以第三方应用客户端要想进行用户统一登录的操作,只需要传入用户认证所需要的数据信息,即可成功的获取到Token令牌,考虑到令牌的安全性以及实用性,在每一个JWT数据中会包含有三类信息项: Header头部信息、Payload负载信息、Signature数字签名。
使用JWT的结构特点,可以有效的实现用户数据信息的携带,每次进行服务调用时都需要传递此JWT数据,目标微服务依靠此数据实现用户登录状态检测,同时也可以根据其保存的用户角色数据,来进行当前操作执行的合法性校验
在实际项目开发中,不同的项目会存在有不同的数字签名、发布者等数据信息,考虑到JWT使用的便捷性,可以直接通过application.yml配置JWT的相关属性内容,随后将这些属性注入到指定的配置类中
1、【microboot项目】创建一个新的子模块“microboot-jwt”子模块,随后修改build.gradle配置文件添加依赖:
dependencies.gradle
ext.versions = [
jjwt : '0.9.1', // JWT依赖库
jaxb : '2.3.1', // JAXB依赖库 JDK11需要加这个依赖,8不需要
]
ext.libraries = [
// 以下的配置为JWT所需要的依赖库
'jjwt': "io.jsonwebtoken:jjwt:${versions.jjwt}",
'jaxb-api': "javax.xml.bind:jaxb-api:${versions.jaxb}"
]
build.gradle
project('microboot-jwt') { // 子模块
dependencies { // 配置子模块依赖
compile('org.springframework.boot:spring-boot-starter-web')
compile(libraries.'fastjson')
compile(libraries.'jjwt')
compile(libraries.'jaxb-api')
}
}
2、【microboot-jwt子模块】创建 application.yml配置文件,配置JWT的相关环境属性,同时考虑到了很多的信息需要自定义,同时也需要保存有应用的名称,所以要添加“spring.application.name”配置项
spring:
application:
name: microboot-jwt # 应用名称
muyan: # 自定义配置项
config: # 配置项定义
jwt: # 配置JWT相关属性
sign: muyan # JWT证书签名
issuer: MuyanYootk # 证书签发人
secret: www.yootk.com # 加密密钥
expire: 10 # 有效时间(单位:秒)
如果你现在要做的是一个非常大型的JWT系统,那么这个时候可以考虑通过数据库来实现以上的配置项
3、【microboot-jwt子模块】所有的配置项一定要被程序进行读取,可以创建有一个配置类
package com.yootk.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "muyan.config.jwt")
public class JWTConfigProperties { // 保存JWT配置项
private String sign;
private String issuer;
private String secret;
private long expire;
}
在之前application.yml中所配置的相关属性,实际上都是为了JWT数据服务准备的,因为JWT在整个的项目设计之中是一个核心的结构,所以最佳的做法是通过一个服务的包装类,来包裹jjwt依赖库里面所提供的各个组件。
4、【microboot-jwt子模块】既然要进行服务的开发,首先就应该要提供有一个完善的业务接口。
package com.yootk.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数据
* @param id 这个Token的唯一ID(随意存储,本次可以考虑存储用户ID)
* @param subject 所有附加的信息内容,本次直接接收了一个Map,但是最终存储的时候存放JSON
* @return 返回一个有效的Token数据字符串
*/
public String createToken(String id, Map<String, Object> subject);
/**
* 是根据Token的字符串内容解析出其组成的信息(头信息与附加信息)
* @param token 要解析的Token完整数据
* @return Jws接口实例
* @throws JwtException 如果Token失效或者结构错误
*/
public Jws<Claims> parseToken(String token) throws JwtException;
/**
* 校验当前传递的Token数据是否正确
* @param token 要检查的Token数据
* @return true表示合法、false表示无效
*/
public boolean verifyToken(String token);
/**
* Token存在有效时间的定义,所以一定要提供有Token刷新机制
* @param token 原始的Token数据
* @return 新的Token数据
*/
public String refreshToken(String token);
}
5、【microboot-jwt子模块】定义ITokenService业务接口子类,并且完成所有的抽象方法
package com.yootk.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.yootk.config.JWTConfigProperties;
import com.yootk.service.ITokenService;
import io.jsonwebtoken.*;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Service
public class TokenServiceImpl implements ITokenService {
@Autowired
private JWTConfigProperties jwtConfigProperties; // JWT的相关配置属性
@Value("${spring.application.name}")
private String applicationName;
private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 签名算法
@Override
public SecretKey generalKey() { // 获取加密KEY
byte[] encodedKey = Base64.decodeBase64(Base64.encodeBase64(this.jwtConfigProperties.getSecret().getBytes()));
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
@Override
public String createToken(String id, Map<String, Object> subject) {
Date nowDate = new Date(); // 获取当前的日期时间
// 当前的时间 + 失效时间配置的秒数 = 最终失效的日期时间
Date expireDate = new Date(nowDate.getTime() + this.jwtConfigProperties.getExpire() * 10000);
Map<String, Object> claims = new HashMap<>(); // 附加的Claims信息
claims.put("site", "www.yootk.com"); // 添加信息内容
claims.put("book", "SpringBoot就业编程实战"); // 添加信息内容
claims.put("company", "沐言科技"); // 添加信息内容
Map<String, Object> headers = new HashMap<>(); // 保存的头信息
headers.put("author", "李老师");
headers.put("module", this.applicationName); // 保存应用的名称
headers.put("desc", "喜欢你。");
JwtBuilder builder = Jwts.builder().setClaims(claims) // 保存Claims信息
.setHeader(headers) // 保存Headedr信息
.setId(id) // 保存ID内容
.setIssuedAt(nowDate) // 证书签发日期时间
.setIssuer(this.jwtConfigProperties.getIssuer()) // 证书签发者
.setSubject(JSONObject.toJSONString(subject)) // 附加信息
.signWith(this.signatureAlgorithm, this.generalKey()) // 签名算法
.setExpiration(expireDate); // Token失效时间
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;
}
@Override
public boolean verifyToken(String token) {
try {
Jwts.parser().setSigningKey(this.generalKey()).parseClaimsJws(token).getBody();
return true;// 没有异常,解析成功
} catch (JwtException exception) {
return false;
}
}
@Override
public String refreshToken(String token) {
if (this.verifyToken(token)) { // 正确的Token是可以进行刷新的
Jws<Claims> claimsJws = this.parseToken(token); // 解析数据
return this.createToken(claimsJws.getBody().getId(), JSONObject.parseObject(claimsJws.getBody().getSubject(), Map.class));
}
return null;
}
}
6、【测试】
package com.yootk.test;
import com.alibaba.fastjson.JSONObject;
import com.yootk.StartJWTApplication;
import com.yootk.config.JWTConfigProperties;
import com.yootk.service.ITokenService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
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) // Junit5测试工具
@WebAppConfiguration // 表示需要启动Web配置才可以进行测试
@SpringBootTest(classes = StartJWTApplication.class) // 定义要测试的启动类
public class TestTokenService {
@Autowired
private ITokenService tokenService;
private String token = "eyJhdXRob3IiOiLniIblj6_niLHnmoTlsI_mnY7ogIHluIgiLCJtb2R1bGUiOiJtdXlhbi15b290ay10b2tlbiIsImFsZyI6IkhTMjU2IiwiZGVzYyI6IuaIkeaYr-S4gOS4quW-iOaZrumAmueahOiAgeW4iO-8jOWWnOasouaVmeWtpu-8jOiupOecn-aQnuecn-ato-eahOaVmeiCsuOAgiJ9.eyJzdWIiOiJ7XCJyaWRzXCI6XCJVU0VSO0FETUlOO0RFUFQ7RU1QO1JPTEVcIixcIm5hbWVcIjpcIuaykOiogOenkeaKgC3mnY7lhbTljY5cIixcIm1pZFwiOlwibXV5YW5cIn0iLCJzaXRlIjoid3d3Lnlvb3RrLmNvbSIsImJvb2siOiJTcHJpbmdCb2905bCx5Lia57yW56iL5a6e5oiYIiwiaXNzIjoiTXV5YW5Zb290ayIsImNvbXBhbnkiOiLmspDoqIDnp5HmioAiLCJleHAiOjE2MjQ3Njk5NzgsImlhdCI6MTYyNDc1OTk3OCwianRpIjoieW9vdGstNGQ2YzdkMzItZmE5Mi00ZTc4LWJkN2YtNzE1MGMxMDA3MDRlIn0.B7f11ckb4etMTcxzdzTh_1VubQSHnifl43t2-3atrD4";
@Test
public void testCreate() { // 创建Token数据
Map<String, Object> map = new HashMap<>(); // 保存subject数据信息
map.put("mid", "muyan");
map.put("name", "沐言科技-李兴华");
map.put("rids", "USER;ADMIN;DEPT;EMP;ROLE"); // 保存角色数据
String id = "yootk-" + UUID.randomUUID(); // 随机生成ID
System.out.println(this.tokenService.createToken(id, map));
}
@Test
public void testParse() {
Jws<Claims> claims = this.tokenService.parseToken(token); // 解析得到的Token数据
claims.getHeader().forEach((name, value) -> {
System.out.println("【JWT头信息】name = " + name + "、value = " + value);
});
System.err.println("------------------------------------------------------------------");
claims.getBody().forEach((name, value) -> {
System.out.println("【JWT主题信息】name = " + name + "、value = " + value);
});
System.err.println("------------------------------------------------------------------");
Map<String, Object> map = JSONObject.parseObject(claims.getBody().get("sub").toString(), Map.class); // 用户配置的信息
map.entrySet().forEach(entry -> {
System.out.println("【用户数据】key = " + entry.getKey() + "、value = " + entry.getValue());
});
}
@Test
public void testVerifyJWT() {
System.out.println(this.tokenService.verifyToken(token));
}
@Test
public void testRefreshToken() {
System.out.println(this.tokenService.refreshToken(token));
}
}
本次的Token主要是为了保护最终微服务的资源的,但是所有的资源保护都有一个基个前提,那么就是要有相应的拦截处理,也就是说可以通过拦截器的形式来进行资源的保护,但是毕竟有些资源是需要保护的,而有一些资源是需要保护的,此时可以考虑通过一个自定义注解的形式来区分保护与非保护资源。
1、【microboot-jwt子模块】创建一个用于区分是否要使用Token保护的注解
package com.yootk.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD}) // 该注解主要用于方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface JWTCheckToken { // JWT的检查注解
public boolean required() default true; // 是否要启用Token检查
}
2、【microboot-jwt子模块】为了便于该注解的使用,可以创建一个MessageAction程序类,并且进行信息响应
package com.yootk.action;
import com.yootk.annotation.JWTCheckToken;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/message/*") // 父路径
public class MessageAction {
@RequestMapping("echo")
@JWTCheckToken // 这个资源需要被检查
public Object echo(String msg) {
return "【ECHO】" + msg;
}
}
3、【microboot-jwt子模块】如果最终需要此注解生效,那么就需要定义拦截器。
package com.yootk.interceptor;
import com.yootk.annotation.JWTCheckToken;
import com.yootk.service.ITokenService;
import jdk.jfr.Frequency;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
public class JWTAuthenticationInterceptor implements HandlerInterceptor { // 认证拦截器
// Token可以通过参数传递也可以通过头信息传递
private static final String TOKEN_NAME = "yootkToken"; // Token参数名称
@Autowired
private ITokenService tokenService; // Token业务接口
@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(); // 获取当前要执行的Action方法反射对象
if (method.isAnnotationPresent(JWTCheckToken.class)) { // 判断该方法上是否提供有指定的注解
JWTCheckToken checkToken = method.getAnnotation(JWTCheckToken.class); // 获取指定注解
if (checkToken.required()) { // true表示要进行Token检查
String token = this.getToken(request); // 获取Token数据
if (!this.tokenService.verifyToken(token)) { // 验证失败
throw new RuntimeException("Token数据无效,无法访问。");
}
}
}
return true;
}
public String getToken(HttpServletRequest request) {
String token = request.getParameter(TOKEN_NAME); // 通过参数获取头信息
if (token == null || "".equals(token)) { // 没有接收到Token
token = request.getHeader(TOKEN_NAME); // 通过头信息获取
}
return token;
}
}
4、【microboot-jwt子模块】如果要想让拦截器生效,则需要定义一个配置类。
package com.yootk.config;
import com.yootk.interceptor.JWTAuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer { // 拦截配置类
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this.getJWTAuthenticationInterceptor()).addPathPatterns("/**");
}
@Bean
public HandlerInterceptor getJWTAuthenticationInterceptor() {
return new JWTAuthenticationInterceptor();
}
}
5、【浏览器】直接通过浏览器进行访问
localhost:8080/message/echo?msg=www.yootk.com&yootkToken=eyJhdXRob3IiOiLniIblj6_niLHnmoTlsI_mnY7ogIHluIgiLCJtb2R1bGUiOiJtdXlhbi15b290ay10b2tlbiIsImFsZyI6IkhTMjU2IiwiZGVzYyI6IuaIkeaYr-S4gOS4quW-iOaZrumAmueahOiAgeW4iO-8jOWWnOasouaVmeWtpu-8jOiupOecn-aQnuecn-ato-eahOaVmeiCsuOAgiJ9.eyJzdWIiOiJ7XCJyaWRzXCI6XCJVU0VSO0FETUlOO0RFUFQ7RU1QO1JPTEVcIixcIm5hbWVcIjpcIuaykOiogOenkeaKgC3mnY7lhbTljY5cIixcIm1pZFwiOlwibXV5YW5cIn0iLCJzaXRlIjoid3d3Lnlvb3RrLmNvbSIsImJvb2siOiJTcHJpbmdCb2905bCx5Lia57yW56iL5a6e5oiYIiwiaXNzIjoiTXV5YW5Zb290ayIsImNvbXBhbnkiOiLmspDoqIDnp5HmioAiLCJleHAiOjE2MjQ3NzE1MTYsImlhdCI6MTYyNDc2MTUxNiwianRpIjoieW9vdGstZGJlZDhhMmYtMjBhYi00Njg1LWI2ZDktNmEzNzI0Y2RhZDZiIn0.DDpaXGOdTYDlUZgOMB59uVlgYTZogDYjLW-pvNuvizE
此时通过JWT数据实现的认证检查处理,要比之前使用SpringSecurity、OAuth2、Shiro都简单许多,一个字符串就可以轻松搞定所有的问题了。