在了解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; } }