聊聊 cookies,token


總結:小故事:很久很久以前,http 是無狀態的協議,就是說一次請求你根本不知道是哪個客戶端的,為了解決無狀態搞出了 會話 這個名詞,就是說要記住哪個客戶端發的請求。

        於是乎 瀏覽器 cookies 誕生了,瀏覽器每次請求都會自動帶上 cookies 這里面存了一下信息,第一次訪問 服務端時 服務端會產生一個 唯一 id 然后告訴瀏覽器,瀏覽器每次請求時 就會在同域名 下的信息 帶過來,這樣服務端就知道 是哪個在訪問了,這就叫做會 話。session 就是折磨個 東東 ,存的就是 會話信息,比如你登入了用戶信息 

        移動端流行起來,由於app 端 基本都是api 發請求,cookie是瀏覽器 自動帶的,當然請求也可以帶上,但是還是自定義token 比較多(登入生成token 然后每次請求帶上)好處就是每個服務端都可以自己解析 可以跨平台 應用,回話保持就需要 自己實現了

        token 常用方式 有 1.jwt (直接base編碼 可以解碼) 2.自定義token md5(用戶名+時間戳)緩存 存 這個 用戶的信息。3.對外的 OAuth 2.0  最安全的搞法,一般對外提供接口的基本都是這套 搞法,如第 3方登入

  上述的東東 其實就是為了 保持回話。

 

我也就 簡單 介紹一下下    token 中 JWT/Oauth2.0

JWT介紹

JSON Web Token(JWT)是為了在網絡應用環境間傳遞聲明而執行的一種基於 JSON 的開放標准(RFC 7519)。來自 JWT RFC 7519 標准化的摘要說明:JSON Web Token 是一種緊湊的,URL 安全的方式,表示要在雙方之間傳輸的聲明。JWT 一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該 Token 也可直接被用於認證,也可被加密。

JWT 認證流程

客戶端調用登錄接口(或者獲取 token 接口),傳入用戶名密碼。
服務端請求身份認證中心,確認用戶名密碼正確。
服務端創建 JWT,返回給客戶端。
客戶端拿到 JWT,進行存儲(可以存儲在緩存中,也可以存儲在數據庫中,如果是瀏覽器,可以存儲在 Cookie 中)在后續請求中,在 HTTP 請求頭中加上 JWT。
服務端校驗 JWT,校驗通過后,返回相關資源和數據。

JWT 結構

JWT 是由三段信息構成的,第一段為頭部(Header),第二段為載荷(Payload),第三段為簽名(Signature)。每一段內容都是一個 JSON 對象,將每一段 JSON 對象采用 BASE64 編碼,將編碼后的內容用. 鏈接一起就構成了 JWT 字符串。如下:
header.payload.signature

1. 頭部(Header)

頭部用於描述關於該 JWT 的最基本的信息,例如其類型以及簽名所用的算法等。這也可以被表示成一個 JSON 對象。

 

在頭部指明了簽名算法是 HS256 算法。

2. 載荷(payload)

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

標准中注冊的聲明
公共的聲明
私有的聲明

標准中注冊的聲明(建議但不強制使用):

iss:JWT 簽發者
sub:JWT 所面向的用戶
aud:接收 JWT 的一方
exp:JWT 的過期時間,這個過期時間必須要大於簽發時間
nbf:定義在什么時間之前,該 JWT 都是不可用的
iat:JWT 的簽發時間
jti:JWT 的唯一身份標識,主要用來作為一次性 token, 從而回避重放攻擊。

公共的聲明 :

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

私有的聲明 :

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

示例如下:

 

3. 簽名(signature)

創建簽名需要使用 Base64 編碼后的 header 和 payload 以及一個秘鑰。將 base64 加密后的 header 和 base64 加密后的 payload 使用. 連接組成的字符串,通過 header 中聲明的加密方式進行加鹽 secret 組合加密,然后就構成了 jwt 的第三部分。

比如:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

JWT 的優點:

跨語言,JSON 的格式保證了跨語言的支撐
基於 Token,無狀態
占用字節小,便於傳輸

關於 Token 注銷:

