暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

JWT及Interceptor拦截器案例

加耀 2018-12-24
721


近期项目组停止开发新需求,进行代码安全渗透及代码重构工作,由于项目网关模块代码较为凌乱,所以组长交由我处理登录接口和注册接口代码重构。

 

在最新的一期安全测试中,检测出Token存在重放问题,需要处理,由于网关中使用的是JWT生成token,生效期间的token无法让其失效,所以需要修改原有的项目逻辑,使token失效。

 

不太了解JWT的小伙伴可以先行查阅一下JWT相关资料,JWT生成的token字符串中有携带有效时间,有效时间最长可设置为永久,一般有效期设置为7天。

 

在这7天内,jwt内部代码可解析token,来判断它是否有效,然后从中获取用户信息。但是如果用户在7天内重新登录生成了新的token,那么之前的token没法使其失效,所以存在一种重放安全问题。

 

那么,问题找到了,怎样使token过期了,这里,我在用户登录时,将生成的token存储到缓存中,然后定义一个拦截器,拦截指定请求url或者过滤指定请求url,在拦截器中我们获取token信息。由于先前的token拦截器被封装到了源码中,所以这里只能自己再新定义一个拦截器来做业务处理了。

 

我们先定义一个WebInterceptor类,让其实现HandlerInterceptor类,实现它的preHandle方法,即,在请求前执行,代码如下:

/**
*
@author: JiaYao
*
@demand: 自定义拦截器
* @parameters:
* @creationDate2018/12/15 0015 17:23
*/
@Slf4j
public class WebInterceptor implements HandlerInterceptor {
   private String jwtSecretKey;
   private
RedisService redisService;
   public
WebInterceptor(String jwtSecretKey, RedisService redisService) {
       this.jwtSecretKey = jwtSecretKey;
       this
.redisService = redisService;
   
}
   @Override
   
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
       String authorization = request.getHeader("Authorization");
       if
(authorization == null || !authorization.startsWith("Bearer ")) {
           // 跳过这一个拦截器,让走后面的拦截器
       
} else {
           try {
               String token = authorization.substring(7);
               
log.info("当前请求token:{}", token);
               
Claims claims = (Claims) Jwts.parser().setSigningKey(this.jwtSecretKey).parseClaimsJws(token).getBody();
               
Long customerId = Long.valueOf(claims.get("id").toString());
               
log.info("当前登录用户id:{}", customerId);
               
// 判断当前用户的token在缓存中是否存在
               
Object obj = redisService.get(RedisKeyConstants.USER_TOKEN + customerId.toString());
               if
(obj == null || !obj.toString().equals(token)) {
                   throw new ServiceException("K-000066");
               
}
           } catch (MalformedJwtException | ExpiredJwtException | SignatureException var6) {
               log.error("JwtInterceptor preHandle exception, exMsg:{}", var6.getMessage());
               throw new
SignatureException("Invalid token.");
           
}
       }
       return true;
   
}


然后我们再新建一个类WebRequestInterceptor让其继承WebMvcConfigurerAdapter类,重写它的addInterceptors方法,在方法中,我们将刚刚创建的拦截器类添加到里面来(拦截器是观察者设计模式,即观察者观察被观察者,此处的绑定即为建立观察关系)。代码如下:

/**
*
@author: JiaYao
*
@demand: 将拦截器添加到列表中,即观察者与被观察者
* @parameters:
* @creationDate2018/12/19 0019 9:16
*/
@Configuration
public class WebRequestInterceptor extends WebMvcConfigurerAdapter {
   @Autowired
   private RedisService redisService;
   
@Value("${jwt.secret-key}")
   private String jwtSecretKey;
   
@Override
   
public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(getWebInterceptor())
               // 拦截所有请求
               
.addPathPatterns("/**");
   
}

   private WebInterceptor getWebInterceptor() {
       return new WebInterceptor(this.jwtSecretKey, this.redisService);
   
}
}


上面代码中,我们先定义了一个拦截器,在拦截器中,我们先从request中拿到head,判断header中是否含有指定参数,然后解析参数,获取到token,判断当前token在缓存中是否存在,如果存在则直接放行,否则,直接抛出指定异常,通过统一异常处理返回信息给app开发同事。

 

然后我们定义了一个配置类,将类交由spring容器管理并指定为配置类,我们在addInterceptors方法中将刚定义的拦截器添加在里面来,并且配置拦截路径为全部。

 

 

然后使用postman进行代码测试。首期测试通过。

 

但是,在与ios开发的小伙伴联调接口的时候,发现出了点问题,当用户处于未登录状态时,做一些未登录的处理时,前台会抛异常。寻思了半天,仔细问了一下参数传递方式时,才发现,ios的小姐姐写了一个拦截器,为每一个请求都配置了header中传递了token,并且小姐姐还特意配置了一下错误的token,来演示token失效且未登录状态。

 

怎么办?按理来说,未登录状态下,就不应该拦截未登录的接口请求,这样不管前端是否有传递token,都不影响未登录状态下的正常访问。这时,突然想起来在项目配置文件中,jwt为每个请求都记录了拦截,需要拦截的请求都配置在了配置文件中,我们可以直接从配置文件中获取需要拦截哪些请求,在前面的拦截中,我们就不应该拦截全部,而是应该拦截指定url

 

由于原有的jwt配置在了源码中,找了好久只找到了拦截器的那个class文件,但是没有找到绑定关系的class文件在哪,不过我猜测,源码里面应该也是这样配置的,读取配置文件信息,将需要拦截的url配置到拦截器中进行指定路径拦截。

改动代码后:

/**
*
@author: JiaYao
*
@demand: 将拦截器添加到列表中,即观察者与被观察者
* @parameters:
* @creationDate2018/12/19 0019 9:16
*/
@Configuration
public class WebRequestInterceptor extends WebMvcConfigurerAdapter {

   @Autowired
   private RedisService redisService;
   
@Value("${jwt.secret-key}")
   private String jwtSecretKey;
   
/*需要JWT拦截的*/
   
@Value("${jwt.url-patterns}")
   private String jwtUrlPatterns;
   
@Override
   
public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(getWebInterceptor())
               .addPathPatterns(jwtUrlPatterns.split(","));
   
}

   private WebInterceptor getWebInterceptor() {
       return new WebInterceptor(this.jwtSecretKey, this.redisService);
   
}
}


然后再次和小姐姐联调后,一切ok

 

代码重构后,每次登录操作先校验缓存中是否有当前用户的token,如果有,则使用第三方推送发送通知,账号已在另一台设备登录(推送中需检验机器码),这样就可以解决app的单点登录操作了。

 

目前业务要求这样做,所以就这样写了,虽然感觉还是不太合理。

 

2018122200:02:01


文章转载自加耀,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论