使用jwt来保护你的接口服务

之前写过一篇关于接口服务规范的文章,原文在此,里面关于安全性问题重点讲述了经过appidappkeytimestampnonce以及sign来获取token,使用token来保障接口服务的安全。今天咱们来说述一种更加便捷的方式,使用jwt来生成token。java

1、JWT是什么

JSON Web TokenJWT) 定义了一种紧凑且自包含的方式,用于在各方之间做为 JSON 对象安全地传输信息。该信息能够被验证和信任,由于它是通过数字签名的。JWT能够设置有效期。web

JWT是一个很长的字符串,包含了HeaderPlayloadSignature三部份内容,中间用.进行分隔。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的前面两部分进行加密后的字符串,将HeadersPlayload进行base64编码后使用Headers中规定的加密算法和密钥进行加密,获得JWT的第三部分。ui

2、JWT生成和解析token

在应用服务中引入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();
}

3、token续签问题

上面讲述了关于JWT验证的过程,如今咱们考虑这样一个问题,客户端携带token访问下单接口,token验签经过,客户端下单成功,返回下单结果,而后客户端带着token调用支付接口进行支付,验签的时候发现token失效了,这时候应该怎么办?只能告诉用户token失效,而后让用户从新登陆获取token?这种体验是很是很差的,oauth2在这方面作的比较好,除了签发token,还会签发refresh_token,当token过时后,会去调用refresh_token从新获取token,若是refresh_token也过时了,那么再提示用户去登陆。如今咱们模拟oauth2的实现方式来完成JWTrefresh_token

思路大概就是用户登陆成功后,签发token的同时,生成一个加密串做为refresh_tokenrefresh_token存放在redis中,设置合理的过时时间(通常会将refresh_token的过时时间设置的比较久一点)。而后将tokenrefresh_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做为一个轻量级的鉴权框架,使用起来很是方便,可是也会存在一些问题,

  • JWTPlayload部分只是通过base64编码,这样咱们的信息其实就彻底暴露了,通常不要将敏感信息存放在JWT中。

  • JWT生成的token比较长,每次在请求头中携带token,致使请求偷会比较大,有必定的性能问题。

  • JWT生成后,服务端没法废弃,只能等待JWT主动过时。

下面这段是我网上看到的一段关于JWT比较适用的场景:

  • 有效期短

  • 只但愿被使用一次

好比,用户注册后发一封邮件让其激活帐户,一般邮件中须要有一个连接,这个连接须要具有如下的特性:可以标识用户,该连接具备时效性(一般只容许几小时以内激活),不能被篡改以激活其余可能的帐户,一次性的。这种场景就适合使用JWT

相关文章
相关标签/搜索