如何使用JWT實現(xiàn)單點登錄

本篇內容介紹了“如何使用JWT實現(xiàn)單點登錄”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

創(chuàng)新互聯(lián)公司主要從事成都網站設計、做網站、網頁設計、企業(yè)做網站、公司建網站等業(yè)務。立足成都服務貴德,十年網站建設經驗,價格優(yōu)惠、服務專業(yè),歡迎來電咨詢建站服務:18980820575

一、故事起源

說起 JWT,我們先來談一談基于傳統(tǒng)session認證的方案以及瓶頸。

傳統(tǒng)session交互流程,如下圖:

如何使用JWT實現(xiàn)單點登錄

當瀏覽器向服務器發(fā)送登錄請求時,驗證通過之后,會將用戶信息存入seesion中,然后服務器會生成一個sessionId放入cookie中,隨后返回給瀏覽器。

當瀏覽器再次發(fā)送請求時,會在請求頭部的cookie中放入sessionId,將請求數(shù)據(jù)一并發(fā)送給服務器。

如何使用JWT實現(xiàn)單點登錄

服務器就可以再次從seesion獲取用戶信息,整個流程完畢!

通常在服務端會設置seesion的時長,例如 30 分鐘沒有活動,會將已經存放的用戶信息從seesion中移除。

session.setMaxInactiveInterval(30 * 60);//30分鐘沒活動,自動移除

同時,在服務端也可以通過seesion來判斷當前用戶是否已經登錄,如果為空表示沒有登錄,直接跳轉到登錄頁面;如果不為空,可以從session中獲取用戶信息即可進行后續(xù)操作。

如何使用JWT實現(xiàn)單點登錄

在單體應用中,這樣的交互方式,是沒啥問題的。

但是,假如應用服務器的請求量變得很大,而單臺服務器能支撐的請求量是有限的,這個時候就容易出現(xiàn)請求變慢或者OOM。

解決的辦法,要么給單臺服務器增加配置,要么增加新的服務器,通過負載均衡來滿足業(yè)務的需求。

如果是給單臺服務器增加配置,請求量繼續(xù)變大,依然無法支撐業(yè)務處理。

顯而易見,增加新的服務器,可以實現(xiàn)無限的水平擴展。

但是增加新的服務器之后,不同的服務器之間的sessionId是不一樣的,可能在A服務器上已經登錄成功了,能從服務器的session中獲取用戶信息,但是在B服務器上卻查不到session信息,此時肯定無比的尷尬,只好退出來繼續(xù)登錄,結果A服務器中的session因為超時失效,登錄之后又被強制退出來要求重新登錄,想想都挺尷尬~~

面對這種情況,幾位大佬于是合起來商議,想出了一個token方案。

如何使用JWT實現(xiàn)單點登錄

將各個應用程序與內存數(shù)據(jù)庫redis相連,對登錄成功的用戶信息進行一定的算法加密,生成的ID被稱為token,將token還有用戶的信息存入redis;等用戶再次發(fā)起請求的時候,將token還有請求數(shù)據(jù)一并發(fā)送給服務器,服務端驗證token是否存在redis中,如果存在,表示驗證通過,如果不存在,告訴瀏覽器跳轉到登錄頁面,流程結束。

token方案保證了服務的無狀態(tài),所有的信息都是存在分布式緩存中?;诜植际酱鎯Γ@樣可以水平擴展來支持高并發(fā)。

當然,現(xiàn)在springboot還提供了session共享方案,類似token方案將session存入到redis中,在集群環(huán)境下實現(xiàn)一次登錄之后,每個服務器都可以獲取到用戶信息。

二、JWT是什么

上文中,我們談到的session還有token的方案,在集群環(huán)境下,他們都是靠第三方緩存數(shù)據(jù)庫redis來實現(xiàn)數(shù)據(jù)的共享。

那有沒有一種方案,不用緩存數(shù)據(jù)庫redis來實現(xiàn)用戶信息的共享,以達到一次登錄,處處可見的效果呢?

答案肯定是有的,就是我們今天要介紹的JWT!

JWT全稱JSON Web  Token,實現(xiàn)過程簡單的說就是用戶登錄成功之后,將用戶的信息進行加密,然后生成一個token返回給客戶端,與傳統(tǒng)的session交互沒太大區(qū)別。

交互流程如下:

如何使用JWT實現(xiàn)單點登錄

唯一的不同點就是:token存放了用戶的基本信息,更直觀一點就是將原本放入redis中的用戶數(shù)據(jù),放入到token中去了!

這樣一來,客戶端、服務端都可以從token中獲取用戶的基本信息,既然客戶端可以獲取,肯定是不能存放敏感信息的,因為瀏覽器可以直接從token獲取用戶信息。

JWT具體長什么樣呢?

