Web应用中使用JWT保护RESTful API

常用的认证机制

  • Cookie/Session Auth:在服务端创建一个Session对象,同时在客户端的浏览器端创建一个Cookie对象;通过客户端发来的请求中携带的Cookie对象与服务器端的session对象进行匹配,来实现认证
  • OAuth (Open Authorization, 开放授权)
  • Token Auth:基于JWT的Token认证机制实现

​ JWT(JSON Web Token)是一个规范,一般被用来在客户端和服务端之间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如向服务端发送RESTful请求,更改用户密码,登出操作等。JWT也更适用于移动互联网时代: 当客户端是一个移动平台(iOS, Android)时,Cookie是不被支持的(需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多

JWT相比于Session的优点:

  • 不占用服务器内存开销:session需要保存在服务器,因此会占用服务器内存开销(尽管JWT会让服务器有一些计算压力,比如token的签名和验证)
  • 可扩展性强: 比如有3台机器(A、B、C)组成服务器集群,若session存在机器A上,session只能保存在其中一台服务器,此时你便不能访问机器B、C,因为B、C上没有存放该Session,而使用token就能够验证用户请求合法性,并且我再加几台机器也没事,所以可拓展性好就是这个意思。
  • 前后端分离,支持跨域访问

JWT的缺点:

  • token中不能保存私密信息:Head和Payload使用base64进行编码
  • 无法作废已颁布的token:由于所有的认证信息都在JWT中,如过某个JWT被盗取了,是没有办法在服务端将其作废(自己放出去的token,含着泪也要认证到底)

JWT构成

​ JWT由三段组成,分别是header(头部)、payload(负载)和signature(签名)。

Header 头部

​ 头部描述该JWT的最基本信息,包含两个字段:一个是签名算法,比如HS256(HMAC with SHA-256), RS256(RSA signature with SHA-256);另一个是token类型(基于RFC 7519实现的token机制并不只JWT一种)

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

注:

  • 头部中的信息都用缩写的三个字符,这是由于JWT的目标就是尽可能小巧
  • 如果我们采用的是JWT的话,typ字段可以忽略不写

Payload 负载

​ 负载放三种字段:系统保留的声明(Registered Claim Names),公共声明(Public Claim Names)和私有声明(Private Claim Names)。

  • 系统保留的声明:这类声明不是必须的,但是建议使用,包括iss (签发者), exp (过期时间), sub (主题), aud (目标受众)等
  • 私有声明:按照具体的业务需要而定
{
    "iat": 1441593502,          // issued At签发时间
    "exp": 1441594722,          // Expiration Time,过期时间
    "iss": "John Wu JWT",        // Issuer,该JWT的签发者
    "aud": "www.example.com",    // Audience,该JWT的接收者
    "sub": "[email protected]" // Subject,
}

​ 注:使用token会暴露信息,因为头部和负载使用Base64进行编码,任何人都可以通过base64解码来获取的信息,因此payload中不应该出现私密信息,比如密码。

Signature 签名

​ 将header和payload进行base64编码,然后通过header.payload形式组成在一起,就形成如下的内容:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50dHlwZSI6MSwiZXhwIjoxNTAyMjgwNzY4LCJpYXQiOjE1MDIxOTQzNjgsInN1YiI6IjU5MzdlM2U1YmM0ZmQ2NmYzOTljNGMyMCJ9

​ 签名的过程就是使用base64编码后的 header 和 payload 以及后端中保存的一个密钥,使用 header 中指定的签名算法进行签名。服务器收到JWT后,用同样的算法和密钥对header和payload进验证。

​ 签名过后,将base64编码过后的header和payload以及signature拼接在一起,就组成了完整的JWT,如下:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50dHlwZSI6MSwiZXhwIjoxNTAyMjgwNzY4LCJpYXQiOjE1MDIxOTQzNjgsInN1YiI6IjU5MzdlM2U1YmM0ZmQ2NmYzOTljNGMyMCJ9.wEY7c2uKMtk0Fyu2t5RKhEqViJcRV2cu22VyxGy9aRGC3IrFmfqFSGnhvYA5BqflZku0Fug4UJciVBZDB1Q2Ot-TP7-gC-Mve0cWAgHazWNkWXt5taJqtOrxRvHJbQuXCnHKn-syM9Iq5Jlja0GBu6WQ5PteBW7Ztv_OhDly2_4

​ 签名的目的:保证 JWT 没有被篡改过。使用加密算法能够保证不同的输入产生的输出总是不一样的,如果有人恶意篡改了头部或负载中的信息,通过相同的密钥进行加密得到的签名肯定不再相同,此时即可证明token是无效的。

JWT例子

​ JWT的官网首页给出了非常适合入门的例子,分别提供了HS256和RS256算法的测试。RS256 (RSA Signature with SHA-256)是一种非对称加密算法,HS256 (HMAC with SHA-256)是一种对称加密算法。

http://p50uamua9.bkt.clouddn.com/JWT%20RESTful%20API/2018-02-06_162153.png

http://p50uamua9.bkt.clouddn.com/JWT%20RESTful%20API/2018-02-06_162138.png


JWT的认证过程

  1. 用户在登录页面,使用POST请求发送用户名、密码给后端进行登录
{
    username: "[email protected]",
    password: "123qweASD"
}
  1. 服务器对用户进行鉴权(鉴权过程可以参见:Web安全之如何保护密码),验证通过后生成相应的header和payload,然后使用密钥生成token
  1. 将JWT返回给前端

    返回的方式有两种,可以根据具体的业务而定。一种是使用Cookie保存JWT:

Set-Cookie: jwt=xxx.xxx.xxx;

​ 另一种是将JWT保存在Session Storage中,这种情况下可以将JWT直接在body中返回,然后前端通过sessionStorage对象来保存和取出token:

{
 "success":true,
 "message":"",
 "token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50dHlwZSI6MSwiZXhwIjoxNDk4NTMyNzU5LCJpYXQiOjE0OTg0NDYzNTksInN1YiI6IjU5NDlmNTVhYmFkYThhMjQyMDc2YjgyZCJ9.VhCmcdtBa2IxFrFDH8Nclx8yByeFxQP3LPjOIux1-eZma-vQsHjRXjF_0l3MySh9JPB1cSNvlnBhgOSfjTspuu28vVu6KGpZ3iqMhg-AIRjcoXCbuXkBqkNExmasF7DiMMzcwakslv_hcj-tg16LmnlIY8VAwseuc2AZ4_fHGpc",
 "firstName":"Chris",
 "lastName":"Jiang",
 "title":"SW",
 "companyID":"5775d1702853773840f2cccf",
}
  1. 在jwt有效期间内,每次发送请求给服务端都会携带token。根据第三步中保存token的方式不同,发送token的方式也有两种,第一种在cookie中携带token,另一种是在http的请求头中携带token信息:
headers: {
    'x-access-token': sessionStorage.token,
},
  1. 服务端获取到token后,对jwt进行一系列的有效性检查,检查项有:签名是否正确、token是否过期等
// jjwt的parseClaimsJws()方法可以完成token检查操作
Claims claims = Jwts.parser()
            .setSigningKey(key.getBytes())
            .parseClaimsJws(token)
            .getBody();
  1. 从token中获取需要的信息,比如用户的id
TokenDecodeEntity tokenDecodeEntity = new TokenDecodeEntity();
        tokenDecodeEntity.setIat(claims.getIssuedAt());
        tokenDecodeEntity.setExp(claims.getExpiration());
        tokenDecodeEntity.setAccountID(claims.getAudience());
        tokenDecodeEntity.setAccountType((Integer) claims.get("accountType"));
  1. 根据从token中获取的用户信息,进而在数据库中获取具体的业务信息
AccountInfoEntity accountInfoEntity = accountInfoRepository.findByAccountID(
  tokenDecodeEntity.getAccountID());
  1. 最后后端根据应用的具体请求做出响应

JWT认证的安全问题

确保验证过程的安全性

​ 由于在登录验证过程中需要用户输入帐户和密码,在这一过程中,用户名、密码等敏感信息需要在网络中传输。因此,所有的请求应采用HTTPS,通过SSL加密传输,以确保通道的安全性。

防范重放攻击(Replay Attacks)

​ 如果客户的token被盗取,此时黑客使用盗取的token模拟正常的请求,而服务器对比是毫无办法的。服务端唯一能做的是在限制token的过期时间。避免一个token被无限期的使用。

if (timeToAlive > 0) {
     iat = new Date(System.currentTimeMillis());
    exp = new Date(System.currentTimeMillis() + timeToAlive);
} else {
     throw new InputIsInvalidException("Token's alive time should be positive");
}

JwtBuilder jwtBuilder = Jwts.builder()
    .claim("iat", iat)
     .claim("aud", tokenPayloadEntity.getAccountID())
     .claim("accountType", tokenPayloadEntity.getAccountType());
jwtBuilder.setExpiration(exp);
jwtBuilder.signWith(SignatureAlgorithm.HS256, key.getBytes());

return jwtBuilder.compact();

Token在前端如何保存

Token在前端有两种保存方式:Cookies和HTML5的Web Storage。具体可以参考:Where to Store your JWTs – Cookies vs HTML5 Web Storage.

HTML5 Web Storage(localStorage或sessionStorage)

Cookie Storage


TODO

使用JWT实现SSO

跟Spring Security的结合,实现用户的权限验证


参考

JWT的认证过程参考:八幅漫画理解使用JSON Web Token设计单点登录系统

重拾后端之Spring Boot(四):使用JWT和Spring Security保护REST API

详解SpringCloud服务认证(JWT)

基于Token的WEB后台认证机制

Where to Store your JWTs – Cookies vs HTML5 Web Storage

你可能感兴趣的:(Web应用中使用JWT保护RESTful API)