登录认证模式
[一]概述
OAuth 全称是 Open Authentication
开放授权(OAuth)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。 OAuth 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的 2 小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth 让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
目前使用最广泛的是
OAuth 2.0,OAuth 1.0已经被废弃了。本文中的 OAuth 都是指OAuth 2.1
[1]OAuth2 授权流程中的角色
- 资源拥有者(resource owner):能授权访问受保护资源的一个实体,可以是一个人,那我们称之为最终用户;
- 资源服务器(resource server):存储受保护资源,客户端通过access token请求资源,资源服务器响应受保护资源给客户端;
- 授权服务器(authorization server):成功验证资源拥有者并获取授权之后,授权服务器颁发授权令牌(Access Token)给客户端。
- 客户端(client):第三方应用,也可以是它自己的官方应用;其本身不存储资源,而是资源拥有者授权通过后,使用它的授权(授权令牌)访问受保护资源,然后客户端把相应的数据展示出来/提交到服务器。
[2]令牌与密码
令牌(token)与密码(password)的作用是一样的,都可以进入系统,但是有三点差异。
- (1)令牌是短期的,到期会自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。
- (2)令牌可以被数据所有者撤销,会立即失效。以上例而言,屋主可以随时取消快递员的令牌。密码一般不允许被他人撤销。
- (3)令牌有权限范围(scope),比如只能进小区的二号门。对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。
上面这些设计,保证了令牌既可以让第三方应用获得权限,同时又随时可控,不会危及系统安全。这就是 OAuth 2.0 的优点。
重要
只要知道了令牌,就能进入系统。系统一般不会再次确认身份,所以令牌必须保密,泄漏令牌与泄漏密码的后果是一样的。 这也是为什么令牌的有效期,一般都设置得很短的原因。
[二]认证模式的组成
[1]OAuth 2.0
OAuth 2.0 协议根据使用不同的适用场景,规定了四种获得令牌的流程。你可以选择最适合自己的那一种,向第三方应用颁发令牌。下面就是这四种授权方式。
- 授权码模式(Authorization Code Grant)
- 隐式模式(Implicit Grant)
- 密码模式(Resource Owner Password Credentials Grant)
- 客户端凭证模式(Client Credentials Grant)
[2]OAuth 2.1
OAuth 2.1 是 OAuth 2.0 的下一个版本, OAuth 2.1 根据最佳安全实践(BCP), 目前是第18个版本,对 OAuth 2.0 协议进行整合和精简, 移除不安全的授权流程。
在 OAuth 2.1 中,密码模式和隐式模式均已经被弃用,主要支持的模式为:
- 授权码模式(Authorization Code Grant)
- 客户端凭证模式(Client Credentials Grant)
[3]OAuth2扩展协议PKCE授权码模式
PKCE 全称是 Proof Key for Code Exchange, 在2015年发布, 它是 OAuth 2.0 核心的一个扩展协议, 所以可以和现有的授权模式结合使用,比如 Authorization Code + PKCE, 这也是最佳实践,PKCE 最初是为移动设备应用和本地应用创建的, 主要是为了减少公共客户端的授权码拦截攻击。
 在最新的 OAuth 2.1 规范中(草案), 推荐所有客户端都使用 PKCE, 而不仅仅是公共客户端, 并且移除了 Implicit 隐式和 Password 模式, 那之前使用这两种模式的客户端怎么办? 是的, 您现在都可以尝试使用 Authorization Code + PKCE 的授权模式。那 PKCE 为什么有这种魔力呢? 实际上它的原理是客户端提供一个自创建的证明给授权服务器, 授权服务器通过它来验证客户端,把访问令牌(access_token) 颁发给真实的客户端而不是伪造的。
