本课程对JWT讲解涉及到以下相关知识点,需要提前准备:
Java Web
Spring/SpringMVC/SpringBoot
Spring Security
官网:JSON Web Tokens - jwt.io
整理一下:
JSON Web Token,简称 JWT,读音是 [dʒɒt]( jot 的发音),是一个基于 RFC 7519 的开放数据标准,它定义了一种宽松且紧凑的数据组合方式。其作用是:JWT是一种加密后数据载体,可在各应用之间进行数据传输。
JWT中一般涵盖了用户身份信息,每次访问时,server校验信息合法性即可。
一个 JWT 通常有 HEADER (头),PAYLOAD (有效载荷)和 SIGNATURE (签名)三个部分组成,三者之间使用“.”链接,格式如下:
header.payload.signature
一个简单的JWT案例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 //header
.eyJ1c2VyX2luZm8iOlt7ImlkIjoiMSJ9LHsibmFtZSI6ImRhZmVpIn0seyJhZ2UiOiIxOCJ9XSwiaWF0IjoxNjgxNTcxMjU3LCJleHAiOjE2ODI3ODM5OTksImF1ZCI6InhpYW9mZWkiLCJpc3MiOiJkYWZlaSIsInN1YiI6ImFsbHVzZXIifQ //payload
.v1TxJ0mngnVx4t9O3uibAHPSLUyMM7sUM06w8ODYjuE //signature
注意三者之间有一个点号(“.”)相连
JWT的头部承载两部分信息:
声明类型,默认是JWT
声明加密的算法 常用的算法:HMAC 、RSA、ECDSA等
{
"alg": "HS256",
"typ": "JWT"
}
alg:表示签名的算法,默认是 HMAC SHA256(写成 HS256);
typ: 表示令牌(token)的类型,JWT 令牌统一写为 JWT
。
使用Base64加密,构成了JWT第一部分-header:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
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
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.
)分隔,就可以返回给用户。
因为有这个密钥的存在,所以即便调用方偷偷的修改了前两部分的内容,在验证环节就会出现签名不一致的情况,所以保证了安全性。
使用Base64加密,构成了JWT第三部分-signature:
l6JdYARw4IHmjliSbh9NP6ji1L15qVneWTJU5noQ-k8
地址:JWT Token在线编码生成 - ToolTT在线工具箱
地址:JWT在线解码 - 开发工具箱
JWT 不需要在服务端存储任何状态,客户端可以携带 JWT 来访问服务端,从而使服务端变得无状态。这样,服务端就可以更轻松地实现扩展和负载均衡。
JWT 的载荷部分可以自定义,可以存储任何 JSON 格式的数据。这意味着我们可以使用 JWT 来实现一些自定义的功能,例如存储用户喜好、配置信息等等。
JWT 有一套标准规范,因此很容易在不同平台和语言之间共享和解析。此外,开发人员可以根据需要自定义声明(claims)来实现更加灵活的功能。
由于 JWT 的内容是以 Base64 编码后的字符串形式存在的,因此非常容易进行调试和分析。
JWT 的安全性取决于密钥的管理。如果密钥被泄露或者被不当管理,那么 JWT 将会受到攻击。因此,在使用 JWT 时,一定要注意密钥的管理,包括生成、存储、更新、分发等等。
由于 JWT 是无状态的,一旦 JWT 被签发,就无法撤销。如果用户在使用 JWT 认证期间被注销或禁用,那么服务端就无法阻止该用户继续使用之前签发的 JWT。因此,开发人员需要设计额外的机制来撤销 JWT,例如使用黑名单或者设置短期有效期等等。
由于 JWT 包含了用户信息和授权信息,一般需要客户端缓存,这意味着 JWT 有被窃取的风险。
由于 JWT 需要传输到客户端,因此载荷大小也有限制。一般不建议载荷超过 1KB,会影响性能。
无状态:JWT 本身不需要存储在服务器上,因此可以实现无状态的身份验证和授权。
可扩展性:JWT 的载荷可以自定义,因此可以根据需求添加任意信息。
可靠性:JWT 使用数字签名来保证安全性,因此具有可靠性。
跨平台性:JWT 支持多种编程语言和操作系统,因此具有跨平台性。
高效性:由于 JWT 不需要查询数据库,因此具有高效性。
安全性取决于密钥管理:JWT 的安全性取决于密钥的管理,如果密钥被泄露或者被不当管理,那么 JWT 将会受到攻击。
无法撤销令牌:由于 JWT 是无状态的,一旦 JWT 被签发,就无法撤销。
需要传输到客户端:由于 JWT 包含了用户信息和授权信息,因此 JWT 需要传输到客户端,这意味着 JWT 有被攻击者窃取的风险。
载荷大小有限制:由于 JWT 需要传输到客户端,因此载荷大小也有限制。
用户注册成功后发一份激活邮件或者其他业务需要邮箱激活操作,都是可以使用jwt。
原因:
JWT时效性:让该链接具有时效性(比如约定2小时内激活),
JWT不可篡改性:防止篡改以激活其他账户
使用 JWT 来做 RESTful api 的身份凭证:当用户身份校验成功,客户端每次接口访问都带上JWT,服务端校验JWT合法性(是否篡改/是否过期等)
JWT是在各方(项目间/服务间)之间安全传输信息的好方式。 因为JWT可以签名:例如使用公钥/私钥对,所以可以确定发件人是他们自称的人。 此外,由于使用标头和有效载荷计算签名,因此您还可以验证内容是否未被篡改。
JWT 令牌登录也是一种应用场景,但也是JWT被诟病最多的地方,因为JWT令牌存在各种不安全。
1>JWT令牌存储与客户端,容易泄露并被伪造身份搞破坏。
2>JWT 被签发,就无法撤销,当破坏在进行时,后端无法马上禁止。
上面问题可通过监控异常JWT访问,设置黑名单 + 强制下线等方式尽量避免损失。
需求:当用户注册成功,给指定邮箱发送一个激活链接,当用户点击激活链接后,激活账号。
分析:
org.springframework.boot
spring-boot-starter-parent
2.3.2.RELEASE
org.springframework.boot
spring-boot-starter-web
compile
io.jsonwebtoken
jjwt-api
0.11.5
io.jsonwebtoken
jjwt-impl
0.11.5
runtime
io.jsonwebtoken
jjwt-jackson
0.11.5
runtime
1>设计一个注册接口/regist,请求成功后模拟下发激活链接(链接本质是激活接口参数为:jwt)
2>设计激活接口/active,接收参数为jwt
步骤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 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 map = new HashMap<>();
@GetMapping("/regist")
public String regist(User user){
//假装成功
System.out.println("注册成功");
user.setId(new Random().nextLong());
//创建jwt
Map 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
需求:设计/login 与 /list 2个接口实现登录与列表逻辑,注意访问/list接口必须进行登录校验
要求:使用Spring security + JWT
分析:
1>设计2个接口,/login登录成功创建JWT响应到客户端
2>设计登录检查拦截器,当访问/list接口时进行登录拦截
代码设计:
步骤1:创建项目:security-jwt-demo
步骤2:导入相关依赖
org.springframework.boot
spring-boot-starter-parent
2.3.2.RELEASE
org.springframework.boot
spring-boot-starter-web
compile
org.springframework.boot
spring-boot-starter-security
io.jsonwebtoken
jjwt-api
0.11.5
io.jsonwebtoken
jjwt-impl
0.11.5
runtime
io.jsonwebtoken
jjwt-jackson
0.11.5
runtime
步骤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 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 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是无状态的,当JWT令牌颁发后,在有效时间内,是无法进行销毁,所以就存在很大隐患:令牌泄露
避坑:颁发JWT令牌时,在Redis中也缓存一份,当判定某个JWT泄露了,立即移除Redis中的JWT。当接口发起请求时,强制用户重新进行身份验证,直至验证成功。
基于JWT无状态性,同时泄露可能很大,一些涉及到敏感数据变动,执行临检操作。
避坑:在涉及到诸如新增,修改,删除,上传,下载等敏感性操作时,强制检查用户身份,如手机验证码,扫描二维码等手段,确认操作者是用户本人。
当JWT令牌被盗取,一般会出现高频次的系统访问。针对这种情况,监控用户端在单位时间内的请求次数,当单位时间内的请求次数超出预定阈值值,则判定该用户JWT令牌异常。
避坑:当判断JWT令牌异常,直接进行限制(比如:IP限流,JWT黑名单等)。
一般用户活动范围是固定,意味着JWT客户端访问IP相对固定,JWT泄露之后,可能会先异地登录的情况。
避坑:对JWT进行异地访问检查,有效时间内,IP频繁变动可判断为JWT泄露。
对于APP产品来说,一般客户端是固定的,基本为移动设备(APP,平板),可以结合设备机器码进行绑定。
避坑:将JWT与机器码绑定,存储与服务端,当客户端发起请求时,通过检查客户端的机器码与服务端机器码是否匹配判断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可能不是最佳选择。