之前写过一篇关于接口服务规范的文章,原文在此,里面关于安全性问题重点讲述了经过appid
,appkey
,timestamp
,nonce
以及sign
来获取token
,使用token
来保障接口服务的安全。今天咱们来说述一种更加便捷的方式,使用jwt
来生成token。java
JSON Web Token
(JWT
) 定义了一种紧凑且自包含的方式,用于在各方之间做为 JSON 对象安全地传输信息。该信息能够被验证和信任,由于它是通过数字签名的。JWT
能够设置有效期。web
JWT
是一个很长的字符串,包含了Header
,Playload
和Signature
三部份内容,中间用.
进行分隔。redis
Headers算法
Headers
部分描述的是JWT
的基本信息,通常会包含签名算法和令牌类型,数据以下:json
{ "alg": "RS256", "typ": "JWT" }
Playload安全
Playload
就是存放有效信息的地方,JWT
规定了如下7个字段,建议但不强制使用:app
iss: jwt签发者 sub: jwt所面向的用户 aud: 接收jwt的一方 exp: jwt的过时时间,这个过时时间必需要大于签发时间 nbf: 定义在什么时间以前,该jwt都是不可用的 iat: jwt的签发时间 jti: jwt的惟一身份标识,主要用来做为一次性token
除此以外,咱们还能够自定义内容框架
{ "name":"Java旅途", "age":18 }
Signature性能
Signature
是将JWT
的前面两部分进行加密后的字符串,将Headers
和Playload
进行base64
编码后使用Headers
中规定的加密算法和密钥进行加密,获得JWT
的第三部分。ui
在应用服务中引入JWT
的依赖
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
根据JWT
的定义生成一个使用RSA
算法加密的,有效期为30分钟
的token
public static String createToken(User user) throws Exception{ return Jwts.builder() .claim("name",user.getName()) .claim("age",user.getAge()) // rsa加密 .signWith(SignatureAlgorithm.RS256, RsaUtil.getPrivateKey(PRIVATE_KEY)) // 有效期30分钟 .setExpiration(DateTime.now().plusSeconds(30 * 60).toDate()) .compact(); }
登陆接口验证经过后,调用JWT
生成带有用户标识的token响应给用户,在接下来的请求中,头部携带token
进行验签,验签经过后,正常访问应用服务。
public static Claims parseToken(String token) throws Exception{ return Jwts .parser() .setSigningKey(RsaUtil.getPublicKey(PUBLIC_KEY)) .parseClaimsJws(token) .getBody(); }
上面讲述了关于JWT
验证的过程,如今咱们考虑这样一个问题,客户端携带token
访问下单接口,token
验签经过,客户端下单成功,返回下单结果,而后客户端带着token
调用支付接口进行支付,验签的时候发现token失效了,这时候应该怎么办?只能告诉用户token
失效,而后让用户从新登陆获取token
?这种体验是很是很差的,oauth2
在这方面作的比较好,除了签发token
,还会签发refresh_token
,当token
过时后,会去调用refresh_token
从新获取token
,若是refresh_token
也过时了,那么再提示用户去登陆。如今咱们模拟oauth2
的实现方式来完成JWT
的refresh_token
。
思路大概就是用户登陆成功后,签发token
的同时,生成一个加密串做为refresh_token
,refresh_token
存放在redis
中,设置合理的过时时间(通常会将refresh_token
的过时时间设置的比较久一点)。而后将token
和refresh_token
响应给客户端。伪代码以下:
@PostMapping("getToken") public ResultBean getToken(@RequestBody LoingUser user){ ResultBean resultBean = new ResultBean(); // 用户信息校验失败,响应错误 if(!user){ resultBean.fillCode(401,"帐户密码不正确"); return resultBean; } String token = null; String refresh_token = null; try { // jwt 生成的token token = JwtUtil.createToken(user); // 刷新token refresh_token = Md5Utils.hash(System.currentTimeMillis()+""); // refresh_token过时时间为24小时 redisUtils.set("refresh_token:"+refresh_token,token,30*24*60*60); } catch (Exception e) { e.printStackTrace(); } Map<String,Object> map = new HashMap<>(); map.put("access_token",token); map.put("refresh_token",refresh_token); map.put("expires_in",2*60*60); resultBean.fillInfo(map); return resultBean; }
客户端调用接口时,在请求头中携带token
,在拦截器中拦截请求,验证token
的有效性,若是验证token
失败,则去redis中判断是不是refresh_token
的请求,若是refresh_token
验证也失败,则给客户端响应鉴权异常,提示客户端从新登陆,伪代码以下:
HttpHeaders headers = request.getHeaders(); // 请求头中获取令牌 String token = headers.getFirst("Authorization"); // 判断请求头中是否有令牌 if (StringUtils.isEmpty(token)) { resultBean.fillCode(401,"鉴权失败,请携带有效token"); return resultBean; } if(!token.contains("Bearer")){ resultBean.fillCode(401,"鉴权失败,请携带有效token"); return resultBean; } token = token.replace("Bearer ",""); // 若是请求头中有令牌则解析令牌 try { Claims claims = TokenUtil.parseToken(token).getBody(); } catch (Exception e) { e.printStackTrace(); String refreshToken = redisUtils.get("refresh_token:" + token)+""; if(StringUtils.isBlank(refreshToken) || "null".equals(refreshToken)){ resultBean.fillCode(403,"refresh_token已过时,请从新获取token"); return resultbean; } }
refresh_token
来换取token
的伪代码以下:
@PostMapping("refreshToken") public Result refreshToken(String token){ ResultBean resultBean = new ResultBean(); String refreshToken = redisUtils.get(TokenConstants.REFRESHTOKEN + token)+""; String access_token = null; try { Claims claims = JwtUtil.parseToken(refreshToken); String username = claims.get("username")+""; String password = claims.get("password")+""; LoginUser loginUser = new LoginUser(); loginUser.setUsername(username); loginUser.setPassword(password); access_token = JwtUtil.createToken(loginUser); } catch (Exception e) { e.printStackTrace(); } Map<String,Object> map = new HashMap<>(); map.put("access_token",access_token); map.put("refresh_token",token); map.put("expires_in",30*60); resultBean.fillInfo(map); return resultBean; }
经过上面的分析,咱们简单的实现了token
的签发,验签以及续签问题,JWT
做为一个轻量级的鉴权框架,使用起来很是方便,可是也会存在一些问题,
JWT
的Playload
部分只是通过base64编码,这样咱们的信息其实就彻底暴露了,通常不要将敏感信息存放在JWT
中。
JWT
生成的token
比较长,每次在请求头中携带token
,致使请求偷会比较大,有必定的性能问题。
JWT
生成后,服务端没法废弃,只能等待JWT
主动过时。
下面这段是我网上看到的一段关于JWT
比较适用的场景:
有效期短
只但愿被使用一次
好比,用户注册后发一封邮件让其激活帐户,一般邮件中须要有一个连接,这个连接须要具有如下的特性:可以标识用户,该连接具备时效性(一般只容许几小时以内激活),不能被篡改以激活其余可能的帐户,一次性的。这种场景就适合使用JWT
。