在瞭解JWT以前先來回顧一下傳統session認證和基於token認證。web
http協議是一種無狀態協議,即瀏覽器發送請求到服務器,服務器是不知道這個請求是哪一個用戶發來的。爲了讓服務器知道請求是哪一個用戶發來的,須要讓用戶提供用戶名和密碼來進行認證。當瀏覽器第一次訪問服務器(假設是登陸接口),服務器驗證用戶名和密碼以後,服務器會生成一個sessionid(只有第一次會生成,其它會使用同一個sessionid),並將該session和用戶信息關聯起來,而後將sessionid返回給瀏覽器,瀏覽器收到sessionid保存到Cookie中,當用戶第二次訪問服務器是就會攜帶Cookie值,服務器獲取到Cookie值,進而獲取到sessionid,根據sessionid獲取關聯的用戶信息。redis
public User login(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userService.login(username, password); if (user == null) { throw new AuthenticationException("用戶名或密碼錯誤"); } // 返回此次請求關聯的當前會話,若是沒有會話則建立一個新的 // 須要在服務器端記錄該session HttpSession session = request.getSession(); session.setAttribute("user", user); // 讓瀏覽器保存sessionid到cookie中 // Cookie cookie = new Cookie("sessionid", session.getId()); // cookie.setPath("/"); // response.addCookie(cookie); return user; } public Object getUserInfo(HttpServletRequest request){ // 從request中獲取Cookie // 從Cookie中獲取sessionid // 根據sessionid獲取對應的Session對象 // 從session中獲取關聯的用戶信息 HttpSession session = request.getSession(); Object user = session.getAttribute("user"); return user; }
session的缺點:算法
token原理:數據庫
public String auth(String username, String password) throws AuthenticationException { User user = userService.login(username, password); if (user == null) { throw new AuthenticationException("用戶名或密碼錯誤"); } String token = UUID.randomUUID().toString(); redisClient.set(token, user); return token; } public Object getUserInfo(@RequestHeader("token") String token) throws AuthenticationException { User user = redisClient.get(token); if (user == null) { throw new AuthenticationException("token不可用"); } return user; }
session和token的區別:json
此種方式原理上和session方式差很少,都是客戶端調用接口時攜帶一個值,服務器經過該值來獲取用戶的信息。segmentfault
不一樣的是session是將信息保存到本機內存中,對負載均衡有限制(只能負載到同一臺機器),token是保存到緩存服務器(redis)中,對負載均衡沒有限制,若是使用同一個redis服務器還能夠保證單點登陸。session通常用在PC上,token便可用在PC上也能夠用在APP上。api
JSON Web Token(JWT)是爲了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準((RFC 7519),它定義了一種緊湊(Compact)且自包含(Self-contained)的方式,用於在各方之間以JSON對象安全傳輸信息。 這些信息能夠經過數字簽名進行驗證和信任。 可使用祕密(使用HMAC算法)或使用RSA的公鑰/私鑰對對JWT進行簽名。JWT的聲明通常被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也能夠增長一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。是目前最流行的跨域認證解決方案。跨域
jwt有3個組成部分,每部分經過點號來分割 header.payload.signature瀏覽器
① 頭部header緩存
Jwt的頭部是一個JSON,而後使用Base64URL編碼,承載兩部分信息:
var header = Base64URL({ "alg": "HS256", "typ": "JWT"})
Base64URL:Header 和 Payload 串型化的算法是 Base64URL。這個算法跟 Base64 算法基本相似,但有一些小的不一樣。JWT 做爲一個令牌(token),有些場合可能會放到 URL(好比 api.example.com/?token=xxx)。Base64 有三個字符+、/和=,在 URL 裏面有特殊含義,因此要被替換掉:=被省略、+替換成-,/替換成_ 。這就是 Base64URL 算法。
② 載荷payload
payload也是一個JSON字符串,是承載消息具體內容的地方,也須要使用Base64URL編碼,payload中能夠包含預約義的7個可用,它們不是強制性的,但推薦使用,也能夠添加任意自定義的key
// 該token簽發給1234567890,姓名爲John Doe(自定義的字段),簽發時間爲1516239022
var payload = Base64URL( {"sub": "1234567890", "name": "John Doe", "iat": 1516239022})
注意,JWT中payload是不加密的,只是Base64URL編碼一下,任何人拿到均可以進行解碼,因此不要把敏感信息放到裏面。
③ signature
Signature 部分是對前兩部分的簽名,防止數據篡改。
var header = Base64URL({ "alg": "HS256", "typ": "JWT"}); var payload = Base64URL( {"sub": "1234567890", "name": "John Doe", "iat": 1516239022}); var secret = "私鑰"; var signature = HMACSHA256(header + "." + payload, secret); var jwt = header + "." + payload + "." + signature;
咱們可使用jwt.io調試器來解碼,驗證和生成JWT:
注意:secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,因此,它就是你服務端的私鑰,在任何場景都不該該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是能夠自我簽發jwt了。
JWT 的幾個特色
(1)JWT 默認是不加密,但也是能夠加密的。生成原始 Token 之後,能夠用密鑰再加密一次。
(2)JWT 不加密的狀況下,不能將敏感數據(如密碼)寫入 JWT,除非對payload進行加密。保護好secret私鑰,該私鑰很是重要。
(3)JWT 不只能夠用於認證,也能夠用於交換信息。有效使用 JWT,能夠下降服務器查詢數據庫的次數。
(4)JWT 的最大缺點是,因爲服務器不保存 session 狀態,所以沒法在使用過程當中廢止某個 token,或者更改 token 的權限。也就是說,一旦 JWT 簽發了,在到期以前就會始終有效,除非服務器部署額外的邏輯。
(5)JWT 自己包含了認證信息,一旦泄露,任何人均可以得到該令牌的全部權限。爲了減小盜用,JWT 的有效期應該設置得比較短。對於一些比較重要的權限,使用時應該再次對用戶進行認證。
(6)爲了減小盜用,JWT 不該該使用 HTTP 協議明碼傳輸,要使用 HTTPS 協議傳輸。
JWT被確實存在被竊取的問題,可是若是能獲得別人的token,其實也就至關於能竊取別人的密碼,這其實已經不是JWT安全性的問題。網絡是存在多種不安全性的,對於傳統的session登陸的方式,若是別人能竊取登陸後的sessionID,也就能模擬登陸狀態,這和JWT是相似的。爲了安全,https加密很是有必要,對於JWT有效時間最好設置短一點。
① JWT 安全嗎?
Base64編碼方式是可逆的,也就是透過編碼後發放的Token內容是能夠被解析的。通常而言,不建議在有效載荷內放敏感信息,好比使用者的密碼。
② JWT Payload 內容能夠被僞造嗎?
JWT其中的一個組成內容爲Signature,能夠防止經過Base64可逆方法回推有效載荷內容並將其修改。由於Signature是經由Header跟Payload一塊兒Base64組成的。
③ 若是個人 Cookie 被竊取了,那不就表示第三方能夠作 CSRF 攻擊?
是的,Cookie丟失,就表示身份就能夠被僞造。故官方建議的使用方式是存放在LocalStorage中,並放在請求頭中發送。
④ 空間及長度問題?
JWT Token一般長度不會過小,特別是Stateless JWT Token,把全部的數據都編在Token裏,很快的就會超過Cookie的大小(4K)或者是URL長度限制。
⑤ Token失效問題?
無狀態JWT令牌(Stateless JWT Token)發放出去以後,不能經過服務器端讓令牌失效,必須等到過時時間過纔會失去效用。
假設在這之間Token被攔截,或者有權限管理身份的差別形成受權Scope修改,都不能阻止發出去的Token失效並要求使用者從新請求新的Token。
jwt使用流程
通常是在請求頭裏加入Authorization,並加上Bearer標註:
// Authorization: Bearer <token> getToken('api/user/1', { headers: { 'Authorization': 'Bearer ' + token } })
io.jsonwebtoken是最經常使用的工具包。
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
application.properties
jwt.secret=JO6HN3NGIU25G2FIG8V7VD6CK9B6T2Z5 jwt.expire=60000
JwtToken類
@Configuration public class JwtToken { private static Logger logger = LoggerFactory.getLogger(JwtToken.class); /** 祕鑰 */ @Value("${jwt.secret}") private String secret; /** 過時時間(秒) */ @Value("${jwt.expire}") private long expire; /** * 生成jwt token */ public String generateToken(Long userId) { Date nowDate = new Date(); Date expireDate = new Date(nowDate.getTime() + expire * 1000); return Jwts.builder() .setHeaderParam("typ", "JWT") .setSubject(userId + "") .setIssuedAt(nowDate) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public Claims getClaimByToken(String token) { if (StringUtils.isEmpty(token)) { return null; } String[] header = token.split("Bearer"); token = header[1]; try { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); }catch (Exception e){ logger.debug("validate is token error ", e); return null; } } /** * token是否過時 * @return true:過時 */ public static boolean isTokenExpired(Date expiration) { return expiration.before(new Date()); } // Getter && Setter } JwtController @RestController public class JwtController { @Autowired private JwtToken jwtToken; @PostMapping("/login") public String login(User user) { // 1. 驗證用戶名和密碼 // 2. 驗證成功生成token Long userId = 666L; String token = jwtToken.generateToken(userId); return token; } @GetMapping("/getUserInfo") public String getUserInfo(@RequestHeader("Authorization") String authHeader) throws AuthenticationException { // 黑名單token List<String> blacklistToken = Arrays.asList("禁止訪問的token"); Claims claims = jwtToken.getClaimByToken(authHeader); if (claims == null || JwtToken.isTokenExpired(claims.getExpiration()) || blacklistToken.contains(authHeader)) { throw new AuthenticationException("token 不可用"); } String userId = claims.getSubject(); // 根據用戶id獲取接口數據返回接口 return userId; } }
@Configuration public class JwtToken { private static Logger logger = LoggerFactory.getLogger(JwtToken.class); /** 祕鑰 */ @Value("${jwt.secret}") private String secret; /** 過時時間(秒) */ @Value("${jwt.expire}") private long expire; /** * 生成jwt token */ public String generateToken(Long userId) { Date nowDate = new Date(); Date expireDate = new Date(nowDate.getTime() + expire * 1000); return Jwts.builder() .setHeaderParam("typ", "JWT") .setSubject(userId + "") .setIssuedAt(nowDate) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public Claims getClaimByToken(String token) { if (StringUtils.isEmpty(token)) { return null; } String[] header = token.split("Bearer"); token = header[1]; try { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); }catch (Exception e){ logger.debug("validate is token error ", e); return null; } } /** * token是否過時 * @return true:過時 */ public static boolean isTokenExpired(Date expiration) { return expiration.before(new Date()); } // Getter && Setter } JwtController @RestController public class JwtController { @Autowired private JwtToken jwtToken; @PostMapping("/login") public String login(User user) { // 1. 驗證用戶名和密碼 // 2. 驗證成功生成token Long userId = 666L; String token = jwtToken.generateToken(userId); return token; } @GetMapping("/getUserInfo") public String getUserInfo(@RequestHeader("Authorization") String authHeader) throws AuthenticationException { // 黑名單token List<String> blacklistToken = Arrays.asList("禁止訪問的token"); Claims claims = jwtToken.getClaimByToken(authHeader); if (claims == null || JwtToken.isTokenExpired(claims.getExpiration()) || blacklistToken.contains(authHeader)) { throw new AuthenticationException("token 不可用"); } String userId = claims.getSubject(); // 根據用戶id獲取接口數據返回接口 return userId; } }