01JWT原理+token分析


JWT json web token

1.身份验证的两种方式

1.1、cookie+session

image-20210831155640723

原理过程:服务端产生session后,将sessionid通过cookie发送到客户端,客户端携带sessionid便能顺利找到session。
存在问题:集群环境下,如果访问没有session生成的机器,将会造成用户状态丢失;单点登录(一个系统如果有多个子系统,在其中一个子系统中登录,其他受信任的子系统无需再次登录,例如百度文库登陆后,百度知道不必再次登录)
解决方案:
session共享办法,集群搭建时可以配置,缺点:同步速度慢,消费性能。
session持久化,缺点:工程量较大,持久层挂掉依旧会造成用户状态丢失。
负载均衡 ip_hash:根据ip分配服务器,不会再访问别的服务器。

1.2、jwt验证机制

2、jwt验证机制

2.1工作原理:

image-20210829235809720

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。

1
2
3
4
5
{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2018年7月1日0点0分"
}

以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。

服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

2.2.JWT 的数据结构

实际的 JWT 大概就像下面这样。(一般都是签名后的token)

image-20210830000604883

它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。

JWT 的三个部分依次如下。

1
2
3
Header(头部)
Payload(负载)
Signature(签名)

写成一行,就是下面的样子。

1
Header.Payload.Signature	

2.2.1.Header

Header 部分是一个 JSON 对象,描述 JWT 的元数据(描述数据的数据),通常是下面的样子

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT

最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。

2.2.2.Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

1
2
3
4
5
6
7
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。

这个 JSON 对象也要使用 Base64URL 算法转成字符串。

2.2.3.Signature

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.)分隔,就可以返回给用户。

2.2.4.Base64URL

前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+/=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-/替换成_ 。这就是 Base64URL 算法。

2.3 springboot整合jwt

2.3.1pom文件中添加如下依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!--jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<!--解决jdk版本过高导致java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>

2.3.2全局配置文件application.properties

1
2
3
4
5
6
7
8
9
10
#jwt
#header:凭证(校验的变量名)
app.jwt.header = token
#过期时间,单位:s
app.jwt.expire = 7200

##设置密钥(随机字符串)
app.jwt.secret= luguodejiamianqishi


2.3.3 jwt配置文件

2.3.3.1Jwt.java(jwt类用以创建或初始化jwt)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package com.hybs.config;

import java.util.Date;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

