1. 用户认证与权限
Basic 认证
Bearer 认证
Digest 认证
2. RFC 7235 HTTP 身份验证详解
WWW-Authenticate
Proxy-Authenticate
Authorization
Proxy-Authorization
3. 权限的存储
4. 常见攻击方式
Referer
Origin
Host
Set-Cookie
5. HTTPS 协议与 SSL
5.4 使用 HTTP严格传输安全协议
(HSTS
/ HTTP Strict Transport Security)
SECURE_HSTS_SECONDS
SECURE_PROXY_SSL_HEADER
Strict-Transport-Security
6. 实践1:前端权限控制的一些实现
beforeEach
beforeResolve
afterEach
beforeEnter
beforeRouteEnter
,beforeRouteUpdate
,beforeRouteLeave
7. 通过移动端代理认证(扫码登陆)
8. 第三方权限接入的实现
认证 和 权限是两个概念,也是两个过程。如果混为一谈将给编程实践带来很多不必要的困惑。
★ 从过程上看,用户身份认证
主要即用户登陆
。
★ 从功能上看,用户身份认证
的目标是标识和区分不同的用户。
当用户进行身份认证之前,可能并未到达身份认证处。这时用户可能正在操作一些表单数据或者浏览其它界面,因此如果用户需要从这些表单或者页面转向达身份认证处时,需要先对必要数据或者页面位置进行存储。
当用户从某个位置到达用户身份认证处时认证开始。
先由用户填写并提交表单,对相应的数据进行前端校验,比如邮箱与密码的格式等等。用户填写后,提交表单。由客户端向服务端提交用户数据。
当服务端接受到客户端提交的用户信息数据后,与数据库或者其它数据存储媒介中查询和比对请求的用户。接着:
1 如果用户校验不成功,则返回失败状态代码与用户校验失败的相关信息。
2 如果用户校验成功,则通过用户信息生成用于能够唯一标识用户的令牌
(token
),令牌需要一个仅在服务端能够进行解密的密钥
。
令牌
的作用是凭借加密的方式在接下来的一段时间内标识唯一身份的某些操作,是用户身份的凭证。它就像一个只能往其中放入信息的盒子,它是一经创建就是给前端携带操作数据的。密钥
相当于打开用户信息的钥匙,它是给加密者自己用的。当前端将一系列操作结果数据打包提交后,加密者自己也就是后端,通过使用密钥
解密令牌
获取其中的用户信息,这样就能在多个提交者中区分每个提交者的身份。这也可以理解成用户把自己的身份告诉作为加密者的服务端后,服务端为用户量身定制了一个用户的签名工具(印章),用户之后只需要给其数据印上这个签名印章,那么印章的制作者就可以用其密钥将印章上的签名与用户的身份进行对应。用户一旦认证成功,将从服务器获取一个 token 存储于本地备用。此后当客户端需要访问资源或者提交数据时,通过在 HTTP 请求头(header)中添加Authorization
字段,将 token 作为该字段的值发送给服务端以向表明自己自己身份。服务端使用自己保存的密钥
计算、验证签名以判断与关联一个可信的身份。
通过引导用户填写相关认证表单,再将数据发送到服务器,从而完成认证。这是最普通的认证方式。在Web上这一认证方式的地位不断地随着安全问题被挑战,相关技术也不断地随着新的安全漏洞地发现得到进一步完善。
如今国内手机已经完成了实名认证,排除手机被盗用的风险,目前手机验证码认证算是一种十分便捷的认证方式。手机验证码一般以短信的形式发出,这需要从各大电信运营商处购买相关服务。你也可以选择从各大云服务提供商处进行购买,经常这更加方便。如腾讯云:
手机认证的逻辑假设是建立在手机号码被成功注册的基础上的。也就是说在注册用户或者验证手机是该用户本人的这一环节显得比较重要,如果不能保证是通过自己长期使用的手机进行注册,后期也就无法证明是该用户本人进行操作。
这里显然有一个风险,那就是用户换号。一个手机号用户不再使用后,电信运营商那边经过一段时间后将对这些号码进行回收以分配给新的用户。这时往往我们系统并不知道原用户已经换号,因为电信运营商一般不会通知到我们的系统。因此作为开发者而言,我们应该考虑对于长期未登陆地用户配合其它验证方式校验用户是否还是那个用户。目前可用的其它方式也有很多,比如通过“声纹校验”、“扫脸校验”等等,这也就对应分别要求提前采集用户地“声纹”和“面纹”数据。目前在移动端上,类似的校验方式已经得到广泛普片的使用。
邮箱验证码认证于手机短信认证的方式比较像,但是邮件几乎是免费的。我们可以几乎没有成本地通过自动邮件将验证码发送给我们地用户。
扫码认证是一种通过已经认证的设备进行授权赋值的方式。在 通过移动端代理认证(扫码登陆) 章节中,有具体实现流程的详细讲解。
第三方认证指的是调用第三方地用户接口获取用户相关授权数据地认证方式。
JWT
)JSON WEB TOKEN
(JWT
)就是令牌的一种实现形式,它用于在各方之间作为 JSON 对象安全地传输信息。由于此信息经过数字签名,因此可以验证和信任。讲白了,就是说再制作Token时,需要加密的数据为一些特定格式的JSON字符串,这些JSON字符串将被分别使用某种验证方案加密成一段字符串,再通过一定的方式组合再一起。JWT本身并不是加密的验证方案,关于验证方法在后一小节中有介绍。
JWT 可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。在身份验证中,当用户使用其凭据成功登录时,将返回一个令牌(Token)。由于令牌是凭证,因此必须非常小心以防止出现安全问题。通常不应将令牌保留的时间超过所需的时间。(引用jwt.io)这里如果不明吧请看验证方案小节后再回看。
JSON Web Token 的有三部分构成:
头
(header
){
"alg": "HS256", // 表示正在使用的签名算法,例如 HMAC SHA256 或 RSA
"typ": "JWT" // 表示令牌的类型,即 JWT
}
[2] 负载
(payload
)
负载中包含声明
。声明
是关于实体(通常是用户)和附加数据的声明。共有三种类型的声明:注册声明
、公共声明
和私人声明
:
注册声明(Registered claims):这些是一组预定义的声明,这些声明不是强制性的,而是推荐的,以提供一组有用的、可互操作的声明。其中一些是: iss(发行者)、 exp(到期时间)、 sub(主题)、 aud(受众)等。
其中,声明名称只有三个字符,因为 JWT 是紧凑的。
公共声明(Public claims):这些可以由使用 JWT 的人随意定义。但是为了避免冲突,它们应该在IANA JSON Web Token Registry中定义,或者定义为包含抗冲突命名空间的 URI。
私有声明(Private claims):这些都是使用它们同意并既不是当事人之间建立共享信息的自定义声明注册或公众的权利要求。
[3] 签名
(Signature
)
签名用于验证消息在此过程中没有更改,并且在使用私钥签名的令牌的情况下,它还可以验证 JWT 的发送者是它所说的那个人。要创建签名部分,您必须获取编码的头
(header
)和的负载
(payload
)、密钥、标头中指定的算法,并对其进行签名。
例如,如果要使用 HMAC SHA256 算法,则签名将通过以下方式创建:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
这里有一个细节需要说明,就是认证中所需要的HTTP 验证方案
。常见的验证方案(参考mozilla)如:
Basic 认证是 HTTP/1.0 中定义的认证方式,现在仍有很多网站回使用该方式进行认证。Basic
HTTP 验证方案使用用户的 ID/密码作为凭证信息,并且使用 base64 算法进行编码。用户 ID 与密码是是以明文的形式在网络中进行传输的(尽管采用了 base64 编码,但是 base64 算法是可逆的。也就是说攻击者也可以使用该编码的解码算法进行解密),所以Basic
验证方案并不安全。因此Basic
验证方案应与 HTTPS / TLS 协议
搭配使用,关于 HTTPS 的相关内容在后文中将进一步阐述。假如没有这些安全方面的增强,那么Basic
验证方案不应该被来用保护敏感或者极具价值的信息。
使用不同的验证方案,写在请求头``字段的内容就不一样,如
Authorization: Basic
又如:
Authorization: Bearer
更具体地例子,假设你在用 JavaScript 的一个封装请求库axios
来写传递带密钥签名的GET
请求,并且采用BasicHTTP 验证方案进行加密,你所写的大概形式是这样的:
async authorised_get(url:string){
const res = await axios({
url: url,
method:'get',
headers: {
'Authorization': this._basic_auth()
}
})
return res
}
_basic_auth(){
// 先读取认证后服务器返回后的以键名'my_token'存储在客户端的token
const token = localStorage.getItem('my_token')
// 以 Base64 编码方式进行编码
const encoded = Base64.encode(`${token}:`)
// 将字符串拼凑成`Bearer `的格式返回给调用处
return `Basic ${encoded}`
}
对于服务端而言,一旦接受到认证用户相关权限访问时,应当对token
进行解码并校验。这里有不同的情况,比如可能客户端会传来一个签名错误的token
,也有可能会传来一个过期的token
。不论怎么样,作为服务端开发者,你可以选择性的返回提示客户端一些错误的具体信息外,建议采用401
状态码来标识本次请求为Unauthorized
的。还有一种情况是缺少令牌凭证,当由于缺乏位于浏览器与可以访问所请求资源的服务器之间的代理服务器所要求的身份验证凭证toekn时,应返回407
状态码以标识Proxy Authentication Required
。
前端也采用Base64编码的目的是为了保护用户输入数据。但是Basic 认证采用Base64编码但没有加密处理,同时不需要任何附加信息即可对其解码,这就相当于用户输入数据以明文形式再互联网上进行传播,因此仅仅靠这种方式来确保安全是做不到的。
Firefox 使用 MD5 算法实现 Digest 认证。MD5算法即 MD5信息摘要算法,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。这套算法的程序在 RFC 1321 标准中被加以规范。1996年后该算法被证实存在弱点,可以被加以破解,对于需要高度安全性的数据,专家一般建议改用其他算法,如SHA-2。2004年,证实MD5算法无法防止碰撞(collision),因此不适用于安全性认证,如SSL公开密钥认证或是数字签名等用途。详细请参考:https://baike.baidu.com/item/MD5/212708?fr=aladdin
为了弥补Basic认证的缺点, Digest 认证应运而生。相比于Basic认证,Digest认证不会像Basic认证那样几乎铭文发送密码,但同样也是采用 质询/响应的方式。
在上文中我们已经反复提到过 token 的安全性问题,并且指出其中不应该被来用保护敏感或者极具价值的信息。 token 一经交给客户端,那么服务端实际上是无法保证在长时间内该 token 被不应该获得认证的人手里。因此有必要更具业务需求让token在一个相对较短的时间内才有效,这就是 JWT 认证的时效性。
Web 技术中有多种可供选择的存储方式。我们考虑的第一个问题是,token的存储是否在关闭浏览器后下次打开时任然有效,这决定我们是否可以采用某些框架如vue
或者react
中的状态共享
方式进行存储。如采用这种存储方式,实际上是在一个应用加载到内存时进行了临时的存储,只作为应用的不同组成部分在内存中共享数据,一旦结束则数据全部丢失。
从实践角度出发,由于很多情况下用户正在使用应用的过程中,可能会短暂的关闭浏览器,这时如果token数据丢失会导致用户再次打开时无法签名,需要重新登陆,这时很麻烦的。从安全角度看,由于token的时效性我们是做在服务端的,一般如用户连续操作停止一段事件后,服务端将自动让token失效,因此我们不用太过于担心安全性的问题,这样我们完全可以考虑在客户端本地化持续存储token。
常用的方式莫过于 cookie
或者 localStorage
。
我的另一篇博文介绍了操作
cookie
更简单的方式,可供使用cookie
进行存储的读者参考:https://blog.csdn.net/qq_28550263/article/details/122098726。
那么到底将Token存储在Cookie还是LocalStorage呢?
其实从功能上看都是可以的,并且这两种做法的网站都有很多。比较而言:
Cookie 是一个请求首部,其中含有先前由服务器通过 Set-Cookie 首部投放并存储到客户端的 HTTP cookies,这个首部可能会被完全移除,例如在浏览器的隐私设置里面设置为禁用cookie(引用 mozilla HTTP Cookie)
将 Token 存储在 cookie 中可指定 httponly 来防止 js 被读取,也可以指定 secure 来保证 Token 只在 HTTPS 下传输,使用这些方式从而避免引入很多第三方脚本时同域的js访问泄露,但是却不能有效阻止 CSRF攻击,因为Cookie即使不被js所访问,但攻击者可以伪造用户向服务端发起请求(而不是用户自己发起),这样一个token通过首部发给服务器校验,成了攻击者伪造用户身份的方式。
localStorage同样有缺点,因为很容易被同域的js访问,也就是当你使用第三方脚本时,他们都是同域的。另外,由于HTTP是不加密传输数据的,当你采用HTTP请求传输时,更是容易受到注入攻击。如XSS攻击者注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。因此你大概需要花点小钱买个SSL证书,以确保安全性。
用户身份认证只是用户权限的一个基本环节但不是权限的全部。身份认证的目的是提供了授权的依据目标,使我们可以依据业务上的需求将一个个稳定的用户分为不同的角色,赋予与各类角色的业务逻辑相匹配的功能。因此权限在于区分与控制
。归纳起来:
★ 区分
是权限的必要环节;
★ 控制
是权限的目的。
因此简单来说,权限是对特定资源的访问控制。所庆幸的是当我们实现用户认证后已经完成了最原始的区分
,即有认证身份者和没有认证身份者(以下称之为“访客”)的区分。在很多的业务逻辑下,访客是能够进行一些最基础的操作的。
但是随着我们业务的扩展,一个系统慢慢需要有人进行专门的管理等操作,这时就要求我们对认证用户的身份进一步细化,说到底还是区分
。现有不同的框架在这里有不同的处理方式。
标识的作用即区分。
以 Django Web 框架为例,该框架的自带用户权限系统中提供了两个装饰器。
一个是logging_required(...)
装饰器,用于认证用户访问视图装饰,实际是执行了一些要求登陆后访问的路由守卫。
而另外一个是permission_required(...)
装饰器,该装饰器接受一个标识权限的字符串,只有当访问者具有这些指定的权限时,才会被路由守卫允许访问。从数据模型上看,Django Web 的用户模型拥有一个权限字段,是用来专门存储该用户所拥有的权限字符串的。
这种方式的特点是一个字符串即一类权限。如果不特别定义,不同字符串标识的不同权限之间不存在高低比较关系,这与接下来要说的基于数字标识的认证用户权限有所不同。
数字等级是区分不同类型用户权限的另外一个具体实现方式。不同等级的权限需要不同的授权,授权数据一般来源于服务器端。这种方式的缺点在于数字标识从含义上远不如字符串标识容易被理解。因从从开发角度而言,我们更喜欢将数字对应成一个能以字符串表现的形式。
你可能想说,这不是又回到字符串了吗,多此一举。别急。
我们之所以使用基于数字等级标识的认证用户权限是因为数字不仅仅在同异上对用户进行了区分,它还是可以直接进行比较的——这是字符串相对不方便做的。
在很多变成语言中,都提供了 枚举
。以TypeScript
为例,使用其数字枚举
:
enum UserAuths{
guest,
user,
staff,
admin
}
我们不指定枚举值,将权限等级从低到高依次写下各种类型用户,那么他们将成为自然数从小到大的别名。比如:
UserAuths.guest // 1
UserAuths.user // 2
// ...
在权限控制时,我们只需要设定一个权限阈值
,就可以使以该阈值进行分类的两大类用户要么都有某种权限,要么都没有某种权限。如:
has_permission(user_class: UserAuths, permission_level){
if(user_class >= permission_level) return true;
else return false;
}
const login_admin = 4
has_permission(UserAuths.user, login_admin) // false
在编程实践中,访问代表获取某一种数据资源。在前后端分离的多数情况下为了保证数据的安全性,访问控制一般被后端所把控从而避免关键数据被泄露出去。
路由控制有时候也被称为路由守卫。作为权限的实现环节而言,路由守卫不等同于路由导航因为普通的导航未必取决于权限,而基于权限的路由控制却一定是以权限为逻辑依据的路由导航。
组件一般是一个特定功能的模块,如vue
等前端框架的组件。在有些时候我们以权限作为判断依据来决定组件的触发事件。比如当用户点击了一个按钮,对于有权限 “A” 的用于需要触发页面跳转完成路由控制,而另一部分没有权限“A”的用户只需要触发信息提示以告知用户没有操作权限或者获取权限的方法等等。
上文中已经提交了相关的验证逻辑,但仍有需要进一步详细讲解的。
服务器可以用来针对 客户端的请求 发送质询信息(challenge),客户端则在请求中提供身份验证凭证。
当用户(客户端)向一个需要需要认证身份的地址发起请求时,由于起初用户不带有任何标识其身份的签名,服务器客户端返回 401(Unauthorized,未被授权的) 状态码并在 WWW-Authenticate 首部 或者代理模式下使用 Proxy-Authenticate 首部 提供如何进行验证的信息,其中至少包含有一种 质询方式(type)。
当用户(客户端)收到服务器的该401响应后,应让客户端使用者填写相关的认证信息,从而证明自己身份。身份一经确认,接着客户端向服务端发起新的请求时,中添加 Authorization 首部字段进行验证。也就是说,该首部中包含对用户身份进行标识的东西,也称作 签名工具 ,这个工具是用户提交自己的信息到服务器后 由服务端制作后并返回给客户端的,即所谓 令牌(Token)。这在上面Json Web Token(JWT)部分有相关阐述。就认证部分的意义和原理而言,下图做了形象的描述:
WWW-Authenticate
用于定义使用何种验证方式去获取对资源的连接,一般与一个 401 (Unauthorized / 未授权的) 的响应一同被发送。其语法格式为:
WWW-Authenticate: <type> realm=<realm>
其中:
: 标识校验的类型,如 Basic,它将凭据作为用户 ID/密码对传输,并使用 base64 进行编码。其它常用类型在上文中有相关介绍realm=
:一个保护区域的描述。如果未指定realm, 客户端通常显示一个格式化的主机名来替代。charset=
:当提交用户名和密码时,告知客户端服务器首选的编码方案。唯一的允许值是不区分大小写的字符串"UTF-8"。这与realm字符串的编码无关。WWW-Authenticate: Basic realm="Access to the staging site"
Proxy-Authenticate
该请求头与 请求头 WWW-Authenticate
类似,也是用于指定为获取资源访问权限而进行身份验证的方法,但是不同在于 Proxy-Authenticate
是用于代理认证的情形,相对应的,它一般与一个 407 (Proxy Authentication Required / 需要代理身份验证) 的响应一同被发送。其格式与 请求头 WWW-Authenticate
完全一样:
Proxy-Authenticate: <type> realm=<realm>
其中:
: 指的是验证的方案,也就是 请求头 WWW-Authenticate
格式中的方案。realm
: 用来描述进行保护的区域,或者指代保护的范围。它可以是类似于 “Access to the staging site” 的消息,这样用户就可以知道他们正在试图访问哪一空间。Authorization
请求消息首部包含有用来向(代理)服务器证明用户代理身份的凭证。这里同样需要指明验证的类型,其后跟有凭证信息,该凭证信息可以被编码或者加密,取决于采用的是哪种验证方案。
Authorization: <type> <credentials>
Proxy-Authorization
与请求头 Authorization
类似,是相应地用于代理认证的情形所使用的对应请求头,其格式也与 请求头 Authorization
一样:
Proxy-Authorization: <type> <credentials>
权限存储问题主要是基于项目而言的。对于一个互联网项目,不论是前后端分离还是前后端不分离,一般来说权限相关数据都应当存储在后端,以防止数据被篡改。也有一些情况权限相关的数据是本地的,比如通过 Electron
框架(见 https://www.electronjs.org/) 开发一个具有本地权限的桌面级应用。可见权限存储在何处需要更具项目的类型和需求进行具体确定。
前后端分离的项目中,权限在后端即一条条数据,其具体内容最终将由API的方式传给前端,有前端最终转换为具体功能的控制。比如以下是 Django Web 框架提供的自带用户权限:
对于提供分组管理功能的情况,还可以建立用户组的权限表。
权限可以以不同的数据组织方式存储于数据库中,本质上就是一个用于 比对
和 比较
的数据,实际上在章节1.2.1权限的标识中,就已经介绍过了两种标识权限的数据组织方式。
角色
角色
除了一些如单机使用的桌面项目写死了权限的项目,一般权限不存储在客户端本地。前端虽然不是权限的存储处但却是权限的密集使用地,因此获取某种权限后应该作为一个前端存储以保证在当前进行的前端项目全局权限是统一的。比如在某个单页面应用(SPA)中的多个功能公用了一个权限,在从后端请求权限状态时该权限对象认为是unsettled
(未决定的),当用户某一次用到其中一个要求该权限的功能时触发的权限更新从而向后端请求相关数据,获取权限状态,这时权限对象进入settled
(已决定的),已决定的权限应该有一个具体的值表示当前用户是否具有该权限,或者是一个具体的行为,操作用户在当前项目共享的一个存储值。
上文已经提到CSRF攻击
(跨站点请求伪造)和XSS攻击
(跨站点脚本)
上文已经说过,CSRF攻击者可以伪造用户的身份向服务端发起请求(伪装用户自己发起请求),使用用户的令牌(token)通过首部发给服务器校验。发起 CSRF 攻击的人可以使用其他用户的令牌(token)执行操作,且是在其不知情或不同意的情况下。
Django 是基于Python的重型Web框架,也是Python Web中最主流的一款框架。在Django通过检查每一个 POST 请求中的密文来实现。这保证恶意用户不能“复现”一个表单并用 POST 提交到你的网页,并让一个已登录用户无意中提交该表单。恶意用户必须知道特定于用户的密文(使用 cookie)。该 cookie 是由 CsrfViewMiddleware 中间件
设置的一个基于随机密钥值(出于安全考虑,每次用户登录时都会改变密钥的值。)的 CSRF cookie,其他网站无法访问。如果在请求中还没有设置的话,那么它将与调用了 django.middleware.csrf.get_token()
(内部用于获取 CSRF 令牌的函数)的每个响应一起发送。为了防止 BREACH 攻击,令牌不是简单的密钥,而是在密钥前面加上一个随机掩码,用来打乱密钥。
对于所有不使用 HTTP GET、HEAD、OPTIONS 或 TRACE 的传入请求,必须存在一个 CSRF cookie,并且 csrfmiddlewaretoken 字段
必须存在且正确,否则将返回给客户端一个 403 错误的响应。
其中,【csrfmiddlewaretoken 字段】:
Django 中一个隐藏的表单字段,存在于所有发出的 POST 表单中。这个字段的值也是密钥的值,但有一个掩码,这个掩码会被添加到字段中,并被用来扰乱字段。掩码在每次调用 get_token() 时都会重新生成,所以表单字段的值在每次响应时都会改变。
CsrfViewMiddleware 中间件
根据当前主机和Django配置文件 settings.py 中 CSRF_TRUSTED_ORIGINS
字段的设置,验证 Origin header ,如果是由浏览器提供的。这提供了对跨子域攻击的保护。CSRF_TRUSTED_ORIGINS
字段默认是一个空列表([]
),专用于列举不安全请求(例如POST)的可信来源列表。对于包含 Origin请求头 的请求,Django 的 CSRF 保护要求标头与标头中存在的源匹配Host。
注意:
由于在Web技术中 CSS 一般指层叠样式表,故将 Cross Site Scripting 缩写为XSS
XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。攻击成功后,攻击者可能得到包括但不限于更高的权限(如执行一些操作)、私密网页内容、会话和cookie等各种内容。(百度百科中XSS攻击的解释)
XSS 攻击允许用户将 客户端脚本 注入其他用户的浏览器。这通常通过将恶意脚本存储在数据库中进行检索并显示给其他用户来实现,或者通过让用户单击链接导致攻击者的 JavaScript 被用户的浏览器执行来实现。然而,XSS 攻击可能源自任何不受信任的数据源,例如 cookie 或 Web 服务,只要数据在包含在页面中之前没有得到充分净化。
Referer
格式:
Referer: <url>
其中:
:当前页面被链接而至的前一页面的绝对路径或者相对路径。不包含 URL fragments (例如 “#section”) 和 userinfo (例如 “https://username:[email protected]/foo/bar/” 中的 “username:password” )。例如:
Referer: https://xxxx.com/xxx
Referer 请求头包含了当前请求页面的来源页面的地址,即表示当前页面是通过此来源页面里的链接进入的。
服务端一般使用 Referer 请求头识别访问来源,可能会以此进行统计分析、日志记录以及缓存优化等。
Referer 请求头可能暴露用户的浏览历史,涉及到用户的隐私问题。
在以下两种情况下,Referer 不会被发送:
Origin
格式:
Origin: ""
Origin: <scheme> "://" <host> [ ":" <port> ]
其中:
:请求所使用的协议,通常是HTTP协议或者它的安全版本HTTPS协议。
:服务器的域名或 IP 地址。
: 可选。服务器正在监听的TCP 端口号。缺省为服务的默认端口(对于 HTTP 请求而言,默认端口为 80)。例如:
Origin: https://xxx.xxxxx.com
请求头 Origin 字段指示了请求来自于哪个站点。
该字段仅指示服务器名称,并不包含任何路径信息。用于 CORS 请求 或者 POST 请求。除了不包含路径信息,该字段与 Referer 首部字段相似。浏览器一般会将 Origin请求头 添加到:
浏览器会将Origin请求头添加到所有跨域的请求中,除GET或HEAD请求外的同源请求。如果在no-cors模式下发出跨源GET或HEAD请求,则不会添加Origin头。
Host
Host: <host>:<port>
其中:
: 服务器的域名(用于虚拟主机)。
: 可选,服务器监听的 TCP 端口号。例如:
Host: xxx.xxxxx.com
Host 请求头指明了请求将要发送到的服务器主机名和端口号。
如果没有包含端口号,会自动使用被请求服务的默认端口(比如HTTPS URL使用443端口,HTTP URL使用80端口)。
所有HTTP/1.1 请求报文中必须包含一个Host头字段。对于缺少Host头或者含有超过一个Host头的HTTP/1.1 请求,可能会收到400(Bad Request)状态码。
Set-Cookie
格式
Set-Cookie: <cookie-name>=<cookie-value>
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<non-zero-digit>
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
Set-Cookie: <cookie-name>=<cookie-value>; Secure
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Strict
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Lax
# 也支持多设备,例如:
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly
其中:
:一个 cookie 开始于一个名称/值对:
: 可以是除了控制字符 (CTLs)、空格 (spaces) 或制表符 (tab)之外的任何 US-ASCII 字符。同时不能包含以下分隔字符: ( ) < > @ , ; : \ " / [ ] ? = { }.
: 可选,如果存在的话,那么需要包含在双引号里面。支持除了控制字符(CTLs)、空格(whitespace)、双引号(double quotes)、逗号(comma)、分号(semicolon)以及反斜线(backslash)之外的任意 US-ASCII 字符。关于编码:许多应用会对 cookie 值按照URL编码(URL encoding)规则进行编码,但是按照 RFC 规范,这不是必须的。不过满足规范中对于 所允许使用的字符的要求是有用的。__Secure-
前缀:以 __Secure- 为前缀的 cookie(其中连接符是前缀的一部分),必须与 secure 属性一同设置,同时必须应用于安全页面(即使用 HTTPS 访问的页面)。__Host-
前缀: 以 __Host- 为前缀的 cookie,必须与 secure 属性一同设置,必须应用于安全页面(即使用 HTTPS 访问的页面),必须不能设置 domain 属性 (也就不会发送给子域),同时 path 属性的值必须为“/”。Expires=
:可选,cookie 的最长有效时间,形式为符合 HTTP-date 规范的时间戳。参考 Date 可以获取详细信息。如果没有设置这个属性,那么表示这是一个会话期 cookie 。一个会话结束于客户端被关闭时,这意味着会话期 cookie 在彼时会被移除。然而,很多Web浏览器支持会话恢复功能,这个功能可以使浏览器保留所有的tab标签,然后在重新打开浏览器的时候将其还原。与此同时,cookie 也会恢复,就跟从来没有关闭浏览器一样。
Max-Age=
:可选,在 cookie 失效之前需要经过的秒数。秒数为 0 或 -1 将会使 cookie 直接过期。一些老的浏览器(ie6、ie7 和 ie8)不支持这个属性。对于其他浏览器来说,假如二者 (指 Expires 和Max-Age) 均存在,那么 Max-Age 优先级更高。
Domain=
:可选,指定 cookie 可以送达的主机名。假如没有指定,那么默认值为当前文档访问地址中的主机部分(但是不包含子域名)。与之前的规范不同的是,域名之前的点号会被忽略。假如指定了域名,那么相当于各个子域名也包含在内了。
Path=
:可选,指定一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie 首部。字符 %x2F (“/”) 可以解释为文件目录分隔符,此目录的下级目录也满足匹配的条件(例如,如果 path=/docs,那么 “/docs”, “/docs/Web/” 或者 “/docs/Web/HTTP” 都满足匹配的条件)。
Secure
:可选,一个带有安全属性的 cookie 只有在请求使用SSL和HTTPS协议的时候才会被发送到服务器。然而,保密或敏感信息永远不要在 HTTP cookie 中存储或传输,因为整个机制从本质上来说都是不安全的,比如前述协议并不意味着所有的信息都是经过加密的。
注意:非安全站点(http:)已经不能再在 cookie 中设置 secure 指令了(在Chrome 52+ and Firefox 52+ 中新引入的限制)。
HttpOnly
:可选,设置了 HttpOnly 属性的 cookie 不能使用 JavaScript 经由 Document.cookie 属性、XMLHttpRequest 和 Request APIs 进行访问,以防范跨站脚本攻击(XSS (en-US))。
SameSite=Strict
:可选,允许服务器设定一则 cookie 不随着跨域请求一起发送,这样可以在一定程度上防范跨站请求伪造攻击(CSRF)。
例如:
会话期
会话期 cookies 将会在客户端关闭时被移除。 会话期 cookie 不设置 Expires 或 Max-Age 指令。注意浏览器通常支持会话恢复功能:
Set-Cookie: sessionid=38afes7a8; HttpOnly; Path=/
持久化
持久化 Cookie 不会在客户端关闭时失效,而是在特定的日期(Expires)或者经过一段特定的时间之后(Max-Age)才会失效。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
非法域
Set-Cookie: qwerty=219ffwef9w0f; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2019 00:00:00 GMT
上文已经说过,尽管我们用了一些加密算法对关键数据及逆行加密,然而这些算法是可以解密的,安全性并不高。因此我们在Json web token 中采取重要数据不存放在令牌中,仅仅是将 令牌(Token) 制作为一个交给客户端的签名工具,并且为了安全我们需要定更新 Token,以免 Token 被攻击者劫持。
即使如此,安全漏洞也是显然的。那就是正好这个攻击者获取了当前有效期内的 Token ,那么这时不就又能够伪装用户进行对攻击者自己接下来的操作进行签名了吗?
基于以上各种不安全因素,HTTPS协议就应运而生。HTTPS 协议是 HTTP 协议的加密版本。它通常使用 SSL (en-US) 或者 TLS 来加密客户端和服务器之间所有的通信 。这安全的链接允许客户端与服务器安全地交换敏感的数据。
SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。SSL协议可分为两层:
TLS 协议是 SSL 协议的升级版,事实上,我们现在普片使用的都是 TLS ,只是习惯上我们仍称之为SSL。SSL包含记录层(Record Layer)和传输层,记录层协议确定传输层数据的封装格式。传输层安全协议使用X.509认证,之后利用非对称加密演算来对通信方做身份认证,之后交换对称密钥作为会谈密钥(Session key)。这个会谈密钥是用来将通信两方交换的数据做加密,保证两个应用间通信的保密性和可靠性,使客户与服务器应用之间的通信不被攻击者窃听。TLS协议的优势是与高层的应用层协议(如HTTP、FTP、Telnet等)无耦合。应用层协议能透明地运行在TLS协议之上,由TLS协议进行创建加密通道需要的协商和认证。应用层协议传送的数据在通过TLS协议时都会被加密,从而保证通信的私密性。
HTTP协议传输的数据都是未加密的,这就意味着用户填写的密码、帐号、交易记录等机密信息都是明文,随时可能被泄露、窃取、篡改,被黑客加以利用,因此使用HTTP协议传输隐私信息非常不安全。
HTTPS是一种基于SSL协议的网站加密传输协议,网站安装SSL证书后,使用HTTPS加密协议访问,可激活客户端浏览器到网站服务器之间的“SSL加密通道”(SSL协议),实现高强度双向加密传输,防止传输数据被泄露或篡改。简单讲HTTPS=HTTP+SSL,是HTTP的安全版。
要将HTTP协议更换为HTTPS协议,就需要花点小钱购买一个SSL证书并部署在网站对应的服务器上。SSL证书采用SSL协议进行通信,SSL证书部署到服务器后,服务器端的访问将启用HTTPS协议。您的网站将会通过HTTPS加密协议来传输数据,可帮助服务器端和客户端之间建立加密链接,从而保证数据传输的安全。
如今可以很方便地从各大云服务提供商处购买SSL证书,如 阿里云、华为云、腾讯云、百度云、京东云。
在搜索云服务提供商的SSL购买页面,如华为云SSL https://www.huaweicloud.com/product/scm.html:
购买完成后,可以登陆控制台后,展开左侧的“服务列表”,在输入框中输入SSL即可找到SSL页面的链接。
进入之后可以对你购买过的SSL证书进行管理:
这个是更具你的需求来决定的,你甚至可以使用免费证书,并且你在华为云、百度云等各大服务提供商处都能获得免费证书。以华为云为例,在笔者成文时华为云提供的免费于付费的证书差别如下:
区别项 | 免费证书 | 收费证书 |
---|---|---|
安全等级 | 一般 | 高 |
证书运行环境的兼容性 | 一般 | 高 |
CA中心对证书的安全保险赔付 | 不支持 | 支持 |
证书数量限制 | 每个自然年20张 | 不限制 |
支持保护的网站域名类型 | 仅支持保护一个单域名 | 支持保护单域名、多域名、泛域名 |
支持绑定IP地址 | 不支持 | GlobalSign品牌的OV型证书支持 |
支持的证书类型 | 仅DV | DV、OV、EV |
人工客服支持 | 不支持 | 支持 |
请参考:https://zh.wikihow.com/%E5%AE%89%E8%A3%85SSL%E8%AF%81%E4%B9%A6
HTTP严格传输安全协议
(HSTS
/ HTTP Strict Transport Security)HSTS可以用来抵御SSL剥离攻击。SSL剥离攻击是中间人攻击的一种。SSL剥离的实施方法是阻止浏览器与服务器创建HTTPS连接。它的前提是用户很少直接在地址栏输入https://,用户总是通过点击链接或3xx重定向,从HTTP页面进入HTTPS页面。所以攻击者可以在用户访问HTTP页面时替换所有https://开头的链接为http://,达到阻止HTTPS的目的。
一个网站接受一个HTTP
的请求,然后跳转到HTTPS
,用户可能在开始跳转前,通过没有加密的方式和服务器对话,比如,用户输入http://foo.com或者直接foo.com。这样存在中间人攻击潜在威胁,跳转过程可能被恶意网站利用来直接接触用户信息,而不是原来的加密信息。
例如:你连接到一个免费WiFi接入点,然后访问你的网上银行,并且支付一些订单。不幸的是你接入的WiFi实际上是黑客的笔记本热点,他们拦截了你最初的HTTP请求,然后跳转到一个你银行网站一模一样的钓鱼网站。 现在,你的隐私数据暴露给黑客了。
而HTTP严格传输安全协议
就解决了这个问题。只要你通过HTTPS请求访问银行网站,并且银行网站配置好Strict Transport Security,你的浏览器知道自动使用HTTPS请求,这可以阻止黑客的中间人攻击的把戏。
网站通过 HTTP Strict Transport Security 通知浏览器,这个网站禁止使用HTTP方式加载,浏览器应该自动把所有尝试使用HTTP的请求自动替换为HTTPS请求。
网站可以选择使用HSTS策略,来让浏览器强制使用HTTPS与网站进行通信,以减少会话劫持风险。对于只能通过 HTTPS 访问的网站,你可以通过设置 Strict-Transport-Security 请求头 来指示现代浏览器拒绝通过不安全的连接连接到你的域名(在给定的时间内)。这可以减少你受到一些 SSL 剥离中间人(MITM)的攻击。
从浏览器端看:
在Django中已经为我们提供了现成的 HSTS 解决方案。
SECURE_HSTS_SECONDS
在Django项目的全局配置文件settings.py
中可以添加该配置项,若不配置时默认为 0
。如果设置为非零的整数值,则 Django 中间件 SecurityMiddleware
会对所有尚未设置 HTTP 严格传输安全 头的响应进行设置。
SECURE_PROXY_SSL_HEADER
在Django项目的全局配置文件settings.py
中可以添加该配置项,表示请求的 HTTP 标头/值组合的元组是安全的。这控制了请求对象is_secure()
方法的行为。
无法保护首次访问
用户 首次访问
某网站是不受HSTS保护的。
这是因为首次访问时,浏览器还未收到HSTS,所以仍有可能通过明文HTTP来访问。
Strict-Transport-Security
格式:
Strict-Transport-Security: max-age=<expire-time>
Strict-Transport-Security: max-age=<expire-time>; includeSubDomains
Strict-Transport-Security: max-age=<expire-time>; preload
其中:
max-age=
:设置在浏览器收到这个请求后的秒的时间内凡是访问这个域名下的请求都使用HTTPS请求。includeSubDomains
:可选,如果这个可选的参数被指定,那么说明此规则也适用于该网站的所有子域名。preload
:可选,查看 预加载 HSTS 获得详情。不是标准的一部分。例如:
现在和未来的所有子域名会自动使用 HTTPS 连接长达一年。同时阻止了只能通过 HTTP 访问的内容:
Strict-Transport-Security: max-age=31536000; includeSubDomains
HTTP Strict Transport Security(通常简称为HSTS)是一个安全功能,它告诉浏览器只能通过HTTPS访问当前资源,而不是HTTP。
这里介绍前端权限控制更为具体的实现。虽是以实践为话题,不过仍然需要补充很多相关的理论。
导航表示路由正在发生改变,路由权限的目的是依据权限来控制路由正在发生改变时的改变效果。因此在前端权限控制的实现中,采用导航守卫来控制路由权限。以vue-router为例,其中提供了导航守卫的API。
beforeEach
当一个导航触发时,全局前置守卫按照创建顺序调用。
(守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。)
beforeResolve
导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
afterEach
后置钩子在概念上并不是守卫,没有执行路由守卫操作,即它不会接受 next 函数也 不会改变导航 本身,只是每一次路由跳转后的回调。后置钩子用于路由跳转后的相应页面操作(勾起跳转后的的一些行为,如进度条的完成等等)。
import router from './router'
import store from './store'
import { getToken } from '@/utils/auth' // 从 cookie 获取令牌(token)
import getPageTitle from '@/utils/get-page-title'
// 可直接访问的路由白名单
const whiteList = ['/login']
// 全局前置路由守卫
router.beforeEach(async(to, from, next) => {
// 设定页面标题
document.title = getPageTitle(to.meta.title)
// 判断用户是否已经登陆
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// 如果已经登陆则重定向到 home 页
next({ path: '/' })
} else {
const hasGetUserInfo = store.getters.name
if (hasGetUserInfo) {
next()
} else {
try {
// 获取用户信息
await store.dispatch('user/getInfo')
next()
} catch (error) {
// 移除 token 并转到 login 页面以重新登陆
await store.dispatch('user/resetToken')
next(`/login?redirect=${to.path}`)
}
}
}
} else {
/* 没有 token*/
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单中,直接进入这些页面
next()
} else {
// 其它没有访问权限的页面则重定向到登陆页面
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
// 全局后置路由守卫
router.afterEach(() => {
// do something...
})
beforeEnter
路由独享的守卫仅在被守卫的单条路由发生改变时产生效果。
beforeRouteEnter
,beforeRouteUpdate
,beforeRouteLeave
组件内的守卫在路由组件内直接定义。
- beforeRouteEnter:在渲染该组件的对应路由被 confirm 前调用;
- beforeRouteUpdate (2.2 新增):在当前路由改变,但是该组件被复用时调用;
- beforeRouteLeave:导航离开该组件的对应路由时调用。
移动端设备上的客户端软件往往是由开发公司专门定制的特定应用相比于浏览器来说能够实现更加安全的登陆过程,同时由于移动设备普及,这相比于基于表单的登陆具有更好的用户体验。
这也就是需要先在一个移动设备上进行登陆,一般是专用应用而不能是移动端的浏览器页面。
在阅读这部分内容前请确保你已经完全掌握认证的含义,关于认证已在文章开篇有详细的讲解。
一旦前端对于从服务端发来的 Key ,便对其进行编码成一个二维码,如 qrCode
,用户用户在移动端进行扫码:
安全起见,二维码是有有效期的,可以手动设置一个有效期,也可以给定一个刷新按钮来从服务器更新二维码信息并在客户端中重新渲染。
这一阶段中:
对于 服务端而言,它时刻监听着来自多个移动端和Web客户端发过来的请求(可能有多个用户同时在进行认证操作)。 一旦监听到了移动端传来的key与用户身份签名,意味着之前发给Web客户端用户生成二维码的Key被移动端扫描登陆中,这时需要响应此Key对应的客户端的轮询。可想而知为了额能确定相应的到底是哪一个Web客户端的轮询,这就要求服务端的Key必须是全局唯一的。
当客户端在某次轮询后得到了签名工具(令牌 token)后,意味着以后可以使用该令牌对需要回传服务器的数据进行身份签名。这时可以提示用户“扫码登陆成功”,并跳转到相关的页面。但是需要注意的是,处于安全考虑仍不建议将与用户相关的重要数据存储在 token 中,因为这样容易导致 token 被破解带来的用户信息泄露。此处妥当的做法仍然是,将 token 用作某个时间端内在服务端标识某特定用户的键,但其中所有与用户相关的信息(值)仍然是存储在服务端的。
OpenID 按照最大自由方式授权,使用它不需要任何费用任何注册或者许可证。任何网站都可以使用OpenID来作为用户登录的一种方式,任何网站也都可以作为OpenID身份提供者。也就是说,OpenID是一种去中心化的认证方式。对于支持OpenID的网站,用户不需要记住像用户名和密码这样的传统验证标记。取而代之的是,他们只需要预先在一个作为 OpenID 身份提供者(identity provider, IdP)的网站上注册。
只需要输入你注册好的 OpenID 用户名,然后你登录的网站会跳转到你的 OpenID 服务网站,在你的 OpenID 服务网站输入密码(或者其它需要填写的信息)验证通过后,你会回到登录的网站并且已经成功登录。OpenID 系统可以应用于所有需要身份验证的地方,既可以应用于单点登录系统,也可以用于共享敏感数据时的身份认证。
授权代码流程经过以下步骤:
授权服务器必须验证收到的请求如下:
授权服务器必须根据 OAuth 2.0 规范验证所有 OAuth 2.0 参数。
验证是否存在范围参数并包含openid范围值。(如果不存在openid范围值,则该请求可能仍然是有效的 OAuth 2.0 请求,但不是 OpenID Connect 请求。)
授权服务器必须验证所有必需的参数都存在并且它们的使用符合本规范。
如果使用ID 令牌的特定值请求子(主题)声明,则授权服务器必须仅在由该子值标识的最终用户与授权服务器具有活动会话或已被验证为请求的结果。授权服务器不得为不同的用户回复 ID 令牌或访问令牌,即使他们与授权服务器有活动会话。如果实现支持声明参数,则可以使用 id_token_hint参数或通过请求第 特定声明值 来发出此类请求。
如OAuth 2.0 [RFC6749] 中所述,授权服务器应该忽略无法识别的请求参数。
标准声明(必须支持):OpenID 提供者直接声明的声明。表示为 JSON 对象中的成员。Claim Name 是成员名称,Claim Value 是成员值。以下是包含正常声明的非规范性回复:
{
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"email": "[email protected]",
"picture": "http://example.com/janedoe/me.jpg"
}
聚合声明(Aggregated Claims,可选):在这个非规范示例中,来自声明提供者 A 的声明与 OpenID 提供者持有的其他声明相结合,来自声明提供者 A 的声明作为聚合声明返回。
{
"address": {
"street_address": "1234 Hollywood Blvd.",
"locality": "Los Angeles",
"region": "CA",
"postal_code": "90210",
"country": "US"
},
"phone_number": "+1 (310) 123-4567"
}
{
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"birthdate": "0000-03-22",
"eye_color": "blue",
"email": "[email protected]",
"_claim_names": {
"address": "src1",
"phone_number": "src1"
},
"_claim_sources": {
"src1": {"JWT": "jwt_header.jwt_part2.jwt_part3"}
}
}
分布式声明(Distributed Claims,可选):由 OpenID 提供者以外的声明提供者声明但由 OpenID 提供者作为引用返回的声明。在这个非规范性示例中,OpenID 提供者将其持有的正常声明与对两个不同声明提供者 B 和 C 持有的声明的引用相结合,并将对 B 和 C 持有的一些声明的引用合并为分布式声明。
例如,在此示例中,这些关于 Jane Doe 的索赔由索赔提供者 B(Jane Doe 的银行)持有:
{
"shipping_address": {
"street_address": "1234 Hollywood Blvd.",
"locality": "Los Angeles",
"region": "CA",
"postal_code": "90210",
"country": "US"},
"payment_info": "Some_Card 1234 5678 9012 3456",
"phone_number": "+1 (310) 123-4567"
}
同样在此示例中,关于 Jane Doe 的索赔由索赔提供者 C(一家信用机构)持有:
{
"credit_score": 650
}
OpenID 提供者返回 Jane Doe 的声明以及来自声明提供者 B 和声明提供者 C 的分布式声明的引用,方法是发送访问令牌和可以从中检索分布式声明的位置的 URL:
{
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"email": "[email protected]",
"birthdate": "0000-03-22",
"eye_color": "blue",
"_claim_names": {
"payment_info": "src1",
"shipping_address": "src1",
"credit_score": "src2"
},
"_claim_sources": {
"src1": {"endpoint":
"https://bank.example.com/claim_source"},
"src2": {"endpoint":
"https://creditagency.example.com/claims_here",
"access_token": "ksj3n283dke"}
}
}
聚合和分布式声明通过使用包含声明的 JSON 对象的特殊_claim_names
和 _claim_sources
成员来表示:
_claim_names
:JSON 对象,其成员名称是聚合和分布式声明的声明名称。成员值是对_claim_sources
成员中成员名称的引用,可以从中检索实际的声明值。_claim_sources
:JSON 对象,其成员名称由 _claim_names成员的成员值引用。成员值包含聚合声明集或分布式声明的参考位置。成员值可以具有以下格式之一,具体取决于它是提供聚合声明还是分布式声明:
endpoint
:必需。可以从中检索相关声明的 OAuth 2.0 资源端点。端点 URL 必须将声明作为 JWT 返回。access_token
:可选,访问令牌允许使用OAuth 2.0 Bearer Token Usage
[RFC6750] 协议从端点 URL 检索声明。声明应该使用授权请求头域来请求,声明提供者必须支持这个方法。如果访问令牌不可用,RP 可能需要带外检索访问令牌或使用声明提供者和 RP 之间预先协商的访问令牌,或者声明提供者可以重新验证最终用户和/或重新授权RP。这些声明可以在 UserInfo 响应中或在 ID 令牌中请求返回它们。
注册会员定义:
成员 | 类型 | 描述 |
---|---|---|
sub | string |
Subject - 发行方最终用户的标识符。 |
name | string |
以可显示形式显示的最终用户(End-User)全名,包括所有名称部分,可能包括标题和后缀,根据最终用户的区域设置和偏好进行排序 |
given_name | string |
最终用户的给定名称或名字。请注意,在某些文化中,人们可以有多个名字;所有的名字都可以用空格分隔。 |
family_name | string |
最终用户的姓氏或姓氏。请注意,在某些文化中,人们可以有多个姓氏或没有姓氏;all 都可以存在,名称由空格字符分隔。 |
middle_name | string |
最终用户的中间名。请注意,在某些文化中,人们可以有多个中间名;all 都可以存在,名称由空格字符分隔。另请注意,在某些文化中,不使用中间名。 |
nickname | string |
最终用户的临时名称,可能与 given_name 相同也可能不同。例如,Mike 的昵称值 可能与Michael 的 given_name 值一起返回。 |
preferred_username | string |
最终用户希望在 RP 上引用的简写名称,例如 janedoe或j.doe。此值可以是任何有效的 JSON 字符串,包括特殊字符,例如 @ , / 或空格。 RP 绝不能依赖此值是唯一的。 |
profile | string |
最终用户个人资料页面的 URL。这个网页的内容应该是关于最终用户的。 |
picture | string |
最终用户个人资料图片的 URL。此 URL 必须引用图像文件(例如,PNG、JPEG 或 GIF 图像文件),而不是包含图像的网页。请注意,此 URL 应该专门引用适合在描述最终用户时显示的最终用户的个人资料照片,而不是最终用户拍摄的任意照片。 |
website | string |
最终用户的网页或博客的 URL。此网页应包含最终用户或最终用户所属组织发布的信息。 |
string |
最终用户的首选电子邮件地址。它的值必须符合RFC 5322 [RFC5322] addr-spec 语法。RP 绝不能依赖此值是唯一的。 | |
email_verified | boolean |
如果最终用户的电子邮件地址已经过验证,则为true ;否则为false 。当此 Claim Value 为true 时,这意味着 OP 采取了肯定的措施来确保在执行验证时此电子邮件地址由最终用户控制。验证电子邮件地址的方式是特定于上下文的,并且取决于各方在其中运作的信任框架或合同协议。 |
gender | string |
最终用户的性别。本规范定义的值为 female 和 male 。当定义的值都不适用时,可以使用其他值。 |
birthdate | string |
最终用户的生日,以 ISO 8601:2004 [ISO8601‑2004] YYYY-MM-DD 格式表示。年份可能是0000,表示它被省略。为了只表示年份,允许使用YYYY格式。请注意,根据底层平台的日期相关功能,仅提供年份可能会导致月份和日期不同,因此实施者需要考虑这一因素才能正确处理日期。 |
zoneinfo | string |
来自 zoneinfo [zoneinfo]时区数据库的字符串,表示最终用户的时区。例如, Europe/Paris 或 America/Los_Angeles . |
locale | string |
最终用户的区域设置,表示为 BCP47 [RFC5646] 语言标记。这通常是小写的ISO 639-1 Alpha-2 [ISO639‑1] 语言代码和大写的ISO 3166-1 Alpha-2 [ISO3166‑1] 国家代码,用破折号分隔。例如, en-US 或fr-CA 。作为兼容性说明,一些实现使用下划线作为分隔符而不是破折号,例如 en_US;依赖方也可以选择接受这种语言环境语法。 |
phone_number | string |
最终用户的首选电话号码。建议将 [E.164] 作为本声明的格式,例如+1 (425) 555-1212 或+56 (2) 687 2400 。如果电话号码包含分机,建议使用 [RFC 3966] 分机语法来表示分机,例如+1 (604) 555-1234;ext=5678 。 |
phone_number_verified | boolean |
如果最终用户的电话号码已经过验证,则为真;否则为假。当此 Claim Value 为true时,这意味着 OP 采取了肯定的措施来确保此电话号码在执行验证时由最终用户控制。验证电话号码的方式是特定于上下文的,并且取决于各方在其中运作的信任框架或合同协议。如果为真,phone_number 声明必须采用 E.164 格式,并且任何扩展必须以 RFC 3966 格式表示。 |
address | JSON 对象 |
最终用户的首选邮寄地址。地址成员的值是一个 JSON [RFC4627]结构,其中包含第 5.1.1 节中定义的部分或全部成员。 |
updated_at | number |
最终用户信息的最后更新时间。它的值是一个 JSON 数字,表示从 1970-01-01T0:0:0Z 到日期/时间的秒数,以 UTC 度量。 |
地址声明:
地址声明代表一个物理邮寄地址。根据可用信息和最终用户的隐私偏好,实现可能仅返回地址字段的子集。例如,可能会返回国家和地区,而不返回更细粒度的地址信息。
实现可以只返回完整地址作为格式化子字段中的单个字符串,或者它们可以使用其他子字段只返回单个组件字段,或者它们可以返回两者。如果两个变体都返回,它们应该描述相同的地址,格式化的地址指示组件字段是如何组合的。
locality
:城市或地区。region
:州、省、州或地区组成部分。postal_code
:邮政编码或邮政编码组件。country
:国家名称组件。声明请求 的 userinfo
和 id_token
成员 都是 JSON 对象,请求的单个声明的名称作为成员名称。成员值必须是以下之一:
"given_name": null
"auth_time": {"essential": true}
可用于指定返回 auth_time 声明值
是必不可少的。如果值为 false
,则表明它是自愿声明。默认值为 false
。"sub": {"value": "248289761001"}
"acr": {"essential": true,"values": ["urn:mace:incommon:iap:silver", "urn:mace:incommon:iap:bronze"]}
OpenID 基金会是一个由致力于启用、推广和保护 OpenID 技术的个人和公司组成的非营利性国际标准化组织。是一个代表开发者、供应商和用户的开放社区的公共信托组织。OIDF通过提供所需的基础设施来帮助社区,并帮助促进和支持 OpenID 的扩展采用。OPenID 基金会网站为:https://openid.net/:
OAuth协议 为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的。
OAuth 2.0是OAuth目前的版本(RFC 6749)。
“授权类型”:是指应用程序获取访问令牌
的方式,常见的如:
Web 和移动应用程序使用授权码授权类型时,首先要求应用程序启动浏览器才能开始流程。其流程为:
授权码
将用户重定向回应用程序;授权码
交换为访问令牌
;PKCE授权是一种增强的授权流程,用于保护公共客户端免受授权码攻击。PKCE代表“Proof Key for Code Exchange”,它通过在授权请求中添加一个随机的密钥来增强授权流程的安全性。这个密钥只有客户端知道,因此攻击者无法使用授权码来获取访问令牌。
PKCE授权的流程如下:
PKCE授权的原理是,客户端在授权请求中添加一个随机的密钥,该密钥只有客户端知道。授权服务器将该密钥与授权码相关联,并将其存储在服务器上。当客户端使用授权码请求访问令牌时,它必须提供相同的密钥。令牌服务器将验证该密钥是否与授权码相关联,并且只有在密钥匹配时才会颁发访问令牌。
客户凭证授予是OAuth的一种授权方式,它允许客户端使用自己的凭证向授权服务器请求访问令牌。客户凭证包括客户端ID和客户端密钥,它们是由授权服务器颁发给客户端的。客户端使用客户凭证向授权服务器请求访问令牌时,需要提供客户端ID和客户端密钥,以证明自己的身份。
设备代码授权是一种用于在没有浏览器的情况下授权的流程。它适用于智能电视、智能家居等设备。设备代码授权的流程如下:
设备代码授权的原理是通过设备代码和用户代码来进行授权。设备代码是设备生成的,用户代码是授权服务器生成的。设备代码和用户代码的组合是唯一的,用于标识设备和用户。用户在另一设备上进行授权时,需要输入设备代码和用户代码,授权服务器通过验证设备代码和用户代码来授权。
## 8.3 微信登陆
这里说的微信登陆不是指登陆微信账号本身,而是指通过调用已经登陆的移动设备上的微信扫码登陆第三方网站的过程。目前微信采用的是 OAuth 2.0协议
。
Python
后端 中的 JSON Web Token
实现有一些比较好的开源项目帮我们完成了其中的很多事情,因此这里我们将推荐这些项目,介绍他们的使用方法。
jwk 模块实现JSON Web Key标准。JSON Web Key 由 JWK 对象表示,该模块中也提供了相关的实用程序类和函数。
pip install jwcrypto
from jwcrypto import jwk
key = jwk.JWK.generate(kty='oct', size=256)
key.export()
Out[]:
'{"k":"X6TBlwY2so8EwKZ2TFXM7XHSgWBKQJhcspzYydp5Y-o","kty":"oct"}'
jwk.JWK.generate(kty='RSA', size=2048)
key = jwk.JWK.generate(kty='EC', crv='P-256')
key.export(private_key=False)
Out[]:
'{"y":"VYlYwBfOTIICojCPfdUjnmkpN-g-lzZKxzjAoFmDRm8",
"x":"3mdE0rODWRju6qqU01Kw5oPYdNxBOMisFvJFH1vEu9Q",
"crv":"P-256","kty":"EC"}'
expkey = {"y":"VYlYwBfOTIICojCPfdUjnmkpN-g-lzZKxzjAoFmDRm8",
"x":"3mdE0rODWRju6qqU01Kw5oPYdNxBOMisFvJFH1vEu9Q",
"crv":"P-256","kty":"EC"}
key = jwk.JWK(**expkey)
with open("public.pem", "rb") as pemfile:
key = jwk.JWK.from_pem(pemfile.read())
jws 模块实现了JSON Web 签名标准。JSON Web 签名由 JWS 对象表示,该模块中也提供了相关的实用程序类和函数。
jwe 模块实现了JSON Web 加密标准。JSON Web 加密由 JWE 对象表示,该模块中也提供了相关的实用程序类和函数。
jwt 模块实现JSON Web Token标准。JSON Web Token 由 JWT 对象表示,该模块中也提供了相关的实用程序类和函数。
from jwcrypto import jwt, jwk
key = jwk.JWK(generate='oct', size=256)
key.export()
Out[]:
'{"k":"Wal4ZHCBsml0Al_Y8faoNTKsXCkw8eefKXYFuwTBOpA","kty":"oct"}'
Token = jwt.JWT(header={"alg": "HS256"}, claims={"info": "I'm a signed token"})
Token.make_signed_token(key)
Token.serialize()
u'eyJhbGciOiJIUzI1NiJ9.eyJpbmZvIjoiSSdtIGEgc2lnbmVkIHRva2VuIn0.rjnRMAKcaRamEHnENhg0_Fqv7Obo-30U4bcI_v-nfEM'
Out[]:
Etoken = jwt.JWT(header={"alg": "A256KW", "enc": "A256CBC-HS512"}, claims=Token.serialize())
Etoken.make_encrypted_token(key)
Etoken.serialize()
Out[]:
u'eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0.ST5RmjqDLj696xo7YFTFuKUhcd3naCrm6yMjBM3cqWiFD6U8j2JIsbclsF7ryNg8Ktmt1kQJRKavV6DaTl1T840tP3sIs1qz.wSxVhZH5GyzbJnPBAUMdzQ.6uiVYwrRBzAm7Uge9rEUjExPWGbgerF177A7tMuQurJAqBhgk3_5vee5DRH84kHSapFOxcEuDdMBEQLI7V2E0F57-d01TFStHzwtgtSmeZRQ6JSIL5XlgJouwHfSxn9Z_TGl5xxq4TksORHED1vnRA.5jPyPWanJVqlOohApEbHmxi3JHp1MXbmvQe2_dVd8FI'
from jwcrypto import jwt, jwk
k = {"k": "Wal4ZHCBsml0Al_Y8faoNTKsXCkw8eefKXYFuwTBOpA", "kty": "oct"}
key = jwk.JWK(**k)
e = u'eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0.ST5RmjqDLj696xo7YFTFuKUhcd3naCrm6yMjBM3cqWiFD6U8j2JIsbclsF7ryNg8Ktmt1kQJRKavV6DaTl1T840tP3sIs1qz.wSxVhZH5GyzbJnPBAUMdzQ.6uiVYwrRBzAm7Uge9rEUjExPWGbgerF177A7tMuQurJAqBhgk3_5vee5DRH84kHSapFOxcEuDdMBEQLI7V2E0F57-d01TFStHzwtgtSmeZRQ6JSIL5XlgJouwHfSxn9Z_TGl5xxq4TksORHED1vnRA.5jPyPWanJVqlOohApEbHmxi3JHp1MXbmvQe2_dVd8FI'
ET = jwt.JWT(key=key, jwt=e)
ST = jwt.JWT(key=key, jwt=ET.claims)
ST.claims
Out[]:
u'{"info":"I\'m a signed token"}'
NodeJS
后端 中的 JSON Web Token
实现同样,我们将推荐一些比较好的开源项目并介绍用法。
点击跳转:https://github.com/auth0/node-jsonwebtoken
使用npm
npm install jsonwebtoken
使用yarn
yarn add jsonwebtoken
点击跳转:https://github.com/panva/jose
使用npm
npm install jose
使用yarn
yarn add jose