Token 的注銷,由於 Token 不存儲在服務端,由客戶端存儲,當用戶注銷時,Token 的有效時間還沒有到,還是有效的。所以如何在用戶注銷登錄時讓 Token 注銷是一個要關注的點。一般有如下幾種方式:

Token 存儲在 Cookie 中,這樣客戶端注銷時,自然可以清空掉
注銷時,將 Token 存放到分布式緩存中,每次校驗 Token 時區檢查下該 Token 是否已注銷。不過這樣也就失去了快速校驗 Token 的優點。
多采用短期令牌,比如令牌有效期是 20 分鍾,這樣可以一定程度上降低注銷后 Token 可用性的風險。

四、OAuth 2.0 介紹

OAuth 的官網介紹:An open protocol to allow secure API authorization in a simple and standard method from desktop and web applications。OAuth 是一種開放的協議,為桌面程序或者基於 BS 的 web 應用提供了一種簡單的,標准的方式去訪問需要用戶授權的 API 服務。OAUTH 認證授權具有以下特點:

簡單:不管是 OAuth 服務提供者還是應用開發者,都很容易於理解與使用;
安全:沒有涉及到用戶密鑰等信息,更安全更靈活;
開放:任何服務提供商都可以實現 OAuth,任何軟件開發商都可以使用 OAuth;

OAuth 2.0 是 OAuth 協議的下一版本,但不向后兼容 OAuth 1.0,即完全廢止了 OAuth 1.0。OAuth 2.0 關注客戶端開發者的簡易性。要么通過組織在資源擁有者和 HTTP 服務商之間的被批准的交互動作代表用戶,要么允許第三方應用代表用戶獲得訪問的權限。同時為 Web 應用,桌面應用和手機,和起居室設備提供專門的認證流程。2012 年 10 月,OAuth 2.0 協議正式發布為 RFC 6749。

授權流程

OAuth 2.0 的流程如下:

 

(A)用戶打開客戶端以后,客戶端要求用戶給予授權。(B)用戶同意給予客戶端授權。(C)客戶端使用上一步獲得的授權,向認證服務器申請令牌。(D)認證服務器對客戶端進行認證以后,確認無誤,同意發放令牌。(E)客戶端使用令牌,向資源服務器申請獲取資源。(F)資源服務器確認令牌無誤,同意向客戶端開放資源。

四大角色

由授權流程圖中可以看到 OAuth 2.0 有四個角色:客戶端、資源擁有者、資源服務器、授權服務器。

客戶端:客戶端是代表資源所有者對資源服務器發出訪問受保護資源請求的應用程序。
資源擁有者:資源擁有者是對資源具有授權能力的人。
資源服務器:資源所在的服務器。
授權服務器:為客戶端應用程序提供不同的 Token,可以和資源服務器在統一服務器上,也可以獨立出去。

客戶端的授權模式

客戶端必須得到用戶的授權(Authorization Grant),才能獲得令牌(access token)。OAuth 2.0 定義了四種授權方式:authorizationcode、implicit、resource owner password credentials、client credentials。

1. 授權碼模式(authorization code)

授權碼模式(authorization code)是功能最完整、流程最嚴密的授權模式。它的特點就是通過客戶端的后台服務器,與"服務提供商"的認證服務器進行互動。流程如下:

用戶訪問客戶端,后者將前者導向認證服務器。
用戶選擇是否給予客戶端授權。
假設用戶給予授權,認證服務器將用戶導向客戶端事先指定的"重定向 URI"(redirection URI),同時附上一個授權碼。
客戶端收到授權碼,附上早先的"重定向 URI",向認證服務器申請令牌。這一步是在客戶端的后台的服務器上完成的,對用戶不可見。
認證服務器核對了授權碼和重定向 URI,確認無誤后,向客戶端發送訪問令牌(access token)和更新令牌(refresh token)。

2. 簡化模式(implicit)

簡化模式(Implicit Grant Type)不通過第三方應用程序的服務器,直接在瀏覽器中向認證服務器申請令牌,跳過了"授權碼"這個步驟,因此得名。所有步驟在瀏覽器中完成,令牌對訪問者是可見的,且客戶端不需要認證。流程如下:

