JWT是基於token的身份認證的方案。前端
json web token全稱。能夠保證安全傳輸的前提下傳送一些基本的信息,以減輕對外部存儲的依賴,減小了分佈式組件的依賴,減小了硬件的資源。web
可實現無狀態、分佈式的Web應用受權,jwt的安全特性保證了token的不可僞造和不可篡改。redis
本質上是一個獨立的身份驗證令牌,能夠包含用戶標識、用戶角色和權限等信息,以及您能夠存儲任何其餘信息(自包含)。任何人均可以輕鬆讀取和解析,並使用密鑰來驗證真實性。算法
缺陷:
JWT就是一個字符串,通過加密處理與校驗處理的字符串,形式爲:sql
A.B.C數據庫
A由JWT頭部信息header加密獲得
B由JWT用到的身份驗證信息json數據加密獲得
C由A和B加密獲得,是校驗部分json
將header用base64加密,獲得A。後端
載荷部分payload
{
「iss」: 「發行者」,
「sub」: 主題」,
「aud」: 「觀衆」,
「exp」:」過時時間」,
「iat」:」簽發時間」
如下能夠添加自定義數據
「id」:」1」,
「nickname」:」暱稱」
} 瀏覽器
根據JWT claim set[用base64]加密獲得的。claim set是一個json數據,是代表用戶身份的數據,可自行指定字段很靈活,也有固定字段表示特定含義(但不必定要包含特定字段,只是推薦)。
Base64算法是可逆的,不能夠在載荷部分保存用戶密碼等敏感信息。若是業務須要,也能夠採用對稱密鑰加密。緩存
簽名部分signature
HMACSHA256(Base64(Header) + 「.」 + Base64(Payload), secret),secret是加密的鹽。
簽名的目的是用來驗證頭部和載荷是否被非法篡改。
驗簽過程描述:獲取token值,讀取Header部分並Base64解碼,獲得簽名算法。根據以上方法算出簽名,若是簽名信息不一致,說明是非法的。
3、JSON Web Token工做原理
初次登陸:用戶初次登陸,輸入用戶名密碼
密碼驗證:服務器從數據庫取出用戶名和密碼進行驗證
生成JWT:服務器端驗證經過,根據從數據庫返回的信息,以及預設規則,生成JWT
返還JWT:服務器的將token放在cookie中將JWT返還
帶JWT的請求:之後客戶端發起請求,帶上cookie中的token信息。
4、jwt+redis的登陸方案流程:
前端服務器收到用戶登陸請求,傳給後臺API網關。
後臺用戶服務驗證經過,而後從帳號信息抽取出userName、login_time等基本信息組成payload,進而組裝一個JWT,把JWT放入redis(由於退出的時候沒法使jwt當即做廢,因此使用保存在redis中,退出的時候delete掉就能夠了,鑑權的時候加一層判斷jwt是否在redis裏,若是不在則證實jwt已過時做廢),而後包裝cookie中返回到前端服務器,這就登陸成功了。
前端服務器拿到JWT,進行存儲(能夠存儲在緩存中,也能夠存儲在數據庫中,若是是瀏覽器,能夠存儲在 localStorage 中,我實現的是放入到cookie裏面)
登陸後,再訪問其餘微服務的時候,前端會攜帶jwt訪問後臺,後臺校驗 JWT,驗籤經過後,返回相應資源和數據就能夠了。
(這裏沒有將redis畫出來)
結合攔截器與上篇session-cookie方式的區別:
首次登陸步驟:
1.首先AuthInterceptor攔截器攔截用戶請求,在preHandle中看cookie中是否有token信息,沒有就接着攔截器AuthActionInterceptor攔截須要登陸的url,看threadlocal當中是否有user對象,若是沒有就跳轉到登陸頁面進行登陸,登陸成功後會將user對象放到threadlocal中。(注意這個地方和上篇中提到的登陸成功後將user放到session的不一樣)
登陸處理流程:在數據庫中查詢驗證用戶名密碼,經過就講帳號信息抽取出username、email等信息組成一個payload,進而組裝成一個JWT,而後將JWT放到redis當中,設置過時時間。
生成token:
給定簽名算法、給定載荷的map、進行簽名
2.當業務邏輯處理完以後在AuthInterceptor的postHandle中,從threadlocal獲取user對象中的token信息,將token放到cookie中返回給前端。
3.請求結束後在AuthInterceptor的afterCompletion將user從threadlocal中移除。
驗證流程:
前端將攜帶jwt的cookie傳到後臺,AuthInterceptor會根據token驗證解析出user,(注意根以前在session中取對象的不一樣)驗證後再將user放到threadlocal中,AuthActionInterceptor一看threadlocal有user對象,直接經過。後面的步驟同樣。
驗證token:
1)從token的Header中拿出簽名算法,看和以前生成token的簽名算法是否一致。
2)驗證簽名,獲取載荷map,從中獲取用戶標識email,在redis中看是否失效,若是失效,拋出未登陸錯誤;若是未失效,更新redis的失效時間,返回用戶的信息。
AuthInterceptor
@Component public class AuthInterceptor implements HandlerInterceptor { private static final String TOKEN_COOKIE = "token"; @Autowired private UserDao userDao; @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception { Map<String, String[]> map = req.getParameterMap(); map.forEach((k,v) ->req.setAttribute(k, Joiner.on(",").join(v))); String requestURI = req.getRequestURI(); if (requestURI.startsWith("/static") || requestURI.startsWith("/error")) { return true; } Cookie cookie = WebUtils.getCookie(req, TOKEN_COOKIE); if (cookie != null && StringUtils.isNoneBlank(cookie.getValue())) { User user = userDao.getUserByToken(cookie.getValue()); if (user != null) { req.setAttribute(CommonConstants.LOGIN_USER_ATTRIBUTE, user); // req.setAttribute(CommonConstants.USER_ATTRIBUTE, user); UserContext.setUser(user); } } return true; } @Override public void postHandle(HttpServletRequest req, HttpServletResponse res, Object handler, ModelAndView modelAndView) throws Exception { String requestURI = req.getRequestURI(); if (requestURI.startsWith("/static") || requestURI.startsWith("/error")) { return ; } User user = UserContext.getUser(); if (user != null && StringUtils.isNoneBlank(user.getToken())) { String token = requestURI.startsWith("logout")? "" : user.getToken(); Cookie cookie = new Cookie(TOKEN_COOKIE, token); //此處的參數,是相對於應用服務器存放應用的文件夾的根目錄而言的(好比tomcat下面的webapp),所以cookie.setPath("/"); //以後,能夠在webapp文件夾下的全部應用共享cookie,而cookie.setPath("/webapp_b/"); //是指cas應用設置的cookie只能在webapp_b應用下的得到,即使是產生這個cookie的cas應用也不能夠。 cookie.setPath("/"); //若是在Cookie中設置了"HttpOnly"爲true屬性,那麼經過JavaScript腳本將沒法讀取到Cookie信息,這樣能有效的防止XSS攻擊,讓網站應用更加安全。 //這裏可讓js讀取,置爲false cookie.setHttpOnly(false); res.addCookie(cookie); } } @Override public void afterCompletion(HttpServletRequest req, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserContext.remove(); } }
AuthActionInterceptor
@Component public class AuthActionInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception { User user = UserContext.getUser(); if (user == null) { String msg = URLEncoder.encode("請先登陸", "utf-8"); StringBuffer sb = req.getRequestURL(); String target = URLEncoder.encode(sb.toString(), "utf-8"); if ("GET".equalsIgnoreCase(req.getMethod())) { res.sendRedirect("/accounts/signin?errorMsg=" + msg + "&target=" + target); }else { res.sendRedirect("/accounts/signin?errorMsg=" + msg); } return false; } return true; } @Override public void postHandle(HttpServletRequest req, HttpServletResponse res, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest req, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
UserService
/** * 校驗用戶名密碼、生成token並返回用戶對象 * @param email * @param passwd * @return */ public User auth(String email, String passwd) { if (StringUtils.isBlank(email) || StringUtils.isBlank(passwd)) { throw new UserException(Type.USER_AUTH_FAIL,"User Auth Fail"); } User user = new User(); user.setEmail(email); user.setPasswd(HashUtils.encryPassword(passwd)); //user.setEnable(1); List<User> list = getUserByQuery(user); if (!list.isEmpty()) { User retUser = list.get(0); onLogin(retUser); return retUser; } throw new UserException(Type.USER_AUTH_FAIL,"User Auth Fail"); } //生成token的操做 private void onLogin(User user) { //最後一個是時間戳 String token = JwtHelper.genToken(ImmutableMap.of("email", user.getEmail(), "name", user.getName(),"ts",Instant.now().getEpochSecond()+"")); renewToken(token,user.getEmail()); user.setToken(token); } //從新設置緩存過時時間 private String renewToken(String token, String email) { redisTemplate.opsForValue().set(email, token); redisTemplate.expire(email, 30, TimeUnit.MINUTES); return token; } //驗證token獲取登陸用戶 public User getLoginedUserByToken(String token) { Map<String, String> map = null; try { map = JwtHelper.verifyToken(token); } catch (Exception e) { throw new UserException(Type.USER_NOT_LOGIN,"User not login"); } String email = map.get("email"); Long expired = redisTemplate.getExpire(email); //判斷是否失效 if (expired > 0L) { renewToken(token, email); User user = getUserByEmail(email); user.setToken(token); return user; } throw new UserException(Type.USER_NOT_LOGIN,"user not login"); } private User getUserByEmail(String email) { User user = new User(); user.setEmail(email); List<User> list = getUserByQuery(user); if (!list.isEmpty()) { return list.get(0); } throw new UserException(Type.USER_NOT_FOUND,"User not found for " + email); } public void invalidate(String token) { Map<String, String> map = JwtHelper.verifyToken(token); redisTemplate.delete(map.get("email")); }
JWTHelper
public class JwtHelper { private static final String SECRET = "session_secret"; //發佈者 後面一塊去校驗 private static final String ISSUER = "mooc_user"; //生成token的操做 public static String genToken(Map<String, String> claims){ try { //簽名算法 Algorithm algorithm = Algorithm.HMAC256(SECRET); JWTCreator.Builder builder = JWT.create().withIssuer(ISSUER).withExpiresAt(DateUtils.addDays(new Date(), 1)); //至關於將claims存儲在token中 claims.forEach((k,v) -> builder.withClaim(k, v)); return builder.sign(algorithm).toString(); } catch (IllegalArgumentException | UnsupportedEncodingException e) { throw new RuntimeException(e); } } //驗證token public static Map<String, String> verifyToken(String token) { Algorithm algorithm = null; try { algorithm = Algorithm.HMAC256(SECRET); } catch (IllegalArgumentException | UnsupportedEncodingException e) { throw new RuntimeException(e); } JWTVerifier verifier = JWT.require(algorithm).withIssuer(ISSUER).build(); DecodedJWT jwt = verifier.verify(token); Map<String, Claim> map = jwt.getClaims(); Map<String, String> resultMap = Maps.newHashMap(); map.forEach((k,v) -> resultMap.put(k, v.asString())); return resultMap; } }