JSON Web Token (JWT)是一個開放標準(RFC 7519),它定義了一種緊湊的、自包含的方式,用於做爲JSON對象在各方之間安全地傳輸信息。該信息能夠被驗證和信任,由於它是數字簽名的。html
下列場景中使用JSON Web Token是頗有用的:java
JSON Web Token由三部分組成,它們之間用圓點(.)鏈接。這三部分分別是:web
所以,一個典型的JWT看起來是這個樣子的:算法
xxxxx.yyyyy.zzzzzspring
接下來,具體看一下每一部分:數據庫
header典型的由兩部分組成:token的類型(「JWT」)和算法名稱(好比:HMAC SHA256或者RSA等等)。json
例如:api
而後,用Base64對這個JSON編碼就獲得JWT的第一部分跨域
JWT的第二部分是payload,它包含聲明(要求)。聲明是關於實體(一般是用戶)和其餘數據的聲明。聲明有三種類型: registered, public 和 private。瀏覽器
下面是一個例子:
對payload進行Base64編碼就獲得JWT的第二部分
注意,不要在JWT的payload或header中放置敏感信息,除非它們是加密的。
爲了獲得簽名部分,你必須有編碼過的header、編碼過的payload、一個祕鑰,簽名算法是header中指定的那個,然對它們簽名便可。
例如:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
簽名是用於驗證消息在傳遞過程當中有沒有被更改,而且,對於使用私鑰簽名的token,它還能夠驗證JWT的發送方是否爲它所稱的發送方。
看一張官網的圖就明白了:
在認證的時候,當用戶用他們的憑證成功登陸之後,一個JSON Web Token將會被返回。此後,token就是用戶憑證了,你必須很是當心以防止出現安全問題。通常而言,你保存令牌的時候不該該超過你所須要它的時間。
不管什麼時候用戶想要訪問受保護的路由或者資源的時候,用戶代理(一般是瀏覽器)都應該帶上JWT,典型的,一般放在Authorization header中,用Bearer schema。
header應該看起來是這樣的:
Authorization: Bearer <token>
服務器上的受保護的路由將會檢查Authorization header中的JWT是否有效,若是有效,則用戶能夠訪問受保護的資源。若是JWT包含足夠多的必需的數據,那麼就能夠減小對某些操做的數據庫查詢的須要,儘管可能並不老是如此。
若是token是在受權頭(Authorization header)中發送的,那麼跨源資源共享(CORS)將不會成爲問題,由於它不使用cookie。
下面這張圖顯示瞭如何獲取JWT以及使用它來訪問APIs或者資源:
在討論基於Token的身份認證是如何工做的以及它的好處以前,咱們先來看一下之前咱們是怎麼作的:
HTTP協議是無狀態的,也就是說,若是咱們已經認證了一個用戶,那麼他下一次請求的時候,服務器不知道我是誰,咱們必須再次認證
傳統的作法是將已經認證過的用戶信息存儲在服務器上,好比Session。用戶下次請求的時候帶着Session ID,而後服務器以此檢查用戶是否定證過。
這種基於服務器的身份認證方式存在一些問題:
相同點是,它們都是存儲用戶信息;然而,Session是在服務器端的,而JWT是在客戶端的。
Session方式存儲用戶信息的最大問題在於要佔用大量服務器內存,增長服務器的開銷。
而JWT方式將用戶狀態分散到了客戶端中,能夠明顯減輕服務端的內存壓力。
Session的狀態是存儲在服務器端,客戶端只有session id;而Token的狀態是存儲在客戶端。
基於Token的身份認證是無狀態的,服務器或者Session中不會存儲任何用戶信息。
沒有會話信息意味着應用程序能夠根據須要擴展和添加更多的機器,而沒必要擔憂用戶登陸的位置。
雖然這一實現可能會有所不一樣,但其主要流程以下:
注意:
<!--引入JWT依賴,因爲是基於Java,因此須要的是java-jwt--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency>
package com.pjb.springbootjjwt.jimisun; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface LoginToken { boolean required() default true; }
package com.pjb.springbootjjwt.jimisun; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface CheckToken { boolean required() default true; }
package com.pjb.springbootjjwt.jimisun; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** * @Author:jimisun * @Description: * @Date:Created in 14:08 2018/8/15 * @Modified By: */ public class JwtUtil { /** * 用戶登陸成功後生成Jwt * 使用Hs256算法 私匙使用用戶密碼 * * @param ttlMillis jwt過時時間 * @param user 登陸成功的user對象 * @return */ public static String createJWT(long ttlMillis, User user) { //指定簽名的時候使用的簽名算法,也就是header那部分,jjwt已經將這部份內容封裝好了。 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //生成JWT的時間 long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); //建立payload的私有聲明(根據特定的業務須要添加,若是要拿這個作驗證,通常是須要和jwt的接收方提早溝通好驗證方式的) Map<String, Object> claims = new HashMap<String, Object>(); claims.put("id", user.getId()); claims.put("username", user.getUsername()); claims.put("password", user.getPassword()); //生成簽名的時候使用的祕鑰secret,這個方法本地封裝了的,通常能夠從本地配置文件中讀取,切記這個祕鑰不能外露哦。它就是你服務端的私鑰,在任何場景都不該該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是能夠自我簽發jwt了。 String key = user.getPassword(); //生成簽發人 String subject = user.getUsername(); //下面就是在爲payload添加各類標準聲明和私有聲明瞭 //這裏其實就是new一個JwtBuilder,設置jwt的body JwtBuilder builder = Jwts.builder() //若是有私有聲明,必定要先設置這個本身建立的私有的聲明,這個是給builder的claim賦值,一旦寫在標準的聲明賦值以後,就是覆蓋了那些標準的聲明的 .setClaims(claims) //設置jti(JWT ID):是JWT的惟一標識,根據業務須要,這個能夠設置爲一個不重複的值,主要用來做爲一次性token,從而回避重放攻擊。 .setId(UUID.randomUUID().toString()) //iat: jwt的簽發時間 .setIssuedAt(now) //表明這個JWT的主體,即它的全部人,這個是一個json格式的字符串,能夠存放什麼userid,roldid之類的,做爲何用戶的惟一標誌。 .setSubject(subject) //設置簽名使用的簽名算法和簽名使用的祕鑰 .signWith(signatureAlgorithm, key); if (ttlMillis >= 0) { long expMillis = nowMillis + ttlMillis; Date exp = new Date(expMillis); //設置過時時間 builder.setExpiration(exp); } return builder.compact(); } /** * Token的解密 * @param token 加密後的token * @param user 用戶的對象 * @return */ public static Claims parseJWT(String token, User user) { //簽名祕鑰,和生成的簽名的祕鑰如出一轍 String key = user.getPassword(); //獲得DefaultJwtParser Claims claims = Jwts.parser() //設置簽名的祕鑰 .setSigningKey(key) //設置須要解析的jwt .parseClaimsJws(token).getBody(); return claims; } /** * 校驗token * 在這裏可使用官方的校驗,我這裏校驗的是token中攜帶的密碼於數據庫一致的話就校驗經過 * @param token * @param user * @return */ public static Boolean isVerify(String token, User user) { //簽名祕鑰,和生成的簽名的祕鑰如出一轍 String key = user.getPassword(); //獲得DefaultJwtParser Claims claims = Jwts.parser() //設置簽名的祕鑰 .setSigningKey(key) //設置須要解析的jwt .parseClaimsJws(token).getBody(); if (claims.get("password").equals(user.getPassword())) { return true; } return false; } }
package com.pjb.springbootjjwt.interceptor; import com.auth0.jwt.JWT; import com.auth0.jwt.exceptions.JWTDecodeException; import com.pjb.springbootjjwt.jimisun.CheckToken; import com.pjb.springbootjjwt.jimisun.JwtUtil; import com.pjb.springbootjjwt.jimisun.LoginToken; import com.pjb.springbootjjwt.jimisun.User; import com.pjb.springbootjjwt.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; /** * jimisun */ public class AuthenticationInterceptor implements HandlerInterceptor { @Autowired UserService userService; @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception { // 從 http 請求頭中取出 token String token = httpServletRequest.getHeader("token"); // 若是不是映射到方法直接經過 if (!(object instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) object; Method method = handlerMethod.getMethod(); //檢查是否有LoginToken註釋,有則跳過認證 if (method.isAnnotationPresent(LoginToken.class)) { LoginToken loginToken = method.getAnnotation(LoginToken.class); if (loginToken.required()) { return true; } } //檢查有沒有須要用戶權限的註解 if (method.isAnnotationPresent(CheckToken.class)) { CheckToken checkToken = method.getAnnotation(CheckToken.class); if (checkToken.required()) { // 執行認證 if (token == null) { throw new RuntimeException("無token,請從新登陸"); } // 獲取 token 中的 user id String userId; try { userId = JWT.decode(token).getClaim("id").asString(); } catch (JWTDecodeException j) { throw new RuntimeException("訪問異常!"); } User user = userService.findUserById(userId); if (user == null) { throw new RuntimeException("用戶不存在,請從新登陸"); } Boolean verify = JwtUtil.isVerify(token, user); if (!verify) { throw new RuntimeException("非法訪問!"); } return true; } } return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }
package com.pjb.springbootjjwt.interceptorconfig; import com.pjb.springbootjjwt.interceptor.AuthenticationInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns("/**"); // 攔截全部請求,經過判斷是否有 @LoginRequired 註解 決定是否須要登陸 } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); } }
package com.pjb.springbootjjwt.jimisun; import com.alibaba.fastjson.JSONObject; import com.pjb.springbootjjwt.annotation.PassToken; import com.pjb.springbootjjwt.annotation.UserLoginToken; import com.pjb.springbootjjwt.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.UUID; /** * @Author:jimisun * @Description: * @Date:Created in 15:04 2018/8/15 * @Modified By: */ @RestController @RequestMapping("/api") public class UserController { @Autowired private UserService userService; //登陸 @PostMapping("/login") @LoginToken public Object login(@RequestBody @Valid com.pjb.springbootjjwt.jimisun.User user) { JSONObject jsonObject = new JSONObject(); com.pjb.springbootjjwt.jimisun.User userForBase = userService.findByUsername(user); if (userForBase == null) { jsonObject.put("message", "登陸失敗,用戶不存在"); return jsonObject; } else { if (!userForBase.getPassword().equals(user.getPassword())) { jsonObject.put("message", "登陸失敗,密碼錯誤"); return jsonObject; } else { String token = JwtUtil.createJWT(6000000, userForBase); jsonObject.put("token", token); jsonObject.put("user", userForBase); return jsonObject; } } } //查看我的信息 @CheckToken @GetMapping("/getMessage") public String getMessage() { return "你已經過驗證"; } }