文章目录
JWT (JSON Web Token) 是一种用于在各方之间安全地传输信息的开放标准(RFC 7519)。它可以被用于身份验证和信息交换,常见于Web应用中,以支持单点登录(SSO)或API身份验证。JWT 的内容可以通过数字签名保护,以确保信息的完整性和真实性。
基本结构
JWT 是由三部分组成的,分别是:
Header(头部):通常由两部分组成:
- 类型(type):通常为
JWT
,表明该令牌是一个 JSON Web Token。 - 算法(algorithm):指定签名所使用的算法,如
HS256
、HS512
、RS256
等。
头部通常是这样的:
{ "alg": "HS256", "typ": "JWT" }
- 类型(type):通常为
Payload(有效载荷):JWT 的第二部分包含了声明(Claims),即传递的信息。声明分为三种类型:
注册声明(Registered Claims)
:这些是 JWT 规定的声明,具有特定的含义。常见的有:
iss
(issuer):发行者。sub
(subject):主题(例如用户ID)。exp
(expiration):过期时间(Unix时间戳)。aud
(audience):受众。iat
(issued at):签发时间。jti
(JWT ID):JWT 的唯一标识符。
公共声明(Public Claims):可以自定义的声明,避免与 JWT 注册声明冲突,可以使用 URI 命名空间。
私有声明(Private Claims):在两个系统之间共享的私有声明,通常是应用于内部数据传递的声明。
例如:
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
Signature(签名):为了防止数据被篡改,JWT 会使用头部和载荷的内容以及一个密钥进行签名,确保数据的完整性。如使用
SHA512
加密算法,对应签名的计算方式为:HMACSHA512( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名部分需要
base64
加密后的headers
和base64
加密后的payload
使用连接组成的字符串,再通过headers
中声明的加密方式进行加盐secret组合加密,即Jwt
的Signature
签名部分。服务器收到JWT后,会先对头部和载荷内容用同一套算法再次进行签名,若服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明Token的内容被恶意更改,因此后端应拒绝由此Token发来的Http请求,返回一个401 Unauthorized非法请求的响应,表示此请求为非法请求禁止用户访问该接口。
如果使用非对称加密(如 RS256),则使用私钥对 JWT 进行签名,为了密钥的安全性,私钥一般存储在服务器的环境变量中或定义在后端项目的配置文件中。
在Jwt官网进行测验,结果如下图所示:
工作原理
JWT 的工作原理是基于生成、传递和验证三大核心步骤来实现的。它的核心思想是:通过一个自包含的令牌携带用户信息,令牌可以被安全地传递,服务器只需要验证令牌,而无需存储任何会话数据。
1. JWT 的生成(签发)
- 过程:
- 用户通过登录接口向服务器提供凭据(如用户名和密码)。
- 服务器验证用户凭据是否有效(例如检查数据库中的用户名和密码)。
- 验证成功后,服务器生成一个 JWT,通常包含以下部分:
- Header(头部):指定令牌的类型(JWT)和签名算法(如
HS512
)。 - Payload(有效载荷):包含用户的身份信息和声明(如
user_id
、role
等),以及时间相关的声明(如iat
(签名时间戳)、exp
(过期时间戳))。 - Signature(签名):将 Header 和 Payload 用服务器的 私钥 或 密钥加密生成签名,确保令牌的完整性。
- Header(头部):指定令牌的类型(JWT)和签名算法(如
- 服务器将生成的 JWT 返回给客户端。
2. JWT 的传递
过程:
客户端将生成的 JWT 存储起来,常见的存储方式有:
- 浏览器的
localStorage
或sessionStorage
- Cookie(需要配置为
HttpOnly
和Secure
)
- 浏览器的
客户端每次向服务器发送请求时,将 JWT 附加在请求头中,通常使用以下格式:
Authorization: Bearer <JWT>
3. JWT 的验证
- 过程:
- 当服务器接收到带有 JWT 的请求时,它会解码 JWT 的三个部分:
- 解码 Header 和 Payload,以获取令牌中的声明。
- 使用 Header 中指定的签名算法,结合服务器存储的密钥(或私钥),验证 Signature 是否匹配。
- 服务器会检查以下内容:
- 完整性:检查签名是否被篡改。
- 时效性:检查
iat
(签发时间)和exp
(过期时间)是否有效。 - 权限:根据 Payload 中的信息(如角色、权限声明)判断是否允许用户执行某项操作。
- 如果验证通过,服务器允许请求执行;如果验证失败,则返回错误(如
401 Unauthorized
)。
- 当服务器接收到带有 JWT 的请求时,它会解码 JWT 的三个部分:
使用场景
主要分四步:生成+签名 --> 返回前端 --> 前端携带token请求后端 --> 通过验证,允许访问接口数据
后端生成token(以SpringBoot
项目为例实现jwt token验证)
(1)导入依赖(使用jsonwebtoken的依赖,版本可自选)
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
(2)书写JwtUtil工具类
public class JwtUtils {
//定义密钥(也可以放在配置springboot配置文件中或服务器环境变量中)
private static final String SHARK = "abcdefghigklmnopqrstuvwxyz123456";
//生成token
public static String generateToken(User user) {
Date now = new Date();
int expire = 3600; //设置过期时间
Date expiration = new Date(now.getTime() + 1000 * expire);
Map<String, String> map = new HashMap<>();
map.put("stuId", String.valueOf(user.getStuId()));
map.put("name", user.getName());
map.put("permission", String.valueOf(user.getPermission()));
ObjectMapper objectMapper = new ObjectMapper();
String subject = null;
try {
subject = objectMapper.writeValueAsString(map);
} catch (Exception e) {
System.out.println(e.toString());
}
return Jwts.builder()
.setHeaderParam("type", "JWT")
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(SignatureAlgorithm.HS512, SHARK) //采取SHA512加密算法
.compact();
}
//解析token
public static Claims getClaimsVyToken(String token) {
return Jwts.parser()
.setSigningKey(SHARK)
.parseClaimsJws(token)
.getBody();
}
}
(3)编写拦截器
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (request.getMethod().equals("OPTIONS")) {
return true;
}
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
ObjectMapper objectMapper = new ObjectMapper();
String jsonResponse = objectMapper.writeValueAsString(ApiResponse.error(403, "令牌失效!"));
response.getWriter().write(jsonResponse);
return false;
}
token = token.replace("Bearer ", "");
try {
Claims claims = JwtUtils.getClaimsVyToken(token);
System.out.println("claims = " + claims);
request.setAttribute("user", claims.getSubject());
return true;
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
}
}
提取前端中请求头headers
中的Authorization
字段,提取其中的token
进行校验判断,合法就通过,非法将直接拦截,返回错误,如:401 Unauthorized(非法的请求)。
(4)通过配置类注入到Spring容器
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final JwtInterceptor jwtInterceptor;
public WebConfig(JwtInterceptor jwtInterceptor) {
this.jwtInterceptor = jwtInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/api/user/login/"); //排除登录接口的校验
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}
在配置类中使用Jwt
拦截器进行请求拦截,使拦截器生效。
(4)启动项目
前端进行登录请求(获取token)
axios.post("http://localhost:8001/api/user/login/", form.value)
.then(response => {
if (response.data.code === 200) {
router.push("/select");
const token = response.data.data.token;
localStorage.setItem('token', token);
const decoded = jwtDecode(token);
ElMessage.success(response.data.message);
if (decoded.sub) {
if (JSON.parse(decoded.sub).permission === '1') router.push("/teacher");
else router.push("/select");
}
} else {
ElMessage.error(response.data.message);
}
})
.catch(error => {
console.error('登录失败:', error);
ElMessage.error("登录失败!");
})
.finally(() => {
loading.close();
})
请求成功后,将返回的token
写入缓存localStorage
中,以备后来的调用以及状态/权限的判定。
前端携带token
请求后端接口
一般会放在headers
中的Authorization
中,格式一般为:
Authorization: Bearer <JWT>
服务器验证 JWT 后,如果验证通过,则允许访问受保护的资源。
反之,若携带了非法的token,则返回错误/空数据(401 Unauthorized)。
JWT 的优点
- 无状态(Stateless):JWT 本身包含了所有的必要信息,无需服务器端存储会话状态。服务器只需验证 JWT 的签名和有效性即可,无需查找会话数据。
- 跨平台支持:JWT 是基于 JSON 的,具有良好的跨平台兼容性,可以用于不同的应用程序或服务(如Web、移动端等)。
- 可扩展性:JWT 可以在有效载荷中携带自定义的声明,可以传递多种数据,支持多种业务场景。
- 适用于分布式系统:因为 JWT 是无状态的,它适合用在微服务架构中,每个服务只需要验证 JWT,而不需要依赖中心化的会话存储。
JWT 的缺点
- 过期后的处理:JWT 一旦生成并发送给客户端后,无法修改或撤销。如果 JWT 设置了较长的过期时间,一旦它被泄露,攻击者可能在有效期内使用它。如果需要修改用户的权限或状态,JWT 可能不及时反映最新的变更。
- 较大的数据负载:由于 JWT 的有效载荷可以包含很多信息,这会使得 JWT 的大小相对较大,尤其是当使用对称加密(如 HMAC)时,会使得签名部分增加。
- 需要安全的存储:JWT 存储在客户端时必须安全,通常使用 HTTP Only 和 Secure 属性来保护 JWT,防止 XSS 和 CSRF 攻击。
主要用途
- 身份验证(Authentication):
- JWT 广泛用于 Web 应用的身份验证,尤其是在单点登录(SSO)场景中,用户可以通过一个 JWT 令牌访问多个应用程序或微服务。
- 信息交换(Information Exchange):
- 由于 JWT 具有自包含的特点,它不仅可以用于身份验证,还可以用来传输其他信息。例如,在微服务架构中,服务之间可能会交换包含用户信息的 JWT,保证信息的完整性和不可篡改性。
- 授权(Authorization):
- JWT 常常用于授权场景中,通过包含用户的角色或权限等信息,服务可以根据 JWT 中的数据来决定用户是否有权访问某些资源。
JWT 的安全性
- 签名:
- JWT 使用签名来保证数据的完整性和真实性。签名确保了 JWT 中的数据未被篡改,并且可以验证发送方的身份。
- 加密:
- JWT 也可以被加密,防止敏感数据被泄露。JWT 本身的规范支持加密,但常见的实现(如使用
RS256
或HS256
)主要关注签名,而加密可以额外使用如 JWE(JSON Web Encryption)等技术。
- JWT 也可以被加密,防止敏感数据被泄露。JWT 本身的规范支持加密,但常见的实现(如使用
- 过期时间(exp):
- JWT 中通常会设置一个
exp
(过期时间)字段,规定该 JWT 的有效期。过期后的 JWT 无效,防止过期令牌的滥用。
- JWT 中通常会设置一个
记一次SpringBoot
+Jwt
的项目学习记录~
@鲨鱼爱兜兜