首先理一下登录流程
前端登录--->账号密码验证--->成功返回token--->后续请求携带token---->用户异地登录---->本地用户token不能用,不能再访问需要携带token的网页
jwt工具类
java
package com.nageoffer.shortlink.admin.util;
import cn.hutool.core.util.ObjectUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.nageoffer.shortlink.admin.common.constant.UserConstant;
import com.nageoffer.shortlink.admin.common.convention.exception.ClientException;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
// 默认过期时间 1 小时
private static final long EXPIRE_TIME = 60 * 60 * 1000L;
// 签名密钥
private static final String SECRET = "short-link-secret-key";
/**
* 生成 token
*
* @param claims 自定义的载荷
* @return JWT token
*/
public static String generateToken(Map<String, Object> claims) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + EXPIRE_TIME);
return JWT.create()
.withIssuedAt(now) // 签发时间
.withExpiresAt(expireDate) // 过期时间
.withPayload(claims) // 自定义载荷
.sign(Algorithm.HMAC256(SECRET)); // 签名算法
}
/**
* 验证 token 是否有效
*
* @param token 待验证的 JWT
* @return 是否有效
*/
public static boolean verifyToken(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(token);
return true;
} catch (JWTVerificationException e) {
return false;
}
}
/**
* 获取 token 中的某个 claim
*
* @param token JWT token
* @param key claim 的 key
* @return claim 对应的值
*/
public static String getClaim(String token, String key) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(key).asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 获取 token 的过期时间
*
* @param token JWT token
* @return 过期时间
*/
public static Date getExpireAt(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt();
} catch (JWTDecodeException e) {
return null;
}
}
public static String getCurrentUser() {
String username = null;
try {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader(UserConstant.TOKEN);
if (ObjectUtil.isNotEmpty(token)) {
username = JWT.decode(token).getClaim("username").asString();
}
} catch (Exception e) {
throw new ClientException("获取当前用户信息出错");
}
return username;
}
}
JWT拦截器
每次更新token的过期时间
java
package com.nageoffer.shortlink.admin.config;
import com.nageoffer.shortlink.admin.common.constant.UserConstant;
import com.nageoffer.shortlink.admin.common.convention.exception.ClientException;
import com.nageoffer.shortlink.admin.util.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.concurrent.TimeUnit;
import static com.nageoffer.shortlink.admin.common.constant.RedisCacheConstant.USER_LOGIN_KEY;
/**
* jwt拦截器
*/
@Component
@RequiredArgsConstructor
public class JwtInterceptor implements HandlerInterceptor {
private final StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader(UserConstant.TOKEN);
if (token == null || !JwtUtil.verifyToken(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
throw new ClientException("token无效或已过期");
}
// 从 token 获取用户名
String username = JwtUtil.getClaim(token,"username");
// 可选:检查 Redis 是否存在 token,实现单点登录
String redisToken = stringRedisTemplate.opsForValue().get(USER_LOGIN_KEY + username);
if (redisToken == null || !redisToken.equals(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
throw new ClientException("您已经在其他地方登录,请重新登录");
}
// 可选:刷新 Redis token 过期时间
String redisKey = USER_LOGIN_KEY + username;
stringRedisTemplate.expire(redisKey, 30, TimeUnit.MINUTES);
// 将用户名放入请求上下文,供 Controller 使用
request.setAttribute("username", username);
return true;
}
}
注册JWT拦截器,并选择放行哪些接口
java
package com.nageoffer.shortlink.admin.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns(
"/api/short-link/admin/v1/user/login" // 登录接口不拦截
);
}
}
登录方法
首先判断账号密码,正确以后,判断redis是否有这个用户,如果有,说明已经登录过了,把原来的token删除了。
接下来统一生成新token,存入redis
java
@Override
public UserLoginRespDTO login(UserLoginReqDTO requestParam) {
LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery(UserDO.class)
.eq(UserDO::getUsername, requestParam.getUsername())
.eq(UserDO::getPassword, requestParam.getPassword())
.eq(UserDO::getDelFlag, 0);
UserDO userDO = baseMapper.selectOne(queryWrapper);
if (userDO == null) {
throw new ClientException("用户不存在");
}
String redisKey = USER_LOGIN_KEY + requestParam.getUsername();
// 检查 Redis 是否已存在 token,实现单点登录
String existingToken = stringRedisTemplate.opsForValue().get(redisKey);
if (existingToken != null) {
stringRedisTemplate.delete(redisKey);
}
// 自定义载荷,如何还需要添加别的信息,可以继续添加,如用户ID
Map<String, Object> claims = new HashMap<>();
claims.put("username", requestParam.getUsername());
// 生成新 token
String token = JwtUtil.generateToken(claims);
// 存入 Redis,实现单点登录
stringRedisTemplate.opsForValue().set(redisKey, token, 30, TimeUnit.MINUTES);
return new UserLoginRespDTO(token);
}
退出登录
在redis中删除用户即可
java
@Override
public void logout(String username) {
if (checkLogin(username)) {
stringRedisTemplate.delete(USER_LOGIN_KEY + username);
return;
}
throw new ClientException("用户Token不存在或用户未登录");
}