[4]OpenID Connect(OIDC)
OpenID Connect 是在 OAuth2.0 协议之上的标识层。它拓展了 OAuth2.0,使得认证方式标准化。
OAuth 不会立即提供用户身份,而是会提供用于授权的访问令牌。 OpenID Connect 使客户端能够通过认证来识别用户,其中,认证在授权服务端执行。它是这样实现的:在向授权服务端发起用户登录和授权告知的请求时,定义一个名叫openid的授权范围。在告知授权服务器需要使用 OpenID Connect 时,openid 是必须存在的范围。
[5]Dante Cloud
Dante Cloud 认证组件使用的是 Spring 生态中的 Spring Authorization Server,Spring Authorization Server 是基于 OAuth 2.1 协议实现。
默认支持以下认证模式:
- 授权码模式(Authorization Code Grant)
- 客户端凭证模式(Client Credentials Grant)
默认支持以下扩展模式:
- Authorization Code + PKCE
- OpenID Connect
除此以外,在 Spring Authorization Server 新版本中,已经支持自 OAuth 2.0 开始支持的扩展认证模式:
- 设备码模式(Device Authorization Grant)
考虑到老旧项目的兼容性,以及实际应用需求,Dante Cloud 在 Spring Authorization Server 基础之上,有扩展了两种认证模式
- 密码模式(Resource Owner Password Credentials Grant)
- 社会化模式(Social Credentials Grant)
所以,Dante Cloud 目前支持的认证模式有:
- 授权码模式(Authorization Code Grant)
- 客户端凭证模式(Client Credentials Grant)
- 设备码模式(Device Authorization Grant)
- 密码模式(Resource Owner Password Credentials Grant)
- 社会化模式(Social Credentials Grant)
- Authorization Code + PKCE
- OpenID Connect
说明
- 密码模式:Dante Cloud 之所以又扩展了 OAuth 2.1中已经弃用的密码模式,是考虑到好多旧版本时代(使用Spring Security OAuth2)的客户端,大多数使用的是密码模式登录,为了兼容这部分客户端所以保留了密码模式
- 社会化模式:是 Dante Cloud 为了支持互联网应用登录的需求,将比较常见的短信验证码登录、微信小程序登录、第三方系统认证登录等多种认证方式,容易融合为了社会化登录模式
[三]认证模式的原理
[1]授权码授权模式
授权码授权模式主要流程如下图所示:

- 第一步:用户访问页面或者出发认证地址
- 第二步:访问的页面将请求重定向到认证服务器
- 第三步:用户登录成功只有,认证服务器向用户展示授权页面,等待用户授权
- 第四步:用户授权,认证服务器生成一个 code和带上client_id发送给应用服务器。然后,应用服务器拿到code,并用client_id去后台查询对应的client_secret
- 第五步:将 code,client_id,client_secret传给认证服务器换取access_token和refresh_token
- 第六步:将 access_token和refresh_token传给应用服务器
- 第七步:验证 token,访问真正的资源页面
[2]客户端凭证模式
客户端凭证模式主要流程如下图所示:

- 第一步:用户访问应用客户端
- 第二步:通过客户端定义的验证方法,拿到 token,无需授权
- 第三步:访问资源服务器 A
- 第四步:拿到一次 token 就可以畅通无阻的访问其他的资源页面。
说明
这是一种最简单的模式,只要 client 请求,我们就将 AccessToken 发送给它。这种模式是最方便但最不安全的模式。因此这就要求我们对 client 完全的信任,而 client 本身也是安全的。
因此这种模式一般用来提供给我们完全信任的服务器端服务。在这个过程中不需要用户的参与。
[3]密码模式
密码模式主要流程如下图所示:

- 第一步:用户访问用页面时,输入第三方认证所需要的信息(QQ/微信账号密码)
- 第二步:应用页面那种这个信息去认证服务器授权
- 第三步:认证服务器授权通过,拿到 token,访问真正的资源页面
说明
优点:不需要多次请求转发,额外开销,同时可以获取更多的用户信息。(都拿到账号密码了)
缺点:局限性,认证服务器和应用方必须有超高的信赖。(比如亲兄弟?)
应用场景:自家公司搭建的认证服务器
[4]PKCE
PKCE 主要流程如下图所示:

