认证功能是项目基础建设之一,要实现认证功能,很容易就会想到 JWT 或者 Session,但是两者有啥区别?各自的优缺点?
JWT 是 JSON Web Token 的缩写,是一种轻量级的身份验证和授权的标准。通过数字签名的方式,以 JSON 对象为载体,在不同服务终端之间进行安全的数据传输。
JWT 通常用于Web 应用程序中的身份认证和授权,它通过在用户登录后生成一个包含用户信息和过期时间等数据的 JSON 格式令牌,并将其使用密钥进行加密,然后在之后的请求中将该令牌发送给服务器,服务器可以根据令牌中的信息验证用户身份并进行授权操作。
JWT 相较于传统的基于会话(Session)的认证机制,具有以下优势:
总结来说,使用 JWT 相较于传统的基于会话的认证机制,可以减少服务器存储开销和管理复杂性,实现跨域支持和水平扩展,并且更适应无状态和微服务架构。
JWT 由三部分组成分别是:头部(Header),载荷(Payload), 签名 (Signature)。
头部通常用于描述JWT的元数据,如算法类型,token类型等信息,采用Base64编码。
typ:token 的类型,这里固定为 JWT
alg:使用的 hash 算法,例如 HMAC、SHA256 或者 RSA
JWT 官方规定了 7 个字段,可供使用:
1.iss (Issuer):签发者
2.sub (Subject):主题
3.aud (Audience):接收者
4.exp (Expiration time):过期时间
5.nbf (Not Before):生效时间
6.iat (Issued At):签发时间
7.jti (JWT ID):编号
载荷是 JWT 实际要传输的内容,通过 Base64 编码进行传输。载荷中可以包含各种声明,如用户 ID、角色、权限等信息。
签名用于验证消息的完整性和真实性,可以使用 HMAC 算法或者 RSA 签名算法生成,,由头部和载荷一起进行签名,防止 token 被篡改。
JWT 本质是将秘钥存放在服务器端,并通过某种加密手段进行加密和验证的机制。加密签名 = 某加密算法(header + payload + 服务器端私钥),因为服务端私钥别人不能获取,所以 JWT 能保证自身其安全性。
1.3.1 引入依赖
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
1.3.2 编写 JwtUtil 工具类
public class JwtUtil {
//token有效期为一个小时
public static final Long JWT_TTL = 60 * 60 *1000L;
//设置秘钥明文
public static final String JWT_KEY = "JWT_KEY";
/**
* 获取token
* @return
*/
public static String getUUID(){
return UUID.randomUUID().toString().replaceAll("-", "").toLowerCase();
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact();
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 获取 JwtBuilder 对象
* @param subject
* @param ttlMillis
* @param uuid
* @return
*/
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis == null){
ttlMillis = JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("zhangSan") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}
/**
* 创建token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);
return builder.compact();
}
/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
/**
* 生成加密后的秘钥 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
}
1.3.3 测试加密
@Test
public void encrypt() {
String jwt = JwtUtil.createJWT("zhangSan");
System.out.println(jwt);
}
加密后数据
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2OWNkZjM2OWRiOWM0YmMxOGExNGZjYzYxNGIzMGYwYyIsInN1YiI6Imh1YW5nQngiLCJpc3MiOiJodWFuZ0JYIiwiaWF0IjoxNzAzMTY5NDI3LCJleHAiOjE3MDMxNzMwMjd9.rpFpaXm2ih7cqYL43JucRBZnWBx-Wu-OdyJS9NxpNUI
1.3.4 测试解密
@Test
public void decrypt() throws Exception {
Claims claims = JwtUtil.parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2OWNkZjM2OWRiOWM0YmMxOGExNGZjYzYxNGIzMGYwYyIsInN1YiI6Imh1YW5nQngiLCJpc3MiOiJodWFuZ0JYIiwiaWF0IjoxNzAzMTY5NDI3LCJleHAiOjE3MDMxNzMwMjd9.rpFpaXm2ih7cqYL43JucRBZnWBx-Wu-OdyJS9NxpNUI");
String subject = claims.getSubject();
System.out.println(subject);
}
解密后数据
zhangSan
@RestController
@PostMapping("/test")
public Result<String> login(@RequestBody UserLoginRequest userLoginRequest) {
AssertUtil.isNotNull(userLoginRequest, ResultCodeEnum.PARAMETER_ERROR);
AssertUtil.isNotNull(userLoginRequest.getAccountId(), ResultCodeEnum.PARAMETER_ERROR, "accountId不能为空!");
AssertUtil.isNotNull(userLoginRequest.getUserName(), ResultCodeEnum.PARAMETER_ERROR, "userName不能为空!");
AssertUtil.isNotNull(userLoginRequest.getPassword(), ResultCodeEnum.PARAMETER_ERROR, "password不能为空!");
//根据账号id获取账号信息
TAccount account = accountService.findById(userLoginRequest.getAccountId());
//校验用户名和密码
AssertUtil.isTrue(userLoginRequest.getUserName().equals(account.getName()), ResultCodeEnum.USERNAME_NO_EXISTS);
AssertUtil.isTrue(userLoginRequest.getPassword().equals(account.getPassword()), ResultCodeEnum.PASSWORD_ERROR);
//如果账号密码都正确,则生成token
String token = JwtUtil.createJWT(userLoginRequest.getUserName());
//缓存到redis
stringRedisTemplate.opsForValue().set(RedisPrefix.TOKEN + userLoginRequest.getUserName(), token, 1, TimeUnit.HOURS);
return Result.success(token);
}
}
返回结果
{
"code": "200",
"msg": "成功",
"data": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxYTljNjRkMDRhNWE0MjQxOThhZjhjZWRjMThlYzZmYyIsInN1YiI6InpoYW5nU2FuIiwiaXNzIjoiaHVhbmdCWCIsImlhdCI6MTcwMzQwMTI3MiwiZXhwIjoxNzAzNDA0ODcyfQ.OAK0Fi05pmwF2ZTaGWYyON_VmGFv03G7Lk5jujhJAzA"
}
Cookie、Session 和 Token 通常都是用来保存用户登录信息的技术,但三者有很大的区别,简单来说 Cookie 适用于简单的状态管理,Session 适用于需要保护用户敏感信息的场景,而 Token 适用于状态无关的身份验证和授权。
Token 状态无关性解析:在传统的基于会话的认证方式中,服务器需要在后端保存用户的会话状态,通过 Session ID 进行会话的管理。而 Token 机制不需要在服务器上保存任何关于用户的状态信息,只需要在登录成功时,服务器端通过某种算法生成一个唯一的 Token 值,之后再将此 Token 发送给客户端存储(存储在 localStorage 或 sessionStorage 中),注意此时服务端是不存储这个 Token 值的,服务器端只进行效验而不保存此 Token,这就叫“状态无关性”。这样就可以减轻服务器存储和管理会话状态的负担,所以它比较适用于大型系统和分布式系统。
具体来说,Cookie、Session 和 Token 的区别主要有以下几点区别:
准确来说 Cookie 的实现和 Session 是没有任何关系的,但 Session 的实现需要借助于 Cookie。
Session 机制的实现流程如下:
所以默认情况下,Session 是借助 Cookie 来完成身份标识的传递的,这样服务器端才能根据 Session ID 和保存的会话信息进行关联,用于找到某个具体登录的用户,所以说:默认情况下,Session 机制是依赖 Cookie 实现的。
通过上文我们知道,默认情况下 Session 机制是依赖 Cookie 实现的,那么是不是禁用了 Cookie 之后,Session 机制也就无法使用了呢?其实不然。
除了默认情况下,我们可以使用 Cookie 来传递 Session ID 之外,我们可以通过一些特殊的手段来自行传递 Session ID,以此来摆脱禁用 Cookie 之后 Session 无法使用的情况,例如以下两种实现手段:
通过以上手段都可以将 Session ID 传递到服务器端(虽然麻烦点),然后在服务器端,我们再对以上传递的 Session ID 进行获取和映射,这样就手动完成了传递和匹配登录用户的工作了,Session 机制也得已继续使用了。