目录
1、什么是JWT
(1)、JWT简介
(2)、JWT与传统的seesion的区别是什么
(3)、基于session认证所显露的问题
2、JWT能帮们做什么,为什么需要JWT
3、JWT的结构
4、如何使用JWT及工具类封装
6、JWT项目集成
7、总结
优点
安全相关
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA. --JWT官网
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
在互联网应用中,http协议本身是一种无状态的协议(无状态协议就是每次请求服务端都是一个新的请求,服务器端无法知晓请求是否来自一个客户端甚至某个用户),例如登录系统,用户使用用户名和密码进行登录认证,由于http请求为无状态,那么用户下一次请求服务端还得让用户登录,为什么识别是否是同一用户,服务器端需要存储用户的信息,然后把登录信息返回给浏览器(传统做法就是生成seesionId,然后往session里面存入用户信息---应用服务器缓存),这样下一次客户端请求带上sessionid,这样服务器就根据seesionid去缓存中查询此id对应的用户信息,用来识别是否为同一用户。我们的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来.
Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。分布式系统下,还需要做session同步。
CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
流程上是这样的:
这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)
策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *(实现跨域访问)
。
(1)JWT的结构
JWT是由三段信息构成的,将这三段信息文本用.
链接一起就构成了Jwt字符串。就像这样
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiLlnLDlnYAiLCJwaG9uZSI6IjE4MTA4MjQyOTNYIiwiZXhwIjoxNjA5MTMxMTA1LCJ1c2VySWQiOiJ6aGVuZ3dlaSIsImFnZSI6IjMwIn0.NCOJUZDNQRdB6ApGfXRkEGM8rQA1kMOMj1U12aZ7vhg
这三段中间通过(英文点符号隔开)
第一段:头部(header)
第二段:载荷(payload)
第三段:签证(signature)
(2)JWT的三段结构详解
第一段:头部header
jwt的头部承载两部分信息:
完整的头部就像下面这样的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.
JWT源码
JWT-Builder类的方法可以看到,如果默认不设置header时,由JWT底层默认
{
"typ":"JWT",
"alg":"HS256"//这个由具体的签名类型提供
}
public String sign(Algorithm algorithm) throws IllegalArgumentException, JWTCreationException {
if(algorithm == null) {
throw new IllegalArgumentException("The Algorithm cannot be null.");
} else {
this.headerClaims.put("alg", algorithm.getName());
this.headerClaims.put("typ", "JWT");
String signingKeyId = algorithm.getSigningKeyId();
if(signingKeyId != null) {
this.withKeyId(signingKeyId);
}
return (new JWTCreator(algorithm, this.headerClaims, this.payloadClaims)).sign();
}
}
第二部分:载荷(payload)
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
标准中注册的声明 (建议但不强制使用) :
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后将载荷进行base64加密(该加密是可以对称解密的),构成了第二部分.
第三段:签证(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.
连接成一个完整的字符串,构成了最终的jwt:
签名部分的生成:
private String sign() throws SignatureGenerationException {
String header = Base64.encodeBase64URLSafeString(this.headerJson.getBytes(StandardCharsets.UTF_8));
String payload = Base64.encodeBase64URLSafeString(this.payloadJson.getBytes(StandardCharsets.UTF_8));
//待签名的内容;header签名+载荷签名
String content = String.format("%s.%s", new Object[]{header, payload});
byte[] signatureBytes = this.algorithm.sign(content.getBytes(StandardCharsets.UTF_8));
String signature = Base64.encodeBase64URLSafeString(signatureBytes);
return String.format("%s.%s", new Object[]{content, signature});
}
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
(1)基于maven的项目使用
需要在pom.xml引用JWT的依赖
com.auth0
java-jwt
3.4.0
(2)JWT工具类
import com.alibaba.fastjson.JSON;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.apache.commons.codec.binary.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* Created by user on 2020/12/27.
*/
public class JwtUtil {
/**
* 过期时间5分钟
*/
private static final long EXPIRE_TIME = 5 * 60 * 1000;
/**
* jwt 密钥
*/
private static final String SECRET = "jwt_secret";
/**
* 生成签名,五分钟后过期
*
* @return
*/
public static String sign() {
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
Map headerMap = new HashMap();
headerMap.put("alg", "HS256");
headerMap.put("typ", "JWT");
return JWT.create()
.withHeader(headerMap)
// 将 user id 保存到 token 里面
//只能保留一个
.withAudience("zhengwei").withAudience("地址")
//.withAudience()
.withClaim("userId", "zhengwei") // payload
.withClaim("age", "30") // payload
.withClaim("phone", "1810824293X") // payload
// 五分钟后token过期
.withExpiresAt(date)
//.withNotBefore() 在这个时间之前,不能用
// token 的密钥
.sign(algorithm);
} catch (Exception e) {
return null;
}
}
/**
* 根据token获取userId
*
* @param token
* @return
*/
public static String getUserId(String token) {
try {
String userId = JWT.decode(token).getAudience().get(0);
return userId;
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 校验token
*
* @param token
* @return
*/
public static boolean checkSign(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
// .withClaim("username", username)
.build();
DecodedJWT jwt = verifier.verify(token);
System.out.println("header:" + jwt.getHeader());
System.out.println(JSON.toJSONString(jwt.getClaims()));
System.out.println(jwt.getClaims().get("userId").asString());
System.out.println(jwt.getClaims().get("age").asString());
System.out.println(jwt.getClaims().get("phone").asString());
System.out.println(jwt.getClaims().get("exp").asDate());
System.out.println(jwt.getAudience().get(0));
return true;
} catch (JWTVerificationException exception) {
System.out.println(exception.getMessage());
throw new RuntimeException("token 无效,请重新获取");
}
}
public static void main(String[] args) {
String token = JwtUtil.sign();
System.out.println("token:" + token);
boolean checkSign = JwtUtil.checkSign(token);
System.out.println("checkSign:" + checkSign);
System.out.println(new String(Base64.decodeBase64("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9")));
System.out.println(new String(Base64.decodeBase64("eyJhdWQiOiLlnLDlnYAiLCJwaG9uZSI6IjE4MTA4MjQyOTNYIiwiZXhwIjoxNjA5MDc0MjYyLCJ1c2VySWQiOiJ6aGVuZ3dlaSIsImFnZSI6IjMwIn0")));
System.out.println(new String(Base64.decodeBase64("DJLbf61nDoayJP5h3SpRm_VnIofGZ5lGflUQjR-vhR4")));
//JwtUtil.checkSign("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiLlnLDlnYAiLCJwaG9uZSI6IjE4MTA4MjQyOTNYIiwiZXhwIjoxNjA5MDc0MzIzLCJ1c2VySWQiOiJ6aGVuZ3dlaSIsImFnZSI6IjMwIn0.e0wqMwC6PBsPKqjTsRtU5BJivMn8p1sVyPpiTLnBnC8");
}
}
在spring-boot项目中集成JWT
(1)在登陆成功后,生成JWT的token返给前端
在controller的登录方法中,调用getSign生成token,具体载荷里面要放那些信息,根据实际需要
.withClaim("iss", "Service") .withClaim("aud", "APP")// payload 这样就表示在载荷中存放了iss和aud
通过JWT源码分析,有些key只能是唯一的(key不可重复)
KEY:
kid keyId
iss 签发人issuer
sub jwt所面向的用户,一般我们可以设置为APP、PC、WAP、XCX等,用于标识用户渠道来源
aud 接收jwt的一方
exp jwt的过期时间,这个过期时间必须要大于签发时间
nbf 定义在什么时间之前,该jwt都是不可用的.
iat jwt的签发时间
jti jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
public JWTCreator.Builder withKeyId(String keyId) {
this.headerClaims.put("kid", keyId);
return this;
}
public JWTCreator.Builder withIssuer(String issuer) {
this.addClaim("iss", issuer);
return this;
}
public JWTCreator.Builder withSubject(String subject) {
this.addClaim("sub", subject);
return this;
}
public JWTCreator.Builder withAudience(String... audience) {
this.addClaim("aud", audience);
return this;
}
public JWTCreator.Builder withExpiresAt(Date expiresAt) {
this.addClaim("exp", expiresAt);
return this;
}
public JWTCreator.Builder withNotBefore(Date notBefore) {
this.addClaim("nbf", notBefore);
return this;
}
public JWTCreator.Builder withIssuedAt(Date issuedAt) {
this.addClaim("iat", issuedAt);
return this;
}
public JWTCreator.Builder withJWTId(String jwtId) {
this.addClaim("jti", jwtId);
return this;
}
示例:
public static String sign(String userId) {
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
return JWT.create()
// 将 user id 保存到 token 里面
.withAudience(userId)
// 五分钟后token过期
.withExpiresAt(date)
// token 的密钥
.sign(algorithm);
} catch (Exception e) {
return null;
}
}
(2)在请求拦截器中去拦截请求,做token验证判断
定义拦截器InterceptorConfig
//config作为一个配置类
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
//添加过滤器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
//要拦截的请求
.addPathPatterns("/**"); // 拦截所有请求
}
//声明一个过滤器
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
过滤器类编写
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//检查是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(UserLoginToken.class)) {
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()) {
// 执行认证
if (token == null) {
throw new RuntimeException("无token,请重新登录");
}
// 获取 token 中的 user id
//这里调用JWTutil方法进行验签
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new RuntimeException("401");
}
// User user = userService.findUserById(userId);
// if (user == null) {
// throw new RuntimeException("用户不存在,请重新登录");
// }
// 验证 token user.getPassword()
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("")).build();
try {
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new RuntimeException("401");
}
return true;
}
}
return true;
}
}
JWT验签会返回常用四种异常,根据具体的异常类型做出不同的提示
1、签名不匹配
throw new AlgorithmMismatchException
2、签名异常
throw new SignatureVerificationException
3、载荷中载体参数类型格式化异常
throw new InvalidClaimException
4、token过期
throw new TokenExpiredException
全部异常
JWT抽象类
public abstract class JWT {
public JWT() {
}
public static DecodedJWT decode(String token) throws JWTDecodeException {
return new JWTDecoder(token);
}
public static Verification require(Algorithm algorithm) {
return JWTVerifier.init(algorithm);
}
//对于create来讲,这里使用的工厂方法,在创建一个Builder
public static Builder create() {
return JWTCreator.init();
}
}
JWTCreator类
public final class JWTCreator {
//初始化JWT,其实就是创建了一个Builder,这个builder存储JWT相关所有的东西
static JWTCreator.Builder init() {
return new JWTCreator.Builder();
}
}
Builder为JWTCreator的内部类
public static class Builder {
//存储载荷内容的map属性
private final Map payloadClaims = new HashMap();
//存储heander内容的map属性
private Map headerClaims = new HashMap();
Builder() {
}
public JWTCreator.Builder withHeader(Map headerClaims) {
this.headerClaims = new HashMap(headerClaims);
return this;
}
public JWTCreator.Builder withKeyId(String keyId) {
this.headerClaims.put("kid", keyId);
return this;
}
public JWTCreator.Builder withIssuer(String issuer) {
this.addClaim("iss", issuer);
return this;
}
public JWTCreator.Builder withSubject(String subject) {
this.addClaim("sub", subject);
return this;
}
public JWTCreator.Builder withAudience(String... audience) {
this.addClaim("aud", audience);
return this;
}
public JWTCreator.Builder withExpiresAt(Date expiresAt) {
this.addClaim("exp", expiresAt);
return this;
}
public JWTCreator.Builder withNotBefore(Date notBefore) {
this.addClaim("nbf", notBefore);
return this;
}
public JWTCreator.Builder withIssuedAt(Date issuedAt) {
this.addClaim("iat", issuedAt);
return this;
}
public JWTCreator.Builder withJWTId(String jwtId) {
this.addClaim("jti", jwtId);
return this;
}
public JWTCreator.Builder withClaim(String name, Boolean value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
public JWTCreator.Builder withClaim(String name, Integer value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
public JWTCreator.Builder withClaim(String name, Long value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
public JWTCreator.Builder withClaim(String name, Double value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
public JWTCreator.Builder withClaim(String name, String value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
public JWTCreator.Builder withClaim(String name, Date value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
public JWTCreator.Builder withArrayClaim(String name, String[] items) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, items);
return this;
}
public JWTCreator.Builder withArrayClaim(String name, Integer[] items) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, items);
return this;
}
public JWTCreator.Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, items);
return this;
}
public String sign(Algorithm algorithm) throws IllegalArgumentException, JWTCreationException {
if(algorithm == null) {
throw new IllegalArgumentException("The Algorithm cannot be null.");
} else {
this.headerClaims.put("alg", algorithm.getName());
this.headerClaims.put("typ", "JWT");
String signingKeyId = algorithm.getSigningKeyId();
if(signingKeyId != null) {
this.withKeyId(signingKeyId);
}
return (new JWTCreator(algorithm, this.headerClaims, this.payloadClaims)).sign();
}
}
private void assertNonNull(String name) {
if(name == null) {
throw new IllegalArgumentException("The Custom Claim\'s name can\'t be null.");
}
}
private void addClaim(String name, Object value) {
if(value == null) {
this.payloadClaims.remove(name);
} else {
this.payloadClaims.put(name, value);
}
}
}