原理分析
授权码拦截攻击, 它是指在整个授权流程中, 只需要拦截到从授权服务器回调给客户端的授权码 code, 就可以去授权服务器申请令牌了, 因为客户端是公开的, 就算有密钥 client_secret 也是形同虚设, 恶意程序拿到访问令牌后, 就可以光明正大的请求资源服务器了。
PKCE 是怎么做的呢? 既然固定的 client_secret 是不安全的, 那就每次请求生成一个随机的密钥(code_verifier), 第一次请求到授权服务器的 authorize endpoint时, 携带 code_challenge 和 code_challenge_method, 也就是 code_verifier 转换后的值和转换方法, 然后授权服务器需要把这两个参数缓存起来, 第二次请求到 token endpoint 时, 携带生成的随机密钥的原始值 (code_verifier) , 然后授权服务器使用下面的方法进行验证:
- plain:code_challenge = code_verifier
- S256:code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
通过后才颁发令牌, 那向授权服务器 authorize endpoint 和 token endpoint 发起的这两次请求,该如何关联起来呢? 通过 授权码 code 即可, 所以就算恶意程序拦截到了授权码 code, 但是没有 code_verifier, 也是不能获取访问令牌的, 当然 PKCE 也可以用在机密(confidential)的客户端, 那就是 client_secret + code_verifier 双重密钥了。
code_verifier
对于每一个 OAuth 授权请求, 客户端会先创建一个代码验证器 code_verifier, 这是一个高熵加密的随机字符串, 使用URI 非保留字符 (Unreserved characters), 范围 [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~", 因为非保留字符在传递时不需要进行 URL 编码, 并且 code_verifier 的长度最小是 43, 最大是 128, code_verifier 要具有足够的熵它是难以猜测的。
code_verifier 的扩充巴科斯范式 (ABNF) 如下:
code-verifier = 43*128unreserved
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
ALPHA = %x41-5A / %x61-7A
DIGIT = %x30-39简单点说就是在
[A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"范围内,生成43-128位的随机字符串。
// Required: Node.js crypto module
// https://nodejs.org/api/crypto.html#crypto_crypto
function base64URLEncode(str) {
    return str.toString('base64')
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=/g, '');
}
var verifier = base64URLEncode(crypto.randomBytes(32));// Required: Apache Commons Codec
// https://commons.apache.org/proper/commons-codec/
// Import the Base64 class.
// import org.apache.commons.codec.binary.Base64;
SecureRandom sr = new SecureRandom();
byte[] code = new byte[32];
sr.nextBytes(code);
String verifier = Base64.getUrlEncoder().withoutPadding().encodeToString(code);
public static string randomDataBase64url(int length)
{
    RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
    byte[] bytes = new byte[length];
    rng.GetBytes(bytes);
    return base64urlencodeNoPadding(bytes);
}
public static string base64urlencodeNoPadding(byte[] buffer)
{
    string base64 = Convert.ToBase64String(buffer);
    base64 = base64.Replace("+", "-");
    base64 = base64.Replace("/", "_");
    base64 = base64.Replace("=", "");
    return base64;
}
string code_verifier = randomDataBase64url(32);code_challenge_method
对 code_verifier 进行转换的方法, 这个参数会传给授权服务器, 并且授权服务器会记住这个参数, 颁发令牌的时候进行对比, code_challenge == code_challenge_method(code_verifier) , 若一致则颁发令牌。
code_challenge_method可以设置为plain(原始值) 或者S256(sha256哈希)。
code_challenge
使用 code_challenge_method 对 code_verifier 进行转换得到 code_challenge, 可以使用下面的方式进行转换
- plain:code_challenge = code_verifier
- S256:code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
客户端应该首先考虑使用
S256进行转换, 如果不支持,才使用plain, 此时code_challenge和code_verifier的值相等。
// Required: Node.js crypto module
// https://nodejs.org/api/crypto.html#crypto_crypto
function sha256(buffer) {
  return crypto.createHash("sha256").update(buffer).digest();
}
var challenge = base64URLEncode(sha256(verifier));// Dependency: Apache Commons Codec
// https://commons.apache.org/proper/commons-codec/
// Import the Base64 class.
// import org.apache.commons.codec.binary.Base64;
byte[] bytes = verifier.getBytes("US-ASCII");
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(bytes, 0, bytes.length);
byte[] digest = md.digest();
String challenge = Base64.encodeBase64URLSafeString(digest);public static string base64urlencodeNoPadding(byte[] buffer)
{
    string base64 = Convert.ToBase64String(buffer);
    base64 = base64.Replace("+", "-");
    base64 = base64.Replace("/", "_");
    base64 = base64.Replace("=", "");
    return base64;
}
string code_challenge = base64urlencodeNoPadding(sha256(code_verifier));[四]认证模式的验证
[1]授权码授权模式验证
常见的应用场景是:第三方应用先申请一个授权码,然后再用该码获取令牌。
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
第一步:拼装申请 code 请求 URL
拼装客户端向资源端申请 code 请求的 URL。这里所说的客户端,是指任意想要使用 Dante Cloud 通过授权码授权模式获取 Token 的应用。
主要逻辑:假设,某网站或者系统(客户端),我们称之为 A 。Dante Cloud 向 A 网站提供一个认证链接,A 网站会把这个连接做成一个按钮,用户点击后就会根据这个连接跳转到 Dante Cloud,在 Dante Cloud 认证通过后授权用户数据给 A 网站使用。
http://192.168.101.10:8847/herodotus-cloud-uaa/oauth2/authorize?response_type=code&client_id=14a9cf797931430896ad13a6b1855611&client_secret=a05fe1fc50ed42a4990c6c6fc4bec398&scope=read-user-by-page&state=0CI0ziUDEnqMgqW0nzRNRCzLrs-9IMbqJzGZ47Zb0gY&redirect_uri=http://192.168.101.10:3000/authorization-code参数说明
- response_type:必选参数。值固定为“code”。
- client_id:必选参数
- client_secret:必选参数
- state:客户端 提供的一个字符串,服务器会原样返回给客户端。这个要自己实现,用于防止恶意攻击。
- redirect_uri:必选参数(授权成功后的重定向地址)
- scope:可选参数(表示授权范围)
第二步:资源端返回 code 给客户端
可以采用以下两种方式进行验证:
- 第一种:如果有想要接入的系统,那么就在这个系统中做一个图标按钮,点击后跳转到上面的地址。(比如:很多系统都支持微信登录,那么在页面上就会有一个微信的图标按钮,点击后跳转到一个地址)
- 第二种:如果没有想接入的系统,可以将上面的地址输入到浏览器,获取信息后配合 Postman等工具进行验证。
下面采用第二种浏览器的方式
在浏览器中输入上面的地址,会跳出如下登录界面。

输入用户名,密码和验证码进行用户验证。
可以使用系统默认用户:system 密码:123456
登录成功后,会跳转到授权页面进行授权,如下图所示:

授权成功后,就会跳转到一个新的地址,同时在地址的后面会跟随生成的 code, 如下所示。这个地址就是系统中设置的 redirect_uri
http://192.168.101.10:3000/authorization-code?code=Jw8NDqRqJB9dZPGKATCOwlEWDemCO7VIfVqozlpKOLuDSji6SyvuhZ-f3GYtURkBuF5l-WuBnLYV7wDTq_ikz7XkcYL1bFHMrthf5FL4P9I0Dam09aTHnX7lbkqlqwNX&state=0CI0ziUDEnqMgqW0nzRNRCzLrs-9IMbqJzGZ47Zb0gY说明
redirect_uri 是由开发人员根据需要配置的,也是需要开发人员实现代码逻辑的。
URL 本质就是一个请求地址,授权服务器会将 code 和 state 参数发送到这个地址。这个请求地址对应的业务逻辑,就是接收 code 和 state 参数,然后拼装请求来获取 Token。
第三步:客户端根据 code 向资源端请求令牌
在你习惯的工具中,输入下面的地址:
http://192.168.101.10:8847/dante-cloud-uaa/oauth/token?client_id=010e659a-4005-4610-98f6-00b822f4758e&client_secret=04165a07-cffd-45cf-a20a-1c2a69f65fb1&grant_type=authorization_code&code=P6dxH5&scope=all&redirect_uri=http://localhost:9999/passport/login参数说明
- grant_type:必选参数(固定值“authorization_code”)
- code: 必选参数。上一步通过- redirect_uri返回的- code
- state: 可选参数。上一步通过- redirect_uri返回的- state。避免在请求的过程中被篡改。如果在第一步就没有传递- state,这里就可以省略。
- redirect_uri:必选参数(必须和 Request 中提供的 redirect_uri 相同)
- client_id:必选参数
- client_secret:必选参数
- scope:可选参数(表示授权范围)。如果有多个值,以“空格”进行分隔。
下图以 POSTMAN 为例:

[2]客户端凭证模式验证
这种模式直接根据 client 的 id 和密钥即可获取 token,无需用户参与 这种模式比较合适消费 api 的后端服务,比如拉取一组用户信息等
直接使用如下地址获取 Token 即可
http://192.168.101.10:8847/herodotus-cloud-uaa/oauth2/tokenPOSTMAN 请求参考截图如下:

参数说明
- grant_type:必选参数(固定值“client_credentials”)
- client_id:必选参数
- client_secret:必选参数
- scope:可选参数(表示授权范围)。如果有多个值,以“空格”进行分隔。虽然说是可选,但是如果没有该参数将没有权限访问任何接口
注意
使用客户端凭证模式,只能使用 Content-Type 为 x-www-form-urlencoded 的 POST 请求
[3]刷新令牌模式验证
刷新令牌模式,即使用 refresh_token 来重新申请 access_token。利用这种方式,可以简化操作,避免用户的重复操作。
刷新令牌模式与社会化凭证模式与密码模式的使用方式非常相似
直接使用如下地址获取 Token 即可:
http://192.168.101.10:8847/herodotus-cloud-uaa/oauth2/token
参数说明
- grant_type:必选参数(固定值“refresh_token”)
- client_id:必选参数
- client_secret:必选参数
- refresh_token:必选参数。即第一次申请- access_token参数时一同返回的- refresh_token。
警告
正因为刷新令牌模式的特性,而且refresh_token 也是存在有效期的,所以在使用时都会将 refresh_token 的有效期设置为比 access_token 有效期长。
[4]密码模式验证
重要
Spring Authorization Server 基于 OAuth2.1 协议开发并不支持密码模式。Dante Cloud 中的密码模式是自定义授权模式,为了兼容老的、还在使用 OAuth 2.0 协议的系统
密码模式,需要用户输入用户名和密码进行 OAuth 授权登录。密码模式安全性较低,为了保证安全性,首先要确保“客户端”是可信的。那么,在使用密码模式时,除了需要提供用户名和密码之外,还要有这个客户端对应的 Client ID 和 Client Secret。
直接使用如下地址获取 Token 即可:
http://192.168.101.10:8847/herodotus-cloud-uaa/oauth2/tokenPOSTMAN 请求参考截图如下:

参数说明
- grant_type:必选参数(固定值“password”)
- username: 必选参数
- password:必选参数
- client_id:必选参数
- client_secret:必选参数
- scope:可选参数(表示授权范围)。如果有多个值,以“空格”进行分隔。
参数传递方式一
密码模式的参数传递可以有几种方式,前面截图只是方式之一。使用 Content-Type 为 x-www-form-urlencoded 的 POST 请求,所有的参数都放入在 form 中
注
client_id、client_secret 两个参数和其他参数统一放在 form,需要将客户端的 ClientAuthenticationMethod 配置为 CLIENT_SECRET_POST
参数传递方式二
这种方式下client_id、client_secret 两个参数,并不是和其他参数一起放在 form 中。而是采用 Basic Auth 方式,将 client_id、client_secret 两个参数使用 Base64 编码后,放入请求头中。
POSTMAN 请求参考截图如下:


注
这种方式下,需要将客户端的 ClientAuthenticationMethod 配置为 CLIENT_SECRET_BASIC
参数传递方式三
第三种方式, client_id、client_secret 两个参数还是使用 Base64 编码后放入请求头中。不同的是,不再采用 Content-Type 为 x-www-form-urlencoded 形式将参数放入 Form 中,而是将参数直接一 QueryParam 的形式拼装在请求路径中

注
这种方式,不是和容易让人理解,所以不推荐使用。还是建议采用前面两种方式,这样还可以实现获取 Token 请求方式的统一,减少出错。
[5]社会化凭证模式验证
社会化凭证模式(Social Credentials),是 Dante Cloud 中自定义授权模式的授权模式。主要用于解决微信小程序等第三方系统认证登录的问题。
当然,不仅仅是微信小程序,还包括手机验证码登录、微信公众号以及JustAuth组件支持的所有第三方系统。而且是将所有这种涉及外部系统的登录方式,全部统一为社会化凭证模式,而且使用 OAuth2 统一的 /oauth2/token 地址进行登录。
这样做无需为各种第三方系统单独实现登录,而且与 Spring Authorization Server 认证体系完全一致,极大地简化了集成的复杂度,提升了系统使用的便捷性。
社会化凭证模式与密码模式的使用方式非常相似,直接使用如下地址获取 Token 即可:
http://192.168.101.10:8847/herodotus-cloud-uaa/oauth2/token下图就分别以短信验证码登录以及微信小程序登录作为示例:


参数说明
- grant_type:必选参数(固定值“social-credentials”)
- client_id:必选参数
- client_secret:必选参数
- source:必选参数,登录类型。
- 其它参数。根据不同类型的登录所需的参数不同
当前支持的 source 类型,如下表所示:
| 编码 | 名称 | 
|---|---|
| INSTITUTION | 机构人员 | 
| SMS | 手机验证码 | 
| WXAPP | 微信小程序 | 
| 微博 | |
| BAIDU | 百度 | 
| WECHAT_OPEN | 微信开放平台 | 
| WECHAT_MP | 微信公众号 | 
| WECHAT_ENTERPRISE | 企业微信二维码 | 
| WECHAT_ENTERPRISE_WEB | 企业微信网页 | 
| DINGTALK | 钉钉 | 
| DINGTALK_ACCOUNT | 钉钉账号 | 
| ALIYUN | 阿里云 | 
| TAOBAO | 淘宝 | 
| ALIPAY | 支付宝 | 
| TEAMBITION | Teambition | 
| HUAWEI | 华为 | 
| FEISHU | 飞书 | 
| JD | 京东 | 
| DOUYIN | 抖音 | 
| TOUTIAO | 今日头条 | 
| MI | 小米 | 
| RENREN | 人人 | 
| MEITUAN | 美团 | 
| ELEME | 饿了么 | 
| KUJIALE | 酷家乐 | 
| XMLY | 喜马拉雅 | 
| GITEE | 码云 | 
| OSCHINA | 开源中国 | 
| CSDN | CSDN | 
| GITHUB | Github | 
| GITLAB | Gitlab | 
| STACK_OVERFLOW | Stackoverflow | 
| CODING | Coding | 
| GOOGL | 谷歌 | 
| MICROSOFT | 微软 | 
| 脸书 | |
| 领英 | |
| 推特 | |
| AMAZON | 亚马逊 | 
| SLACK | Slack | 
| LINE | Line | 
| OKTA | Okta | 
重要
上表中绝大多数登录方式均由 JustAuth 提供。每种登录所需的参数不同,而且随着时间的推移存在登录方式是否可用、参数变化、接口变化等问题。没有条件和精力保证所有登录方式都可用,如果你确实需要使用到某种登录,但是目前 Dante Cloud 不支持或者无效。可以提 ISSUE,会尽快补充。
[6]设备码模式验证
补充中...
正在努力补充中,多点点 Star,会写得更快哦!
[7]Authorization Code + PKCE模式验证
客户端生成 code_verifier 和 code_challenge
这里我们以 Herodotus Cloud 前端代码作为示例。Herodotus Cloud 前端采用的是 Vue 工程,生成 code_verifier 和 code_challenge 代码逻辑如下:
class PkceUtilities {
  private static instance = new PkceUtilities();
  private constructor() {}
  public static getInstance(): PkceUtilities {
    return this.instance;
  }
  /**
   * Thanks to @SEIAROTg on stackoverflow:
   * "Convert a 32bit integer into 4 bytes of data in javascript"
   * @param num The 32bit integer
   * @returns An ArrayBuffer representing 4 bytes of binary data
   */
  private toBytesInt32(num: number): ArrayBuffer {
    const arr = new ArrayBuffer(4); // an Int32 takes 4 bytes
    const view = new DataView(arr);
    view.setUint32(0, num, false); // byteOffset = 0; litteEndian = false
    return arr;
  }
  /**
   * Creates an array of length `size` of random bytes
   * @param size
   * @returns Array of random ints (0 to 255)
   */
  private getRandomValues(size: number): number[] {
    const randoms = lib.WordArray.random(size);
    const randoms1byte: number[] = [];
    randoms.words.forEach((word) => {
      const arr = this.toBytesInt32(word);
      const fourByteWord = new Uint8Array(arr);
      for (let i = 0; i < 4; i++) {
        randoms1byte.push(fourByteWord[i]);
      }
    });
    return randoms1byte;
  }
  /** Generate cryptographically strong random string
   * @param size The desired length of the string
   * @returns The random string
   */
  private random(size: number): string {
    const mask =
      "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~";
    let result = "";
    const randomUints = this.getRandomValues(size);
    for (let i = 0; i < size; i++) {
      // cap the value of the randomIndex to mask.length - 1
      const randomIndex = randomUints[i] % mask.length;
      result += mask[randomIndex];
    }
    return result;
  }
  /** Generate a PKCE challenge verifier
   * @param length Length of the verifier
   * @returns A random verifier `length` characters long
   */
  private generateVerifier(length: number): string {
    return this.random(length);
  }
  /** Generate a PKCE code challenge from a code verifier
   * @param code_verifier
   * @returns The base64 url encoded code challenge
   */
  public generateChallenge(code_verifier: string) {
    return SHA256(code_verifier).toString(enc.Base64url);
  }
  /** Generate a PKCE challenge pair
   * @param length Length of the verifer (between 43-128). Defaults to 43.
   * @returns PKCE challenge pair
   */
  public generateCodePair(length: number = 43): PkceCodePair {
    if (length < 43 || length > 128) {
      throw `Expected a length between 43 and 128. Received ${length}.`;
    }
    const verifier = this.generateVerifier(length);
    const challenge = this.generateChallenge(verifier);
    return {
      codeVerifier: verifier,
      codeChallenge: challenge,
    };
  }
  /** Verify that a code_verifier produces the expected code challenge
   * @param code_verifier
   * @param expectedChallenge The code challenge to verify
   * @returns True if challenges are equal. False otherwise.
   */
  public verifyChallenge(code_verifier: string, expectedChallenge: string) {
    const actualChallenge = this.generateChallenge(code_verifier);
    return actualChallenge === expectedChallenge;
  }
}发起授权码请求 code
- 请求格式
拼装获取授权码 code 的请求 URL,在浏览器中直接访问或者通过请求客户端以 GET 形式发送给后端。
http://192.168.101.10:8847/herodotus-cloud-uaa/oauth2/authorize?response_type=code&client_id=67601992f3574c75809a3d79888bf16e&scope=openid&state=0CI0ziUDEnqMgqW0nzRNRCzLrs-9IMbqJzGZ47Zb0gY&redirect_uri=http://192.168.101.10:8847/herodotus-cloud-upms/open/authorized&code_challenge_method=S256&code_challenge=l5QmgnSFbUY0yguIXAmuUrP_170tluDLLFlcRsAxI7k- 参数说明:
| 参数 | 是否必须 | 说明 | 
|---|---|---|
| response_type | 必选 | 值固定为“code” | 
| client_id | 必选 | 第三方应用的标识 ID,告诉服务器谁需要得到授权 | 
| code_challenge | 必选 | 前端生成 | 
| code_challenge_method | 必选 | S256- 指代 SHA 256 算法 | 
| redirect_uri | 必选 | 授权成功后的重定向地址 | 
| state | 可选 | Client 提供的一个字符串,服务器会原样返回给 Client。用于防止恶意攻击。 | 
| scope | 可选 | 表示授权范围 | 
- 获取 Code
通过前面的请求,系统会跳转到 OAuth2 登录页面,登录成功后返回 Code。
返回参数:
| 参数 | 说明 | 
|---|---|
| code | 授权码模式中返回的 code | 
| state | 上一步中如果包含该参数,则原样返回 | 
- 获取 Token
示例请求:
POST http://192.168.101.10:8847/herodotus-cloud-uaa/oauth2/token| 参数 | 是否必须 | 说明 | 
|---|---|---|
| client_id | 必选 | 第三方应用的标识 ID,告诉服务器谁需要得到授权 | 
| code | 必选 | 上一步返回的code | 
| code_verifier | 必选 | 前端生成 | 
| grant_type | 必选 | authorization_code | 
| redirect_uri | 必选 | 授权成功后的重定向地址 | 
POSTMAN 请求示例

注意事项
因为 Authorization Code + PKCE 模式,并不需要 client_secret,更不需要对 client_id 和 client_secret 进行匹配验证。所以,在 Spring Authorization Server 中,使用 PKCE 客户端的 clientAuthenticationMethods 配置项的值中需要包含 NONE。
如果不包含该项配置,获取 Token 时将会抛错。代码逻辑如下:
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) authentication;
  if (!ClientAuthenticationMethod.NONE.equals(clientAuthentication.getClientAuthenticationMethod())) {
    return null;
  }
  String clientId = clientAuthentication.getPrincipal().toString();
  RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
  if (registeredClient == null) {
    throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
  }
  if (this.logger.isTraceEnabled()) {
    this.logger.trace("Retrieved registered client");
  }
  if (!registeredClient.getClientAuthenticationMethods()
    .contains(clientAuthentication.getClientAuthenticationMethod())) {
    throwInvalidClient("authentication_method");
  }
  if (this.logger.isTraceEnabled()) {
    this.logger.trace("Validated client authentication parameters");
  }
  // Validate the "code_verifier" parameter for the public client
  this.codeVerifierAuthenticator.authenticateRequired(clientAuthentication, registeredClient);
  if (this.logger.isTraceEnabled()) {
    this.logger.trace("Authenticated public client");
  }
  return new OAuth2ClientAuthenticationToken(registeredClient,
      clientAuthentication.getClientAuthenticationMethod(), null);
  }[8]Passkey通行密钥模式验证
