不管用哪种方式认证用户,都可能被中间人攻击窃取 SessionID 或 Token,从而发生 CSRF 攻击。解决方式就是全站 HTTPS。现在 Let’s Encrypt 已经支持免费的通配符 HTTPS 证书了。
HTTP 协议是无状态的,要保存用户状态需要额外的机制。
刚开始时,多数公司使用的技术栈是:单台云服务器上安装所需的所有软件,包括 Nginx 提供 Web 服务,MySQL 数据库,PHP-FPM 应用程序服务。这时候使用的用户认证协议使用最简单的 Session。客户端的每个请求都会携带 Cookie,其中保存了 SessionID 字段,服务器可以通过这个 SessionID 字段访问到对应的 Session(例如 PHP 中的 $_SESSION ),从而识别出用户登录状态。Session 中还可以添加一些常用的字段进来(比如用户名、手机号等),避免对数据库的频繁访问。
后来,随着用户量增大、并发增大,单台服务器搞不定了,于是搞了个水平扩展的服务器集群,通过 Nginx 或 LVS 实现负载均衡。这时发现个问题,用户登录后 Session 是保存到集群中的某一台服务器上的。要使 Session 机制可以在分布式环境下继续工作,需要一些额外操作。而且对于现在的大前端(浏览器、APP、小程序)趋势来说,Cookie 机制略显累赘。
而这时,JWT 认证协议完全满足需求。协议简单清晰,花一个下午就可以搞清楚。
公司发展过程中,产品线会慢慢增多,比如百度的贴吧、网盘、浏览器等。这时,需要一套单点登录机制 SSO(Single sign-on),用户只要一次登录,就可以使用这一系列产品。SSO 描述了认证的问题。
SSO 需要一个独立的认证中心 CAS(Central Authentication Service,中央认证服务),只有认证中心能提供登录入口,接受用户的用户名密码等凭证,其他系统无登录入口,只接受认证中心的间接授权。这里有个开源的 CAS:apereo CAS,其服务端用 Java 实现,客户端支持多种语言。其架构文档可以参考 这里。
单体项目拆分成微服务后,可以更加灵活。通常所有的服务都在网关之后,所有请求都发送到网关,由网关统一转发。微服务的网关通常实现了 OAuth,成为认证授权中心,用于判断是否有足够权限。微服务之间可以通过 JWT 进行访问鉴权,避免身份认证。
随着公司用户增多(假设跟微信一样,有几亿用户),合作企业也越来越多。如果每次都要在后台通过人工给合作伙伴配置账号密码,分配权限管理,那太麻烦了。同时,一些企业有自己的平台,想要利用我的用户账号体系实现在这些平台上的登录(授权登录)。对于用户的图片,一些图片打印公司也想在经过用户同意后,直接访问到我服务器上的用户图片,优化体验。
总之,就是只要用户同意,他可以分享自己的所有资源(账号、图片等)。这时,就需要 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 中的各个字段解释如下,完整的中文版解释参考 这里:
ci_session:SessionID,这个会话对应的服务器上的 Session 的唯一标识符。expires:Cookie 的有效期。Max-Age:Cookie 过期前的秒数。path:可以在 Header 中使用这个 Cookie 的 URL 路径,这里表示这个域名下的所有请求都会携带这个 Cookie。HttpOnly:表示这个 Cookie 无法通过 JavaScript 的 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 的主要问题有:
服务器压力大:每个用户在认证后,Session 信息都会保存在服务器的内存中,开销大。难以扩展:对于基于 Session 的分布式系统,要实现负载均衡,有两个办法:确保同一用户始终访问同一个服务器,或在多台服务器之间同步 Session。对于前者,Nginx 也可以用 ip_hash 把同一来源的 IP(同一 C 段)指向后端的同一台机器。对于后者则需要通过 Session Sticky 机制在多台服务器之间同步 Session(例如 Nginx 的扩展模块 nginx-sticky-module。假设 Session 存储在 A 服务器上,而用户访问了 B 服务器,则可以将 Session 从 A 同步到 B,但是如果存储 Session 的 A 服务器挂掉,还是会导致用户掉线)。还有,就是目前大前端的发展,除了浏览器外,各种 APP、小程序层出不穷,而非浏览器下环境下避免使用 Cookie 可能会更简单。
JWT 官网的详细介绍 Larval + Vue 案例
Session 之所以这么麻烦,是因为需要在服务器端保存信息,那我把信息保存在客户端,不就可以避免这个麻烦了嘛。JWT 就是这么个思路,服务器端保存加密机制及密钥,对用户指定字段进行加密后的字符串保存在客户端,用户下次请求时携带加密前的字段和加密后的字符串,如果跟服务器加密结果匹配,则认为登录成功。
JWT(JSON web token)是一种认证协议,可以发布接入令牌(Access Token,保持在客户端)并对发布的签名接入令牌进行验证。令牌(Token)本身包含一系列声明,应用程序可以根据这些声明限制用户对资源的访问。
JWT 由三段信息构成的:
headerpayloadsignatureJWT 示例:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjRmMWcyM2ExMmFhIn0.eyJpc3MiOiJodHRwOlwvXC9leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHA6XC9cL2V4YW1wbGUub3JnIiwianRpIjoiNGYxZzIzYTEyYWEiLCJpYXQiOjE1MjU5NDM4MTksIm5iZiI6MTUyNTk0Mzg3OSwiZXhwIjoxNTI1OTQ3NDE5LCJ1aWQiOjF9.jL-Hrl8obZlLGutjr-nVPCSoF2ObFh-rWfSwSZxoxzsHeader 部分用于声明协议类型和加密方式。
上面的 JWT 示例的 header 部分经过 base64_decode 后得到原始 JSON 字符串,内容如下:
{ "typ":"JWT", "alg":"HS256", "jti":"4f1g23a12aa" }其中,typ 内容固定为 JWT,alg 表示加密算法,这里使用的是 HMAC SHA256。
payload 部分用于存放负载,将明文信息经过 base64 编码后存储,未经加密,不可存储敏感信息。包括以下三种:
JWT 标准中注册的声明公共声明私有声明JWT 标准中注册的声明(不强制使用)有以下几种,完整版可以 参考这里:
iat:Issued At,签发时间iss:Issuer,JWT 签发者sub:subject,JWT 所面向的订阅者,每个 Issuer 范围内是唯一的aud:Audience,JWT 的接收方exp:Expiration Time,过期时间,这个过期时间必须要大于签发时间nbf:定义在什么时间之前,该 JWT 都是不可用的.jti:JWT 的唯一身份标识,主要用来作为一次性 Token,避免重放攻击上面 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 支持两种签名方式:
密钥:基于字符串,简单,安全性低RSA 和 ECDSA 签名:基于公钥和私钥,需要先生成私钥文件,签名时指定这个文件的位置用户登陆后,可以把一些常用字段(用户标识,是否是管理员,权限有哪些等等可以公开的信息)用 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 相关字段。
服务器端从 Cookie 中提取 jwt 这个字段后验证签名,如果通过验证则认为内容可靠,解析其中的内容并以此决定用户登录状态、权限等:
-----------------------------------------请求的 HTTP Header------------------------------------------- GET http://jwt.com/welcome/ HTTP/1.1 Host: jwt.com Cookie: jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjRmMWcyM2ExMmFhIn0.eyJpc3MiOiJodHRwOlwvXC9leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHA6XC9cL2V4YW1wbGUub3JnIiwianRpIjoiNGYxZzIzYTEyYWEiLCJpYXQiOjE1MjYwMDU2MzksIm5iZiI6MTUyNjAwNTY5OSwiZXhwIjoxNTI2MDA5MjM5LCJ1c2VySUQiOjY2NjYsInVzZXJOYW1lIjoia2lrYSIsInVzZXJTZXgiOiJtIn0.MvYG6L71mM_AJj5FT4--RzCluIQ__nqgYSe9RTj8VCk ...后端服务器对这个 Authorization 进行判断即可。
对于 PHP,可以使用的 JWT 库有 jwt、jwt-auth。这里以第一个 jwt 为例,具体操作请结合所使用语言及框架和安装的 JWT 库。
注意,PHP 版本需要 5.5+,同时需要开启 OpenSSL 扩展。
把上面使用字符串加密的这一行:
$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');内容比较多,另写一篇,参考 这里。
转载于:https://www.cnblogs.com/kika/p/10851618.html
相关资源:数据结构—成绩单生成器