客戶端將用戶導向認證服務器。
用戶決定是否給於客戶端授權。
假設用戶給予授權,認證服務器將用戶導向客戶端指定的"重定向 URI",並在 URI 的 Hash 部分包含了訪問令牌。
瀏覽器向資源服務器發出請求,其中不包括上一步收到的 Hash 值。
資源服務器返回一個網頁,其中包含的代碼可以獲取 Hash 值中的令牌。
瀏覽器執行上一步獲得的腳本,提取出令牌。
瀏覽器將令牌發給客戶端。

3. 密碼模式(Resource Owner Password Credentials)

密碼模式中,用戶向客戶端提供自己的用戶名和密碼。客戶端使用這些信息,向"服務商提供商"索要授權。在這種模式中,用戶必須把自己的密碼給客戶端,但是客戶端不得儲存密碼。這通常用在用戶對客戶端高度信任的情況下,比如客戶端是操作系統的一部分,或者由一個著名公司出品。而認證服務器只有在其他授權模式無法執行的情況下,才能考慮使用這種模式。流程如下:

用戶向客戶端提供用戶名和密碼。
客戶端將用戶名和密碼發給認證服務器,向后者請求令牌。
認證服務器確認無誤后,向客戶端提供訪問令牌。

4. 客戶端模式(Client Credentials)

客戶端模式(Client Credentials Grant)指客戶端以自己的名義,而不是以用戶的名義,向"服務提供商"進行認證。嚴格地說,客戶端模式並不屬於 OAuth 框架所要解決的問題。

在這種模式中,用戶直接向客戶端注冊,客戶端以自己的名義要求"服務提供商"提供服務,其實不存在授權問題。流程如下:

客戶端向認證服務器進行身份認證,並要求一個訪問令牌。
認證服務器確認無誤后,向客戶端提供訪問令牌。

五、思考總結

正如 David Borsos 所建議的一種方案,在微服務架構下,我們更傾向於將 Oauth 和 JWT 結合使用,Oauth 一般用於第三方接入的場景,管理對外的權限,所以比較適合和 API 網關結合,針對於外部的訪問進行鑒權(當然,底層 Token 標准采用 JWT 也是可以的)。

JWT 更加輕巧,在微服務之間進行訪問鑒權已然足夠,並且可以避免在流轉過程中和身份認證服務打交道。當然,從能力實現角度來說,類似於分布式 Session 在很多場景下也是完全能滿足需求,具體怎么去選擇鑒權方案,還是要結合實際的需求來。

 

參考:

https://mp.weixin.qq.com/s/GIL606vsUXbyeQySTomLCA

 

jwt 小例子:

JWT+Interceptor實現無狀態登錄和鑒權

 

無狀態登錄原理#

先提一下啥是有狀態登錄#

單台tomcat的情況下:編碼的流程如下

  1. 前端提交表單里用戶的輸入的賬號密碼
  2. 后台接受,查數據庫,
  3. 在數據庫中找到用戶的信息后,把用戶的信息存放到session里面,返回給用戶cookie
  4. 以后用戶的請求都會自動攜帶着cookie去訪問后台,后台根據用戶的cookie辨識用戶的身份

但是有缺點

  • 如果有千千萬的用戶都往session里面存放信息,
  • session很難跨服務器,也就是說,用戶每次請求,都必須訪問同一台tomcat,新的tomcat不認識用戶的cookie

無狀態登錄#

  • 對於服務端,不再保存任何用戶的信息,對於他們來說,所有的用戶地位平等
  • 對於用戶,他們需要自己攜帶着信息去訪問服務端,攜帶的信息可以被所有服務端辨認,所以,對於用戶,所有的服務地位平等

具體如何實現呢?

  • 用戶登錄,服務端返回給用戶的個人信息+token令牌
  • 前端為了自己使用方便,將用戶的個人信息緩存進瀏覽器(因為后端只是給了他一個token)
  • 用戶攜帶自己的token去訪問服務端
  • 認證通過,把用戶請求的數據返回給客戶端
  • 以后不論用戶請求哪個微服務,都攜帶着token
  • 微服務對token進行解密,判斷他是否有效


