1 项目概述
1.1 背景和目标
1.1.1 背景
1.1.2 目标
对平台所有接口进行访问控制
1.2 名词解释
认证 (authentication): 你是谁 (who you are), 身份验证
鉴权 (authorization): 你可以做什么 (what can you do), 权限验证
RBAC (Role-Based policies Access Control) 基于角色的访问控制
JWT (Json Web Token), 有 JWS 和 JWE 两种实现方式
JWS (JSON Web Signature), 由三个部分组成,"头部 (header). 载荷 (payload). 签名 (signature)"
JWE (JSON Web Encryption), 由五个部分组成,"(JWE header).(JWE Encrypted Key).(JWE initialization vector).(JWE Ciphertext).(JWE Authentication Tag)"
2 功能需求
2.1 功能 List
2.1.1 认证
后端的所有接口都可以进行认证
2.1.2 鉴权
用户访问后端接口时进行鉴权
2.2 策略详述
2.2.1 用户场景
//todo
3 设计思路及折衷
3.1 身份验证
3.1.1 认证方式
五种常用的 Web 安全认证方式
互联网概念的 token
认证,大抵是在RESTful
流行后提出的,在开始token
认证之前,我们先梳理下常见的互联网认证机制。
HTTP Basic Auth
HTTP Basic Auth
常见的有两种:
一种是最常见的,即我们在登陆一些web
页面时,会让我们输入用户名密码;
另一种是将用户名密码信息通过base64
这类算法变成一条字符串在http
请求头中增加auth
字段,再传输给服务器。
这个属于最原始的认证,缺点比较明显,存在http
头中的密码信息容易被抓包获取,用户名密码后端服务器需要在查询数据库里的信息,进行信息比对,这也增加了服务器的负担。
优点
基本认证的一个优点是基本上所有流行的网页浏览器都支持基本认证。
缺点
由于用户名和密码都是 Base64 编码的,而 Base64 编码是可逆的,所以用户名和密码可以认为是明文。所以只有在客户端和服务器主机之间的连接是安全可信的前提下才可以使用。
session+cookie 模式
假设目前我们有一个查询类的web
站点,不可以每次查询都要登陆一次,为解决一次登录,可以在固定一段时间内登陆查询,就出现了session+cookie
模式。 该模式是第一次拿不出来时使用HTTP Basic Auth
,认证成功后,为避免每次都到数据库里校验用户名密码信息,就在主机上存储一份登陆的信息,cookie
里同时保存expire time
。
该模式优缺点都比较明显:session
信息需要额外的数据库存储,例如一般需要增加redis
、memached
等应用。在多机负载时,需要考虑session
共享; 但好处也是明显的,session
信息统一,可以在服务端统一挖掘认证的过期时间或个别用户的过期时间。
简单 token 认证
token
认证最常用的应用场景就是查询接口的调用RESTful API
,查询接口的信息在没有安全需求时,大家都可以通过get
或post
方法取得所需信息。 但在有安全需要时一般需要认证后才能获取所需的信息,这时候可以通过先HTTP Basic Auth
认证,HTTP Basic Auth
认证完成后,服务端返回给客户端一个类似于UUID
的唯一标识,我们称之为token
。 该token
可以在URL
或head
头里加入,如以下的常见的URL
模式:http://api/361way.com/getinfo?token=xxxxx
或http://api.361way.com/getinfo?t=xxxxx
,后面再加上相应的查询信息,就可以获取到相应的数据。
token
的好处是服务端不需要存储相应信息,但被不怀好意的人从早间获取到该信息时,也容易被利用,非法获取数据。
由于这个简单的token
返回串里未返回相应的过期时间信息,如果想增强安全性,一般可以在服务端生成时配合时间戳使用,服务端在接收到client
发来带token
的信息时, 先检测反解token
获取时间戳信息,如果该时间戳在超过某个时间点时凡认为过期,需要重新获取。
OAuth 认证
OAuth
(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web
服务上存储的私密的资源(如照片、视频、联系人列表),而无需将用户名和密码提供给第三方应用。
OAuth
允许用户提供一个令牌,而不是用户名和密码来访问他们存在在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统 (例如,视频编辑网站)在特定的时段(例如,接下来的 2 小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。 这样,OAuth
让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
这个理解起来比较绕口,举个例子,如很多网站上面有qq
或微信登陆接口,qq
或微信提供的就是OAuth
认证。 OAuth
虽然名字很洋气,其本质还是token
谁,仔细研究下上面的话,是不是原理上和上面提到的简单token
认证类似,只不过其加了几个callback
函数而已。
这里以duoban
豆瓣网调用QQ
的OAuth
接口为例,其调用方式如下:
# 发起认证
http://www.douban.com/leadToAuthorize
# 重定向到 QQ 认证,并指定回调
http://www.qq.com/authorize?callback=www.douban.com/callback
# 返回授权码,并 callback 到 douban
http://www.douban.com/callback
优点
快速开发
实施代码量小
维护工作减少
如果设计的 API 要被不同的 App 使用,并且每个 App 使用的方式也不一样,使用 OAuth2 是个不错的选择。
缺点
OAuth2 是一个安全框架,描述了在各种不同场景下,多个应用之间的授权问题。有海量的资料需要学习,要完全理解需要花费大量时间。
OAuth2 不是一个严格的标准协议,因此在实施过程中更容易出错。
JWT 认证
JSON WEB Token(JWT)
是一个非常轻巧的规范。这个规范允许我们使用JWT
在用户和服务器之间传递安全可靠的信息。
一个JWT
实际上就是一个字符串,它由三部分组成,头部 (Header
)、载荷(Payload
)与签名(Signature
)。
JWT 字符串示例
jwt = base64(头部).base64(载荷).hash256(base64(头部).base(载荷). 密钥)
base64 转换过程:
+------------------------------
| 原文本数据
+------------------------------
| ^ "二进制格式".decode("utf-8")
| |
| |
V "文本".encode("utf-8")
+------------------------------
| 二进制格式
+------------------------------
| ^ base64.b64decode("base64格式")
| |
| |
V base64.b64encode("二进制格式")
+------------------------------
| base64 格式
+------------------------------
这里只简单说下理论。
Payload
里存放的存储签发者、签发时间、过期时间、一些数据信息的JSON
格式的内容。
载荷 Payload(可以被前端直接解析到的)
{
"iss": "Online JWT Builder",
"iat": 1416797419,
"exp": 1448333419,
"aud": "www.example.com",
"sub": "jrocket@example.com",
"GivenName": "Johnny",
"Surname": "Rocket",
"Email": "jrocket@example.com",
"Role": ["Manager","Project Administrator"]
}
这里面的前五个字段都是由 JWT 的标准所定义的。
exp(expires): 什么时候过期,这里是一个 Unix 时间戳(到期时间)
Header
里指定存放的是指定使用协议JWT
、加密签名的类型,示例如下:
头部 Header
{
"typ": "JWT",
"alg": "HS256"
}
签名 Signature
最后签名就是将上面两者的信息通过 BASE64 编码,再加上密钥信息,三者之间通过点号连接,就组合成 JWT token
可以对比简单 token 和 JWT 的区别,两者本质上都是 token,只不过 JWT 在 token 里多传输了一些信息
JWT 签名算法中,一般有两个选择,一个采用 HS256, 另外一个就是采用 RS256。
签名实际上是一个加密的过程,生成一段标识(也是 JWT 的一部分)作为接收方验证信息是否被篡改的依据。
(1) RS256 (采用 SHA-256 的 RSA 签名) 是一种非对称算法,它使用公共 / 私钥对:标识提供方采用私钥生成签名,JWT 的使用方获取公钥以验证签名。由于公钥 (与私钥相比) 不需要保护,因此大多数标识提供方使其易于使用方获取和使用 (通常通过一个元数据 URL)。
(2) HS256 (带有 SHA-256 的 HMAC 是一种对称算法,双方之间仅共享一个 密钥。由于使用相同的密钥生成签名和验证签名,因此必须注意确保密钥不被泄密。
优点
快速开发
不需要 cookie
JSON 在移动端的广泛应用
不依赖于社交登录
相对简单的概念理解
缺点
Token 有长度限制
Token 不能撤销
需要 token 有失效时间限制 (exp)
3.1.2 折衷
基于 session 和基于 jwt 的方式的主要区别就是用户的状态保存的位置,session 是保存在服务端的,而 jwt 是保存在客户端的。
jwt 的优点
(1) 可扩展性好
应用程序分布式部署的情况下,session 需要做多机数据共享,通常可以存在数据库或者 redis 里面。而 jwt 不需要。
(2) 无状态
jwt 不在服务端存储任何状态。RESTful API 的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外 jwt 的载荷中可以存储一些常用信息,用于交换信息,有效地使用 JWT,可以降低服务器查询数据库的次数。
jwt 的缺点
(1) 安全性
由于 jwt 的 payload 是使用 base64 编码的,并没有加密,因此 jwt 中不能存储敏感数据。而 session 的信息是存在服务端的,相对来说更安全。
(2) 性能
jwt 太长。由于是无状态使用 JWT,所有的数据都被放到 JWT 里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致 jwt 非常长,cookie 的限制大小一般是 4k,cookie 很可能放不下,所以 jwt 一般放在 local storage 里面。并且用户在系统中的每一次 http 请求都会把 jwt 携带在 Header 里面,http 请求的 Header 可能比 Body 还要大。而 sessionId 只是很短的一个字符串,因此使用 jwt 的 http 请求比使用 session 的开销大得多。
(3) 一次性
无状态是 jwt 的特点,但也导致了这个问题,jwt 是一次性的。想修改里面的内容,就必须签发一个新的 jwt。
1> 无法废弃
通过上面 jwt 的验证机制可以看出来,一旦签发一个 jwt,在到期之前就会始终有效,无法中途废弃。例如你在 payload 中存储了一些信息,当信息需要更新时,则重新签发一个 jwt,但是由于旧的 jwt 还没过期,拿着这个旧的 jwt 依旧可以登录,那登录后服务端从 jwt 中拿到的信息就是过时的。为了解决这个问题,我们就需要在服务端部署额外的逻辑,例如设置一个黑名单,一旦签发了新的 jwt,那么旧的就加入黑名单(比如存到 redis 里面),避免被再次使用。
2> 续签
如果你使用 jwt 做会话管理,传统的 cookie 续签方案一般都是框架自带的,session 有效期 30 分钟,30 分钟内如果有访问,有效期被刷新至 30 分钟。一样的道理,要改变 jwt 的有效时间,就要签发新的 jwt。最简单的一种方式是每次请求刷新 jwt,即每个 http 请求都返回一个新的 jwt。这个方法不仅暴力不优雅,而且每次请求都要做 jwt 的加密解密,会带来性能问题。另一种方法是在 redis 中单独为每个 jwt 设置过期时间,每次访问时刷新 jwt 的过期时间。
可以看出想要破解 jwt 一次性的特性,就需要在服务端存储 jwt 的状态。但是引入 redis 之后,就把无状态的 jwt 硬生生变成了有状态了,违背了 jwt 的初衷。而且这个方案和 session 都差不多了。
3.2 访问控制
基于角色的访问控制 (Role-Based policies Access Control)
用户 - 角色 关联 (User-Role-Map):用户所分配的角色(一个用户,可以分配多个角色), 用户分组
角色 - 资源 关联 (Role-Resource-Map):某个角色下的用户,可以操作哪些资源,操作权限
4 系统设计
单点登录及认证例子
+--------------------------------------------------------------------------+
| butterfly-nginx |
+--------------------------------------------------------------------------+
| | |
V V V
+-------------+ +---------------------+ +----------+
|~* /static/ | |= /auth/verification | |/ |
|= /index.html| |= /butterfly_401 | | |
|= / | |= /auth/ssologin | | |
+-------------+ +---------------------+ +----------+
| | |
V V V
+----------+ +------------+ +--------------+ +----------+ +-----------+
|web browse| |butterfly-fe| |butterfly-auth| |cas-server| |app-backend|
+----------+ +------------+ +--------------+ +----------+ +-----------+
| | | | |
+-------route------->|/ | | |
|<-------page--------+/index.html | | |
| | | | |
====================================================================not have token
| | | | |
+--V----------------request api-------------------------------------------------->|
| +-sub request-header not have token->|(/auth/verification) | |
|<-code=401,targetURL=../auth/ssologin--+ | |
| | | | |
+--window.location.herf=directurl------>|(/auth/ssologin) | | 前端 [AngularJWT]
|<----code=302,Location=cas-server------+ | |
| | | | |
+-----302 http://cas-server/login login page --------------->|(/login) |
|<-------------code=302,set Cookie TGT=xxx -------------------+ |
| | | | |
+-----302 /auth/ssologin?ticket=xxx --->|(/auth/ssologin) | |
| | +-------check st----->|(/session/validate)|
| | |<-------st vaild-----+ |
|<--code=302 set Cookie butterfly_token-+ | |
| | | | |
+--302 / ----------->| | | |
|<-------page--------+/index.html | | |
| | | | |
======================================================================== have token
| | | | |
+---V----------------request api------------------------------------------------->|
| +-sub request-header have token---->|/auth/verification | |
|<-------------------response-----------------------------------------------------+
4.1 模块说明
4.1.1 butterfly-auth
butterfly-auth 即为 cas-client(客户端),含 /auth/verification 认证接口及 /auth/ssologin 单点登录接口
/auth/verification 验证 jwt token 是否有效
验证 token
curl -H "Authorization:Bearer: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJidXR0ZXJmbHkiLCJpYXQiOjE1NzcwODQwOTQsInVzZXJuYW1lIjoibWVldGJpbGwiLCJqdGkiOiIzZjIxMmFiOS1iZDJkLTRkYWUtYWJiNy0xMjA1YWVmOGY1NGEiLCJleHAiOjE1NzcxMTI4OTR9.prj_yZIce9pwPC5qj7Lry8vFaFX3pyRbBne-d_GyQus" -v http://IP:PORT/xxxx使用 nginx auth_request
Nginx auth_request 请求流程
NGINX ---- auth request ----> /auth/verification
| |
| <--- 200 <------ SUCCESS FAILED -----> 401 -----> /butterfly_401
|
----> underlying request ----> BACKEND SERVER
4.2 访问控制
//todo
5 nginx 配置文件
5.1 butterfly-fe 静态文件
首页
# 首页处理
location = / {
root ../templates;
index index.html;
}
location = /index.html {
root ../templates;
}
location = /favicon.ico {
root ../templates;
}
其他静态文件
#静态文件,nginx 自己处理,不去 backend 请求 butterfly
location ~* /static/ {
root ..;
add_header Cache-Control no-store;
}
5.2 butterfly-auth 单点登录
前后端分离后,前端请求后端的请求为 ajax 请求,而我们知道,ajax 是不允许跨域访问的,更不会让浏览器自动重定向
所以说,对于后台可能会返回 302 跳转的请求,都需要修改成在前端进行跳转
# 认证接口
location = /auth/verification {
internal;
proxy_pass http://127.0.0.1:8001;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
# 真实的请求路径
proxy_set_header X-Original-URI $request_uri;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 单点登录客户端接口
location /auth/ssologin {
proxy_pass http://127.0.0.1:8011;
# 这个 X-Target 是给认证完以后重定向的
# proxy_set_header X-Target $request_uri;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 返回认证失败错误
location = /butterfly_401 {
internal;
default_type application/json;
if ($butterfly_location) {
return 401 '{"success":false,"message":"You are not authorized","data":{"Target_url":"$butterfly_location"}}';
}
}
5.3 butterfly-app 后端 API
#对 / 所有做负载均衡 + 反向代理
location / {
auth_request /auth/verification;
auth_request_set $butterfly_location $upstream_http_location;
error_page 401 = /butterfly_401;
proxy_redirect off;
# 后端的 Web 服务器可以通过 X-Forwarded-For 获取用户真实 IP
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_pass http://backend;
}
6 传送门
7 他山之石
Last updated