JWT是由三段信息構成的,將這三段信息文本用.鏈接一起就構成了JWT字符串。就像這樣:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
  • 第一部分:我們稱它為頭部(header),用于存放token類型和加密協(xié)議,一般都是固定的;

  • 第二部分:我們稱其為載荷(payload),用戶數(shù)據(jù)就存放在里面;

  • 第三部分:是簽證(signature),主要用于服務端的驗證;

1、header

JWT的頭部承載兩部分信息:

  • 聲明類型,這里是JWT;

  • 聲明加密的算法,通常直接使用 HMAC SHA256;

完整的頭部就像下面這樣的JSON:

{   'typ': 'JWT',   'alg': 'HS256' }

使用base64加密,構成了第一部分。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

2、playload

載荷就是存放有效信息的地方,這些有效信息包含三個部分:

  • 標準中注冊的聲明;

  • 公共的聲明;

  • 私有的聲明;

其中,標準中注冊的聲明 (建議但不強制使用)包括如下幾個部分 :

  • iss: jwt簽發(fā)者;

  • sub: jwt所面向的用戶;

  • aud: 接收jwt的一方;

  • exp: jwt的過期時間,這個過期時間必須要大于簽發(fā)時間;

  • nbf: 定義在什么時間之前,該jwt都是不可用的;

  • iat: jwt的簽發(fā)時間;

  • jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊;

公共的聲明部分:公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業(yè)務需要的必要信息,但不建議添加敏感信息,因為該部分在客戶端可解密。

私有的聲明部分:私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因為base64是對稱解密的,意味著該部分信息可以歸類為明文信息。

定義一個payload:

{   "sub": "1234567890",   "name": "John Doe",   "admin": true }

然后將其進行base64加密,得到Jwt的第二部分:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

3、signature

jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:

  • header (base64后的);

  • payload (base64后的);

  • secret (密鑰);

這個部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進行加鹽secret組合加密,然后就構成了jwt的第三部分。

//javascript var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);  var signature = HMACSHA256(encodedString, '密鑰');

加密之后,得到signature簽名信息。

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

將這三部分用.連接成一個完整的字符串,就構成了最終的jwt:

//jwt最終格式 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

這個只是通過javascript實現(xiàn)的一個演示,JWT的簽發(fā)和密鑰的保存都是在服務端來完成。

secret用來進行jwt的簽發(fā)和jwt的驗證,所以,在任何場景都不應該流露出去。

三、實戰(zhàn)

介紹了這么多,怎么實現(xiàn)呢?廢話不多說,下面我們直接開擼!

  • 創(chuàng)建一個springboot項目,添加JWT依賴庫

<!-- jwt支持 --> <dependency>     <groupId>com.auth0</groupId>     <artifactId>java-jwt</artifactId>     <version>3.4.0</version> </dependency>
  • 然后,創(chuàng)建一個用戶信息類,將會通過加密存放在token中

@Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) public class UserToken implements Serializable {      private static final long serialVersionUID = 1L;      /**      * 用戶ID      */     private String userId;      /**      * 用戶登錄賬戶      */     private String userNo;      /**      * 用戶中文名      */     private String userName; }
  • 接著,創(chuàng)建一個JwtTokenUtil工具類,用于創(chuàng)建token、驗證token

