使用JWT认证登录流程
作者是初学者,本文是根据我自己的练习小项目编写测试,从逻辑上来讲其实有很多地方应该改动,但我只为实现对应简单流程功能,如有错误请告知立刻修改。
流程如下:
1、首先在pom文件中导入JWT的maven依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
2、编写JWT的工具类
这个工具类里只有生成token方法和解析token方法变成自己要拿取的信息。
/**
* @Author: wxy
* @Date: 2023/12/12 17:29
* @Description: jwt工具类
*/
public class JWTUtil {
/**
* token过期时间
*/
private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000;
/**
* 自动生成的密钥
*/
private static final SecretKey TOKEN_SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
/**
* 使用jwt生成token
* @param customerDTO 客户id和客户姓名
* @return token
*/
public static String generateToken(CustomerDTO customerDTO) {
HashMap<String, Object> header = new HashMap<>(1);
header.put("type", "jwt");
HashMap<String, Object> claims = new HashMap<>(2);
claims.put("id", customerDTO.getId());
claims.put("name", customerDTO.getName());
Date now = new Date();
Date expireTime = new Date(System.currentTimeMillis() + EXPIRE_TIME);
return Jwts.builder()
.setHeader(header)
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expireTime)
.signWith(TOKEN_SECRET_KEY)
.compact();
}
/**
* 解析token获得携带的信息
* @param token token
* @return 包含客户id和name的dto
*/
public static R parseToken(String token) {
if(StringUtils.isNotBlank(token)){
Claims claims = null;
try {
claims = Jwts.parserBuilder()
.setSigningKey(TOKEN_SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
e.printStackTrace();
return R.fail("token解析错误");
}
Long id = Long.valueOf(claims.get("id").toString());
String name = claims.get("id").toString();
CustomerDTO dto = new CustomerDTO();
dto.setId(id);
dto.setName(name);
return R.success(dto);
}
return R.fail();
}
}
3、编写登录的接口和逻辑层
这里的逻辑就是拿客户端传来的信息去数据库中去比对,比对成功生成token并保存到redis中。
/**
* @Author: wxy
* @Date: 2023/12/13 17:05
* @Description: 登录控制器
*/
@RestController
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
/**
* 登录接口
* @param customerDTO 用来接收id和name的dto
* @return 是否登录成功的信息
*/
@PostMapping("/login")
public R login(@RequestBody CustomerDTO customerDTO) {
return loginService.login(customerDTO);
}
}
/**
* @Author: wxy
* @Date: 2023/12/14 08:42
* @Description: 登录业务逻辑层
*/
@Service
@RequiredArgsConstructor
public class LoginService {
private final CustomerMapper customerMapper;
private final StringRedisTemplate stringRedisTemplate;
/**
* 登录接口
* @param customerDTO 用来接收id和name的dto
* @return 是否登录成功的信息
*/
public R login(CustomerDTO customerDTO) {
LambdaQueryWrapper<Customer> wrapper = new LambdaQueryWrapper<>();
if (customerDTO.getId() != null){
wrapper.eq(Customer::getId, customerDTO.getId());
}
if(StringUtils.isNotBlank(customerDTO.getName())){
wrapper.eq(Customer::getName, customerDTO.getName());
}
Customer customer = customerMapper.selectOne(wrapper);
String token = null;
if (customer != null) {
// 生成token
token = JWTUtil.generateToken(customerDTO);
// 将token存入redis并设置7天过期时间
stringRedisTemplate.opsForValue()
.set(TokenPrefixConstant.TOKEN_REDIS_PREFIX, token, 7, TimeUnit.DAYS);
System.out.println("token = " + token);
return R.success("登录成功", token);
}
return R.fail(401,"登录失败,请输入正确的id和姓名!");
}
}
4、编写JWT的拦截器
这个拦截器的逻辑是从客户端的请求头中拿取token并和redis中的token进行比对,成功就调用接口,不成功就返回自定义的JSON信息。
/**
* @Author: wxy
* @Date: 2023/12/13 16:39
* @Description: jwt拦截器
*/
@RequiredArgsConstructor
public class JWTInterceptor implements HandlerInterceptor {
private final StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
String token = request.getHeader("Authorization");
if (StringUtils.isNotBlank(token)) {
String redisToken = stringRedisTemplate.opsForValue().get(TokenPrefixConstant.TOKEN_REDIS_PREFIX);
if (redisToken != null && redisToken.equals(token)) {
return true;
}
}
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
LinkedHashMap<String, Object> res = new LinkedHashMap<>();
res.put("code", 401);
res.put("message", "认证失败,token已失效请重新登录!");
res.put("data", Collections.emptyList());
PrintWriter out = response.getWriter();
out.append(new JSONObject(res).toString());
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
5、编写JWT拦截器配置类
这里的逻辑是添加自定义拦截器,并设置拦截的路径和不拦截的路径,这里有一个比较容易出错的地方,在上面的拦截器中我直接注入StringRedisTemplate,但是不能用,返回的NPE,在查阅了一些资料和视频后,我个人理解大概应该是拦截器在spring的bean实例化之前初始化的,我需要给他实例化一下并把stringRedisTemplate通过构造器注入进去。
/**
* @Author: wxy
* @Date: 2023/12/13 16:57
* @Description: jwt拦截器配置类
*/
@Configuration
@RequiredArgsConstructor
public class InterceptorConfig implements WebMvcConfigurer {
private final StringRedisTemplate stringRedisTemplate;
@Bean
public JWTInterceptor jwtInterceptor() {
return new JWTInterceptor(stringRedisTemplate);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor(stringRedisTemplate))
.addPathPatterns("/**")
.excludePathPatterns("/login");
}
}
6、总结
到这里应该流程已经结束了,我这个大概的逻辑流程就是:
成功:
- 携带信息登录 => 登录成功返回token => 客户端携带token访问其他接口
失败:
- 携带信息登录 => 登录信息不成功直接返回登录失败信息
- 携带token信息不对或者已经失效 => token信息不对或者失效返回重新登录信息
其实还有很多的东西没有添加,比如双token,一个access_token一个refresh_token,refresh_token的过期时间比access_token多一段时间,当access_token失效时,校验refresh_token再重新生成双token,就不用再重新登录获取token,从而提高体验。
也可以编写对应的工具从请求头信息中拿取用户信息来进行业务逻辑的编写会更加的方便。