最近被要求修复一个账号可以在不同的浏览器和不同的pc上登录,只要jwt有效,就能访问系统,要求限制用户账号的有效性,简而言之就是当同一个账户登录不同的电脑,后登录的有效,而之前登录的失效。
这里由于是单机部署,不涉及分布式缓存的问题,所以使用单机缓存就可以了,网上看了springboot cache推荐的ganva ,咖啡因,Ehcache,这里我们不使用redis,而网上推荐的这几种说句实话,没看懂如何用。最后发现hutool有一个cacheutil可以用,就是用其中的FIFO实现一下。
1.引入hutool依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.9</version>
</dependency>
2. 设置全局bean
import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.FIFOCache;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CacheConfig {
@Bean
public FIFOCache<String, Object> globalCache() {
// 初始化并设置缓存容量大小为100
return CacheUtil.newFIFOCache(100);
}
}
3.设置web过滤器
# LoginCheckInterceptor.java
public class LoginCheckInterceptor implements HandlerInterceptor {
static Logger log = LoggerFactory.getLogger(LoginCheckInterceptor.class);
@Autowired
private FIFOCache<String, Object> globalCache;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("LoginCheckInterceptor preHandle " + request.getRequestURL());
Object dLoginUser = request.getSession().getAttribute("USER_INFO");
UserInfo userinfo=(UserInfo)dLoginUser;
log.info("checkuser userinfo:"+userinfo.getName());
String token = request.getHeader("Authorization");
String salt=(String)globalCache.get(userinfo.getName());
log.info("salt is:"+salt);
if ( !JWTTokenUtils.verify(token.replaceAll("Bearer ", ""),salt)) {
// 未登录,转向登录页面!
JsonHeaderWrapper baseDTO = new JsonHeaderWrapper<>();
response.setContentType("text/html;charset=UTF-8");
baseDTO.setErrmsg("Token已失效或用户未登录!");
PrintWriter writer = response.getWriter();
Map<String, Object> data = new HashMap<>(2);
baseDTO.setData(data);
baseDTO.setStatus(401);
writer.print(Jackson2Helper.toJsonString(baseDTO));
return false;
}else{
UserInfo userInfo=JWTTokenUtils.decode(token);
log.info("decode userinfo is:"+userInfo.getName());
}
return true;
}
}
@Configuration
public class KongxConfig implements WebMvcConfigurer {
@Value("${portal.exclude.paths:/shell,/index,/authorize/login.do,/inner/monitor/ping,/health/check,/authorize/getUserInfo.do,/authorize/logout.do," +
"/index.html,/cdn/**,/css/**,/img/**,/js/**,/svg/**,/util/**,/favicon.ico}")
private String excludePaths;
@Bean
public LoginCheckInterceptor loginCheckInterceptor(){
return new LoginCheckInterceptor();
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new ServletWebArgumentResolverAdapter(new UserArgumentResolver()));
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginValidateInterceptor()).excludePathPatterns(excludePaths.split(","));
registry.addInterceptor(loginCheckInterceptor()).excludePathPatterns(excludePaths.split(","));
}
}
在这个过程中发现Spring中的Interceptor拦截器中使用@Autowired注解,在运行时会出现空指针,
在Web开发过程中,我们经常会使用拦截器,做登录权限校验,一般在拦截器里,我们也会用到各种Service方法 。
参考这篇:https://juejin.cn/post/7085246509591035940
4.jwt 设置
import java.util.Date;
public abstract class JWTTokenUtils {
private final static String SECRET = "ADBC";
//设置jwt失效时间
private static final long EXPIRATION_TIME_MILLIS = 3600000; // 1 hour
public static String getToken(UserInfo user) {
String token = "";
Date expirationDate = new Date(System.currentTimeMillis() + EXPIRATION_TIME_MILLIS);
token = JWT.create().withAudience(Jackson2Helper.toJsonString(user)).withExpiresAt(expirationDate)
.sign(Algorithm.HMAC256(SECRET));
return token;
}
public static String getToken(UserInfo user,String salt) {
String token = "";
Date expirationDate = new Date(System.currentTimeMillis() + EXPIRATION_TIME_MILLIS);
token = JWT.create().withAudience(Jackson2Helper.toJsonString(user)).withExpiresAt(expirationDate)
.sign(Algorithm.HMAC256(salt));
return token;
}
public static UserInfo decode(String token) {
token=token.replace("Bearer","");
String user = JWT.decode(token).getAudience().get(0);
return Jackson2Helper.parsonObject(user, new TypeReference<UserInfo>() {
});
}
public static boolean verify(String token) {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
try {
DecodedJWT decodedJWT = jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
return false;
}
return true;
}
public static boolean verify(String token,String salt) {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(salt)).build();
try {
DecodedJWT decodedJWT = jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
return false;
}
return true;
}
5.思路
基本思路就是在用户密码校验通过后,随机生成一个salt,使用这个salt生成jwt,由于jwt是无状态的,所以当重新登录会又生成一个salt,这时候 请求拦截发现用现在的salt无法验证上一次的jwt token 就实现了踢人的效果。