无论用哪一种方式认证用户,均可能被中间人攻击窃取 SessionID 或 Token,从而发生 CSRF 攻击。解决方式就是全站 HTTPS。如今 Let’s Encrypt 已经支持免费的通配符 HTTPS 证书了。php
HTTP 协议是无状态的,要保存用户状态须要额外的机制。html
刚开始时,多数公司使用的技术栈是:单台云服务器上安装所需的全部软件,包括 Nginx 提供 Web 服务,MySQL 数据库,PHP-FPM 应用程序服务。这时候使用的用户认证协议使用最简单的 Session。客户端的每一个请求都会携带 Cookie,其中保存了 SessionID 字段,服务器能够经过这个 SessionID 字段访问到对应的 Session(例如 PHP 中的 $_SESSION
),从而识别出用户登陆状态。Session 中还能够添加一些经常使用的字段进来(好比用户名、手机号等),避免对数据库的频繁访问。前端
后来,随着用户量增大、并发增大,单台服务器搞不定了,因而搞了个水平扩展的服务器集群,经过 Nginx 或 LVS 实现负载均衡。这时发现个问题,用户登陆后 Session 是保存到集群中的某一台服务器上的。要使 Session 机制能够在分布式环境下继续工做,须要一些额外操做。并且对于如今的大前端(浏览器、APP、小程序)趋势来讲,Cookie 机制略显累赘。java
而这时,JWT 认证协议彻底知足需求。协议简单清晰,花一个下午就能够搞清楚。nginx
公司发展过程当中,产品线会慢慢增多,好比百度的贴吧、网盘、浏览器等。这时,须要一套单点登陆机制 SSO(Single sign-on),用户只要一次登陆,就可使用这一系列产品。SSO 描述了认证的问题。laravel
SSO 须要一个独立的认证中心 CAS(Central Authentication Service,中央认证服务),只有认证中心能提供登陆入口,接受用户的用户名密码等凭证,其余系统无登陆入口,只接受认证中心的间接受权。这里有个开源的 CAS:apereo CAS,其服务端用 Java 实现,客户端支持多种语言。其架构文档能够参考 这里。git
单体项目拆分红微服务后,能够更加灵活。一般全部的服务都在网关以后,全部请求都发送到网关,由网关统一转发。微服务的网关一般实现了 OAuth,成为认证受权中心,用于判断是否有足够权限。微服务之间能够经过 JWT 进行访问鉴权,避免身份认证。github
随着公司用户增多(假设跟微信同样,有几亿用户),合做企业也愈来愈多。若是每次都要在后台经过人工给合做伙伴配置帐号密码,分配权限管理,那太麻烦了。同时,一些企业有本身的平台,想要利用个人用户帐号体系实如今这些平台上的登陆(受权登陆)。对于用户的图片,一些图片打印公司也想在通过用户赞成后,直接访问到我服务器上的用户图片,优化体验。web
总之,就是只要用户赞成,他能够分享本身的全部资源(帐号、图片等)。这时,就须要 OAuth2 了。这是一个受权框架,描述了各类受权的问题。算法
例如,用户登陆论坛时,须要先用用户名和密码认证用户有没有权限登陆,若是密码正确则认证经过,登陆成功。用户登陆后,判断其角色并授予相应的权限,例如超级管理员能够删除全部人,版主能够删除其版块的帖子。
最传统的用户认证方式。用户首次访问应用服务器后创建会话,服务器可使用 Set-Cookie 这个 HTTP Header,将会话的 SessionID 写入在用户端保存的 Cookie 中(具体的名字能够自行设置,系统中统一便可)。下次用户再次向这个域名发请求时会携带全部 Cookie 信息,包括这个 SessionID。
Session 信息保存在服务器端,而用于惟一标识这个 Session 的 SessionID 则保存在对应客户端的 Cookie 中。SessionID 这个会话标识符本质上是一个随机字符串,每一个用户的 SessionID 都不同。
Session 中能够保存不少信息。例如设置一个 IsLogin 字段,用户经过帐号密码登陆后,将这个字段设置为 TRUE。这样,在 Session 的有效期内(好比 2 小时),即便用户关闭网页,再次打开后仍会保持登陆状态(除非用户清理了 Cookie,致使其访问服务器时没有携带 SessionID 字段)。对于其余的经常使用字段(如 userID、userName等)也能够添加到 Session 中,以减小数据库的访问压力,但注意不要太大,由于全部用户的会话信息都是保存在服务器的内存中的。
下面的示例,基于 PHP 语言,CodeIgniter 框架。同时,省略了无关的 HTTP Header,重点分析 Session 相关字段。
在第一次访问一个网站时,浏览器中没有对应 Cookie 信息,全部请求的 HTTP Header 中没有 Cookie 这个字段。若是应用服务器支持会话,能够在为这个用户建立 Session 后,经过在响应的 HTTP Header 中使用 Set-Cookie 字段将这个会话的 SessionID 保存到浏览器的 Cookie 中。能够看到我这里对应的 SessionID 的名字是 ci_session:
-----------------------------------------请求的 HTTP Header-----------------------------------------
GET http://tuan.local.cn/ HTTP/1.1
Host: tuan.local.cn
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
If-Modified-Since: Thu, 10 May 2018 06:20:36 GMT
...
-----------------------------------------响应的 HTTP Header-----------------------------------------
HTTP/1.1 200 OK
Date: Thu, 10 May 2018 06:21:13 GMT
Content-Type: text/html; charset=UTF-8
Set-Cookie: ci_session=hfvn0lq3ct62mppl86n2du535bc4stcf; expires=Thu, 10-May-2018 08:21:13 GMT; Max-Age=7200; path=/; HttpOnly
...
这里 Set-Cookie 中的各个字段解释以下,完整的中文版解释参考 这里:
Document.cookie
属性或 XMLHttpRequest 和 Request 这两个 API 访问,避免 XSS(cross-site scripting,跨站脚本攻击)。每次经过域名或 IP 地址访问时,浏览器都会检查是否有可用的 Cookie,若是有,则放到请求的 HTTP Header 中一同发送到服务器:
-----------------------------------------请求的 HTTP Header-------------------------------------------
GET http://tuan.local.cn/ HTTP/1.1
Host: tuan.local.cn
Cookie: ci_session=hfvn0lq3ct62mppl86n2du535bc4stcf
...
-----------------------------------------响应的 HTTP Header-------------------------------------------
HTTP/1.1 200 OK
Date: Thu, 10 May 2018 06:22:02 GMT
Content-Type: text/html; charset=UTF-8
...
登陆成功以后,登陆请求对应的响应会再次设置 Cookie 字段,从新设置 Cookie 字段的有效期。个人应用程序中设置 Session 为两个小时的有效期:
这里演示的是经过 AJAX 登陆,因此有 Origin 和 X-Requested-With 这两个由浏览器自动设置的字段:
-----------------------------------------请求的 HTTP Header-------------------------------------------
POST http://tuan.local.cn/index/login_password HTTP/1.1
Host: tuan.local.cn
Origin: http://tuan.local.cn
X-Requested-With: XMLHttpRequest
Content-Type: application/json
Referer: http://tuan.local.cn/
Cookie: ci_session=hfvn0lq3ct62mppl86n2du535bc4stcf
...
{"Mobile":"18866668888","Password":"888666"}
-----------------------------------------响应的 HTTP Header-------------------------------------------
HTTP/1.1 200 OK
Set-Cookie: ci_session=hfvn0lq3ct62mppl86n2du535bc4stcf; expires=Thu, 10-May-2018 08:22:33 GMT; Max-Age=7200; path=/; HttpOnly
...
跟正常访问没有区别,只是携带的 Cookie 中有 SessionID,且服务器端对应的 Session 中须要(好比 IsLogin=true,本身设置)标识已登陆状态:
-----------------------------------------请求的 HTTP Header-------------------------------------------
GET http://tuan.local.cn/ HTTP/1.1
Host: tuan.local.cn
Cookie: ci_session=hfvn0lq3ct62mppl86n2du535bc4stcf
...
-----------------------------------------响应的 HTTP Header-------------------------------------------
HTTP/1.1 200 OK
Date: Thu, 10 May 2018 06:22:34 GMT
...
Session 的主要问题有:
还有,就是目前大前端的发展,除了浏览器外,各类 APP、小程序层出不穷,而非浏览器下环境下避免使用 Cookie 可能会更简单。
Session 之因此这么麻烦,是由于须要在服务器端保存信息,那我把信息保存在客户端,不就能够避免这个麻烦了嘛。JWT 就是这么个思路,服务器端保存加密机制及密钥,对用户指定字段进行加密后的字符串保存在客户端,用户下次请求时携带加密前的字段和加密后的字符串,若是跟服务器加密结果匹配,则认为登陆成功。
JWT(JSON web token)是一种认证协议,能够发布接入令牌(Access Token,保持在客户端)并对发布的签名接入令牌进行验证。令牌(Token)自己包含一系列声明,应用程序能够根据这些声明限制用户对资源的访问。
JWT 由三段信息构成的:
JWT 示例:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjRmMWcyM2ExMmFhIn0.eyJpc3MiOiJodHRwOlwvXC9leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHA6XC9cL2V4YW1wbGUub3JnIiwianRpIjoiNGYxZzIzYTEyYWEiLCJpYXQiOjE1MjU5NDM4MTksIm5iZiI6MTUyNTk0Mzg3OSwiZXhwIjoxNTI1OTQ3NDE5LCJ1aWQiOjF9.jL-Hrl8obZlLGutjr-nVPCSoF2ObFh-rWfSwSZxoxzs
Header 部分用于声明协议类型和加密方式。
上面的 JWT 示例的 header 部分通过 base64_decode 后获得原始 JSON 字符串,内容以下:
{
"typ":"JWT",
"alg":"HS256",
"jti":"4f1g23a12aa" }
其中,typ 内容固定为 JWT,alg 表示加密算法,这里使用的是 HMAC SHA256。
payload 部分用于存放负载,将明文信息通过 base64 编码后存储,未经加密,不可存储敏感信息。包括如下三种:
JWT 标准中注册的声明(不强制使用)有如下几种,完整版能够 参考这里:
上面 JWT 示例中的 payload 部分对应的 JSON 字符串为:
{
"iss":"http:\/\/example.com",
"aud":"http:\/\/example.org",
"jti":"4f1g23a12aa",
"iat":1525943995,
"nbf":1525944055,
"exp":1525947595,
"userID":6666,
"userName":"kika",
"userSex":"m" }
这个 payload 中添加了几个自定义字段。
将 header 和 payload 通过 base64 编码后,用 .
句点拼接成一个字符串,经过 HMACSHA256(Java 的方法)或 hash_hmac(PHP 的方法),使用指定密钥加密这个字符串获得 signature。
JAVA:
sig = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret);
PHP:
$sig = hash_hmac('sha256', base64_encode($header) + "." + base64_decode($payload), $secret);
JWT 支持两种签名方式:
用户登录后,能够把一些经常使用字段(用户标识,是不是管理员,权限有哪些等等能够公开的信息)用 JWT 编码存储在 Cookie 中,每次服务器读取到 Cookie 后就能够解析到当前用户对应的信息,减少数据库压力。也能够用 Authorization: Bearer <jwttoken>
的方式经过 HTTP Header 仅发送 JWT 的 Token。
发送请求时,Token 放在请求的 HTTP Header 中。另外,若是发生跨域,例如 www.xx.com
下发出到 api.xx.com
的请求,须要在服务端开启 CORS(跨域资源共享):
Access-Control-Allow-Origin: *
下面的示例,基于 PHP 语言,CodeIgniter 框架。同时,省略了无关的 HTTP Header,重点分析 JWT 相关字段。
-----------------------------------------请求的 HTTP Header-------------------------------------------
GET http://jwt.com/welcome/create_token HTTP/1.1
Host: jwt.com
...
-----------------------------------------响应的 HTTP Header-------------------------------------------
HTTP/1.1 200 OK
Date: Fri, 11 May 2018 02:27:19 GMT
Set-Cookie: jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjRmMWcyM2ExMmFhIn0.eyJpc3MiOiJodHRwOlwvXC9leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHA6XC9cL2V4YW1wbGUub3JnIiwianRpIjoiNGYxZzIzYTEyYWEiLCJpYXQiOjE1MjYwMDU2MzksIm5iZiI6MTUyNjAwNTY5OSwiZXhwIjoxNTI2MDA5MjM5LCJ1c2VySUQiOjY2NjYsInVzZXJOYW1lIjoia2lrYSIsInVzZXJTZXgiOiJtIn0.MvYG6L71mM_AJj5FT4--RzCluIQ__nqgYSe9RTj8VCk; expires=Fri, 11-May-2018 04:27:19 GMT; Max-Age=7200; path=/
Content-Length: 1052
...
服务器端从 Cookie 中提取 jwt 这个字段后验证签名,若是经过验证则认为内容可靠,解析其中的内容并以此决定用户登陆状态、权限等:
-----------------------------------------请求的 HTTP Header-------------------------------------------
GET http://jwt.com/welcome/ HTTP/1.1
Host: jwt.com
Cookie: jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjRmMWcyM2ExMmFhIn0.eyJpc3MiOiJodHRwOlwvXC9leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHA6XC9cL2V4YW1wbGUub3JnIiwianRpIjoiNGYxZzIzYTEyYWEiLCJpYXQiOjE1MjYwMDU2MzksIm5iZiI6MTUyNjAwNTY5OSwiZXhwIjoxNTI2MDA5MjM5LCJ1c2VySUQiOjY2NjYsInVzZXJOYW1lIjoia2lrYSIsInVzZXJTZXgiOiJtIn0.MvYG6L71mM_AJj5FT4--RzCluIQ__nqgYSe9RTj8VCk
...
-----------------------------------------请求的 HTTP Header-------------------------------------------
GET http://jwt.com/welcome/create_token HTTP/1.1
Host: jwt.com
...
-----------------------------------------响应的 HTTP Header-------------------------------------------
HTTP/1.1 200 OK
Date: Fri, 11 May 2018 02:35:19 GMT
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjRmMWcyM2ExMmFhIn0.eyJpc3MiOiJodHRwOlwvXC9leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHA6XC9cL2V4YW1wbGUub3JnIiwianRpIjoiNGYxZzIzYTEyYWEiLCJpYXQiOjE1MjYwMDc3NTQsIm5iZiI6MTUyNjAwNzgxNCwiZXhwIjoxNTI2MDExMzU0LCJ1c2VySUQiOjY2NjYsInVzZXJOYW1lIjoia2lrYSIsInVzZXJTZXgiOiJtIn0.CBsw7_rDC-GeJBob2JwCOITp7L80g_VT9KUtSVmKYKY
Host: jwt.com
-----------------------------------------请求的 HTTP Header-------------------------------------------
GET http://jwt.com/welcome/ HTTP/1.1
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjRmMWcyM2ExMmFhIn0.eyJpc3MiOiJodHRwOlwvXC9leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHA6XC9cL2V4YW1wbGUub3JnIiwianRpIjoiNGYxZzIzYTEyYWEiLCJpYXQiOjE1MjYwMDc3NTQsIm5iZiI6MTUyNjAwNzgxNCwiZXhwIjoxNTI2MDExMzU0LCJ1c2VySUQiOjY2NjYsInVzZXJOYW1lIjoia2lrYSIsInVzZXJTZXgiOiJtIn0.CBsw7_rDC-GeJBob2JwCOITp7L80g_VT9KUtSVmKYKY
Host: jwt.com
后端服务器对这个 Authorization 进行判断便可。
对于 PHP,可使用的 JWT 库有 jwt、jwt-auth。这里以第一个 jwt 为例,具体操做请结合所使用语言及框架和安装的 JWT 库。
composer require lcobucci/jwt
注意,PHP 版本须要 5.5+,同时须要开启 OpenSSL 扩展。
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Hmac\Sha256;
public function create_token() {
$builder = new Builder();
$signer = new Sha256();
// 设置签发者
$builder->setIssuer('http://xx.com');
// 设置接收者
$builder->setAudience('http://xx.com');
// 设置 ID,能够用来区分
$builder->setId('4f1g23a12aa', true);
// 设置签发时间
$builder->setIssuedAt(time());
// 在 60 秒内该 token 没法使用
$builder->setNotBefore(time() + 60);
// 设置过时时间位 2 小时
$builder->setExpiration(time() + 7200);
// 设置自定义的 payload 信息
$builder->set('userID', 6666);
$builder->set('userName', 'kika');
$builder->set('userSex', 'm');
// sha256 签名,密钥字符串能够自定义
$builder->sign($signer, 'signatureString');
// 获取生成的token
$token = $builder->getToken();
// 能够经过 Cookie 传输
set_cookie('jwt', $token, 7200);
// 也能够经过 HTTP Header 传输,在前端保存 token 后添加到 HTTP Header 便可:Authorization: Bearer xx.xx.xx
// 查看字段内容
$token = explode('.', $token);
echo base64_decode($token[0]).'<br/>';
echo base64_decode($token[1]).'<br/>';
}
把上面使用字符串加密的这一行:
$builder->sign($signer, 'signatureString');
替换为使用密钥文件加密便可,须要提供私钥地址:
$builder->sign($signer, $keychain->getPrivateKey('私钥地址'));
在每个请求头里加入 Authorization,并加上 Bearer:
fetch('api/user', {
headers: {
'Authorization': 'Bearer ' + token
}
})
经过 Cookie 传输 JWT 信息:
if ($token = get_cookie('jwt')) {
$rs = $this->verify_token($token);
if ($rs) {
echo 'you have right jwt<br />';
} else {
echo 'error<br />';
}
}
经过 HTTP Header 传输 JWT 信息:
$headers = apache_request_headers();
if (!empty($headers['Authorization']) && $token = $headers['Authorization']) {
$token = substr($token, strpos($token, 'Bearer ') + 7);
$rs = $this->verify_token($token);
if ($rs) {
echo 'you have right jwt from Authorization<br />';
} else {
echo 'error Authorization<br />';
}
}
直接从 $token
中获取全部数据:
public function get_claims ($token) {
$parser = new Parser();
$parse = $parser->parse($token);
return $parse->getClaims();
}
也能够获取单条数据:
$parse->getClaim('aud');
内容比较多,另写一篇,参考 这里。