JWT应用
-
用户使用用户名和密码等凭据向身份验证服务器发送登录认证请求。
-
身份验证服务器将验证该信息,用户提供的登录信息验证通过后,身份验证服务器将向用户颁发JWT有效访问令牌。
-
用户将获取到的jwt令牌储存在 Cookie 里面,也可以储存在 localStorage。
-
用户向后端服务器发送的每一个请求都要携带有效的JWT访问令牌,服务器验证令牌并向用户提供所请求的资源。 你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。 Authorization: Bearer 另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。
-
当令牌过期时,用户向服务器发送请求,服务器对对用户做出响应告诉其令牌失效,请求失败
-
此时用户需要再次登录,将用户凭据发送到身份验证服务器,以获取新的JWT有效访问令牌,并将其存储在客户端的本地存储中。
一、前置知识点
- 互联网服务离不开用户认证。一般流程是下面这样。
1、用户向服务器发送用户名和密码。
2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
3、服务器向用户返回一个 session_id,写入用户的 Cookie。
4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
问题 :无法实现SSO单点登录时session共享
解决
1、 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
2、数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。服务器就不保存任何 session 数据了,服务器变成无状态了,比较容易实现扩展。
cookie、session、token鉴权的区别
-
session是诞生并保存在服务器那边的,由服务器主导一切。
-
cookie这是一种数据载体,把session放在cookie中送到客户端那边,cookie跟随HTTP的每个请求发送出去。
-
token是诞生在服务器,但保存在浏览器这边的,由客户端主导一切,可以放在cookie或者storage里面,持有token,就像持有令牌一样,可以允许访问服务器。服务器验证是前提。
-
cookie保存在客户端,服务器不加密不保存
-
session保存在客户端,服务器要加密并保存
-
token保存在客户端,服务器要加密不保存。
-
Session是一种服务器端的机制,用于存储和管理用户的身份和状态信息
-
而Cookie是用于在客户端存储Session ID等数据的机制
-
Token是一种轻量级的身份验证和授权机制,可以作为无状态的凭据进行传输
-
Cookie和Token都可以用于在客户端和服务器之间传递身份信息和状态信息,实现身份验证和状态管理的功能。
JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。
以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。
二、JWT介绍
2.1 概念
JSON Web Token,简称 JWT,读音是 [dʒɒt]( jot 的发音),是一个基于 RFC 7519 的开放数据标准,它定义了一种宽松且紧凑的数据组合方式。其作用是:JWT是一种加密后数据载体,可在各应用之间进行数据传输。JWT中一般涵盖了用户身份信息,每次访问时,server校验信息合法性即可。
2.2 JWT组成
一个 JWT 通常有 HEADER (头),PAYLOAD (有效载荷)和 SIGNATURE (签名)三个部分组成,三者之间使用“.”链接,格式如下:
header.payload.signature
base64UrlEncode(header) + "." + base64UrlEncode(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)
算出签名以后,使用Base64加密,构成了JWT第三部分-signature:
l6JdYARw4IHmjliSbh9NP6ji1L15qVneWTJU5noQ-k8
因为有这个密钥的存在,所以即便调用方偷偷的修改了前两部分的内容,在验证环节就会出现签名不一致的情况,所以保证了安全性。
2.3 在线生成/解析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.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可能不是最佳选择。