public class JwtTokenUtil {      //定義token返回頭部     public static final String AUTH_HEADER_KEY = "Authorization";      //token前綴     public static final String TOKEN_PREFIX = "Bearer ";      //簽名密鑰     public static final String KEY = "q3t6w9z$C&F)J@NcQfTjWnZr4u7x";          //有效期默認為 2hour     public static final Long EXPIRATION_TIME = 1000L*60*60*2;       /**      * 創(chuàng)建TOKEN      * @param content      * @return      */     public static String createToken(String content){         return TOKEN_PREFIX + JWT.create()                 .withSubject(content)                 .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))                 .sign(Algorithm.HMAC512(KEY));     }      /**      * 驗證token      * @param token      */     public static String verifyToken(String token) throws Exception {         try {             return JWT.require(Algorithm.HMAC512(KEY))                     .build()                     .verify(token.replace(TOKEN_PREFIX, ""))                     .getSubject();         } catch (TokenExpiredException e){             throw new Exception("token已失效,請重新登錄",e);         } catch (JWTVerificationException e) {             throw new Exception("token驗證失??!",e);         }     } }
  • 編寫配置類,允許跨域,并且創(chuàng)建一個權限攔截器

@Slf4j @Configuration public class GlobalWebMvcConfig implements WebMvcConfigurer {        /**      * 重寫父類提供的跨域請求處理的接口      * @param registry      */     @Override     public void addCorsMappings(CorsRegistry registry) {         // 添加映射路徑         registry.addMapping("/**")                 // 放行哪些原始域                 .allowedOrigins("*")                 // 是否發(fā)送Cookie信息                 .allowCredentials(true)                 // 放行哪些原始域(請求方式)                 .allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS", "HEAD")                 // 放行哪些原始域(頭部信息)                 .allowedHeaders("*")                 // 暴露哪些頭部信息(因為跨域訪問默認不能獲取全部頭部信息)                 .exposedHeaders("Server","Content-Length", "Authorization", "Access-Token", "Access-Control-Allow-Origin","Access-Control-Allow-Credentials");     }      /**      * 添加攔截器      * @param registry      */     @Override     public void addInterceptors(InterceptorRegistry registry) {         //添加權限攔截器         registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/**").excludePathPatterns("/static/**");     } }
  • 使用AuthenticationInterceptor攔截器對接口參數(shù)進行驗證

@Slf4j public class AuthenticationInterceptor implements HandlerInterceptor {      @Override     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {         // 從http請求頭中取出token         final String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);         //如果不是映射到方法,直接通過         if(!(handler instanceof HandlerMethod)){             return true;         }         //如果是方法探測,直接通過         if (HttpMethod.OPTIONS.equals(request.getMethod())) {             response.setStatus(HttpServletResponse.SC_OK);             return true;         }         //如果方法有JwtIgnore注解,直接通過         HandlerMethod handlerMethod = (HandlerMethod) handler;         Method method=handlerMethod.getMethod();         if (method.isAnnotationPresent(JwtIgnore.class)) {             JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class);             if(jwtIgnore.value()){                 return true;             }         }         LocalAssert.isStringEmpty(token, "token為空,鑒權失敗!");         //驗證,并獲取token內部信息         String userToken = JwtTokenUtil.verifyToken(token);                  //將token放入本地緩存         WebContextUtil.setUserToken(userToken);         return true;     }      @Override     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {         //方法結束后,移除緩存的token         WebContextUtil.removeUserToken();     } }
  • 最后,在controller層用戶登錄之后,創(chuàng)建一個token,存放在頭部即可

/**  * 登錄  * @param userDto  * @return  */ @JwtIgnore @RequestMapping(value = "/login", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"}) public UserVo login(@RequestBody UserDto userDto, HttpServletResponse response){     //...參數(shù)合法性驗證      //從數(shù)據(jù)庫獲取用戶信息     User dbUser = userService.selectByUserNo(userDto.getUserNo);      //....用戶、密碼驗證      //創(chuàng)建token,并將token放在響應頭     UserToken userToken = new UserToken();     BeanUtils.copyProperties(dbUser,userToken);      String token = JwtTokenUtil.createToken(JSONObject.toJSONString(userToken));     response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, token);       //定義返回結果     UserVo result = new UserVo();     BeanUtils.copyProperties(dbUser,result);     return result; }

到這里基本就完成了!

其中AuthenticationInterceptor中用到的JwtIgnore是一個注解,用于不需要驗證token的方法上,例如驗證碼的獲取等等。

@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface JwtIgnore {      boolean value() default true; }

而WebContextUtil是一個線程緩存工具類,其他接口通過這個方法即可從token中獲取用戶信息。

public class WebContextUtil {      //本地線程緩存token     private static ThreadLocal<String> local = new ThreadLocal<>();      /**      * 設置token信息      * @param content      */     public static void setUserToken(String content){         removeUserToken();         local.set(content);     }      /**      * 獲取token信息      * @return      */     public static UserToken getUserToken(){         if(local.get() != null){             UserToken userToken = JSONObject.parseObject(local.get() , UserToken.class);             return userToken;         }         return null;     }      /**      * 移除token信息      * @return      */     public static void removeUserToken(){         if(local.get() != null){             local.remove();         }     } }

最后,啟動項目,我們來用postman測試一下,看看頭部返回結果。

如何使用JWT實現(xiàn)單點登錄

我們把返回的信息提取處理,使用瀏覽器的base64對前兩個部分進行解密。

  • 第一部分,也就是header,結果如下:

  • 如何使用JWT實現(xiàn)單點登錄

  • 第二部分,也就是playload,結果如下:

  • 如何使用JWT實現(xiàn)單點登錄

可以很清晰的看到,頭部、載荷的信息都可以通過base64解密出來。

所以,一定別在token中存放敏感信息!

當我們需要請求其它服務接口時,只需要在請求頭部headers中加入Authorization參數(shù)即可。

如何使用JWT實現(xiàn)單點登錄

當權限攔截器驗證通過之后,在接口方法中只需要通過WebContextUtil工具類就可以獲取用戶信息。

//獲取用戶token信息 UserToken userToken = WebContextUtil.getUserToken();

“如何使用JWT實現(xiàn)單點登錄”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關的知識可以關注創(chuàng)新互聯(lián)網站,小編將為大家輸出更多高質量的實用文章!

網站欄目:如何使用JWT實現(xiàn)單點登錄
網頁鏈接:http://muchs.cn/article8/johjip.html

成都網站建設公司_創(chuàng)新互聯(lián),為您提供網站策劃、外貿建站、網頁設計公司、App開發(fā)、網站收錄App設計

廣告

聲明:本網站發(fā)布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)

h5響應式網站建設