JWT(Json Web Token)生成規則#

整個登錄.授權.鑒權的過程token的安全性至關重要,而JWT就是一門有關於如何生成一個不可仿造的token的規范

他是JSON風格輕量級的授權和身份認證規范,可實現無狀態、分布式的Web應用授權,而且它不是技術,和語言無關,java有對這個規范的實現 叫做 jjwt -- 點擊進入jjwt的github項目

由JWT算法得到的token格式如上圖,由頭部,載荷,簽名三部分組成

  • 頭部
    由兩部分組成,alg表示使用的加密算法 typ表示使用的token的類型

  • 載荷,存放用戶相關的信息
Copy
// 下面是它已經定義好的載荷部分key,也允許我們自定義載荷部分key iss: jwt簽發者 sub: jwt所面向的用戶 aud: 接收jwt的一方 exp: jwt的過期時間,這個過期時間必須要大於簽發時間 nbf: 定義在什么時間之前,該jwt都是不可用的. iat: jwt的簽發時間 jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。
  • 第三步分的簽名是由 頭+載荷+鹽 三部分加密組成

從圖可以看出,頭部和載荷被串改后,生成的編碼會發生改變,因此保證了token的安全 ,還有,載荷部分可解密,因此不要往里面放入敏感的信息(比如密碼 )

只要密鑰不泄露,別人就無法偽造任何信息

jwt的交互過程

RSA非對稱加密算算法#

由美國麻 省理工 學院三 位學者 Riv est、Sh amir 及Adleman 研 究發 展出 一套 可實 際使用 的公 開金 鑰密 碼系 統,那 就是
RSA(Rivest-Shamir-Adleman)密碼系統

jwt(是一種非對稱加密算法) JWT不一定非要搭配RSA算法,但是擁有RSA算法公鑰私鑰的特性,會使我們的業務邏輯變的簡單,做到校驗變少

  • 對稱加密,如AES(Advanced Encryption Standard) 高級加密標准
    • 基本原理:將明文分成N個組,然后使用密鑰對各個組進行加密,形成各自的密文,最后把所有的分組密文進行合並,形成最終的密文。
    • 優勢:算法公開、計算量小、加密速度快、加密效率高
    • 缺陷:雙方都使用同樣密鑰,安全性得不到保證
  • 非對稱加密,如RSA
    • 基本原理:同時生成兩把密鑰:私鑰和公鑰,私鑰隱秘保存,公鑰可以下發給信任客戶端
    • 私鑰加密,持有私鑰或公鑰才可以解密
    • 公鑰加密,持有私鑰才可解密
    • 優點:安全,難以破解
    • 缺點:算法比較耗時
  • 不可逆加密,如MD5,SHA

使用JJWT實現JWT#

JJWT(java json web token)

坐標

Copy
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>

創建jwt令牌#

token最終會頒發給前端,這時候就得去和前端的哥們商量它想要什么信息

Copy
// 生成令牌,主要是用它生成載荷 JwtBuilder builder = Jwts.builder() // 設置頭部,使用hs256加密, + key,也就是鹽 .signWith(SignatureAlgorithm.HS256,"changwu") // 添加載荷 .setId("666") // 用戶id .setSubject("張三") // 用戶名 .setExpiration(new Date(new Date().getTime()+60*1000)) // 過期時間 .setIssuedAt(new Date())// 登錄時間 // 添加自定義的鍵值對 .claim("role","admin"); System.out.println(builder.compact());

經過它處理的token長這個樣子, 三部組成

Copy
XXX.YYY.ZZZ

解析token#

能成功解析出結果的前提是兩次的鹽是一樣的才行

Copy
Claims map = Jwts.parser().setSigningKey("changwu") .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiLlvKDkuIkiLCJleHAiOjE1NjU2MTg1MjUsImlhdCI6MTU2NTYxODQ2NSwicm9sZSI6ImFkbWluIn0.GDVfLq-ehSnMCRoxVcziXkirjOg34SUUPBK5vAEHu80") .getBody(); System.out.println("用戶id" + map.getId()); System.out.println("用戶名" + map.getSubject()); System.out.println("token過期時間" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(map.getExpiration())); System.out.println("用戶登錄時間" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(map.getIssuedAt())); System.out.println("用戶的角色是:"+map.get("role")); 

