API的身份认证

API设计中最开始的步骤就是设计鉴权,当前这篇介绍的认证只是鉴权一部分,当然在一个权限设计不完备的系统里,这就是鉴权的全部。

HTTP(不包涵Websocket)是无状态的,所以获得认证的客户端(用户)每次发起请求,都必须携带服务端签发的Token、SessionID等信息。当然这个信息可能不是服务端签发的,比如Basic Auth就是请求携带username+password。从token携带方式上有使用http请求参数的,有使用Cookie方式,还有放到请求body中的。下文我们将分析一下各种认证方式,以及推荐的使用场景。

一、Simple Method With Key

我甚至不能给这种方式起个合适的名字,这种甚至不能称为认证,简单到只需要在请求头或者URL中附带一个密码。

比如https://example.com/api?key={key}, 一些内网或者非公开API非常常见的认证方式。早些年一些云厂商给我服务的公司开发了很多使用这种方式的鉴权的API,鉴于网络环境比较简单且都是在HTTPS下运行的这种方式也没有太大问题。

二、Key/Secret Authentication

这其实也不是一种统一认证方式,与上面这种比较粗放的鉴权方式不同。稍微优雅点的设计回增加个参数签名机制。比如下面这个案例。

t = hex(timestamp)
key = 服务端签发
url = https://example.com/file?sign=HASH(file+key+t)&t1=t

你看这个就一定程度上避免了重放,还比较安全。事实上这也是大部分国内CDN厂商对资源鉴权的普遍实现方案。

1. HMAC

这种方式的标准实现是(keyed-hash message authentication code, 密钥散列消息认证码),感兴趣可以参照RFC 2104定义。下面的伪代码展示了如何实现HMAC。当使用以下散列函数之一时,块大小为64(字节):SHA-1、MD5、RIPEMD-128/160。

keyed-hash message authentication code
 function hmac (key, message) {
if (length(key) > blocksize) {
key = hash(key) // keys longer than blocksize are shortened
}
if (length(key) < blocksize) {
// keys shorter than blocksize are zero-padded (where ∥ is concatenation)
key = key ∥ [0x00 * (blocksize - length(key))] // Where * is repetition.
}

o_key_pad = [0x5c * blocksize] ⊕ key // Where blocksize is that of the underlying hash function
i_key_pad = [0x36 * blocksize] ⊕ key // Where ⊕ is exclusive or (XOR)

return hash(o_key_pad ∥ hash(i_key_pad ∥ message)) // Where ∥ is concatenation
}

2. HMAC + Secret

上面这种方案再进一步就是开放平台最常见采用的AK/SK鉴权方案。AK明文传输用于角色识别,SK与必要参数做HMAC签名用于用户认证。

这里有必要说明一下,不同厂商给ak/sk起了不同的名字,AppID/AppSecret、AppKey/AppSecret、AccessKey/AccessSecret都是一样的东西。

1)典型AK/SK 鉴权过程
  1. 拼接请求串
    CanonicalRequest =
    HTTPRequestMethod + '\n' + #
    CanonicalURI + '\n' +
    CanonicalQueryString + '\n' +
    CanonicalHeaders + '\n' +
    HexEncode(Hash(RequestPayload))
    参数 说明
    HTTPRequestMethod HTTP 请求方法 GET
    CanonicalURI RFC 3986规范 URI 参数 /
    CanonicalQueryString 发起 HTTP 请求 URL 中的查询字符串 Limit=10&Offset=0
    HashedRequestPayload 请求Body {“name”:”bob”}
  2. 拼接待签名字符串
    StringToSign =
    Algorithm + \n +
    RequestTimestamp + \n +
    Hex(SHA256(CanonicalRequest))
    参数 说明
    Algorithm 签名算法 HMAC-SHA256
    RequestTimestamp 请求时间 1551113065
    CanonicalRequest 上一步的请求字符串
  3. 计算签名
    Signature = Hex(HMAC_SHA256(`${AccessSecret}`, StringToSign))
  4. 拼接 Authorization
    Authorization =
    Algorithm + ' ' +
    'Credential=' + `${AccessKey}` + ', ' +
    'SignedHeaders=' + SignedHeaders + ', ' +
    'Signature=' + Signature
2)非典型AK/SK 鉴权过程

