date: 2025-03-25 09:46:46 title: JWT-RequestHeader 身份认证 author: zaqai tags:
- Java
- SprintBoot
JWT简介
JSON Web Token由三部分组成,它们之间用圆点.进行分割, 一个标准的JWT形如 xxx.yyy.zzz
●Header
●Payload
●Signature
header
由两部分组成:token的类型(JWT)和算法名称(比如:HMAC SHA256或者RSA等等)
{
"alg": "HS256",
"typ": "JWT"
}
用Base64对这个JSON编码就得到了 JWT的第一部分
Payload
可以写入自定义的数据信息,有三种类型
●Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer 签发者), exp (expiration time 有效期), sub (subject), aud (audience)等。
●Public claims : 可以随意定义。
●Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明
{
"iss": "一灰灰blog",
"exp": 1692256049,
"wechat": "https://spring.hhui.top/spring-blog/imgs/info/wx.jpg",
"site": "https://spring.hhui.top",
"uname": "一灰"
}
对payload进行Base64编码就得到了 JWT的第二部分
Signature
为了得到签名部分,我们必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然后对它们签名即可。
如 HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它的发送方。
JWT鉴权流程
将JWT存储在localStorage或sessionStorage中,前端需显式将Token添加到请求头(如Authorization: Bearer token)
JWT配置
@Slf4j
@Component
public class UserSessionHelper {
@Component
@Data
// 配置文件的值自动注入进JwtProperties
@ConfigurationProperties("paicoding.jwt")
public static class JwtProperties {
private String issuer;
private String secret;
private Long expire;
}
private final JwtProperties jwtProperties;
private Algorithm algorithm;
private JWTVerifier verifier;
public UserSessionHelper(JwtProperties jwtProperties) {
this.jwtProperties = jwtProperties;
algorithm = Algorithm.HMAC256(jwtProperties.getSecret());
verifier = JWT.require(algorithm).withIssuer(jwtProperties.getIssuer()).build();
}
}
生成JWT
public String genSession(Long userId) {
// 1.生成jwt格式的会话,内部持有有效期,用户信息
String session = JsonUtil.toStr(MapUtils.create("s", SelfTraceIdGenerator.generate(), "u", userId));
String token = JWT.create().withIssuer(jwtProperties.getIssuer()).withExpiresAt(new Date(System.currentTimeMillis() + jwtProperties.getExpire()))
.withPayload(session)
.sign(algorithm);
// 2.使用jwt生成的token时,后端可以不存储这个session信息, 完全依赖jwt的信息
// 但是需要考虑到用户登出,需要主动失效这个token,而jwt本身无状态,所以再这里的redis做一个简单的token -> userId的缓存,用于双重判定
RedisClient.setStrWithExpire(token, String.valueOf(userId), jwtProperties.getExpire() / 1000);
return token;
}
验证JWT
public void removeSession(String session) {
RedisClient.del(session);
}
public Long getUserIdBySession(String session) {
// jwt的校验方式,如果token非法或者过期,则直接验签失败
try {
DecodedJWT decodedJWT = verifier.verify(session);
String pay = new String(Base64Utils.decodeFromString(decodedJWT.getPayload()));
// jwt验证通过,获取对应的userId
String userId = String.valueOf(JsonUtil.toObj(pay, HashMap.class).get("u"));
// 从redis中获取userId,解决用户登出,后台失效jwt token的问题
String user = RedisClient.getStr(session);
if (user == null || !Objects.equals(userId, user)) {
return null;
}
return Long.valueOf(user);
} catch (Exception e) {
log.info("jwt token校验失败! token: {}, msg: {}", session, e.getMessage());
return null;
}
}
}
后端存储JWT
jwt本身无状态,后端完全可以不保存jwt,纯依赖jwt的特性来完成身份鉴权,但是由于它的无状态,后端失去了token的管控权限,如果我们希望提前失效某些用户身份,则无法支持
JWT vs Session
维度 | Session | JWT |
---|---|---|
状态管理 | 有状态(服务器存储会话数据) | 无状态(客户端存储Token) |
数据存储 | 用户信息存储在服务端(如Redis、数据库) 前端存索引 | 用户信息自包含在Token中(Base64编码) |
传输方式 | 依赖Cookie自动携带Session ID | 通过请求头(如Authorization )手动传递Token |
安全性 | 更安全(敏感数据在服务端) | 需谨慎(Payload未加密,避免存储敏感数据) |
session官配是cookie, session保存在cookie里, 容易受到csrf(跨站请求伪造)攻击, 原因是浏览器发请求时, 会自动携带上cookie, 比如在A网站完成了认证, sessionA保存在cookie里, 在B网站, 有这么一个东西<img src="https://A/logout" style="display:none;"/>
, 浏览器会自动访问这个接口并携带上cookie, 此时A的后端认为这是一个合法的操作, 就给登出了, 这就是csrf攻击
使用jwt预防csrf攻击的主要原理就是jwt是通过请求头,由js主动塞进去传递给后端的,而非cookie的方式
防御csrf攻击还可以通过敏感操作二次验证, 关键接口限制为POST
请求等
较为安全地将jwt放在cookie里
服务端设置Cookie属性
Set-Cookie: jwt=<token>;
Path=/;
Secure; // 仅HTTPS传输
HttpOnly; // 禁止JS读取, 防止XSS窃取Token
SameSite=Lax; // 防御CSRF, 阻止跨站POST请求携带Cookie(GET请求仍允许,如页面跳转)
Max-Age=3600; // 有效期(秒)
回复