攔截器#

注意哦,使用的是SpringMvc的攔截器,而不是Servlet的過濾器

攔截器的體系架構#

攔截器的繼承體系

在攔截器的體系中,我們常用的是上面的來兩個

HandlerInterceptor: 是頂級接口如下:

雖然是接口, 但是擁有jdk8的特性,是默認的方法,所以允許我們挑選它的部分方法實現而不會報錯

prehandler: 請求到達控制器之間被回調,可以在這里進行設置編碼,安全控制,權限校驗, 一般全部返回ture,表示放行

postHandler: 控制器處理請求之后生成了ModelAndView,但是未進行渲染,提供了修改ModelAndView的機會

afterCompletion: 返回給用戶ModelAndView之后執行, 用於收尾工作

第二個是HandlerInterceptorAdapter如下圖

HandlerInterceptorAdapter

這個適配器方法全是空實現,同樣可以滿足我們的需求,但是它同時實現了AsyncHandlerInterceptor,擁有了一個新的方法,afterConcrruentHandingStarted(request,response,handler)

這個方法會在Controller方法異步執行時開始執行, 而Interceptor的postHandle方法則是需要等到Controller的異步執行完才能執行

編碼實現#

其實到這里改如何做已經清晰明了

用戶登錄,授權#

授權的很簡單

  • 用戶發送登錄請求提交form表單
  • 后端根據用戶名密碼查詢用戶的信息
  • 把用戶的信息封裝進jwt的載荷部分
  • 返回給前端token

用戶再次請求,鑒權#

后台會有很多方法需要指定權限的人才能訪問, 所謂鑒定權限,其實就是把前端放在請求頭中的token信息解析出來,如果解析成功了,說明用戶的合法的,否則就提示前端用戶沒有權限

把token從請求頭中解析出來的過程,其實是在大量的重復性工作,所以我們放在攔截器中實現

使用攔截器兩步走

第一步,繼承HandlerInterAdapter,選擇重寫它的方法

  • 設計的邏輯,這個方法肯定要返回true, 因為后台的方法中肯定存在大量的不需要任何權限就能訪問的方法
  • 所以這個方法的作用就是,解析出請求頭中的用戶的權限信息,重新放回到request中,
  • 這樣每個需要進行權限驗證的請求,就不需要再進行解析請求頭,而是直接使用當前回調方法的處理結果
Copy
@Component public class RequestInterceptor extends HandlerInterceptorAdapter { @Autowired JwtUtil jwtUtil; // 在請求進入處理器之前回調這個方法 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 獲取請求頭 String header = request.getHeader("Authorization"); // 請求頭不為空進行解析 if (StringUtils.isNotBlank(header)) { // 按照我們和前端約定的格式進行處理 if (header.startsWith("Bearer ")){ // 得到令牌 String token = header.substring(7); // 驗證令牌 try{ // 令牌的解析這里一定的try起來,因為它解析錯誤的令牌時,會報錯 // 當然你也可以在自定義的jwtUtil中把異常 try起來,這里就不用寫了 Claims claims = jwtUtil.parseJWT(token); String roles =(String) claims.get("roles"); System.err.println("roles=="+roles); if (roles!=null&&"admin".equals(roles)){ request.setAttribute("role_admin",token); } if (roles!=null&&"user".equals(roles)){ request.setAttribute("role_user",token); } }catch (Exception e){ throw new RuntimeException("令牌不存在"); } } } return true; } } 

這樣 控制器中的方法需要進行權限驗證時,就免去了解析的麻煩,直接從request中獲取即可

第二步,將攔截器注冊進容器

Copy
@Configuration public class InterceptorConfig extends WebMvcConfigurationSupport { @Autowired RequestInterceptor requestInterceptor; // 注冊攔截器 protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(requestInterceptor) .addPathPatterns("/**") .excludePathPatterns("/user/login/**"); } }
 
 

 


免責聲明!

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



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