这里只有一个案例,微信公众号.

  1. 获取AccessToken
    GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
    response: {“access_token”:”ACCESS_TOKEN”,”expires_in”:7200}

  2. 带AcccessToken 访问API
    POST https://api.weixin.qq.com/cgi-bin/tags/create?access_token=ACCESS_TOKEN

  3. Token 有效期前重新获取

对比上面两个实现,公众号这个方案更像是Oauth2.0与HMAC的结合体,关于Oauth下面会有更多介绍。


三、Basic Authentication

Basic认证 是一种古老认证技术,基本原理是请求携带用户名密码访问,由服务端确认请求是否合法。下面这个弹框你应该是见过的。

下面我们通过认证过程,来了解下这种鉴权的优缺点。

Basic access authentication 协议简析

服务端

服务端收到请求,且未携带Basic认证信息,响应HTTP Status 401,并在头部设置 www-authenticate: Basic realm="Realm",realm 信息来自服务端定义。

RFC 7617增加了编码设定 WWW-Authenticate: Basic realm="User Visible Realm", charset="UTF-8"

客户端

  1. 用户名密码使用 ':' 拼接到一起, username:password 用户名本身不能包含':'
  2. 拼接好的用户名密码转成服务端指定的编码,经过一次Base64编码,得到类似字符串 dXNlcm5hbWU6cGFzc3dvcmQK
  3. 再次发起请求时 增加HTTP头,authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQK

由上可见每次请求都会传输用户名密码,而且这值不变,没有https的加持密码泄漏风险还是很大的,所以这中方式在中国互联网中并不流行,常见一些内部效率平台的鉴权。

常见搭配使用场景是.htpasswd密码文件 + nginx配合使用,具体使用方法可参照 https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/

四、Digest access authenticatio

摘要认证,可与Basic Auth看作同类型的认证方式,事实上他们同时被定义到 RFC 2617

同样我们通过了解认证过,来看看这种认证方式的优缺点与使用场景。

Digest access authentication authentication 协议简析

客户端发起请求,服务端响应头里同样携带一个一个WWW-Authenticate,包含:realm、qop、nonce、opaque 等信息。

  1. 其中realm与Basic Auth类似;
  2. qoq包含:authauth-inittoken三个选型,来影响认证过程;
  3. nonce是服务端生成的随机数,来解决Basic中认证数据固定问题;
  4. 除此之外还有 opaque domain URL algorithm 等非必要参数就不一一介绍了;

五、Oauth

Oauth 是互联网世界最流行的API鉴权方式。主要面向社区开发者,使开发者能方便调用平台API来管理用户数据。

1. Oauth 1.0

Oauth1.0 已经较少采用了,鉴权过程参照上图1,更多兴趣请参照耗子叔的文章 https://coolshell.cn/articles/19395.html#OAuth_10 (这里顺便说一下,列好大纲发现左耳朵耗子已经写过了这个题目了😓)。
这里需要强调的是,Oauth1.0有两个token,Request Token: 包含AK/SK 签名的HMAC Token(是不是和微信公众号类似啊), Access Token:用户token。

2. Oauth 2.x

1) Oauth2.0

是对Oauth的简化,图二中用户授权后开发者获取authorization code后可调用API获取Accss TokenRefresh Token 。accesstoken到期前可以使用Refresh Token获取新的actoken,refresh token存活期比较长,一般为30天,如果没有保存用户下次进来需要重新授权。

Accss Token 分为 Bearer TokenMAC Token:

  • Bearer Token 不记名token,就是我们常用的Token方式,在https安全传输中采用。
  • MAC Token 类似HMAC。
2) Oauth 2.1

草案阶段,草案地址 https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-06。

这个版本是对2.0的安全补丁:

  • PKCE(Proof Key for Code Exchange) 针对客户端code劫持可能性增加了安全防护。
  • Refresh Token sender-constrained(在mTLS下)或者一次性使用。

六、JSON Web Token (JWT)

严格来说JWT不算认证方式,JWT是一种Token的传输格式。相对于传统令牌,JWT允许开发者传输更多的数据,类似协议还有Simple Web Tokens (SWT)Security Assertion Markup Language Tokens (SAML)

1. JWT 结构

