一、JWT 认证
1.1、对 JWT 的认识
1.1.1、JWT 解释
JWT 是 “JSON Web Token”
的简写,也就是通过 JSON 形式作为 Web 应用中的令牌,用于在各方之间安全的将信息作为 JSON
对象传输. 在数据传输过程中还可以完成数据加密,签名等相关处理.
这就像是皇上的玉玺,谁见到了哪怕没见过皇上长啥样,都知道是皇上来了.
1.1.2、为什么使用的** JWT 认证**,而不是 Session 认证?
a)基于传统的 Session 认证
http 协议是一种无状态的协议,也就意味着当用户第一次通过用户名和密码登录成功以后,下一次再请求的时候,用户还需要再进行登录才行,因为根据** http 协议**,我们不能知道是哪个用户发出的请求.
为了解决上述问题,就是引入了 session 认证机制. 用户第一次登录成功之后,服务端会生成一个 sessionId,将他保存到内存中,然后加入到响应中(set-cookie)发送给客户端,接着客户端保存到本地,之后客户端每次访问服务器时都会带上这个 sessionId
作为身份标识,服务端也就知道这个请求是来自于哪个用户了.
弊端:
-
占用大量服务器内存:每个用户经过认证之后都会再服务器的内存中记录一次,随着用户的增多,服务端的内存开销会明显增加.
-
限制分布式架构的应用:用户认证之后会将** sessionId 保存到内存中,这就意味着用户下次请求必须要请求到这台服务器上,才完成能授权**. 在分布式系统中,一定程度上增大了配置 负载均衡 的复杂度(指定请求打到记录 sessionId 的不同服务器).
-
如果后端需要实现** session 共享机制,还需要进行 redis 集群的部署,加大了部署难度.**
-
因为是基于
cookie
来进行用户识别的(set-cookie
),cookie 如果被捕获,用户很容易受到 CSRF(跨网站请求伪造的攻击).
1.1.3、JWT 认证流程
-
首先前端将用户名和密码发送给后端,后端对用户名和密码校验成功后,会将用户 id 和其他信息作为 payload ,然后将其分别进行
Base64 编码,拼接,最后再签名
,形成一个 JWT(Token). 形成的 JWT 就形同xxx.yyy.zzz
这种结构的字符串. -
后端将 JWT 字符串作为登录成功的响应结构返回给前端. 前端就可以通过
localStorage
保存到本地.用户退出登录时删除保存的 JWT 即可
. -
之后前端每次请求都会将 JWT 放入
HTTP Header 中的 Authorization 位
(解决 XSS 和 XSRF 问题). -
之后后端每次收到请求后都会先验证 JWT 的有效性. 例如,检查签名是否正确、Token 是否过期、Token 的接收方是否是自己(可选).
1.1.4、优势
- 不占用服务器资源:服务器只需要通过 JWT 工具进行校验即可.
- 适用于分布式微服务:不需要通过负载均衡找到指定服务器上的会话,只需要在网关中对请求头中的 JWT 令牌进行统一校验即可.
- 信息安全:由于签名是使用 标头 和 有效负载 计算的,因此还可以验证内容是否遭到篡改.
- 支持多种语言:
Token
时以JSON
加密的形式保存在客户端的. 原则上任何 web 都支持.
1.1.5、JWT 的结构
JWT 令牌由三个部分组成,分别是 标头(Header)、有效载荷(Payload)、签名(Signature)
,并且由 "." 分割.
类似于 xxxx.yyyy.zzzzz
,也就是 Header.Payload.Signature
JWT 第一部分:标头 Header
标头通常由两个部分组成,分别是 令牌的类型(例如 JWT) 和 所使用的签名算法.(例如HMAC、SHA256、RSA. 一般就是用 HS 256 即可).
例如:
{
"alg": "HS256",
"typ": "JWT"
}
最后,他会使用 Base64 编码构造出 JWT 结构中的第一部分(Header).
Ps:Base64 只是一种编码,也就是说,可以被翻译回原来的样子. 因此他并不是一种加密过程.
JWT 第二部分:有效载荷 Payload
令牌的第二部分就是有效载荷,这就是我们一些自定义传输的信息,通常是一些用户信息.
Ps:值得注意的是,Base64 是一种编码,可以被翻译回来,因此在 JWT 中不因该在 负载 中加入任何敏感数据,例如用户密码.
例如:
{
"id": "6",
"username": "cyk",
"admin": true
}
最后,他会使用 Base64 编码构造出 JWT 结构中的第二部分(Payload).
JWT 第三部分:签名 Signature
Signatrue 需要使用 Base64 编码后的 header 和 payload
以及提供的 密钥(私钥)
,然后使用 header 中指定的签名算法(HS256)构建一个签名,保证 JWT 没有被篡改过.
例如,一旦篡改 header 中的信息,那么服务端
拿到传来的 JWT,会先进行验签
,就是拿着新的 JWT 的第一部分和第二部分
,再以同样的 密钥(私钥)
再生成一个 JWT 第三部分(签名)
,然后拿着这个新生成的 签名和 前端刚刚传来的 JWT 的第三部分进行比对
,就会对比出不一致.
- Ps:这就使得后端不需要存储任何数据,就可以校验用户的身份,避免了使用 session 而导致占用大量内存空间.
1.2、JWT 的使用
1.2.1、实例
这里用一个简单的栗子来演示 JWT 的使用.
a)引入 jwt 依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
b)生成 Token
@Test
public void createJwt() {
//设置令牌的过期时间位 100 s
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, 100);
//创建 Token
String token = JWT.create()
.withClaim("userId", 6) //payload
.withClaim("username", "cyk") //payload
.withExpiresAt(instance.getTime()) //设置过期时间
.sign(Algorithm.HMAC256("djafo&*&(988*T*"));//签名(这里自定义密钥即可)
System.out.println(token);
}
执行之后,通过 sout 展示 token 如下:
c)根据密钥创建验证对象,然后验证 Token
@Test
public void test() {
//创建验证对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("djafo&*&(988*T*")).build();
//验证Token(验证失败,会引发异常)
DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2OTcxNjcwNTMsInVzZXJJZCI6NiwidXNlcm5hbWUiOiJjeWsifQ.WsDPqbmeeqyKPMRllrDv4kfUL-m6hZxAfGc21-t51GE");
System.out.println(verify.getClaim("userId").asInt());
System.out.println(verify.getClaim("username").asString());
}
一旦执行失败,则会引发如下异常:
SignatureVerificationException:签名不一致异常.
TokenExpiredException:令牌过期异常.
AlgorithmMismatchException:算法不匹配异常.
InvalidClaimException:失败的 payload 异常.
d)执行结果
1.2.2、封装 Jwt 工具类
未来在我们的项目中,肯定是需要对上述过程进行封装来使用的.
主要有三个方法:
生成 Token.
验证 Token 合法性.
获取 Token.
a)Java 如下:
/**
* Jwt 工具类
*/
public class JwtUtils {
//自定义密钥
private static final String SIGN = "Y*(GY*G^&*%69g*()&";
/**
* 生成 Token
* @param map 自定义的载荷数据
* @return 返回 Token
*/
public static String createToken(Map<String, String> map) {
//1.设置过期时间(默认 1 天过期)
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE, 1);
//2.创建 jwt builder,添加自定义的载荷数据
JWTCreator.Builder builder = JWT.create();
for(Map.Entry<String, String> entry : map.entrySet()) {
builder.withClaim(entry.getKey(), entry.getValue());
}
//3.生成 Token
String token = builder.withExpiresAt(instance.getTime()) //过期时间
.sign(Algorithm.HMAC256(SIGN));// sign
return token;
}
/**
* 验证 Token 合法性
* @param token
*/
public static boolean checkToken(String token) {
try {
JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 获取 Token 信息
* @param token
* @return
*/
public static DecodedJWT getTokenInfo(String token) {
DecodedJWT verify = JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
return verify;
}
}
b)Kotlin 如下:
/**
* Jwt 工具类
*/
object JwtUtils {
//自定义密钥
private const val SIGN = "Y*(GY*G^&*%69g*()&"
//过期时间(默认 1 天过期)
private const val EXPIRE_TIME = 1
/**
* 生成 Token
* @param map 自定义的载荷数据
* @return 返回 Token
*/
fun createToken(map: Map<String, String>): String {
//1.设置过期时间
val expireTime = Calendar.getInstance().apply {
this.add(Calendar.DATE, EXPIRE_TIME)
}.time
//2.创建 jwt builder,添加自定义的载荷数据
val builder = JWT.create()
for ((key, value) in map) {
builder.withClaim(key, value)
}
//3.生成 Token
return builder.withExpiresAt(expireTime) //过期时间
.sign(Algorithm.HMAC256(SIGN)) // sign
}
/**
* 验证 Token 合法性
* @param token
*/
fun checkToken(token: String): Boolean {
return try {
JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token)
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
/**
* 获取 Token 信息
* @param token
* @return
*/
fun getTokenInfo(token: String): DecodedJWT {
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token)
}
}
1.2.3、案例:用户登录和退出登录 用户登录:登录时根据用户信息创建 jwt,并把生成的 token 保存到 redis 上.
@SneakyThrows
@Override
public String login(UserLoginDto loginDto) {
//1.非空校验
if(loginDto == null || !StringUtils.hasLength(loginDto.getPassword()) ||
!StringUtils.hasLength(loginDto.getUsername())) {
throw new ApplicationException(AppResult.fail(CodeMsg.FAIL_NULL_USER));
}
//2.校验密码
User dbUser = userMapper.selectUserByName(loginDto.getUsername());
if(dbUser == null) {
throw new ApplicationException(AppResult.fail(CodeMsg.FAIL_NOT_EXISTS_USER));
}
if(!PasswordUtils.check(loginDto.getPassword(), dbUser.getPassword())) {
throw new ApplicationException(AppResult.fail(CodeMsg.FAIL_ERROR_USER));
}
//3.Jwt 生成 token 作为 key,json 格式的用户信息作为值,保存到 redis 上
HashMap<String, String> map = new HashMap<>();
map.put("username", loginDto.getUsername());
String token = JwtUtils.createToken(map);
String userJson = objectMapper.writeValueAsString(dbUser);
redisTemplate.opsForValue().set(RedisConstants.TOKEN + token, userJson);
//4.将 token 数据返回
log.info("用户上线!{}", dbUser.toString());
return token;
}
问题:jwt 为什么要在 redis 上保存一份?
JWT 一旦生成,有效期内一直有效,无法被销毁,如果不这样保存可能会导致以下情况:
- 如果 jwt 用户登录身份鉴权,也就意味着再** JWT 有效期内,用户无法退出登录**.
- 一旦黑客攻击,已经拿到用户的权限,在服务器上乱搞,作为管理员也没有办法,如果之前用 redis 存储过,此时,就可以就可以直接删除 redis 上的 token
- 因此需要搭配 redis 来销毁 JWT,具体做法就是
jwt
在redis
上也存储一份,当用户登录时,先检验 redis 上是否存有 jwt(如果 redis 上没有 jwt,就不往后执行了
,直接返回失败信息),再用 jwt 工具检验令牌是否有效. 因此就可以通过删除 redis 上的 jwt 达到销毁 jwt 的目的了.
退出登录:退出登录时从 redis 上删除 token(相当于销毁 JWT 令牌).
@SneakyThrows
@Override
public void logout(String token) {
//1.非空校验
if(token == null) {
log.info("token 数据异常!");
throw new ApplicationException(AppResult.fail("token 数据异常!"));
}
//2.从 redis 上获取用户信息(用来打印日志)
String userJson = redisTemplate.opsForValue().get(RedisConstants.TOKEN + token);
log.info("用户退出登录!{}", userJson);
//3.从 redis 上删除该用户的 token 数据
redisTemplate.delete(RedisConstants.TOKEN + token);
}
Ps:这里就不用判断 JWT 是否有效了,因为即使过期无效,用户也是退出登录,不影响整体业务逻辑.
评论( 0 )