1. JWT是什么
JSON Web Token (JWT),它是目前最流行的跨域身份驗證解決方案
2. 為什么使用JWT
JWT的精髓在於:“去中心化”,數據是保存在客戶端的。
3. JWT的工作原理
1. 是在服務器身份驗證之后,將生成一個JSON對象並將其發送回用戶,示例如下:
{"UserName": "Chongchong","Role": "Admin","Expire": "2018-08-08 20:15:56"}
2. 之后,當用戶與服務器通信時,客戶在請求中發回JSON對象
3. 為了防止用戶篡改數據,服務器將在生成對象時添加簽名,並對發回的數據進行驗證
4. JWT組成
一個JWT實際上就是一個字符串,它由三部分組成:頭部(Header)、載荷(Payload)與簽名(signature)
JWT結構原理圖:見資料“JWT的數據結構.jpg”
JWT實際結構:eyJhbGciOiJIUzI1NiJ9.
eyJzdWIiOiJ7fSIsImlzcyI6InpraW5nIiwiZXhwIjoxNTYyODUwMjM3LCJpYXQiOjE1NjI4NDg0MzcsImp0aSI6ImM5OWEyMzRmMDc4NzQyZWE4YjlmYThlYmYzY2VhNjBlIiwidXNlcm5hbWUiOiJ6c3MifQ.
WUfqhFTeGzUZCpCfz5eeEpBXBZ8-lYg1htp-t7wD3I4
它是一個很長的字符串,中間用點(.)分隔成三個部分。注意,JWT 內部是沒有換行的,這里只是為了便於展示,將它寫成了幾行。
寫成一行,就是下面的樣子:Header.Payload.Signature
4.1 Header
{"typ":"JWT","alg":"HS256"}
這個json中的typ屬性,用來標識整個token字符串是一個JWT字符串;它的alg屬性,用來說明這個JWT簽發的時候所使用的簽名和摘要算法
typ跟alg屬性的全稱其實是type跟algorithm,分別是類型跟算法的意思。之所以都用三個字母來表示,也是基於JWT最終字串大小的考慮,
同時也是跟JWT這個名稱保持一致,這樣就都是三個字符了…typ跟alg是JWT中標准中規定的屬性名稱
4.2 Payload(負荷)
{"sub":"123","name":"Tom","admin":true}
payload用來承載要傳遞的數據,它的json結構實際上是對JWT要傳遞的數據的一組聲明,這些聲明被JWT標准稱為claims,
它的一個“屬性值對”其實就是一個claim(要求),
每一個claim的都代表特定的含義和作用。
注1:英文“claim”就是要求的意思
注2:如上面結構中的sub代表這個token的所有人,存儲的是所有人的ID;name表示這個所有人的名字;admin表示所有人是否管理員的角色。
當后面對JWT進行驗證的時候,這些claim都能發揮特定的作用
注3:根據JWT的標准,這些claims可以分為以下三種類型:
A. Reserved claims(保留)
它的含義就像是編程語言的保留字一樣,屬於JWT標准里面規定的一些claim。JWT標准里面定義好的claim有:
iss(Issuser):代表這個JWT的簽發主體;
sub(Subject):代表這個JWT的主體,即它的所有人;
aud(Audience):代表這個JWT的接收對象;
exp(Expiration time):是一個時間戳,代表這個JWT的過期時間;
nbf(Not Before):是一個時間戳,代表這個JWT生效的開始時間,意味着在這個時間之前驗證JWT是會失敗的;
iat(Issued at):是一個時間戳,代表這個JWT的簽發時間;
jti(JWT ID):是JWT的唯一標識。
B. Public claims,略(不重要)
C. Private claims(私有)
這個指的就是自定義的claim,比如前面那個示例中的admin和name都屬於自定的claim。這些claim跟JWT標准規定的claim區別在於:JWT規定的claim,
JWT的接收方在拿到JWT之后,都知道怎么對這些標准的claim進行驗證;而private claims不會驗證,除非明確告訴接收方要對這些claim進行驗證以及規則才行
按照JWT標准的說明:保留的claims都是可選的,在生成payload不強制用上面的那些claim,你可以完全按照自己的想法來定義payload的結構,不過這樣搞根本沒必要:
第一是,如果把JWT用於認證, 那么JWT標准內規定的幾個claim就足夠用了,甚至只需要其中一兩個就可以了,假如想往JWT里多存一些用戶業務信息,
比如角色和用戶名等,這倒是用自定義的claim來添加;第二是,JWT標准里面針對它自己規定的claim都提供了有詳細的驗證規則描述,
每個實現庫都會參照這個描述來提供JWT的驗證實現,所以如果是自定義的claim名稱,那么你用到的實現庫就不會主動去驗證這些claim
4.3 signature
簽名是把header和payload對應的json結構進行base64url編碼之后得到的兩個串用英文句點號拼接起來,然后根據header里面alg指定的簽名算法生成出來的。
算法不同,簽名結果不同。以alg: HS256為例來說明前面的簽名如何來得到。
按照前面alg可用值的說明,HS256其實包含的是兩種算法:HMAC算法和SHA256算法,前者用於生成摘要,后者用於對摘要進行數字簽名。這兩個算法也可以用HMACSHA256來統稱
5. JWT的驗證過程
它驗證的方法其實很簡單,只要把header做base64url解碼,就能知道JWT用的什么算法做的簽名,然后用這個算法,再次用同樣的邏輯對header和payload做一次簽名,
並比較這個簽名是否與JWT本身包含的第三個部分的串是否完全相同,只要不同,就可以認為這個JWT是一個被篡改過的串,自然就屬於驗證失敗了。
接收方生成簽名的時候必須使用跟JWT發送方相同的密鑰
注1:在驗證一個JWT的時候,簽名認證是每個實現庫都會自動做的,但是payload的認證是由使用者來決定的。因為JWT里面可能會包含一個自定義claim,
所以它不會自動去驗證這些claim,以jjwt-0.7.0.jar為例:
A 如果簽名認證失敗會拋出如下的異常:
io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
即簽名錯誤,JWT的簽名與本地計算機的簽名不匹配
B JWT過期異常
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2017-06-13T11:55:56Z. Current time: 2017-06-13T11:55:57Z, a difference of 1608 milliseconds. Allowed
注2:認證失敗,返回401 Unauthorized響應
注3:認證服務作為一個Middleware HOOK 對請求進行攔截,首先在cookie中查找Token信息,如果沒有找到,則在HTTP Authorization Head中查找
6. JWT令牌刷新思路
6.1 登陸成功后,將生成的JWT令牌通過響應頭返回給客戶端
6.2 WEB APP項目每次請求后台數據時(將JWT令牌從請求頭中帶過來),
驗證通過,刷新JWT,並保存在響應頭返回給客戶端,有效時間30分鍾
JwtFilter
注1:修改CorsFilter添加允許的新的請求頭“jwt”
注2:原來在默認的請求上, 瀏覽器只能訪問以下默認的 響應頭
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
如果想讓瀏覽器能訪問到其他的 響應頭的話 需要在服務器上設置 Access-Control-Expose-Headers
Access-Control-Expose-Headers : 'jwt'
// CorsFilter 允許客戶端,發一個新的請求頭jwt
resp.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With, Content-Type, Accept, jwt");
注3:axios從響應頭獲得jwt令牌並保存到vuex
這里有個問題如何獲得項目中Vue的根實例,解決方案:修改main.js
window.vm = new Vue({...});
其它vuex的操作就照舊鳥~~~~~~~~~~~~~~~~~~~~~~~事了拂衣去,深藏身與名
注4:寫在最后的話鳥~~~退出系統請清空vuex中的內容哦
注5:寫在最后最后的話鳥~~~刷新頁面會導致vuex中的state清空,解決方案在前面一章哦^_^
JwtDemo
package com.zking.vue.test; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; import org.junit.Test; import com.zking.vue.util.JwtUtils; import io.jsonwebtoken.Claims; public class JwtDemo { private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); @Test public void test1() {// 生成JWT Map<String, Object> claims = new HashMap<String, Object>(); claims.put("username", "zss"); claims.put("age", 18); String jwt = JwtUtils.createJwt(claims, JwtUtils.JWT_WEB_TTL); System.out.println(jwt); Claims parseJwt = JwtUtils.parseJwt(jwt); for (Map.Entry<String, Object> entry : parseJwt.entrySet()) { System.out.println(entry.getKey() + "=" + entry.getValue()); } Date d1 = parseJwt.getIssuedAt(); Date d2 = parseJwt.getExpiration(); System.out.println("令牌簽發時間:" + sdf.format(d1)); System.out.println("令牌過期時間:" + sdf.format(d2)); } @Test public void test2() {// 解析oldJwt // String oldJwt = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NjI5MDMzNjAsImlhdCI6MTU2MjkwMTU2MCwiYWdlIjoxOCwianRpIjoiZDVjMzE4Njg0MDcyNDgyZDg1MDE5ODVmMDY3OGQ4NjkiLCJ1c2VybmFtZSI6InpzcyJ9.XDDDRRq5jYq5EdEBHtPm7GcuBz4S0VhDTS1amRCdf48"; String oldJwt = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NjM1MjU5MjMsImlhdCI6MTU2MzUyNDEyMywiYWdlIjoxOCwianRpIjoiOTAzNmMwY2Q3NGIwNDBjMzgzMDAxYzdiNmZkMzYzZmIiLCJ1c2VybmFtZSI6InpzcyJ9.sgV9fr4fgmmahDFRJnsfazA6R3H-gNMVcg2ucA227n4"; Claims parseJwt = JwtUtils.parseJwt(oldJwt); for (Map.Entry<String, Object> entry : parseJwt.entrySet()) { System.out.println(entry.getKey() + "=" + entry.getValue()); } Date d1 = parseJwt.getIssuedAt(); Date d2 = parseJwt.getExpiration(); System.out.println("令牌簽發時間:" + sdf.format(d1)); System.out.println("令牌過期時間:" + sdf.format(d2)); } @Test public void test3() {// 復制jwt,並延時30秒 String oldJwt = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NjI5MDMzNjAsImlhdCI6MTU2MjkwMTU2MCwiYWdlIjoxOCwianRpIjoiZDVjMzE4Njg0MDcyNDgyZDg1MDE5ODVmMDY3OGQ4NjkiLCJ1c2VybmFtZSI6InpzcyJ9.XDDDRRq5jYq5EdEBHtPm7GcuBz4S0VhDTS1amRCdf48"; String jwt = JwtUtils.copyJwt(oldJwt, JwtUtils.JWT_WEB_TTL); Claims parseJwt = JwtUtils.parseJwt(jwt); for (Map.Entry<String, Object> entry : parseJwt.entrySet()) { System.out.println(entry.getKey() + "=" + entry.getValue()); } Date d1 = parseJwt.getIssuedAt(); Date d2 = parseJwt.getExpiration(); System.out.println("令牌簽發時間:" + sdf.format(d1)); System.out.println("令牌過期時間:" + sdf.format(d2)); } @Test public void test4() {// 測試JWT的有效時間 Map<String, Object> claims = new HashMap<String, Object>(); claims.put("username", "zss"); String jwt = JwtUtils.createJwt(claims, 3 * 1000L); System.out.println(jwt); Claims parseJwt = JwtUtils.parseJwt(jwt); Date d1 = parseJwt.getIssuedAt(); Date d2 = parseJwt.getExpiration(); System.out.println("令牌簽發時間:" + sdf.format(d1)); System.out.println("令牌過期時間:" + sdf.format(d2)); } @Test public void test5() {// 三秒后再解析上面過期時間只有三秒的令牌,因為過期則會報錯io.jsonwebtoken.ExpiredJwtException String oldJwt = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NjI4NTMzMzAsImlhdCI6MTU2Mjg1MzMyNywidXNlcm5hbWUiOiJ6c3MifQ.e098Vj9KBlZfC12QSDhI5lUGRLbNwb27lrYYSL6JwrQ"; Claims parseJwt = JwtUtils.parseJwt(oldJwt); // 過期后解析就報錯了,下面代碼根本不會執行 Date d1 = parseJwt.getIssuedAt(); Date d2 = parseJwt.getExpiration(); System.out.println("令牌簽發時間:" + sdf.format(d1)); System.out.println("令牌過期時間:" + sdf.format(d2)); } }
CorsFilte
package com.zking.vue.util; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 配置tomcat允許跨域訪問 * * @author Administrator * */ public class CorsFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } // @Override // public void doFilter(ServletRequest servletRequest, ServletResponse // servletResponse, FilterChain filterChain) // throws IOException, ServletException { // HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; // // // Access-Control-Allow-Origin就是我們需要設置的域名 // // Access-Control-Allow-Headers跨域允許包含的頭。 // // Access-Control-Allow-Methods是允許的請求方式 // httpResponse.addHeader("Access-Control-Allow-Origin", "*");// *,任何域名 // httpResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, // DELETE"); // // httpResponse.setHeader("Access-Control-Allow-Headers", "Origin, // // X-Requested-With, Content-Type, Accept"); // // // 允許請求頭Token // httpResponse.setHeader("Access-Control-Allow-Headers", // "Origin,X-Requested-With, Content-Type, Accept, Token"); // HttpServletRequest req = (HttpServletRequest) servletRequest; // System.out.println("Token=" + req.getHeader("Token")); // if("OPTIONS".equals(req.getMethod())) { // return; // } // // // filterChain.doFilter(servletRequest, servletResponse); // } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse resp = (HttpServletResponse) servletResponse; HttpServletRequest req = (HttpServletRequest) servletRequest; // Access-Control-Allow-Origin就是我們需要設置的域名 // Access-Control-Allow-Headers跨域允許包含的頭。 // Access-Control-Allow-Methods是允許的請求方式 resp.setHeader("Access-Control-Allow-Origin", "*");// *,任何域名 resp.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE"); // resp.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With, // Content-Type, Accept"); // 允許客戶端,發一個新的請求頭jwt resp.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With, Content-Type, Accept, jwt"); // 允許客戶端,處理一個新的響應頭jwt resp.setHeader("Access-Control-Expose-Headers", "jwt"); // String sss = resp.getHeader("Access-Control-Expose-Headers"); // System.out.println("sss=" + sss); // 允許請求頭Token // httpResponse.setHeader("Access-Control-Allow-Headers","Origin,X-Requested-With, // Content-Type, Accept, Token"); // System.out.println("Token=" + req.getHeader("Token")); if ("OPTIONS".equals(req.getMethod())) {// axios的ajax會發兩次請求,第一次提交方式為:option,直接返回即可 return; } filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() { } }
JwtFilter