乐趣区

关于java:使用jwt来保护你的接口服务

以前写过一篇对于接口服务标准的文章,原文在此,外面对于安全性问题重点讲述了通过 appidappkeytimestampnonce 以及 sign 来获取 token,应用token 来保障接口服务的平安。明天咱们来讲述一种更加便捷的形式,应用 jwt 来生成 token。

一、JWT 是什么

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

JWT是一个很长的字符串,蕴含了 HeaderPlayloadSignature三局部内容,两头用 . 进行分隔。

Headers

Headers局部形容的是 JWT 的根本信息,个别会蕴含签名算法和令牌类型,数据如下:

{
    "alg": "RS256",
    "typ": "JWT"
}

Playload

Playload就是寄存无效信息的中央,JWT规定了以下 7 个字段,倡议但不强制应用:

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 的第三局部。

二、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();}

三、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

退出移动版