JWT-认证原理
JWT
-认证原理
JWT
能做什么
-
授权
一旦用户登录,
每个后续请求将包含 JWT
,从而允许用户访问该令牌允许的路由,服务和资源, 单点登录是当今广泛使用 JWT
的一项功能, 因为他的开销很小并且可以在同的作用域中轻松使用 -
信息交换
JSON Web Token
是在各方之间安全地传输信息地好方法, 因为可以对 JWT
进行签名 (例如: 使用 公钥
),/ 私钥对 此外, 由于签名是使用标头和有效负载计算的, 因此可以验证内容是否遭到篡改
为什么是JWT
基于传统的Session
认证
-
Session
http
协议本身是一种无状态的协议, 而这就意味着如果用户向我们的应用提供了用户名和密码来进行认证, 那么下一次请求时, 用户还要再一次进行用户认证才行, 因为根据 http
协议, 我们并不知道是那个用户发出的请求, 所以为了让我们的应用能识别是那个用户发出的请求, 我们只能在服务器存储一份用户登录的信息, 这个登录信息会在响应时传递给浏览器, 告诉其保存在 cookie
,以便于下次请求时发送给我们的应用,这样我们的应用就能识别请求来自那个用户了, 这就是传统的基于 session
认证 -
认证流程
认证流程 -
发送请求将携带
cookie
cookie
-
产生问题
-
每个用户经过我们的应用认证之后,
我们的应用都要在服务端做一次记录, 以方便用户下次请求的鉴别, 通常而言 session
都是保存在内存中, 而随着认证用户的增多, 服务器的开销会明显增大 -
用户认证之后,
服务端做认证记录, 如果认证的记录被保存在内存中的化, 这意味着用户下次请求还必须要请求, 这样才能拿到授权的资源, 这样在分布的应用上, 相应的限制了负载均衡的能力, 这也意味着限制了应用的扩展能力 -
因为基于
cookie
来进行用户识别的, cookie
如果被获取, 用户很容易受到跨站请求伪造的攻击 -
在前后端分离系统中更加痛苦:
也就是说前后端分离在应用解耦后增加了部署的复杂性,
通常用户一次请求就要转发多次, 如果用 sessionid
到服务器, 服务器还要查询用户信息, 同时, 如果用户很多, 这些信息存储在服务器内存中, 给服务器增加了负担,还有就是 CSRF(跨站伪造请求攻击)
,session
是基于 cookie
进行用户识别的, cookie
如果被捕获了, 用户就很容易受到跨站伪造攻击, 还有就是 sessionid
就是一个特征值, 表达的信息不够丰富, 不容易扩展, 而且如果你后端应用是多节点部署, 那么就需要实现 session
共享机制, 不方便应用集群
-
-
基于 JWT 认证
-
认证流程图
JWT
-认证流程 -
认证流程
-
首先,
前端通过 web
表单将自己的用户名和密码发送到后端接口, 这一过程一般是 HTTP POST
请求, 建议的方式是用过 SSL
加密的传输 ( https
协议), 从而避免敏感的信息被嗅探 -
后端核对用户名和密码成功后,
将用户的 id
等其他信息作为 JWT Payload(负载)
,将其与头部分别进行 Base64
编码拼接后签名, 形成一个 JWT
,形成的JWT
是一个形同 xxx.xxx.xxx
的字符串 -
后端将
JWT
字符串作为登录成功的返回结果返回给前端, 前端可以将返回结果保存在 localStorage
或 sessionStorage 上, 退出登录时前端删除保存的 JWT
即可 -
前端在每次请求时将
JWT
放入 HTP Header
中的 Authorization(解决
XSS 和 XSRF 问题) -
验证通过后后端使用
JWT
中包含的用户信息进行其他逻辑操作, 返回响应的结果
-
-
JWT
的优势 - 简洁: 可以通过
URL、POST
参数或者在 HTTP Header
发送, 因为数据量小, 传输速度也很快 - 自包含:
负载中包含了所有用户所需的信息, 避免了多次查询数据库 - 因为
Token
是以 JSON
加密的形式保存在客户端的, 所以 JWT
是跨语言的, 原则上任何 web
形式都支持 - 不需要在服务端保存会话信息,
特别适用于分布式微服务
- 简洁: 可以通过
JWT
结构
-
令牌组成
- 标头
( Header
) - 有效载荷
( Payload
) - 签名
(Signature)
- 标头
-
Header
标头通常由两部分组成: 令牌的类型
(即 JWT
)和所使用的签名算法, 例如 MAC、SHA256、RSA
他会使用 Base64
编码组成 JWT
结构的第一部分 Base64
是一种编码, 也就是说, 它是可以被翻译回原来的样子的, 它并不是一种加密过程 1
2
3
4{
"alg": "HS256",
"typ": "JWT"
} -
Payload
令牌的第二部分是有效负载,
其中包含声明, 声明是有关实体 (通常是用户) 和其他数据的声明, 同样的, 它会使用 Base64
编码组成 JWT
结构的第二部分 -
Signature
前面两部分都是使用
Base64
进行编码的, 即前端可以解开知道里面的信息, Signature
需要使用编码后的 header
和 payload
以及我们提供的一个密钥, 然后使用 Base64
中指定的签名算法 ( HS265
)进行签名, 签名的作用是保证 JWT
没有被篡改 -
签名目的
最后一步签名的过程,
实际上是对头部以及负载内容进行签名, 防止内容被篡改, 如果有人对头部以及负载的内容解码之后进行修改, 再进行编码, 最后加上之前的签名组合形成新的 JWT
的话, 那么服务器端会判断出新的头部和负载形成的签名和 JWT
附带上的签名是不一样的, 如果要对新的头部和负载进行签名, 在不知道服务器加密时用密钥的话, 得出来的签名也是不一样的 -
信息安全问题
Base64
是可逆的, 那么信息不久被暴露了吗? 因此,
在 JWT
中, 不应该在负载里面加入任何敏感的数据
-
第一次使用JWT
-
添加依赖
1
2
3
4
5<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.2</version>
</dependency> -
创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/**
* 获取token
*/
void getToken() {
HashMap<String, Object> map = new HashMap<>();
Calendar instance = Calendar.getInstance();
// 20秒
instance.add(Calendar.SECOND, 20);
String token = JWT.create()
.withHeader(map) // header(可以省略)
.withClaim("userId", "xiaohong") // payload: 数据
.withExpiresAt(instance.getTime()) // 指定令牌的过期时间
.sign(Algorithm.HMAC256("coder-itl")); // 签名
System.out.println(token);
} -
验证
1
2
3
4
5
6
7
8
9
10/**
* 签名验证
*/
void signatureToken() {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("coder-itl")).build();
DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NTc4NjM5ODIsInVzZXJJZCI6InhpYW9ob25nIn0.e7-0F99BvA-q9GhH1DZB7ze9b35aaQ8iLkQ3JeQAVEo");
String userId = verify.getClaim("userId").asString();
System.out.println("userId = " + userId);
} -
常见异常
异常 说明 SignatureVerificationException
签名不一致异常 TokenExpiredException
令牌过期异常 AlgorithmMismatchException
算法不匹配异常 InvalidClaimException
失效的 payload
异常
封装工具类
-
工具类
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
43package com.coderitl.jwt.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import jdk.nashorn.internal.parser.Token;
import java.util.Calendar;
import java.util.Map;
public class JWTUtils {
/* 密钥数据 */
private static final String SIGN = "coder-itl";
/**
* 生成 token:
* header.payload.sing
*/
public static String getToken(Map<String, String> map) {
Calendar instance = Calendar.getInstance();
/* 过期时间: 默认7 天 */
instance.add(Calendar.DATE, 7);
/* 创建 JWT Builder */
JWTCreator.Builder builder = JWT.create();
/* payload */
map.forEach((k, v) -> {
builder.withClaim(k, v);
});
/* 生成 token 令牌,指定令牌过期时间 */
String token = builder.withExpiresAt(instance.getTime()).sign(Algorithm.HMAC256(SIGN));
return token;
}
/**
* 验证 token 合法性,异常抛出信息, 正确返回结果
*/
public static DecodedJWT verify(String token) {
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(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
41
42
43
44
45
46
47
48
49
50package com.coderitl.intercepter;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.coderitl.jwt.utils.JWTUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
public class JWTInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Map<String, Object> map = new HashMap<>();
// 获取请求头中的 map
String token = request.getHeader("token");
try {
// 验证令牌
JWTUtils.verify(token);
// 放行请求
return true;
} catch (SignatureVerificationException e) {
e.printStackTrace();
map.put("msg", "无效签名!");
} catch (TokenExpiredException e) {
e.printStackTrace();
map.put("msg", "token已过期!");
} catch (AlgorithmMismatchException e) {
e.printStackTrace();
map.put("msg", "token算法不一致!");
} catch (Exception e) {
e.printStackTrace();
// TODO: 消息未响应在页面
map.put("msg", "token无效!");
}
// 设置状态
map.put("status", false);
// 将 map 转换为 json jackson(@ResponseBody)
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(json);
return false;
}
} -
配置拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package com.coderitl.config;
import com.coderitl.intercepter.JWTInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
public class InterceptorConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
// 其他所有请求都需要携带
.addPathPatterns("/**")
// 生成令牌(排除此路径)
.excludePathPatterns("/user/login");
}
} -
控制器
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
60package com.coderitl.controller;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.coderitl.entity.User;
import com.coderitl.jwt.utils.JWTUtils;
import com.coderitl.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.security.SignatureException;
import java.util.HashMap;
import java.util.Map;
public class UserController {
private UserService service;
public Map<String, Object> login(User user) {
log.info("用户名: [{}]", user.getUsername());
log.info("密码: [{}]", user.getPassword());
Map<String, Object> map = new HashMap<>();
try {
User userDB = service.login(user);
Map<String, String> payload = new HashMap<>();
payload.put("username", userDB.getUsername());
payload.put("userId", String.valueOf(userDB.getId()));
String token = JWTUtils.getToken(payload);
// 响应 token
map.put("state", true);
map.put("msg", "登录成功!");
map.put("token", token);
} catch (Exception e) {
map.put("state", false);
map.put("msg", e.getMessage());
}
return map;
}
public Map<String, Object> carrytoken() {
Map<String, Object> map = new HashMap<>();
// 处理自己的业务逻辑
map.put("status", true);
map.put("msg", "请求成功!");
return map;
}
} -
测试
-
生成
token
生成 token
-
未携带
token
测试 未携带 token
测试 -
携带
token
测试 携带 token
-
单体项目认证流程
-
单体项目
session
单体项目认证流程 -
单体项目特点
在单体项目中,
视图资源和接口都在同一台服务器, 用户的多次请求都是基于用一个会话 session
,因此可以借助session
来进行用户认证判断
前后端分离项目认证流程
-
前端后分离项目
前端后分离项目
默认方法调用 (不需要创建工具类)
-
添加依赖
1
2
3
4
5
6
7
8
9
10<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency> -
创建拦截器
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
61package com.coderitl.intercepter;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.*;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
public class JWTInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getParameter("token");
if (token == null) {
// 提示请先登录
R r = new R();
r.error("请先登录");
doResponse(response, r);
} else {
try {
// 验证 token
JwtParser parser = Jwts.parser();
// 解析 token 的 setSigningKey 必须和生成的 token时设置密码一致
parser.setSigningKey("签名数据");
// 如果 token 正确(签名正确, 有效时间内) 则正常执行, 否则抛出异常
Jws<Claims> claimsJws = parser.parseClaimsJws(token);
// 放行请求
return true;
} catch (ExpiredJwtException e) {
e.printStackTrace();
R r = new R();
r.error("登录过期,请重新登录");
doResponse(response, r);
} catch (UnsupportedJwtException e) {
e.printStackTrace();
R r = new R();
r.error("token不合法, 请自重!");
doResponse(response, r);
} catch (Exception e) {
e.printStackTrace();
R r = new R();
r.error("请登录!");
doResponse(response, r);
}
}
return false;
}
private void doResponse(HttpServletResponse response, R<T> r) throws Exception {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
PrintWriter out = response.getWriter();
String s = new ObjectMapper().writeValueAsString(r);
out.println(s);
out.flush();
out.close();
}
} -
配置拦截器
1
...