gitbook/Go 语言项目开发实战/docs/398410.md
2022-09-03 22:05:03 +08:00

278 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 25 | 认证机制:应用程序如何进行访问认证?
你好,我是孔令飞,今天我们来聊聊如何进行访问认证。
保证应用的安全是软件开发的最基本要求我们有多种途径来保障应用的安全例如网络隔离、设置防火墙、设置IP黑白名单等。不过在我看来这些更多是从运维角度来解决应用的安全问题。作为开发者我们也可以从软件层面来保证应用的安全这可以通过认证来实现。
这一讲我以HTTP服务为例来给你介绍下当前常见的四种认证方法Basic、Digest、OAuth、Bearer。还有很多基于这四种方法的变种这里就不再介绍了。
IAM项目使用了Basic、Bearer两种认证方法。这一讲我先来介绍下这四种认证方法下一讲我会给你介绍下IAM项目是如何设计和实现访问认证功能的。
## 认证和授权有什么区别?
在介绍四种基本的认证方法之前,我想先带你区分下认证和授权,这是很多开发者都容易搞混的两个概念。
* **认证Authentication英文缩写authn**:用来验证某个用户是否具有访问系统的权限。如果认证通过,该用户就可以访问系统,从而创建、修改、删除、查询平台支持的资源。
* **授权Authorization英文缩写authz**:用来验证某个用户是否具有访问某个资源的权限,如果授权通过,该用户就能对资源做增删改查等操作。
这里,我通过下面的图片,来让你明白二者的区别:
![](https://static001.geekbang.org/resource/image/8b/96/8b63cc7a624dbdb32b37898180a37596.jpg?wh=2248x1747)
图中,我们有一个仓库系统,用户 james、colin、aaron分别创建了Product-A、Product-B、Product-C。现在用户colin通过用户名和密码认证成功登陆到仓库系统中但他尝试访问Product-A、Product-C失败因为这两个产品不属于他授权失败但他可以成功访问自己创建的资源Product-B授权成功。由此可见**认证证明了你是谁,授权决定了你能做什么。**
上面,我们介绍了认证和授权的区别。那么接下来,我们就回到这一讲的重心:应用程序如何进行访问认证。
## 四种基本的认证方式
常见的认证方式有四种,分别是 Basic、Digest、OAuth 和 Bearer。先来看下Basic认证。
### Basic
Basic认证基础认证是最简单的认证方式。它简单地将`用户名:密码`进行base64编码后放到HTTP Authorization Header中。HTTP请求到达后端服务后后端服务会解析出Authorization Header中的base64字符串解码获取用户名和密码并将用户名和密码跟数据库中记录的值进行比较如果匹配则认证通过。例如
```
$ basic=`echo -n 'admin:Admin@2021'|base64`
$ curl -XPOST -H"Authorization: Basic ${basic}" http://127.0.0.1:8080/login
```
通过base64编码可以将密码以非明文的方式传输增加一定的安全性。但是base64不是加密技术入侵者仍然可以截获base64字符串并反编码获取用户名和密码。另外即使Basic认证中密码被加密入侵者仍可通过加密后的用户名和密码进行重放攻击。
所以Basic认证虽然简单但极不安全。使用Basic认证的唯一方式就是将它和SSL配合使用来确保整个认证过程是安全的。
IAM项目中为了支持前端通过用户名和密码登录仍然使用了Basic认证但前后端使用HTTPS来通信保证了认证的安全性。
这里需要注意,在设计系统时,要遵循一个通用的原则:**不要在请求参数中使用明文密码,也不要在任何存储中保存明文密码。**
### Digest
Digest认证摘要认证是另一种 HTTP 认证协议它与基本认证兼容但修复了基本认证的严重缺陷。Digest具有如下特点
* 绝不会用明文方式在网络上发送密码。
* 可以有效防止恶意用户进行重放攻击。
* 可以有选择地防止对报文内容的篡改。
摘要认证的过程见下图:
![](https://static001.geekbang.org/resource/image/c6/b5/c693394977b4f91ae14b8c06f69056b5.jpg?wh=2248x1872)
在上图中,完成摘要认证需要下面这四步:
1. 客户端请求服务端的资源。
2. 在客户端能够证明它知道密码从而确认其身份之前,服务端认证失败,返回`401 Unauthorized`,并返回`WWW-Authenticate`头,里面包含认证需要的信息。
3. 客户端根据`WWW-Authenticate`头中的信息选择加密算法并使用密码随机数nonce计算出密码摘要response并再次请求服务端。
4. 服务器将客户端提供的密码摘要与服务器内部计算出的摘要进行对比。如果匹配就说明客户端知道密码认证通过并返回一些与授权会话相关的附加信息放在Authorization-Info中。
`WWW-Authenticate`头中包含的信息见下表:
![](https://static001.geekbang.org/resource/image/59/9e/593e48602465b84165678bdc98467d9e.jpg?wh=2248x1755)
虽然使用摘要可以避免密码以明文方式发送,一定程度上保护了密码的安全性,但是仅仅隐藏密码并不能保证请求是安全的。因为请求(包括密码摘要)仍然可以被截获,这样就可以重放给服务器,带来安全问题。
为了防止重放攻击服务器向客户端发送了密码随机数noncenonce每次请求都会变化。客户端会根据nonce生成密码摘要这种方式可以使摘要随着随机数的变化而变化。服务端收到的密码摘要只对特定的随机数有效而没有密码的话攻击者就无法计算出正确的摘要这样我们就可以防止重放攻击。
摘要认证可以保护密码比基本认证安全很多。但摘要认证并不能保护内容所以仍然要与HTTPS配合使用来确保通信的安全。
### OAuth
OAuth开放授权是一个开放的授权标准允许用户让第三方应用访问该用户在某一Web服务上存储的私密资源例如照片、视频、音频等而无需将用户名和密码提供给第三方应用。OAuth目前的版本是2.0版。
OAuth2.0一共分为四种授权方式,分别为密码式、隐藏式、凭借式和授权码模式。接下来,我们就具体介绍下每一种授权方式。
**第一种,密码式。**密码式的授权方式,就是用户把用户名和密码直接告诉给第三方应用,然后第三方应用使用用户名和密码换取令牌。所以,使用此授权方式的前提是无法采用其他授权方式,并且用户高度信任某应用。
认证流程如下:
1. 网站A向用户发出获取用户名和密码的请求
2. 用户同意后网站A凭借用户名和密码向网站B换取令牌
3. 网站B验证用户身份后给出网站A令牌网站A凭借令牌可以访问网站B对应权限的资源。
**第二种,隐藏式。**这种方式适用于前端应用。认证流程如下:
1. A网站提供一个跳转到B网站的链接用户点击后跳转至B网站并向用户请求授权
2. 用户登录B网站同意授权后跳转回A网站指定的重定向redirect\_url地址并携带B网站返回的令牌用户在B网站的数据给A网站使用。
这个授权方式存在着“中间人攻击”的风险,因此只能用于一些安全性要求不高的场景,并且令牌的有效时间要非常短。
**第三种,凭借式。**这种方式是在命令行中请求授权,适用于没有前端的命令行应用。认证流程如下:
1. 应用A在命令行向应用B请求授权此时应用A需要携带应用B提前颁发的secretID和secretKey其中secretKey出于安全性考虑需在后端发送
2. 应用B接收到secretID和secretKey并进行身份验证验证通过后返回给应用A令牌。
**第四种,授权码模式。**这种方式就是第三方应用先提前申请一个授权码,然后再使用授权码来获取令牌。相对来说,这种方式安全性更高,前端传送授权码,后端存储令牌,与资源的通信都是在后端,可以避免令牌的泄露导致的安全问题。认证流程如下:
![](https://static001.geekbang.org/resource/image/54/a6/547b6362aba9e9ce8b72b511afee94a6.jpg?wh=2248x1127)
1. A网站提供一个跳转到B网站的链接+redirect\_url用户点击后跳转至B网站
2. 用户携带向B网站提前申请的client\_id向B网站发起身份验证请求
3. 用户登录B网站通过验证授予A网站权限此时网站跳转回redirect\_url其中会有B网站通过验证后的授权码附在该url后
4. 网站A携带授权码向网站B请求令牌网站B验证授权码后返回令牌即access\_token。
### Bearer
Bearer认证也称为令牌认证是一种 HTTP 身份验证方法。Bearer认证的核心是bearer token。bearer token是一个加密字符串通常由服务端根据密钥生成。客户端在请求服务端时必须在请求头中包含`Authorization: Bearer <token>`。服务端收到请求后,解析出 `<token>` ,并校验 `<token>` 的合法性如果校验通过则认证通过。跟基本认证一样Bearer认证需要配合HTTPS一起使用来保证认证安全性。
当前最流行的token编码方式是JSON Web TokenJWT音同 jot详见 [JWT RFC 7519](https://tools.ietf.org/html/rfc7519)。接下来我通过讲解JWT认证来帮助你了解Bearer认证的原理。
## 基于JWT的Token认证机制实现
在典型业务场景中,为了区分用户和保证安全,必须对 API 请求进行鉴权,但是不能要求每一个请求都进行登录操作。合理做法是,在第一次登录之后产生一个有一定有效期的 token并将它存储在浏览器的 Cookie 或 LocalStorage 之中。之后的请求都携带这个 token ,请求到达服务器端后,服务器端用这个 token 对请求进行认证。在第一次登录之后,服务器会将这个 token 用文件、数据库或缓存服务器等方法存下来,用于之后请求中的比对。
或者也可以采用更简单的方法直接用密钥来签发Token。这样就可以省下额外的存储也可以减少每一次请求时对数据库的查询压力。这种方法在业界已经有一种标准的实现方式就是JWT。
接下来我就来具体介绍下JWT。
### JWT简介
JWT是Bearer Token的一个具体实现由JSON数据格式组成通过HASH散列算法生成一个字符串。该字符串可以用来进行授权和信息交换。
使用JWT Token进行认证有很多优点比如说无需在服务端存储用户数据可以减轻服务端压力而且采用JSON数据格式比较易读。除此之外使用JWT Token还有跨语言、轻量级等优点。
### JWT认证流程
使用JWT Token进行认证的流程如下图
![](https://static001.geekbang.org/resource/image/48/01/480397e0a0e1503a350a082f44ec5901.jpg?wh=2248x1471)
具体可以分为四步:
1. 客户端使用用户名和密码请求登录。
2. 服务端收到请求后会去验证用户名和密码。如果用户名和密码跟数据库记录不一致则验证失败如果一致则验证通过服务端会签发一个Token返回给客户端。
3. 客户端收到请求后会将Token缓存起来比如放在浏览器Cookie中或者LocalStorage中之后每次请求都会携带该Token。
4. 服务端收到请求后会验证请求中的Token验证通过则进行业务逻辑处理处理完后返回处理后的结果。
### JWT格式
JWT由三部分组成分别是Header、Payload 和 Signature它们之间用圆点`.`连接,例如:
```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJpYW0uYXBpLm1hcm1vdGVkdS5jb20iLCJleHAiOjE2NDI4NTY2MzcsImlkZW50aXR5IjoiYWRtaW4iLCJpc3MiOiJpYW0tYXBpc2VydmVyIiwib3JpZ19pYXQiOjE2MzUwODA2MzcsInN1YiI6ImFkbWluIn0.Shw27RKENE_2MVBq7-c8OmgYdF92UmdwS8xE-Fts2FM
```
JWT中每部分包含的信息见下图
![](https://static001.geekbang.org/resource/image/0c/08/0c6657bc2d0fd2a98737660c7c373e08.jpg?wh=2248x1732)
下面我来具体介绍下这三部分,以及它们包含的信息。
1. Header
JWT Token的Header中包含两部分信息一是Token的类型二是Token所使用的加密算法。
例如:
```
{
"typ": "JWT",
"alg": "HS256"
}
```
参数说明:
* typ说明Token类型是JWT。
* alg说明Token的加密算法这里是HS256alg算法可以有多种
这里我们将Header进行base64编码
```
$ echo -n '{"typ":"JWT","alg":"HS256"}'|base64
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
```
在某些场景下可能还会有kid选项用来标识一个密钥ID例如
```
{
"alg": "HS256",
"kid": "XhbY3aCrfjdYcP1OFJRu9xcno8JzSbUIvGE2",
"typ": "JWT"
}
```
2. Payload载荷
Payload中携带Token的具体内容由三部分组成JWT标准中注册的声明可选、公共的声明、私有的声明。下面来分别看下。
**JWT标准中注册的声明部分有以下标准字段**
![](https://static001.geekbang.org/resource/image/c2/e3/c271d01d41dc7f4a45a9f2e8892057e3.png?wh=2248x2077)
本例中的payload内容为
```
{
"aud": "iam.authz.marmotedu.com",
"exp": 1604158987,
"iat": 1604151787,
"iss": "iamctl",
"nbf": 1604151787
}
```
这里我们将Payload 进行base64编码
```
$ echo -n '{"aud":"iam.authz.marmotedu.com","exp":1604158987,"iat":1604151787,"iss":"iamctl","nbf":1604151787}'|base64
eyJhdWQiOiJpYW0uYXV0aHoubWFybW90ZWR1LmNvbSIsImV4cCI6MTYwNDE1ODk4NywiaWF0Ijox
NjA0MTUxNzg3LCJpc3MiOiJpYW1jdGwiLCJuYmYiOjE2MDQxNTE3ODd9
```
除此之外还有公共的声明和私有的声明。公共的声明可以添加任何的需要的信息一般添加用户的相关信息或其他业务需要的信息注意不要添加敏感信息私有声明是客户端和服务端所共同定义的声明因为base64是对称解密的所以一般不建议存放敏感信息。
3. Signature签名
Signature是Token的签名部分通过如下方式生成将Header和Payload分别base64编码后`.` 连接。然后再使用Header中声明的加密方式利用secretKey对连接后的字符串进行加密加密后的字符串即为最终的Signature。
secretKey是密钥保存在服务器中一般通过配置文件来保存例如
![](https://static001.geekbang.org/resource/image/b1/d3/b183d2695c01cd863f782edf0a6d12d3.png?wh=1024x256)
这里要注意,**密钥一定不能泄露。密钥泄露后入侵者可以使用该密钥来签发JWT Token从而入侵系统**。
最后生成的Token如下
```
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJpYW0uYXV0aHoubWFybW90ZWR1LmNvbSIsImV4cCI6MTYwNDE1ODk4NywiaWF0IjoxNjA0MTUxNzg3LCJpc3MiOiJpYW1jdGwiLCJuYmYiOjE2MDQxNTE3ODd9.LjxrK9DuAwAzUD8-9v43NzWBN7HXsSLfebw92DKd1JQ
```
签名后服务端会返回生成的 Token客户端下次请求会携带该 Token。服务端收到 Token 后会解析出 header.payload然后用相同的加密算法和密钥对 header.payload 再进行一次加密,得到 Signature。并且对比加密后的 Signature 和收到的 Signature 是否相同,如果相同则验证通过,不相同则返回 `HTTP 401 Unauthorized` 的错误。
最后关于JWT的使用我还有两点建议
* 不要存放敏感信息在Token里
* Payload中的exp值不要设置得太大一般开发版本 7 天,线上版本 2 小时。当然,你也可以根据需要自行设置。
## 总结
在开发Go应用时我们需要通过认证来保障应用的安全。认证用来验证某个用户是否具有访问系统的权限如果认证通过该用户就可以访问系统从而创建、修改、删除、查询平台支持的资源。业界目前有四种常用的认证方式Basic、Digest、OAuth、Bearer。其中Basic和Bearer用得最多。
Basic认证通过用户名和密码来进行认证主要用在用户登录场景Bearer认证通过Token来进行认证通常用在API调用场景。不管是Basic认证还是Bearer认证都需要结合HTTPS来使用来最大程度地保证请求的安全性。
Basic认证简单易懂但是Bearer认证有一定的复杂度所以这一讲的后半部分通过JWT Token讲解了Bearer Token认证的原理。
JWT Token是Bearer认证的一种比较好的实现主要包含了3个部分
* Header包含了Token的类型、Token使用的加密算法。在某些场景下你还可以添加kid字段用来标识一个密钥ID。
* PayloadPayload中携带Token的具体内容由JWT标准中注册的声明、公共的声明和私有的声明三部分组成。
* SignatureSignature是Token的签名部分程序通过验证Signature是否合法来决定认证是否通过。
## 课后练习
1. 思考下使用JWT作为登录凭证如何解决token注销问题
2. 思考下Token是存放在LocalStorage中好还是存放在Cookie中好
欢迎你在留言区与我交流讨论,我们下一讲见。