一、引言
登錄權限控制是很多系統具備的功能,實現這一功能的方式有很多,其中使用token是現在用的比較多的
好處:可以防止CSRF攻擊
二、功能實現:
用戶登錄成功後,後臺生成一個token並存在redis中,同時給此用戶的token設置時限,返回一個token給調用者,同時自定義一個@AuthToken註解,被該註解標註的API請求都需要進行token效驗,效驗通過纔可以正常訪問,實現接口級的鑑權控制。同時token具有生命週期,在用戶持續一段時間不進行操作的話,token則會過期,用戶一直操作的話,則不會過期。
具體流程:
1、 用戶輸入賬號密碼,發送請求給後臺,後臺生成一個token,將用戶的用戶名或者手機號等跟token綁定存入redis中,並且設置過期時間,同時將當前時間戳也存入redis中
@PostMapping(value = "login")
public ResponseTemplate login(String phone, String password) {
User user = userService.getUser(phone,password);
JSONObject result = new JSONObject();
if (user != null) {
Jedis jedis = new Jedis("localhost", 6379);
String token = tokenGenerator.generate(phone, password);
//設置token以及過期時間
jedis.set(token, phone);
//設置key生存時間,當key過期時,它會被自動刪除,時間是秒
jedis.expire(token, ConstantKit.TOKEN_EXPIRE_TIME);
//記錄設置token的時間,以便於後續調用時分析token是否過期
Long currentTime = System.currentTimeMillis();
jedis.set(token + phone, currentTime.toString());
jedis.expire(token+phone,ConstantKit.TOKEN_EXPIRE_TIME);
//用完關閉
jedis.close();
result.put("token", token);
result.put("status",1);
} else {
result.put("status",0);
}
return ResponseTemplate.builder()
.code(200)
.message("OK")
.data(result)
.build();
2、每次請求接口都會走攔截器,如果該接口標註了@AuthToken註解,則要檢查客戶端傳過來的Authorization字段,獲取 token。並且根據token是否存在redis中,如果可以獲取則說明 token 正確,反之,說明錯誤,返回鑑權失敗。
3、token可以根據用戶使用的情況來動態的調整自己過期時間。在生成 token 的同時也往 redis 裏面存入了創建 token 時的時間戳,每次請求被攔截器攔截 token 驗證成功之後,將當前時間與存在 redis 裏面的 token 生成時刻的時間戳進行比較,噹噹前時間的距離創建時間快要到達設置的redis過期時間的話,就重新設置token過期時間,將過期時間延長。如果用戶在設置的 redis 過期時間的時間長度內沒有進行任何操作(沒有發請求),則token會在redis中過期。
public class AuthorizationInterceptor implements HandlerInterceptor {
//存放鑑權信息的Header名稱,默認是Authorization
private String httpHeaderName = "Authorization";
//鑑權失敗後返回的錯誤信息,默認爲401 unauthorized
private String unauthorizedErrorMessage = "401 unauthorized";
//鑑權失敗後返回的HTTP錯誤碼,默認爲401
private int unauthorizedErrorCode = HttpServletResponse.SC_UNAUTHORIZED;
/**
* 存放登錄用戶模型Key的Request Key
*/
public static final String REQUEST_CURRENT_KEY = "REQUEST_CURRENT_KEY";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 如果打上了AuthToken註解則需要驗證token
if (method.getAnnotation(AuthToken.class) != null || handlerMethod.getBeanType().getAnnotation(AuthToken.class) != null) {
//從請求體中取出token
//String token = request.getParameter(httpHeaderName);
//從請求頭中取出token
String token = request.getHeader(httpHeaderName);
log.info("Get token from request is {} ", token);
String phone = "";
Jedis jedis = new Jedis("localhost", 6379);
if (token != null && token.length() != 0) {
phone = jedis.get(token);
log.info("Get username from Redis is {}", phone);
}
if (phone != null && !phone.trim().equals("")) {
Long tokeBirthTime = Long.valueOf(jedis.get(token + phone));
log.info("token Birth time is: {}", tokeBirthTime);
Long diff = System.currentTimeMillis() - tokeBirthTime;
log.info("token is exist : {} ms", diff);
//重新設置Redis中的token過期時間
if (diff > ConstantKit.TOKEN_RESET_TIME) {
jedis.expire(token, ConstantKit.TOKEN_EXPIRE_TIME);
log.info("Reset expire time success!");
Long newBirthTime = System.currentTimeMillis();
jedis.set(token + phone, newBirthTime.toString());
jedis.expire(token + phone,ConstantKit.TOKEN_EXPIRE_TIME);
}
//用完關閉
jedis.close();
request.setAttribute(REQUEST_CURRENT_KEY, phone);
return true;
} else {
JSONObject jsonObject = new JSONObject();
PrintWriter out = null;
try {
response.setStatus(unauthorizedErrorCode);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
jsonObject.put("code", ((HttpServletResponse) response).getStatus());
jsonObject.put("message", HttpStatus.UNAUTHORIZED);
out = response.getWriter();
out.println(jsonObject);
return false;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != out) {
out.flush();
out.close();
}
}
}
}
request.setAttribute(REQUEST_CURRENT_KEY, null);
return true;
}
}
登錄成功
當沒有權限時調用接口
代碼demo: github地址