登录接口

1 JWT

1.1 组成

JWT由JWT头,载荷和前面,Signature 部分是对前两部分的签名,防止数据被篡改。其中JWT头和载荷为Base64URL编码,Signature通过密钥根据算法生成。

image-20231206154035829

1.2 使用

  1. 引入依赖
1
2
3
4
5
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>
  1. 加密与解密
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 加密
String token = JWT.create()
    .withClaim(user, 用户数据)
    .withExpiresAt(new Date(System.currentTimeMillis() + 1000*60*60))
    .sign(Algorithm.HMAC256(秘钥));
// 解密
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(“密钥”)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(令牌字符串);//抛异常解析失败
//得到所有载荷
Map<String, Claim> claims = decodedJWT.getClaims();

2 拦截器

对于其他接口,在未登录之前不能访问该接口,因此添加拦截器。

  1. 拦截器声明
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
    @Autowired
    private JwtProperties jwtProperties;
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }
        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());
        //2、校验令牌
        try {
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            BaseContext.setCurrentId(empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}
  1. 拦截器注册
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
    @Autowired
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
    
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtTokenAdminInterceptor)
                .addPathPatterns("/admin/**")
                .excludePathPatterns("/admin/employee/login");
    }
}

3 ThreadLocal

在用户登录后,界面需要展示信息,因此要调用其他接口,此时需要保存用户ID提升代码复用性。

1
2
3
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
BaseContext.setCurrentId(empId);

后续接口需要使用empId从局部变量获取即可。

在请求结束后,清除共享变量。

1
2
3
4
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    BaseContext.removeCurrentId();
}

4 令牌失效

在用户更新密码等操作后,要将旧令牌失效。

使用redis技术,在用户登录成功后令牌保存到redis,修改密码后清除redis。每次校验jwt的同时对比redis是否存在该令牌。

  1. 配置redis起步依赖
1
2
3
4
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 属性配置
1
2
3
4
5
spring:
  data:
    redis:
      host: localhost
      port: 6379
  1. 登录成功后存token到redis中。
1
2
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.set(token, token, 1, TimeUnit.HOURS); //token同时作为键值
  1. 拦截器中获取相同的token
1
2
3
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
String redistoken = operations.get(token); //token同时作为键值
// 若查不到该token,删除
  1. 修改密码后删除token
1
2
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.getOperations().delete(token);
updatedupdated2023-12-062023-12-06