Passkey(通行密钥)是一种新型的 无密码登录 技术,旨在提高在线账户的安全性并简化登录流程。它利用非对称加密技术,通过公钥和私钥对用户身份进行验证,避免了传统密码的使用。
Passkey 的使用非常方便,用户无需记忆复杂的密码,只需通过设备上的生物识别技术(如指纹或面部识别)即可完成登录。此外,Passkey 可以在多个设备上生成和使用,提高了用户的便利性。
Passkey 的安全性依赖于端上密钥的保护,由于非对称加密是目前加密强度最高的加密技术,理论上任何人都无法破解,可以说安全性是100%。即使数据库被盗,黑客也无法获取用户的私钥,因为私钥从未离开用户的设备。
重要
- Passkey通行密钥模式,因为需要调用具体设备的认证接口,涉及前端组件的配合,所以无法像其它认证模式一样,可以在类似于 POSTMAN 的软件中,直接使用接口验证。
- 出于安全性的考虑,Passkey 仅在 http://localhost 或者 https://域名 两种情况下生效
[1]Passkey注册
首先,用正常方式登录到系统当中。在【个人设置】->【账号管理】页面中,找到【Passkey(无密码登录)】。
点击【通行密钥】按钮进行注册,输入完标签后,会根据您当前使用的设备,调出对应的设备认证。下图就是以 Windows 11 为例,调出 Windows 的认证窗口。

输入当前设备的认证信息,通过认证后,将会当定相关信息。

[2]Passkey登录
Passkey 注册完成后之后,就可以使用其进行登录。
在登录页面,找到并点击【Passkey 快速登录】,会弹出以下窗口,用于选择已注册用户信息。如果您只绑定了一个账号,点击【继续】即可

在后续弹出的设备认证窗口,输入当前设的认证信息即可以登录系统。

[五]Dante Cloud 中的特性
[1]自定义授权模式支持 OIDC
Dante Cloud 自定义扩展的授权模式,全部支持 OIDC,以减少获取用户信息的请求次数
- 密码模式(Resource Owner Password Credentials Grant)
- 社会化模式(Social Credentials Grant)
[2]全部使用统一签发接口
Dante Cloud 自定义扩展了密码模式(Resource Owner Password Credentials Grant)和社会化模式(Social Credentials Grant)。其中社会化模式(Social Credentials Grant)支持手机验证登录、小程序登录、第三方系统认证登录等多种模式。
所有自定义授权模式,均采用与 Spring Authorization Server 一致的 Token 签发接口 oauth2/token,通过传递不同的参数就可以实现不同的登录方式。方便开发和使用。
