常用的认证机制
- 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的认证过程
- 用户在登录页面,使用POST请求发送用户名、密码给后端进行登录
{
username: "[email protected]",
password: "123qweASD"
}
- 服务器对用户进行鉴权(鉴权过程可以参见:Web安全之如何保护密码),验证通过后生成相应的header和payload,然后使用密钥生成token
-
将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",
}
- 在jwt有效期间内,每次发送请求给服务端都会携带token。根据第三步中保存token的方式不同,发送token的方式也有两种,第一种在cookie中携带token,另一种是在http的请求头中携带token信息:
headers: {
'x-access-token': sessionStorage.token,
},
- 服务端获取到token后,对jwt进行一系列的有效性检查,检查项有:签名是否正确、token是否过期等
// jjwt的parseClaimsJws()方法可以完成token检查操作
Claims claims = Jwts.parser()
.setSigningKey(key.getBytes())
.parseClaimsJws(token)
.getBody();
- 从token中获取需要的信息,比如用户的id
TokenDecodeEntity tokenDecodeEntity = new TokenDecodeEntity();
tokenDecodeEntity.setIat(claims.getIssuedAt());
tokenDecodeEntity.setExp(claims.getExpiration());
tokenDecodeEntity.setAccountID(claims.getAudience());
tokenDecodeEntity.setAccountType((Integer) claims.get("accountType"));
- 根据从token中获取的用户信息,进而在数据库中获取具体的业务信息
AccountInfoEntity accountInfoEntity = accountInfoRepository.findByAccountID(
tokenDecodeEntity.getAccountID());
- 最后后端根据应用的具体请求做出响应
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