設計思路
- 用戶發出登錄請求,帶着用戶名和密碼到服務器進行驗證,服務器驗證成功就在后台生成一個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按鈕銷毀登錄狀態,然后再次請求數據