/**
* JWT类

*/
@Component
@ConfigurationProperties(prefix = "app.jwt")
public class JWT {
private Logger logger = LoggerFactory.getLogger(getClass());

/**
* 加密秘钥
*/
private String secret;
/**
* 有效时间
*/
private long expire;
/**
* 用户凭证
*/
private String header;

/**
* 获取:加密秘钥
*/
public String getSecret() {
return secret;
}

/**
* 设置:加密秘钥
*/
public void setSecret(String secret) {
this.secret = secret;
}

/**
* 获取:有效期(s)
* */
public long getExpire() {
return expire;
}
/**
* 设置:有效期(s)
* */
public void setExpire(long expire) {
this.expire = expire;
}

/**
* 获取:凭证
* */
public String getHeader() {
return header;
}
/**
* 设置:凭证
* */
public void setHeader(String header) {
this.header = header;
}

/**
* 生成Token签名
* @param userId 用户ID
* @return 签名字符串

*/
public String generateToken(long userId) {
System.out.println("header=" + getHeader() + ", expire=" + getExpire() + ", secret=" + getSecret());
Date nowDate = new Date();
// 过期时间
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder().setHeaderParam("typ", "JWT").setSubject(String.valueOf(userId)).setIssuedAt(nowDate)
.setExpiration(expireDate).signWith(SignatureAlgorithm.HS512, getSecret()).compact();
// 注意: JDK版本高于1.8, 缺少 javax.xml.bind.DatatypeConverter jar包,编译出错
}

/**
* 获取签名信息
* @param token

*/
public Claims getClaimByToken(String token) {
try {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
logger.debug("validate is token error ", e);
return null;
}
}

/**
* 判断Token是否过期
* @param expiration
* @return true 过期

*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}

}
2.3.3.2JwtInterceptor (拦截器用以判断jwt是否有效以决定是否放行)

配置好拦截器后会以preHandle作为函数入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package com.hybs.config;

import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* Token验证拦截器
*/
@Component
public class JwtInterceptor extends HandlerInterceptorAdapter {

@Autowired
private JWT jwt;

public static final String USER_KEY = "userId";

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String servletPath = request.getServletPath();
System.out.println("ServletPath: " + servletPath);
// 不需要验证,直接放行
boolean isNotCheck = isNotCheck(servletPath);
if (isNotCheck) {
return true;
}
// 需要验证
String token = getToken(request);

if (StringUtils.isEmpty(token)) {
throw new RuntimeException(jwt.getHeader() + "失效,请重新登录");
}
// 获取签名用户信息
Claims claims = jwt.getClaimByToken(token);
System.out.println("TOKEN: " + claims);
// 判断签名是否存在或过期
boolean b = claims==null || claims.isEmpty() || jwt.isTokenExpired(claims.getExpiration());
if (b) {
throw new RuntimeException(jwt.getHeader() + "失效,请重新登录");
}
// 将签名中获取的用户信息放入request中;
request.setAttribute(USER_KEY, claims.getSubject());
return true;
}

/**
* 根据URL判断当前请求是否不需要校验, true:需要校验
*/
public boolean isNotCheck(String servletPath) {
// 若 请求接口 以 / 结尾, 则去掉 /
servletPath = servletPath.endsWith("/")
? servletPath.substring(0,servletPath.lastIndexOf("/"))
: servletPath;
System.out.println("servletPath = " + servletPath);
for (String path : NOT_CHECK_URL) {
System.out.println("path = " + path);
// path 以 /** 结尾, servletPath 以 path 前缀开头
if (path.endsWith("/**")) {
String pathStart = path.substring(0, path.lastIndexOf("/")+1);
System.out.println("pathStart = " + pathStart);
if (servletPath.startsWith(pathStart)) {
return true;
}
String pathStart2 = path.substring(0, path.lastIndexOf("/"));
System.out.println("pathStart2 = " + pathStart2);
if (servletPath.equals(pathStart2)) {
return true;
}
}
// servletPath == path
if (servletPath.equals(path)) {
return true;
}
}
return false;
}

/**
* 获取请求Token
*/
private String getToken(HttpServletRequest request) {
String token = request.getHeader(jwt.getHeader());
if (StringUtils.isEmpty(token)) {
token = request.getParameter(jwt.getHeader());
}
return token;
}

/**
* 不用拦截的页面路径(也可存入数据库中), 不要以 / 结尾
*/
private static final String[] NOT_CHECK_URL = {"/app/user/test/**", "/app/user/login/**"};

}
2.3.3.3WebConfig(springboot配置拦截器使拦截器生效)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private JwtInterceptor jwtInterceptor;

/**
* APP接口拦截器
* */
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor).addPathPatterns("/app/**");
}


}
2.3.3.4撰写控制器接口进行测试

基础控制器,根据当前用户token获取用户数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.hybs.config.JwtInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;

public abstract class BaseController {
protected Logger logger = LoggerFactory.getLogger(getClass());

/**
* 获取当前登录用户ID
*/
protected Long getUserId(HttpServletRequest request) {

return Long.parseLong(request.getAttribute(JwtInterceptor.USER_KEY).toString());
}
}

用户控制器,为新用户生成token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import com.hybs.config.JWT;
import com.hybs.util.HybsResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;

@RestController
@RequestMapping("/app/user")
public class AppUserController extends BaseController{

@Autowired
private JWT jwt;

/**
* 获取用户信息
* @return

*/
@GetMapping("info")
public HybsResult info(HttpServletRequest request) {
return HybsResult.ok(getUserId(request));
}

/**
* 用户登录
* @return

*/
@PostMapping("/login/{id}")
public HybsResult login(@PathVariable("id") Integer id) {
//生成token
String token = jwt.generateToken(id);
HashMap<String,Object> map = new HashMap<String,Object>();
map.put("expire", jwt.getExpire());
map.put("token", token);
return HybsResult.ok(map);
}
}

2.3 整体步骤

  1. 用户登录时生成token
  2. 用户再次访问时请求头携带token
  3. 拦截器拦截到请求,解析请求头,获取到token
  4. 判断签名信息是否有效
  5. 将用户信息抽出放入request中

注:token根据密钥进行签名
第三部分是签名信息不是签名,如密钥


文章作者: 哈雅布撒
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 哈雅布撒 !
  目录