前言
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准.该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
一、传统Session和JWT验证对比
1. Session工作原理
在传统的 Web 应用程序中,我们使用 Session 来授权用户,当用户登录到应用程序后,我们会为该用户分配一个唯一的 Session Id。我们将此 Session Id 保存在用户浏览器的安全 cookie 中和服务器的内存中。我们对每个请求都使用相同的会话,以便服务器知道该用户已通过身份验证。对于每个请求,cookie 中的 Session Id 都会与服务器内存中的 Session Id 作匹配,以验证用户是否被授权。
问题:
这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。
举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
-
一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
-
另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。
2. JWT工作原理
JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。例如:
{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2018年7月1日0点0分"
}
以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
二、使用Maven安装JWT依赖
<!--JWT依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.14.0</version>
</dependency>
三、Token间接和方法类封装
Token工具常用的方法主要有:创建Token、验证Token、销毁Token、从Token中获取信息四个操作,下面是封装的一个JWT工具类
package com.example.newsbird.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
import java.util.Map;
public class JWTUtil {
// 1段用于JWT加密的密钥 HELLONEWSBIRD
private static final String SIGN = "HELLONEWSBIRD";
// 生成TOKEN
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);
});
String token = builder.withExpiresAt(instance.getTime()) //指定令牌过期时间
.sign(Algorithm.HMAC256(SIGN)); // sign
return token;
}
// 校验Token
public static DecodedJWT verify(String token) {
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}
// 从Token中获取信息
public static DecodedJWT getTokenInfo(String token){
DecodedJWT verify = JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
return verify;
}
// 销毁Token
public static Boolean distoryToken(String token){
return true;
}
}
四、JWT实际应用
1. 用户登录生成Token
思路:用户通过账号和密码登录,服务端通过用户名和密码查找对应的用户信息,根据查询到的用户信息生成Token,并将Token返回给客户端,此时登录成功。
2. 从Token中获取用户信息
思路:用户在进行操作时,例如发布一篇文章,客户端不可能每次提交表单时都要拿到用户的id等信息放到表单中提交。此时就需要服务端从客户端发送的请求Header中获取Token并解析Token获取用户的关键信息存放到对用的类中。
getClaim(String name): 返回具有特定名称的声明。通过此方法可以获取单个声明的值。
3. Token信息验证
思路:在每次请求接口时,过滤器先通过请求头中的Token验证Token的有效性。
通常使用DecodeJWT类来解析Token获取需要的信息
DecodedJWT
是一个由 java-jwt
库(通常与 com.auth0
相关)提供的对象,用于表示解码后的 JSON Web Token(JWT)。当你解码一个JWT时,DecodedJWT
对象提供了一种方便的方式来访问Token中的信息。
使用DecodeJWT Token解析类,解析Token。
DecodedJWT
包含的信息
DecodedJWT
提供了对JWT的不同部分的访问,包括头部(Header)、有效载荷(Payload)和签名(Signature)。具体来说,DecodedJWT
提供了以下主要信息:
-
Header(头部):
getHeader()
: 返回JWT的头部部分。这通常是一个JSON对象,包含Token的元数据,例如签名算法和类型。你可以使用getHeader()
方法获取头部信息。
-
Payload(有效载荷):
getClaims()
: 返回JWT的有效载荷部分,作为一个Map<String, Claim>
。有效载荷包含Token的声明(claims),包括标准声明(如sub
、iat
、exp
)和自定义声明。你可以通过getClaims()
方法获取所有声明,并通过声明名称获取具体的值。getClaim(String name)
: 返回具有特定名称的声明。通过此方法可以获取单个声明的值。getSubject()
: 返回sub
声明的值,通常表示Token的主体或用户标识符。getIssuer()
: 返回iss
声明的值,表示Token的发行者。getAudience()
: 返回aud
声明的值,表示Token的受众。getIssuedAt()
: 返回iat
(issued at)声明的值,表示Token的发行时间。getExpiresAt()
: 返回exp
(expiration)声明的值,表示Token的过期时间。
-
Signature(签名):
getSignature()
: 返回JWT的签名部分。虽然DecodedJWT
对象本身并不提供直接访问签名的方式(因为签名的验证通常在生成Token时完成),你可以使用库提供的功能在生成Token时验证签名是否匹配。
示例代码
以下是一个示例,演示如何使用 DecodedJWT
获取JWT中的各种信息:
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
public class JwtExample {
public static void main(String[] args) {
String token = "your_jwt_token_here";
String secret = "your_secret_key_here";
// 创建JWT验证器
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).build();
try {
// 解码JWT
DecodedJWT decodedJWT = verifier.verify(token);
// 访问JWT的头部
System.out.println("Header: " + decodedJWT.getHeader());
// 访问JWT的有效载荷
System.out.println("Subject: " + decodedJWT.getSubject());
System.out.println("Issuer: " + decodedJWT.getIssuer());
System.out.println("Audience: " + decodedJWT.getAudience());
System.out.println("Issued At: " + decodedJWT.getIssuedAt());
System.out.println("Expires At: " + decodedJWT.getExpiresAt());
// 访问自定义声明
System.out.println("Custom Claim (example): " + decodedJWT.getClaim("example").asString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
关键点总结
DecodedJWT
提供对JWT的头部、有效载荷和签名的访问。- 通过
getClaims()
方法可以获取所有声明,通过getClaim(String name)
可以获取具体的声明值。 - 可以使用
getSubject()
、getIssuer()
、getAudience()
、getIssuedAt()
和getExpiresAt()
方法访问标准声明。 - 头部信息和签名通常不需要直接访问,因为验证签名是在Token生成时完成的。
这种信息提取方式帮助你在应用程序中安全地处理和验证JWT。
4. 退出系统销毁Token
Token的“销毁”通常不是通过服务器端直接删除Token来实现的,因为JWT是自包含的,服务器通常不会存储它们。相反,Token的有效性和生命周期通常由以下几种方法来管理:
-
设置Token过期时间(Expiration Time):
- JWT中通常会包含一个
exp
字段,指定Token的过期时间。这个字段是Token有效性的关键。如果Token到达过期时间,Token自动失效。你可以在创建Token时设置一个合理的过期时间。
- JWT中通常会包含一个
-
使用刷新Token(Refresh Tokens):
- 在一些实现中,你会使用短期的访问Token和长期的刷新Token。访问Token在短时间内有效,一旦过期,用户需要使用刷新Token重新获取新的访问Token。你可以在服务器端管理刷新Token的有效性,例如通过在数据库中存储刷新Token并在需要时验证。
-
Token黑名单(Token Blacklisting):
- 如果你需要立即使某个Token失效,可以使用Token黑名单技术。服务器端维护一个Token黑名单(通常是一个数据库或缓存系统),当用户登出时,将Token添加到黑名单中。每次验证Token时,服务器会检查Token是否在黑名单中。
-
清除客户端存储的Token:
- 在客户端(例如浏览器、移动设备),你可以在用户登出时清除存储在本地的Token(例如,从
localStorage
或sessionStorage
中移除Token)。
- 在客户端(例如浏览器、移动设备),你可以在用户登出时清除存储在本地的Token(例如,从
-
Token签名密钥轮换:
- 如果你需要使所有Token失效,可以通过更改用于签署Token的密钥来强制所有现有Token失效。这种方法通常是作为一种极端手段,适用于安全策略的更新等场景。
具体操作
- 设置过期时间:
重新设置JWT(JSON Web Token)的过期时间通常涉及到生成一个新的Token,而不是直接修改现有Token的过期时间。JWT是自包含的,意味着Token一旦生成就不能更改。要实现修改过期时间的效果,你通常需要创建一个新的Token并包含新的过期时间。以下是如何在不同编程语言中实现这一功能的步骤和示例。
基本步骤
-
解码原始Token(如果需要)
- 提取原始Token中的数据(如用户ID等),但不直接修改Token本身。
-
创建一个新的Token
- 使用新的过期时间生成新的Token,通常是将原始Token的数据和新的过期时间传递给Token生成函数。
在Java中,你可以使用 jjwt
库来创建一个新的Token。
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class JwtExample {
// 你的签名密钥
private static final String SECRET_KEY = "your_secret_key";
public static String refreshToken(String oldToken) {
// 解析旧Token
var claims = Jwts.parserBuilder()
.setSigningKey(SECRET_KEY.getBytes())
.build()
.parseClaimsJws(oldToken)
.getBody();
// 设置新的过期时间
Date now = new Date();
Date expiration = new Date(now.getTime() + 2 * 60 * 60 * 1000); // 2小时后
// 创建新的Token
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(SignatureAlgorithm.HS256, SECRET_KEY.getBytes())
.compact();
}
public static void main(String[] args) {
String oldToken = "your_old_jwt_token";
String newToken = refreshToken(oldToken);
System.out.println("New Token: " + newToken);
}
}
总结
- 解码原始Token:提取原始Token中的信息,但不直接修改Token本身。
- 创建新的Token:使用原始Token的数据和新的过期时间生成新的Token。
- 实现:具体代码实现可能因编程语言和库的不同而有所不同,但一般步骤类似。
生成新的Token而不是直接修改现有Token的过期时间是一种常见的处理方式,确保了Token的完整性和安全性。
-
使用黑名单示例(伪代码):
# 当用户登出时,将Token添加到黑名单中 blacklisted_tokens.add(token) # 在每次验证Token时检查黑名单 if token in blacklisted_tokens: return "Token is invalid"
-
客户端清除Token示例(JavaScript):
function logout() { localStorage.removeItem('jwtToken'); // 进行其他登出操作 }
通过这些方法,你可以有效地管理和“销毁”JWT,以确保系统的安全性。
五、Token是否需要在服务器上也保存一份?
最后有一个问题,那就是Token到底需不需要存储在服务器上呢?