使用JWT控制登录鉴权的项目如何实现单个账号只允许在一处登录的功能,也就是说在登录状态未失效的情况下,下一次登录必须踢掉上一次的登录。如果我们不做限制的话,单个账号多次登录就会产生多个token,只要未过期就都能调用接口。可能第一反应会想,用JWT实现token手动强行过期,然而JWT并未提供此功能,那么我们在项目中应该如何优雅的解决这个问题呢?<继上篇>
我们可以将用户每次登录后产生的token 存入redis 中,key中加入用户id来识别
//token 存入redis
redisTemplate.opsForValue().set("access_token:member_"+member.getId(), token, 30, TimeUnit.DAYS);
那么第二次同一账号执行登录操作的时候就会覆盖上一次登录存在redis中的token。
写到这里就会发现在token 未失效的情况下,上次登录产生的token已经不存在了。隐约说明了踢掉了上次登录,那么下一步该如何具体判断上一次的登录已失效呢?
其实很简单,我们虽然做不到token真正过期,但是我们能进行新旧token对比来保证旧token 无权限调用接口,从而达到限制单处登录功能。
可能会有人发现token 在未过期的时间段里无论怎么登录都是一模一样,从而无法实现新旧token对比,其实只需要在创建token的时候在 claim 里加上一个随机字符串即可。例:uuid
JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
.claim("nickName", name)
.claim("memId", userId)
.claim("uuid", "我是一个随机字符串")
.setIssuer("francis")//发行者,自定义
.setAudience("client")
.signWith(signatureAlgorithm, generalKey());
下一步:在过滤器中通过解析当前传入的token拿到用户id,然后从redis取出该用户登录所记录的token作对比。
@SuppressWarnings("rawtypes")
@Autowired
private RedisTemplate redisTemplate;
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if("OPTIONS".equals(request.getMethod())){
chain.doFilter(request, response);
return;
}
String auth = request.getHeader("token");
String nowToken = auth;
if ((auth != null) && (auth.length() > 7)) {
String HeadStr = auth.substring(0, 6).toLowerCase();
if (HeadStr.compareTo("bearer") == 0) {
auth = auth.substring(7, auth.length());
if (JwtHelper.parseJWT(auth) != null) {
//解出memId
Integer memId = JwtHelper.getUserId(nowToken);
//取出登录时保存的token
Object oldTokenObj = redisTemplate.opsForValue().get("access_token:member_"+memId);
System.err.println(oldTokenObj);
//对比两个token,(注Validator...为feilong工具包内容,没有使用的自行修改判断即可)
if(Validator.isNullOrEmpty(oldTokenObj) || auth.equals(oldTokenObj.toString())) {
chain.doFilter(request, response);
return;
}
}
}
}
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().write(JsonUtil.getJsonFromObject(new Result(EnumSvrResult.ERROR_TOKEN)));
return;
}
写到这里那么一个新的问题又来了,filter 过滤器是属于servlet里面的,并不存在于spring的上下文中,哪怕你在filter上加@Component注解也没有作用,这点要注意了。那么redisTemplate 该如何Autowired依赖注入?
其实我们在上一篇文章中有注意到有JwtConfig类是启动加载的,在FilterRegistrationBean中将filter注册到了servlet,其实我们只需要在filter注册到servlet之前将它注册到spring 上下文中,如下
@Configuration
public class JwtConfig {
@Bean
public FilterRegistrationBean basicFilterRegistrationBean(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(getJwtFilter());
List urlPatterns = new ArrayList<>();
urlPatterns.add("*.auth");
registrationBean.setUrlPatterns(urlPatterns);
return registrationBean;
}
//注入JwtFilter bean
@Bean
public JwtFilter getJwtFilter(){
return new JwtFilter();
}
}
那么再运行程序filter中的redis 就能成功依赖注入了。
在JWT基础上实现单个账号只允许在一处登录的功能就很简单的实现了。
----我是francis,谨以此记录自己精彩的程序人生!!