视频教程传送门:JWT 两小时极简入门:JWT实战应用与防坑指南~_哔哩哔哩_bilibiliJWT 两小时极简入门:JWT实战应用与防坑指南~共计12条视频,包括:01.课程介绍与前置知识点、02.JWT概念、03.JWT组成等,UP主更多精彩视频,请关注UP账号。https://www.bilibili.com/video/BV1gk4y177DS/?vd_source=aced8a6167c51fbe68b3d65b157734eb
一、前置知识点
本课程对JWT讲解涉及到以下相关知识点,需要提前准备:
-
Java Web
-
Spring/SpringMVC/SpringBoot
-
Spring Security
二、JWT介绍
2.1 概念
官网:JSON Web Tokens - jwt.io
整理一下:
JSON Web Token,简称 JWT,读音是 [dʒɒt]( jot 的发音),是一个基于 RFC 7519 的开放数据标准,它定义了一种宽松且紧凑的数据组合方式。其作用是:JWT是一种加密后数据载体,可在各应用之间进行数据传输。
JWT中一般涵盖了用户身份信息,每次访问时,server校验信息合法性即可。
2.2 JWT组成
一个 JWT 通常有 HEADER (头),PAYLOAD (有效载荷)和 SIGNATURE (签名)三个部分组成,三者之间使用“.”链接,格式如下:
header.payload.signature
一个简单的JWT案例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 //header
.eyJ1c2VyX2luZm8iOlt7ImlkIjoiMSJ9LHsibmFtZSI6ImRhZmVpIn0seyJhZ2UiOiIxOCJ9XSwiaWF0IjoxNjgxNTcxMjU3LCJleHAiOjE2ODI3ODM5OTksImF1ZCI6InhpYW9mZWkiLCJpc3MiOiJkYWZlaSIsInN1YiI6ImFsbHVzZXIifQ //payload
.v1TxJ0mngnVx4t9O3uibAHPSLUyMM7sUM06w8ODYjuE //signature
注意三者之间有一个点号(“.”)相连
2.2.1 Header组成
JWT的头部承载两部分信息:
-
声明类型,默认是JWT
-
声明加密的算法 常用的算法:HMAC 、RSA、ECDSA等
{
"alg": "HS256",
"typ": "JWT"
}
alg:表示签名的算法,默认是 HMAC SHA256(写成 HS256);
typ: 表示令牌(token)的类型,JWT 令牌统一写为 JWT
。
使用Base64加密,构成了JWT第一部分-header:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2.2.2 Payload组成
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的有效信息。
标准载荷:有很多,建议使用,但不强制,对JWT信息作补充。
标准载荷 | 介绍 |
---|---|
iss (issuer) | 签发人(谁签发的) |
exp (expiration time) | 过期时间,必须要大于签发时间 |
sub (subject) | 主题(用来做什么) |
aud (audience) | 受众(给谁用的)比如:http://www.xxx.com |
nbf (Not Before) | 生效时间 |
iat (Issued At) | 签发时间 |
jti (JWT ID) | 编号,JWT 的唯一身份标识 |
自定义载荷:可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。
{
"user_info": [
{
"id": "1"
},
{
"name": "dafei"
},
{
"age": "18"
}
],
"iat": 1681571257,
"exp": 1682783999,
"aud": "xiaofei",
"iss": "dafei",
"sub": "alluser"
}
使用Base64加密,构成了JWT第二部分-payload:
eyJ1c2VyX2luZm8iOlt7ImlkIjoiMSJ9LHsibmFtZSI6ImRhZmVpIn0seyJhZ2UiOiIxOCJ9XSwiaWF0IjoxNjgxNTcxMjU3LCJleHAiOjE2ODI3ODM5OTksImF1ZCI6InhpYW9mZWkiLCJpc3MiOiJkYWZlaSIsInN1YiI6ImFsbHVzZXIifQ
2.2.3 signature组成
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.
)分隔,就可以返回给用户。
因为有这个密钥的存在,所以即便调用方偷偷的修改了前两部分的内容,在验证环节就会出现签名不一致的情况,所以保证了安全性。
使用Base64加密,构成了JWT第三部分-signature:
l6JdYARw4IHmjliSbh9NP6ji1L15qVneWTJU5noQ-k8
2.3 在线生成/解析JWT
2.3.1 编码工具
地址:JWT Token在线编码生成 - ToolTT在线工具箱
2.3.2 解码工具
地址:JWT在线解码 - 开发工具箱
三、JWT特点
3.1 无状态
JWT 不需要在服务端存储任何状态,客户端可以携带 JWT 来访问服务端,从而使服务端变得无状态。这样,服务端就可以更轻松地实现扩展和负载均衡。
3.2 可自定义
JWT 的载荷部分可以自定义,可以存储任何 JSON 格式的数据。这意味着我们可以使用 JWT 来实现一些自定义的功能,例如存储用户喜好、配置信息等等。
3.3 扩展性强
JWT 有一套标准规范,因此很容易在不同平台和语言之间共享和解析。此外,开发人员可以根据需要自定义声明(claims)来实现更加灵活的功能。
3.4 调试性好
由于 JWT 的内容是以 Base64 编码后的字符串形式存在的,因此非常容易进行调试和分析。
3.5 安全性取决于密钥管理
JWT 的安全性取决于密钥的管理。如果密钥被泄露或者被不当管理,那么 JWT 将会受到攻击。因此,在使用 JWT 时,一定要注意密钥的管理,包括生成、存储、更新、分发等等。
3.6 无法撤销
由于 JWT 是无状态的,一旦 JWT 被签发,就无法撤销。如果用户在使用 JWT 认证期间被注销或禁用,那么服务端就无法阻止该用户继续使用之前签发的 JWT。因此,开发人员需要设计额外的机制来撤销 JWT,例如使用黑名单或者设置短期有效期等等。
3.7 需要缓存到客户端
由于 JWT 包含了用户信息和授权信息,一般需要客户端缓存,这意味着 JWT 有被窃取的风险。
3.8 载荷大小有限制
由于 JWT 需要传输到客户端,因此载荷大小也有限制。一般不建议载荷超过 1KB,会影响性能。
四、JWT优缺点
4.1 优点
-
无状态:JWT 本身不需要存储在服务器上,因此可以实现无状态的身份验证和授权。
-
可扩展性:JWT 的载荷可以自定义,因此可以根据需求添加任意信息。
-
可靠性:JWT 使用数字签名来保证安全性,因此具有可靠性。
-
跨平台性:JWT 支持多种编程语言和操作系统,因此具有跨平台性。
-
高效性:由于 JWT 不需要查询数据库,因此具有高效性。
4.2 缺点
-
安全性取决于密钥管理:JWT 的安全性取决于密钥的管理,如果密钥被泄露或者被不当管理,那么 JWT 将会受到攻击。
-
无法撤销令牌:由于 JWT 是无状态的,一旦 JWT 被签发,就无法撤销。
-
需要传输到客户端:由于 JWT 包含了用户信息和授权信息,因此 JWT 需要传输到客户端,这意味着 JWT 有被攻击者窃取的风险。
-
载荷大小有限制:由于 JWT 需要传输到客户端,因此载荷大小也有限制。
五、JWT应用场景
5.1 一次性验证
用户注册成功后发一份激活邮件或者其他业务需要邮箱激活操作,都是可以使用jwt。
原因:
JWT时效性:让该链接具有时效性(比如约定2小时内激活),
JWT不可篡改性:防止篡改以激活其他账户
5.2 RESTful API 的无状态认证
使用 JWT 来做 RESTful api 的身份凭证:当用户身份校验成功,客户端每次接口访问都带上JWT,服务端校验JWT合法性(是否篡改/是否过期等)
5.3 信息交换
JWT是在各方(项目间/服务间)之间安全传输信息的好方式。 因为JWT可以签名:例如使用公钥/私钥对,所以可以确定发件人是他们自称的人。 此外,由于使用标头和有效载荷计算签名,因此您还可以验证内容是否未被篡改。
5.4 JWT令牌登录
JWT 令牌登录也是一种应用场景,但也是JWT被诟病最多的地方,因为JWT令牌存在各种不安全。
1>JWT令牌存储与客户端,容易泄露并被伪造身份搞破坏。
2>JWT 被签发,就无法撤销,当破坏在进行时,后端无法马上禁止。
上面问题可通过监控异常JWT访问,设置黑名单 + 强制下线等方式尽量避免损失。
六、JWT实战案例
6.1 案例1:邮件激活
6.1.1 需求与分析
需求:当用户注册成功,给指定邮箱发送一个激活链接,当用户点击激活链接后,激活账号。
分析:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
1>设计一个注册接口/regist,请求成功后模拟下发激活链接(链接本质是激活接口参数为:jwt)
2>设计激活接口/active,接收参数为jwt
6.1.2 代码实现
步骤1:创建项目:mail-active-demo
步骤2:导入相关依赖
具体选哪个JWT工具包,可以看官网推荐:JSON Web Token Libraries - jwt.io
此处选择:jjwt
步骤3:代码编写
用户注册实体类
package com.langfeiyes.mail.entity;
public class User {
private Long id;
private String username;
private String password;
private int state;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
}
JWT常量类:
package com.langfeiyes.mail.util;
/**
* 常量类
*/
public class JwtConstant {
// 基本url
public static final String BASE_DOMAIN_URL = "http://localhost:8080/";
//jwt密码
public static final String JWT_SECRET = "langfeiyesabcdefghijklmnopqrstuvwxyz11111111111";
//jwt失效时间,单位秒
public static final Long JWT_EXPIRATION = 24 * 60 * 60 * 1000L;
//jwt 创建时间
public static final String JWT_CREATE_TIME = "jwt_create_time";
//jwt 用户信息-key
public static final String USER_INFO_KEY = "user_info_key";
//jwt 用户信息-id
public static final String USER_INFO_ID = "user_info_id";
//jwt 用户信息-username
public static final String USER_INFO_USERNAME = "user_info_username";
}
JWT工具类:
package com.langfeiyes.mail.util;
import com.langfeiyes.mail.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JwtToken工具类
*/
public class JwtTokenUtil {
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
public static String createToken(Map<String, Object> claims) {
String token = Jwts.builder()
//.setHeader(new HashMap<>())
//.setAudience("Audience")
//.setIssuer("Issuer")
//.setSubject("Subject")
//.setNotBefore(new Date())
//.setIssuedAt(new Date())
//.setId("jwt id")
.setClaims(claims)//把荷载存储到里面
.setExpiration(generateExpirationDate())//设置失效时间
.signWith(Keys.hmacShaKeyFor(Decoders.BASE64.decode(JwtConstant.JWT_SECRET))) //签名
.compact();
return token;
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
public static Claims parseToken(String token){
Claims claims=null;
try{
claims = Jwts.parserBuilder()
.setSigningKey(Decoders.BASE64.decode(JwtConstant.JWT_SECRET))
.build()
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
e.printStackTrace();
}
return claims;
}
/**
* 生成token失效时间
*/
private static Date generateExpirationDate() {
//失效时间是当前系统的时间+我们在配置文件里定义的时间
return new Date(System.currentTimeMillis()+JwtConstant.JWT_EXPIRATION);
}
/**
* 根据token获取用户名
*/
public static String getUserName(String token){
Claims claims = parseToken(token);
return getValue(claims, JwtConstant.USER_INFO_USERNAME);
}
/**
* 验证token是否有效
*/
public static boolean validateToken(String token){
//claims 为null 意味着要门jwt被修改
Claims claims = parseToken(token);
return claims != null &&!isTokenExpired(token);
}
/**
* 判断token是否已经失效
* @param token
* @return
*/
public static boolean isTokenExpired(String token) {
//先获取之前设置的token的失效时间
Date expireDate=getExpiredDate(token);
return expireDate.before(new Date()); //判断下,当前时间是都已经在expireDate之后
}
/**
* 根据token获取失效时间
* 也是先从token中获取荷载
* 然后从荷载中拿到到设置的失效时间
* @param token
* @return
*/
private static Date getExpiredDate(String token) {
Claims claims=parseToken(token);
return claims.getExpiration();
}
/**
* 刷新我们的token:重新构建jwt
*/
public static String refreshToken(String token){
Claims claims=parseToken(token);
claims.put(JwtConstant.JWT_CREATE_TIME,new Date());
return createToken(claims);
}
/**
* 根据身份信息获取键值
*
* @param claims 身份信息
* @param key 键
* @return 值
*/
public static String getValue(Claims claims, String key){
return claims.get(key) != null ? claims.get(key).toString():null;
}
}
接口:
package com.langfeiyes.mail.controller;
import com.langfeiyes.mail.util.JwtConstant;
import com.langfeiyes.mail.entity.User;
import com.langfeiyes.mail.util.JwtTokenUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
@RestController
public class UserController {
//模拟需要缓存的用户库
//key: url, value: 要激活用户
private static Map<String, User> map = new HashMap<>();
@GetMapping("/regist")
public String regist(User user){
//假装成功
System.out.println("注册成功");
user.setId(new Random().nextLong());
//创建jwt
Map<String, Object> claims = new HashMap<>();
claims.put(JwtConstant.USER_INFO_ID, user.getId());
claims.put(JwtConstant.USER_INFO_USERNAME, user.getUsername());
claims.put(JwtConstant.JWT_CREATE_TIME, new Date());
String jwt = JwtTokenUtil.createToken(claims);
//缓存jwt
map.put(jwt, user);
return JwtConstant.BASE_DOMAIN_URL + "/active?jwt=" + jwt;
}
@GetMapping("/active")
public String active(String jwt){
User user = map.get(jwt);
if(user != null && JwtTokenUtil.validateToken(jwt)){
map.remove(jwt);
return "执行激活逻辑...";
}else{
return "参数不合法...";
}
}
}
步骤4:启动项目
package com.langfeiyes.mail;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
步骤5:测试
浏览器发起2个请求:
注册
http://localhost:8080/regist?username=dafei&password=666
激活
http://localhost:8080//active?jwt=eyJhbGciOiJIUzUxMiJ9.eyJjcmVhdGVfdGltZSI6MTY4MTYxNzA2MDg3NSwiaWQiOjEsInVzZXJuYW1lIjoiZGFmZWkiLCJleHAiOjE2ODE3MDM0NjB9.vQcsXUaEictz3QgjUBKwAV1qlou9yFCSMo4H6OaArz1ReEFzXt6klziHqonvsEfkv9aYdDc6G-vKVO9Zh1kcXw
6.2 案例2:JWT令牌登录
6.2.1 需求与分析
需求:设计/login 与 /list 2个接口实现登录与列表逻辑,注意访问/list接口必须进行登录校验
要求:使用Spring security + JWT
分析:
1>设计2个接口,/login登录成功创建JWT响应到客户端
2>设计登录检查拦截器,当访问/list接口时进行登录拦截
代码设计:
6.2.2 代码实现
步骤1:创建项目:security-jwt-demo
步骤2:导入相关依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
步骤3:编写代码
实体类:User--登录主体
package com.langfeiyes.jwt.entity;
public class User {
private Long id;
private String username;
private String password;
private int state;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
}
常量类:JwtConstant--配置jwt密码,过期时间
package com.langfeiyes.jwt.util;
/**
* 常量类
*/
public class JwtConstant {
// 基本url
public static final String BASE_DOMAIN_URL = "http://localhost:8080/";
//jwt密码
public static final String JWT_SECRET = "langfeiyesabcdefghijklmnopqrstuvwxyz11111111111";
//jwt失效时间,单位秒
public static final Long JWT_EXPIRATION = 24 * 60 * 60 * 1000L;
//jwt 创建时间
public static final String JWT_CREATE_TIME = "jwt_create_time";
//jwt 用户信息-key
public static final String USER_INFO_KEY = "user_info_key";
//jwt 用户信息-id
public static final String USER_INFO_ID = "user_info_id";
//jwt 用户信息-username
public static final String USER_INFO_USERNAME = "user_info_username";
}
工具类-JwtTokenUtil--JWT方法操作
package com.langfeiyes.jwt.util;
import com.langfeiyes.jwt.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JwtToken工具类
*/
public class JwtTokenUtil {
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
public static String createToken(Map<String, Object> claims) {
String token = Jwts.builder()
//.setHeader(new HashMap<>())
//.setAudience("Audience")
//.setIssuer("Issuer")
//.setSubject("Subject")
//.setNotBefore(new Date())
//.setIssuedAt(new Date())
//.setId("jwt id")
.setClaims(claims)//把荷载存储到里面
.setExpiration(generateExpirationDate())//设置失效时间
.signWith(Keys.hmacShaKeyFor(Decoders.BASE64.decode(JwtConstant.JWT_SECRET))) //签名
.compact();
return token;
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
public static Claims parseToken(String token){
Claims claims=null;
try{
claims = Jwts.parserBuilder()
.setSigningKey(Decoders.BASE64.decode(JwtConstant.JWT_SECRET))
.build()
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
e.printStackTrace();
}
return claims;
}
/**
* 生成token失效时间
*/
private static Date generateExpirationDate() {
//失效时间是当前系统的时间+我们在配置文件里定义的时间
return new Date(System.currentTimeMillis()+JwtConstant.JWT_EXPIRATION);
}
/**
* 根据token获取用户名
*/
public static String getUserName(String token){
Claims claims = parseToken(token);
return getValue(claims, JwtConstant.USER_INFO_USERNAME);
}
/**
* 验证token是否有效
*/
public static boolean validateToken(String token){
//claims 为null 意味着要门jwt被修改
Claims claims = parseToken(token);
return claims != null &&!isTokenExpired(token);
}
/**
* 判断token是否已经失效
* @param token
* @return
*/
public static boolean isTokenExpired(String token) {
//先获取之前设置的token的失效时间
Date expireDate=getExpiredDate(token);
return expireDate.before(new Date()); //判断下,当前时间是都已经在expireDate之后
}
/**
* 根据token获取失效时间
* 也是先从token中获取荷载
* 然后从荷载中拿到到设置的失效时间
* @param token
* @return
*/
private static Date getExpiredDate(String token) {
Claims claims=parseToken(token);
return claims.getExpiration();
}
/**
* 刷新我们的token:重新构建jwt
*/
public static String refreshToken(String token){
Claims claims=parseToken(token);
claims.put(JwtConstant.JWT_CREATE_TIME,new Date());
return createToken(claims);
}
/**
* 根据身份信息获取键值
*
* @param claims 身份信息
* @param key 键
* @return 值
*/
public static String getValue(Claims claims, String key){
return claims.get(key) != null ? claims.get(key).toString():null;
}
}
接口响应类-R
package com.langfeiyes.jwt.util;
public class R {
private int code;
private String msg;
private Object data;
public R() {
}
public R(int code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public static R ok(){
return new R(200, "操作成功", null);
}
public static R ok(Object data){
return new R(200, "操作成功", data);
}
public static R fail(){
return new R(500, "操作失败", null);
}
public static R fail(Object data){
return new R(500, "操作失败", data);
}
public static R fail(String msg){
return new R(500, msg, null);
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
public Object getData() {
return data;
}
}
登录过滤器:JwtLoginFilter--做jwt登录
package com.langfeiyes.jwt.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.langfeiyes.jwt.util.JwtConstant;
import com.langfeiyes.jwt.util.JwtTokenUtil;
import com.langfeiyes.jwt.util.R;
import org.springframework.security.core.Authentication;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JwtLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "GET"));
}
/**
* 拦截登录。获取表单的用户名与密码
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//使用 请求体 传递登录参数,更加安全
String username = request.getParameter("username");
String password = request.getParameter("password");
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
/**
* 登录成功后调用的方法
* 返回token
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//security 登录成功封装实体验证
UserDetails userDetails = (UserDetails) authResult.getPrincipal();
//根据用户名生成token
//创建jwt
Map<String, Object> claims = new HashMap<>();
claims.put(JwtConstant.USER_INFO_USERNAME, userDetails.getUsername());
claims.put(JwtConstant.JWT_CREATE_TIME, new Date());
String jwt = JwtTokenUtil.createToken(claims);
String token = JwtTokenUtil.createToken(claims);
response.setHeader("token", token);
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(new ObjectMapper().writeValueAsString(R.ok("登录成功")));
}
/**
* 登录失败后调用的方法
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(new ObjectMapper().writeValueAsString(R.fail(failed.getMessage())));
}
}
登录拦截过滤器-JwtAuthFilter--登录检查--权限检查
package com.langfeiyes.jwt.filter;
import com.langfeiyes.jwt.util.JwtTokenUtil;
import io.jsonwebtoken.Jwt;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
public class JwtAuthFilter extends BasicAuthenticationFilter {
public JwtAuthFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
/**
* 对HTTP请求头做处理
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//授权
UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
//授权失败
if (authRequest == null) {
chain.doFilter(request, response);
return;
}
//如果有授权,放到权限上下文(容器)中
SecurityContextHolder.getContext().setAuthentication(authRequest);
chain.doFilter(request, response);
}
/**
* 认证token是否合法,若合法,返回认证,否则返回null
*/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
//获取token
String token = request.getHeader("token");
if (!StringUtils.isEmpty(token) && JwtTokenUtil.validateToken(token)) {
//从token中获取username
String username = JwtTokenUtil.getUserName(token);
return new UsernamePasswordAuthenticationToken(username, token, Arrays.asList(new SimpleGrantedAuthority("admin")));
}
return null;
}
}
security整体配置类-JwtWebSecurityConfig
package com.langfeiyes.jwt.config;
import com.langfeiyes.jwt.filter.JwtAuthFilter;
import com.langfeiyes.jwt.filter.JwtLoginFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class JwtWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("dafei")
.password("666")
.roles("admin")
.and()
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.exceptionHandling()
.and().authorizeRequests()
.antMatchers("/login").permitAll() //所有请求都可以访问
.antMatchers("/list").authenticated()
.and().logout()
.and()
.addFilter(new JwtLoginFilter(authenticationManager())) //登录时的过滤器
.addFilter(new JwtAuthFilter(authenticationManager())) //验证JWT的过滤器
.httpBasic();
}
}
访问接口-UserController
package com.langfeiyes.jwt.controller;
import com.langfeiyes.jwt.util.R;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@GetMapping("/list")
public R list(){
return R.ok("list....");
}
}
步骤4:启动项目
package com.langfeiyes.jwt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
步骤5:测试
GET http://localhost:8080/login?username=dafei&password=666
<> 2023-04-16T230507.200.json
###
GET http://localhost:8080/list
Accept: */*
token: eyJhbGciOiJIUzUxMiJ9.eyJjcmVhdGVfdGltZSI6MTY4MTY1NzUwNzE1NCwidXNlcm5hbWUiOiJkYWZlaSIsImV4cCI6MTY4MTc0MzkwN30.q8X7IisUgF8if299exu1jU-0hgZOFzgUABt9SynqQ2HdyVJJqfAZpywVmyvRLQ8-n5hLf-JtF2mjAbQBlfQZwg
<> 2023-04-16T231623.200.json
七、JWT避坑指南
7.1 Redis校验实现令牌泄露保护
因为JWT是无状态的,当JWT令牌颁发后,在有效时间内,是无法进行销毁,所以就存在很大隐患:令牌泄露
避坑:颁发JWT令牌时,在Redis中也缓存一份,当判定某个JWT泄露了,立即移除Redis中的JWT。当接口发起请求时,强制用户重新进行身份验证,直至验证成功。
7.2 临检JWT限制敏感操作
基于JWT无状态性,同时泄露可能很大,一些涉及到敏感数据变动,执行临检操作。
避坑:在涉及到诸如新增,修改,删除,上传,下载等敏感性操作时,强制检查用户身份,如手机验证码,扫描二维码等手段,确认操作者是用户本人。
7.3 异常JWT监控:超频识别与限制
当JWT令牌被盗取,一般会出现高频次的系统访问。针对这种情况,监控用户端在单位时间内的请求次数,当单位时间内的请求次数超出预定阈值值,则判定该用户JWT令牌异常。
避坑:当判断JWT令牌异常,直接进行限制(比如:IP限流,JWT黑名单等)。
7.4 地域检查杜绝JWT泄露可能
一般用户活动范围是固定,意味着JWT客户端访问IP相对固定,JWT泄露之后,可能会先异地登录的情况。
避坑:对JWT进行异地访问检查,有效时间内,IP频繁变动可判断为JWT泄露。
7.5 客户端区分检查防止JWT泄露
对于APP产品来说,一般客户端是固定的,基本为移动设备(APP,平板),可以结合设备机器码进行绑定。
避坑:将JWT与机器码绑定,存储与服务端,当客户端发起请求时,通过检查客户端的机器码与服务端机器码是否匹配判断JWT是否泄露。
7.6 JWT令牌保护:限时,限数,限频
JWT令牌泄露是无法避免,但是我们可以进行泄露识别,做好泄露后补救保证系统安全。
避坑:对客户端进行合理限制,比如限制每个客户端的 JWT 令牌数量、访问频率、JWT令牌时效等,以降低 JWT 令牌泄露的风险。
八、常见的面试题
问:什么是JWT?解释一下它的结构。
JWT是一种开放标准,用于在网络上安全地传输信息。它由三部分组成:头部、载荷和签名。头部包含令牌的元数据,载荷包含实际的信息(例如用户ID、角色等),签名用于验证令牌是否被篡改。
问:JWT的优点是什么?它与传统的session-based身份验证相比有什么优缺点?
JWT的优点包括无状态、可扩展、跨语言、易于实现和良好的安全性。相比之下,传统的session-based身份验证需要在服务端维护会话状态,使得服务端的负载更高,并且不适用于分布式系统。
问:在JWT的结构中,分别有哪些部分?每个部分的作用是什么?
JWT的结构由三部分组成:头部、载荷和签名。头部包含令牌类型和算法,载荷包含实际的信息,签名由头部、载荷和密钥生成。
问:JWT如何工作?从开始到验证过程的完整流程是怎样的?
JWT的工作流程分为三个步骤:生成令牌、发送令牌、验证令牌。在生成令牌时,服务端使用密钥对头部和载荷进行签名。在发送令牌时,将令牌发送给客户端。在验证令牌时,客户端从令牌中解析出头部和载荷,并使用相同的密钥验证签名。
问:什么是JWT的签名?为什么需要对JWT进行签名?如何验证JWT的签名?
JWT的签名是由头部、载荷和密钥生成的,用于验证令牌是否被篡改。签名使用HMAC算法或RSA算法生成。在验证JWT的签名时,客户端使用相同的密钥和算法生成签名,并将生成的签名与令牌中的签名进行比较。
问:什么是JWT的令牌刷新?为什么需要这个功能?
令牌刷新是一种机制,用于解决JWT过期后需要重新登录的问题。在令牌刷新中,服务端生成新的JWT,并将其发送给客户端。客户端使用新的JWT替换旧的JWT,从而延长令牌的有效期。
问:JWT是否加密?如果是,加密的部分是哪些?如果不是,那么它如何保证数据安全性?
JWT本身并不加密,但可以在载荷中包含敏感信息。为了保护这些信息,可以使用JWE(JSON Web Encryption)对载荷进行加密。如果不加密,则需要在生成JWT时确保不在载荷中包含敏感信息。
问:在JWT中,如何处理Token过期的问题?有哪些方法可以处理?
JWT过期后,客户端需要重新获取新的JWT。可以通过在JWT中包含过期时间或使用refresh token等机制来解决过期问题。
问:JWT和OAuth2有什么关系?它们之间有什么区别?
JWT和OAuth2都是用于身份验证和授权的开放标准。JWT是一种身份验证机制,而OAuth2是一种授权机制。JWT用于在不同的系统中安全地传输信息,OAuth2用于授权第三方应用程序访问受保护的资源。
问:JWT在什么场景下使用较为合适?它的局限性是什么?
JWT在单体应用或微服务架构中的使用比较合适。它的局限性包括无法撤销、令牌较大、无法处理并发等问题。在需要针对每次请求进行访问控制或需要撤销令牌的情况下,JWT可能不是最佳选择。