最近做的一個項目用的是前後端分離的開發模式,系統是要登錄後才能進行操作的,所以需要進行身份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)判斷一下,如果請求的資源不存在則不進行處理。