設計思路
- 用戶發出登錄請求,帶着用戶名和密碼到服務器進行驗證,服務器驗證成功就在后台生成一個token返回給客戶端
- 客戶端將token存儲到cookie中,服務端將token存儲到redis中,可以設置存儲token的有效期。
- 后續客戶端的每次請求資源都必須攜帶token,服務端接收到請求首先校驗是否攜帶token,以及token是否和redis中的匹配,若不存在或不匹配直接攔截返回錯誤信息(如未認證)。
-
token管理:生成、校驗、解析、刪除
-
token:這里使用userId_UUID的形式
-
有效期:使用Redis key有效期設置(每次操作完了都會更新延長有效時間)
-
銷毀token:刪除Redis中key為userId的內容
-
token存儲:客戶端(Cookie)、服務端(Redis)
-
Cookie的存取操作(jquery.cookie插件)
-
Redis存取(StringRedisTemplate)
實現
【Redis操作類】
package com.bpf.tokenAuth.utils; import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; @Component public class RedisClient { public static final long TOKEN_EXPIRES_SECOND = 1800; @Autowired private StringRedisTemplate redisTpl; /** * 向redis中設值 * @param key 使用 a:b:id的形式在使用rdm進行查看redis情況時會看到分層文件夾的展示形式,便於管理 * @param value * @return */ public boolean set(String key, String value) { boolean result = false; try { redisTpl.opsForValue().set(key, value); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 向redis中設置,同時設置過期時間 * @param key * @param value * @param time * @return */ public boolean set(String key, String value, long time) { boolean result = false; try { redisTpl.opsForValue().set(key, value); expire(key, time); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 獲取redis中的值 * @param key * @return */ public String get(String key) { String result = null; try { result = redisTpl.opsForValue().get(key); } catch (Exception e) { e.printStackTrace(); } return result; } /** * 設置key的過期時間 * @param key * @param time * @return */ public boolean expire(String key, long time) { boolean result = false; try { if(time > 0) { redisTpl.expire(key, time, TimeUnit.SECONDS); result = true; } } catch (Exception e) { e.printStackTrace(); } return result; } /** * 根據key刪除對應value * @param key * @return */ public boolean remove(String key) { boolean result = false; try { redisTpl.delete(key); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } }
【Token管理類】
package com.bpf.tokenAuth.utils.token; import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.bpf.tokenAuth.utils.RedisClient; @Component public class RedisTokenHelp implements TokenHelper { @Autowired private RedisClient redisClient; @Override public TokenModel create(Integer id) { String token = UUID.randomUUID().toString().replace("-", ""); TokenModel mode = new TokenModel(id, token); redisClient.set(id == null ? null : String.valueOf(id), token, RedisClient.TOKEN_EXPIRES_SECOND); return mode; } @Override public boolean check(TokenModel model) { boolean result = false; if(model != null) { String userId = model.getUserId().toString(); String token = model.getToken(); String authenticatedToken = redisClient.get(userId); if(authenticatedToken != null && authenticatedToken.equals(token)) { redisClient.expire(userId, RedisClient.TOKEN_EXPIRES_SECOND); result = true; } } return result; } @Override public TokenModel get(String authStr) { TokenModel model = null; if(StringUtils.isNotEmpty(authStr)) { String[] modelArr = authStr.split("_"); if(modelArr.length == 2) { int userId = Integer.parseInt(modelArr[0]); String token = modelArr[1]; model = new TokenModel(userId, token); } } return model; } @Override public boolean delete(Integer id) { return redisClient.remove(id == null ? null : String.valueOf(id)); } }
【攔截器邏輯】
package com.bpf.tokenAuth.interceptor; import java.lang.reflect.Method; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import com.bpf.tokenAuth.annotation.NoneAuth; import com.bpf.tokenAuth.constant.NormalConstant; import com.bpf.tokenAuth.entity.JsonData; import com.bpf.tokenAuth.utils.JsonUtils; import com.bpf.tokenAuth.utils.token.TokenHelper; import com.bpf.tokenAuth.utils.token.TokenModel; @Component public class LoginInterceptor extends HandlerInterceptorAdapter { @Autowired private TokenHelper tokenHelper; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println(11); // 如果不是映射到方法直接通過 if (!(handler instanceof HandlerMethod)) { return true; } //如果被@NoneAuth注解代表不需要登錄驗證,直接通過 HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); if(method.getAnnotation(NoneAuth.class) != null) return true; //token驗證 String authStr = request.getHeader(NormalConstant.AUTHORIZATION); TokenModel model = tokenHelper.get(authStr); //驗證通過 if(tokenHelper.check(model)) { request.setAttribute(NormalConstant.CURRENT_USER_ID, model.getUserId()); return true; } //驗證未通過 response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); response.getWriter().write(JsonUtils.obj2String(JsonData.buildError(401, "權限未認證"))); return false; } }
【登錄邏輯】
package com.bpf.tokenAuth.controller; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.bpf.tokenAuth.annotation.NoneAuth; import com.bpf.tokenAuth.constant.MessageConstant; import com.bpf.tokenAuth.constant.NormalConstant; import com.bpf.tokenAuth.entity.JsonData; import com.bpf.tokenAuth.entity.User; import com.bpf.tokenAuth.enums.HttpStatusEnum; import com.bpf.tokenAuth.mapper.UserMapper; import com.bpf.tokenAuth.utils.token.TokenHelper; import com.bpf.tokenAuth.utils.token.TokenModel; @RestController @RequestMapping("/token") public class TokenController { @Autowired private UserMapper userMapper; @Autowired private TokenHelper tokenHelper; @NoneAuth @GetMapping public Object login(String username, String password) { User user = userMapper.findByName(username); if(user == null || !user.getPassword().equals(password)) { return JsonData.buildError(HttpStatusEnum.NOT_FOUND.getCode(), MessageConstant.USERNAME_OR_PASSWORD_ERROR); } //用戶名密碼驗證通過后,生成token TokenModel model = tokenHelper.create(user.getId()); return JsonData.buildSuccess(model); } @DeleteMapping public Object logout(HttpServletRequest request) { Integer userId = (Integer) request.getAttribute(NormalConstant.CURRENT_USER_ID); if(userId != null) { tokenHelper.delete(userId); } return JsonData.buildSuccess(); } }
測試
【login.html】

<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<link rel="stylesheet" href="../res/css/login.css">
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.js"></script>
</head>
<body>
<form>
<input type="text" name="username" id="username">
<input type="password" name="password" id="password">
</form>
<input type="button" value="Login" onclick="login()">
</body>
<script type="text/javascript">
function login(){
$.ajax({
url: "/tokenAuth/token",
dataType: "json",
data: {'username':$("#username").val(), 'password':$("#password").val()},
type:"GET",
success:function(res){
console.log(res);
if(res.code == 200){
var authStr = res.data.userId + "_" + res.data.token;
//把生成的token放在cookie中
$.cookie("authStr", authStr);
window.location.href = "index.html";
}else alert(res.msg);
}
});
}
</script>
</html>
【index.html】

<!DOCTYPE html>
<html>
<head>
<title>Index</title>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.js"></script>
</head>
<body>
<input type="button" value="Get" onclick="get()">
<input type="button" value="logout" onclick="logout()">
</body>
<script type="text/javascript">
function get(){
$.ajax({
url: "/tokenAuth/user/bpf",
dataType: "json",
type:"GET",
beforeSend: function(request) {
//將cookie中的token信息放於請求頭中
request.setRequestHeader("authStr", $.cookie('authStr'));
},
success:function(res){
console.log(res);
}
});
}
function logout(){
$.ajax({
url: "/tokenAuth/token",
dataType: "json",
type:"DELETE",
beforeSend: function(request) {
//將cookie中的token信息放於請求頭中
request.setRequestHeader("authStr", $.cookie('authStr'));
},
success:function(res){
console.log(res);
}
});
}
</script>
</html>
測試環境中兩個頁面login.html和index.html均當做靜態資源處理
【未登錄狀態】
- 首先再未登錄狀態下直接訪問index頁面http://localhost:8080/tokenAuth/page/index.html
- 點擊get按鈕獲取數據,由於沒有攜帶token導致認證失敗
【登錄狀態】
- 訪問登錄網站http://localhost:8080/tokenAuth/page/login.html,輸入username和password進行點擊Login按鈕登錄
-
登錄成功並跳轉到index頁面,並且生成cookie,這里沒有設置cookie有效期,默認關閉瀏覽器失效
再次點擊get按鈕請求數據,請求成功
點擊logout按鈕銷毀登錄狀態,然后再次請求數據
