什么时候使用 JWT
JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。JWT 虽然对于大多数网站都没有用,但是有几种情况它是很有用的。如果你正在构建从服务器到服务器或客户端到服务器(如:移动应用 APP 或单页面应用)的 API服务,那么使用 JWT 是非常明智的。
JWT使用流程
- 初次登录:用户初次登录,输入用户名密码
- 密码验证:服务器从数据库取出用户名和密码进行验证
- 生成JWT:服务器端验证通过,根据从数据库返回的信息,以及预设规则,生成JWT
- 返还JWT:服务器的HTTP RESPONSE中将JWT返还
- 带JWT的请求:以后客户端发起请求,HTTP REQUEST
- HEADER中的Authorizatio字段都要有值,为JWT
- 服务器验证JWT
2. JWT的原则
JWT的原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,如下所示。
{
"UserName": "Chongchong",
"Role": "Admin",
"Expire": "2018-08-08 20:15:56"
}
之后,当用户与服务器通信时,客户在请求中发回JSON对象。服务器仅依赖于这个JSON对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名(有关详细信息,请参阅下文)。服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。
3. JWT的数据结构
典型的,一个JWT看起来如下图。
该对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。注意JWT对象为一个长字串,各字串之间也没有换行符,此处为了演示需要,我们特意分行并用不同颜色表示了。每一个子串表示了一个功能块,总共有以下三个部分:
JWT的三个部分如下。JWT头、有效载荷和签名,将它们写成一行如下。我们将在下面介绍这三个部分。
3.1 JWT头
JWT头部分是一个描述JWT元数据的JSON对象,通常如下所示。
{
"alg": "HS256",
"typ": "JWT"
}
在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。
最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。
3.2 有效载荷
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择。
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
除以上默认字段外,我们还可以自定义私有字段,如下例:
{
"sub": "1234567890",
"name": "chongchong",
"admin": true
}
请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。JSON对象也使用Base64 URL算法转换为字符串保存。
案例演示
下面显示了一个登录请求成功之后服务端返回的Token,它由编码头部(header)、编码有效载(payload)和签名(signature)通过(.)拼接而成:
JWT并不对数据进行加密,而是对数据进行签名,保证不被篡改。除了在登录中可以用到,在进行邮箱校验和图形验证码也可以用到。
JWT缺点及对应的解决方案
登录状态信息续签问题。比如设置token的有效期为一个小时,那么一个小时后,如果用户仍然在这个web应用上,这个时候当然不能指望用户再登录一次。判断还有多久这个token会过期,在token快要过期时,返回一个新的token。
用户主动注销。JWT并不支持用户主动退出登录,当然,可以在客户端删除这个token,但在别处使用的token仍然可以正常访问。为了支持注销,我的解决方案是在注销时将该token加入黑名单。可以给用户加一个登录状态字段。登录之后这个字段为1。然后退出之后这个字段为0。如果退出了,就算有JWT还是退出状态了。
Java maven依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
JWT加解密
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author hudy
* @date 2024/6/7
*/
public class JWTEncrypt {
/**
* 分配给供应商的签发主体
*/
private static final String JWT_ISSUER = "明文内容加密";
// 签名密钥
private static final String APP_SECRET = "33db7be3237f0a29f2150aaca789fa6f";
// Bearer令牌
private static final String BEARER = "Bearer ";
// 日志记录器
private static final Logger logger = LoggerFactory.getLogger(JWTEncrypt.class);
// 生成一个长期有效的Token
public static String genToken() throws JWTCreationException, UnsupportedEncodingException {
return BEARER + JWT.create()
.withIssuer(JWT_ISSUER)
.withIssuedAt(new Date())
.sign(Algorithm.HMAC256(APP_SECRET));
}
// 验证有效信用组织
public static boolean verifyValidCreditOrg(String jwtSignToken, String appToken) {
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(appToken)).build();
verifier.verify(jwtSignToken);
return true;
} catch (Exception e) {
logger.error("JWT token验证异常,原因:{}", e);
}
return false;
}
// 获取签发者
public static String getIssuer(String jwtSignToken) throws RuntimeException {
try {
return JWT.decode(jwtSignToken).getIssuer();
} catch (Exception e) {
logger.error("获取JWT的appkey发生异常,原因:{}", e);
throw new RuntimeException("非法请求客户端");
}
}
// 获取签发时间戳
public static Long getIssuedAt(String jwtSignToken) throws RuntimeException {
try {
return JWT.decode(jwtSignToken).getIssuedAt().getTime() / 1000;
} catch (Exception e) {
logger.error("获取JWT的appkey发生异常,原因:", e);
throw new RuntimeException("非法请求客户端");
}
}
public static void main(String[] args) throws UnsupportedEncodingException {
// 生成token
String authorization = JWTEncrypt.genToken();
System.out.println("请求头授权信息,header token=" + authorization);
// 验证token
String jwtSignToken = authorization.substring(BEARER.length());
boolean verified = JWTEncrypt.verifyValidCreditOrg(jwtSignToken, APP_SECRET);
System.out.println("验证结果=" + verified);
// 解析签发主体
String issuer = JWTEncrypt.getIssuer(jwtSignToken);
System.out.println("签发主体=" + issuer);
}
}