最近做的一个项目用的是前后端分离的开发模式,系统是要登录后才能进行操作的,所以需要进行身份token校验,校验通过后才能得到所请求的资源。我一开始想到的是使用过滤器实现,但系统里的有些controller是不用过滤的,比如一个新增用户的页面,有很多个下拉框,那请求下拉框数据的时候,就不用每个controller都进行校验了。
于是就用了拦截器+自定义注解来实现,思路如下:用户登录成功后生成一个jwt,也就是token,然后存到Redis里去;每次发起请求时首先判断该controller上有没有自定义的注解,如果有则跳过token验证,如果没有则不跳过,然后去Redis里取token,取到了则验证成功,取不到则验证失败
自定义注解代码如下(ExcludeInterceptor.java):
package com.ue.core.web.interceptor;
import java.lang.annotation.*;
/**
* 用于排除不用进行jwt验证的自定义注解
* @author LiJun
* @Date 2019年09月28日
* @Time 11:08
*/
@Target(ElementType.METHOD)//指定被修饰的Annotation可以放置的位置:方法上面
@Retention(RetentionPolicy.RUNTIME)//注解会在class字节码文件中存在,在运行时可以通过反射获取到
@Inherited//指定被修饰的Annotation将具有继承性
public @interface ExcludeInterceptor {
}
拦截器相关逻辑如下(JwtInterceptor.java):
package com.ue.core.web.interceptor;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.ue.core.dao.UserDao;
import com.ue.core.service.JwtService;
import com.ue.core.util.BeanUtil;
import com.ue.core.util.JedisUtil;
import com.ue.core.util.JsonRespData;
import com.ue.core.util.SpringContextHolder;
import com.ue.core.web.interceptor.ExcludeInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import redis.clients.jedis.Jedis;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;
import static com.ue.core.entity.CommonEntity.*;
/**
* 用于处理jwt验证的拦截器
* @author LiJun
* @Date 2019年09月27日
* @Time 18:06
*/
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
private UserDao userDao;//定义用户的dao层
private JwtService jwtService;
private boolean OFF = false;//true为关闭jwt令牌验证功能
/**
* 返回 false:请求被拦截,返回
* 返回 true :请求OK,可以继续执行,放行
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
Jedis jedis = null;
try {
//获取到目标方法对象
HandlerMethod method = (HandlerMethod) o;
//拿到方法上的注解
ExcludeInterceptor methodAnnotation = method.getMethodAnnotation(ExcludeInterceptor.class);
if (OFF || BeanUtil.isNotBlank(methodAnnotation)) {//如果该方法上有自定义的注解,则直接跳过这个拦截器
return true;
}
jedis = JedisUtil.getJedisConn();
String jwt = request.getHeader("jwt");//请求头里的jwt令牌
String userId = request.getParameter("tokenId");//登录者的ID
jwtService = SpringContextHolder.getBean(JwtService.class);//从静态变量applicationContext中取到JwtService
userDao = SpringContextHolder.getBean(UserDao.class);//从静态变量applicationContext中取到UserDao
if (StringUtils.isNotBlank(jwt)){//jwt不为空
if (StringUtils.isBlank(jedis.get(jwt))){//如果Redis里没有这个jwt,说明jwt已经失效
returnErrorResponse(response,JsonRespData.success(REQUEST_204, "token已过期"));
return false;
}
else {//jwt没有失效
Map user = userDao.getUserById(userId);
String isDelete = String.valueOf(user.get("is_delete"));
String state = String.valueOf(user.get("state"));
if ("N".equals(isDelete) && "2".equals(state)) {//该用户没有被逻辑删除且审核已通过
DecodedJWT decodedJWT = jwtService.getDecryptString(jwt);//token解密id
String id = decodedJWT.getClaim("id").asString();
if (StringUtils.isNotBlank(userId) && userId.equals(id)) {
jedis.expire(jwt, 1800);//验证用户的登录状态成功,token有效期重置为30分钟
return true;
} else {
returnErrorResponse(response,JsonRespData.success(REQUEST_203, "用户登录异常,用户id前后不一致"));
return false;
}
} else {
log.info("帐号已被管理员禁用,token有效期不重置");
returnErrorResponse(response,JsonRespData.success("202", "该帐号暂时不能进行登录"));
return false;
}
}
}
else {
log.info("入参jwt为空");
returnErrorResponse(response,JsonRespData.success(REQUEST_205, "token不允许为空"));
return false;
}
}catch (Exception e){
log.info("验证jwt出现异常:" + e);
returnErrorResponse(response,JsonRespData.success(INTERNAL_SERVER_ERROR, "验证jwt出现异常"));
return false;
}finally {
if (jedis != null)
jedis.close();
}
}
/**
* 发送jwt的验证结果到客户端
* @author LiJun
* @Date 2019/9/28
* @Time 15:13
* @param response
* @param result
* @return void
*/
private void returnErrorResponse(HttpServletResponse response, JsonRespData result) throws IOException {
OutputStream out = null;
try{
response.setCharacterEncoding("utf-8");
response.setContentType("text/json");
out = response.getOutputStream();
out.write(JSONObject.toJSONString(result).getBytes("utf-8"));
out.flush();
} finally{
if(out != null){
out.close();
}
}
}
/**
* 请求controller之后,渲染视图之前
*/
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
/**
* 请求controller之后,视图渲染之后
*/
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
在springmvc中配置拦截器(spring-web.xml):
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.ue.core.web.interceptor.JwtInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
登录相关代码如下:
UserController.java:
/**
* APP端登录验证
* @author LiJun
* @Date 2019/9/25
* @Time 9:35
* @param user
* @return com.ue.core.util.JsonRespData
*/
@RequestMapping(value = "login",method = RequestMethod.POST)
@ResponseBody
@ExcludeInterceptor
@ApiOperation(value = "APP端登录验证", notes = "登录", response = JsonRespData.class)
public JsonRespData login(User user){
return userService.login(user);
}
UserService.java:
/**
* APP端登录验证
* @author LiJun
* @Date 2019/9/25
* @Time 9:35
* @param user
* @return com.ue.core.util.JsonRespData
*/
public JsonRespData login(User user){
String name = user.getName();//用户输入的用户名
String password = user.getPassword();//用户输入的密码
User userInfo = userDao.getPwdByName(name);
if (BeanUtil.isNotBlank(userInfo)) {
Integer state = userInfo.getState();
String isDelete = userInfo.getIsDelete();
if (state != 2) {//如果用户状态不是“已通过”
//0:禁用、1:待审核、2:审核已通过、3:审核不通过
return JsonRespData.success(REQUEST_208, "该帐号暂时不能进行登录");
}
else if ("Y".equals(isDelete)){//如果用户已被删除
//Y:未删除、N:已删除
return JsonRespData.success(REQUEST_209, "账号已删除");
}
else {
Jedis jedis = null;
try {
String pwd = userInfo.getPassword();
if (pwd.equals(password)) {//登陆成功,添加token到redis管理
jedis = JedisUtil.getJedisConn();
String id = userInfo.getId().toString();
String accessUrl = "暂未指定";
String token = jwtService.getEncryptString(id, accessUrl);//生成token
jedis.set(token, token);//将token保存到Redis里
jedis.expire(token, 3600);
userInfo.setToken(token);
return JsonRespData.success(REQUEST_SUCCESS, "登录成功", userInfo);
} else {//登录失败
return JsonRespData.success(REJECT_REQUEST, "帐号或密码错误");
}
} catch (Exception e) {
log.error("登录验证发生异常:" + e);
return JsonRespData.success(INTERNAL_SERVER_ERROR, "服务器发生错误");
}finally {
if (jedis != null)
jedis.close();
}
}
} else {
return JsonRespData.success(REJECT_REQUEST, "帐号未注册");
}
}
自定义注解的使用:
使用自定义注解期间遇到的类型转换异常:
java.lang.ClassCastException: org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler cannot be cast to org.springframework.web.method.HandlerMethod
这是JwtInterceptor.java中HandlerMethod method = (HandlerMethod) o强制转换时的异常,出现原因是:所请求的资源不存在,也就是说系统中没有所请求的controller
解决方案:在强转前加上if (o instanceof HandlerMethod)判断一下,如果请求的资源不存在则不进行处理。