在OAuth认证方式下,客户端使用Bearer Token传输另外,一般来说这个令牌就是一段随机数(字符串)。而JWT是一个结构话的令牌,通常包含头(Header)载核(Payload)签名(Signature)三个部分。

这就是一个典型的JWT,三个部分使用`.`分割
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL21sYi50cmVtb2xvLmxhbjo4MDQzL2F1dGgvaWRwL29pZGMiLCJhdWQiOiJrdWJlcm5ldGVzIiwiZXhwIjox
NDc0NTk2NjY5LCJqdGkiOiI2RDUzNXoxUEpFNjJOR3QxaWVyYm9RIiwiaWF0IjoxNDc0NTk2MzY5LCJuYmYiOjE
0NzQ1OTYyNDksInN1YiI6Im13aW5kdSIsInVzZXJfcm9sZSI6WyJ1c2VycyIsIm5ldy1uYW1lc3BhY2Utdmlld2VyIl0sImVtYWlsIjoibXdpbmR1QG5vbW9yZWplZGkuY29tIn0.9bcYkbGNYIhR4Vrl15uMKtahRMIqEUxPcXngeFleExE

那么这三个分别都是啥东西呢,接下来我们分别介绍这三个东西分别起什么作用,当然你也可以参照协议原文RFC 7519

Header 主要声明了签名算法,除了HMAC还可以选择RSA等非对称算法。

Payload 是JWT的claims,由开发者自由设计,当然可以参照RFC 7519的指引设计。一半来说 expsubrole是比较常见的内容,可以减少数据库的查询。

Signature 签名部分,用来就是根据头指定的算法对头+内容签名: HMACSHA256(base64UrlEncode(header) + “.” +base64UrlEncode(payload), ${Secret}) 。

完整的JWT 就是 base64(header) + "." +base64(payload) + "." + Signature

2. 认证

JWT 并不指定认证过程,可以使用Oauth2.0 也可以使用是传统Web登陆。主流使用方式是,鉴权成功后使用Bearer Token 或者 MAC Token做后续借口鉴权。

这里有个值得注意的问题:很多JWT使用者为了方便使用一个固定**Secret**,除了安全性问题,token吊销也很不方便。所以建议每个用户颁发一个临时Secret。

七、TLS证书认证

TLS被广泛应用于Https应用,我们说的证书是服务端证书,任何用户都能获取,用来保证客户端与服务端通信不被第三方窥探修改。在云原生领域X509客户端证书被广泛应用于用户认证,比如Kubernetes就支持这种认证方式。
与服务端证书不同,自签认证即有Grpc的单一证书验证方式,也有Kubernetes这种向每个用户签发证书的方式。

1. Grpc TLS 认证

这种认证不是典型的API认证方式。而且力度比较粗,不太适合OpenAPI的认证设计,可以看下来自Grpc官方的Demo做个简单了解。

Client:

creds, _ := credentials.NewClientTLSFromFile(certFile, "")
conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds))
// error handling omitted
client := pb.NewGreeterClient(conn)
// ...

Server:

creds, _ := credentials.NewServerTLSFromFile(certFile, keyFile)
s := grpc.NewServer(grpc.Creds(creds))
lis, _ := net.Listen("tcp", "localhost:50051")
// error handling omitted
s.Serve(lis)

2. X509 客户端证书认证

X509更强调用户认证,也就是服务端要通过这个认证方式分辨访问者是谁。一些支付平台采用证书序列号,有些采用证书主题(Subject)的通用名称(CN)识别。

客户端证书生成过程如下

openssl req -new -key jbeda.pem -out jbeda-csr.pem -subj "/CN=jbeda/O=app1/O=app2"

此命令将使用用户名 jbeda 生成一个证书签名请求(CSR),且该用户属于 “app” 和 “app2” 两个用户组, 证书密钥存在服务端,认证时做非对称校验。

八、mTLS

双向TLS认证简称mTLS,一些金融级别API运行在零网络中使用mTLS做认证保证API安全。

典型的TLS与mTLS流程对比如下入

通过上图对比我们可以发现,服务端也需要验证客户端的证书。当然这个证书也是一个专用证书,实践上不同的用户都会被签发一个带用户识别的证书,比如微信支付API。

所以mTLS也仅仅是API认证关于安全方面的一个设计,OAuth2.0也可以使用这种方式。