基於JWT的token身份認證方案


一、使用JSON Web Token的好處?

1.性能問題。
JWT方式將用戶狀態分散到了客戶端中,相比於session,可以明顯減輕服務端的內存壓力。
Session方式存儲用戶id的最大弊病在於Session是存儲在服務器端的,所以需要占用大量服務器內存,
對於較大型應用而言可能還要保存許多的狀態,一般還需借助nosql和緩存機制來實現session的存儲,如果是分布式應用還需session共享。 
2.單點登錄。
JWT能輕松的實現單點登錄,因為用戶的狀態已經被傳送到了客戶端。
token 可保存自定義信息,如用戶基本信息,web服務器用key去解析token,就獲取到請求用戶的信息了。
我們也可以配置它以便包含用戶擁有的任何權限。這意味着每個服務不需要與授權服務交互才能授權用戶。
3.前后端分離。
以前的傳統模式下,后台對應的客戶端就是瀏覽器,就可以使用session+cookies的方式實現登錄,
但是在前后分離的情況下,后端只負責通過暴露的RestApi提供數據,而頁面的渲染、路由都由前端完成。因為rest是無狀態的,因此也就不會有session記錄到服務器端。
4.兼容性。
支持移動設備,支持跨程序調用,Cookie 是不允許垮域訪問的,而 Token 則不存在這個問題。
5.可拓展性。
jwt是無狀態的,特別適用於分布式站點的單點登錄(SSO)場景。
比如有3台機器(A、B、C)組成服務器集群,若session存在機器A上,session只能保存在其中一台服務器,此時你便不能訪問機器B、C,因為B、C上沒有存放該Session,
而使用token就能夠驗證用戶請求合法性,並且我再加幾台機器也沒事,所以可拓展性好。
6.安全性。因為有簽名,所以JWT可以防止被篡改。

二、JSON Web Token是什么?

JWT是基於token的身份認證的方案

json web token全稱。可以保證安全傳輸的前提下傳送一些基本的信息,以減輕對外部存儲的依賴,減少了分布式組件的依賴,減少了硬件的資源。

實現無狀態、分布式的Web應用授權,jwt的安全特性保證了token的不可偽造和不可篡改。

本質上是一個獨立的身份驗證令牌,可以包含用戶標識、用戶角色和權限等信息,以及您可以存儲任何其他信息(自包含)。任何人都可以輕松讀取和解析,並使用密鑰來驗證真實性。

 

缺陷:
1)JWT在生成token的時候支持失效時間,但是支持的失效時間是固定的,比如說一天。
但是用戶在等出的時候是隨機觸發的,那么我們jwt token來做這個失效是不可行的,因為jwt在初始化的時候已經定死在什么時候過期了。
采用其他方案,在redis中存儲token,設置token的過期時間,每次鑒權的時候都會去延長時間
2)jwt不適合存放大量信息,信息越多token越長

 

JWT就是一個字符串,經過加密處理與校驗處理的字符串,形式為:

    A.B.C

A由JWT頭部信息header加密得到
B由JWT用到的身份驗證信息json數據加密得到
C由A和B加密得到,是校驗部分

分別是頭部、載荷、簽名。 
頭部部分header 

“alg”: “HS256”, 
“typ”: “JWT” 

alg描述的是簽名算法。默認值是HS256。

將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解碼,得到簽名算法。根據以上方法算出簽名,如果簽名信息不一致,說明是非法的。

 

三、JSON Web Token工作原理

  1. 初次登錄:用戶初次登錄,輸入用戶名密碼

  2. 密碼驗證:服務器從數據庫取出用戶名和密碼進行驗證

  3. 生成JWT:服務器端驗證通過,根據從數據庫返回的信息,以及預設規則,生成JWT

  4. 返還JWT:服務器的將token放在cookie中將JWT返還

  5. 帶JWT的請求:以后客戶端發起請求,帶上cookie中的token信息。

 

四、jwt+redis的登錄方案流程:

  • 前端服務器收到用戶登錄請求,傳給后台API網關。

  • 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();
  }
}
View Code

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 {
  }
}
View Code

 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"));
  }
View Code

 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;
  }

}
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM