工作流程如下,客户端通过登录服务器的 oidc 接口登录后获得 access_token, 使用该 token 访问 api 服务, api 服务到登录服务器验证 token 的合法性,并返回数据。
工作方式分以下几种
登录服务器一般返回如下内容
- 短时间内有效的 AccessToken,可能为 jwt 格式,也可能配置为非 jwt 的非透明 token
- 包括用户信息的也短时间内有效的 IdentityToken
- Authorization: Bearer 传入登录服务端返回 jwt 格式的 IdentityToken, api 服务器接收到的 token 到登录服务器验证通过后解码出 payload, 将数据填充 user 模型, 在工作流程中验证 token 的各种权限
- 验证流程指到登录服务器的 .well-known/openid-configuration 获取签名对 token 进行签名验证
- 一般 identity_token 不应该作为 Authorization: Bearer 发送给 API, 只在特殊情况下从简处理
- Authorization: Bearer 登录服务端直接返回 jwt 格式的 access_token , api 服务器接收到的 token 验证通过后解码出 payload, 将数据直接填充 user 模型, 在工作流程中验证 token 的各种权限
- 验证流程指到登录服务器的 .well-known/openid-configuration 获取签名对 token 进行签名验证
- 与使用上上面的 IdentityToken 的区别是, IdentityToken 内容比较丰富,包括的内容会比较多,但是可能会传递一个庞大的 Bearer。
- access_token 比较短,可能只包含用户 id 等很少的用户信息, 但可以使用它从登录服务器上获取用户详细信息, 而 IdentityToken 无法用来获取用户详细信息
- Authorization: Bearer 登录服务器返回非透明 access_token (非 jwt 格式), 与 jwt 格式的 access_token 的区别是 非透明 access_token 非 jwt 格式, api 服务器必须到登录服务器获取用户详细信息, 但是它是最短的。
- 验证流程指到登录服务器的 /introspection 接口调用并成功并返回用户数据,然后填充到 user 模型中
- 比 jwt 格式的 access_token 还短,但是必须到登录服务器获取用户详细信息。因为上面的 access_token 是 jwt 格式的,包括了部分用户信息
登录验证方式
下面演示了 2 种登录验证试
- 直接传入 jwt 格式的 token , 可以是 jwt 格式 access_token 也可以直接使用 identity_token, 下面例子中的 'Authelia' 策略
- 直接传入非透明格式的 access_token (非 jwt 格式), 下面例子中的 'Authelia_Introspection' 方式
- 因为使用多个策略, 所以需要动态选择当前传入的 token 使用哪个处理策略, 下面例子中的 'JWT_Selector' 策略
代码
下面分为三个部分
.AddPolicyScheme("JWT_Selector")动态选择策略
AddOAuth2Introspection("Authelia_Introspection")内省模式,处理非透明 access_token
.AddJwtBearer("Authelia")jwt token 方案
// 分布式缓存,Microsoft.Extensions.Caching.Hybrid
builder.Services.AddHybridCache(_ => { });
builder.Services.AddAuthentication(options =>
{
# 指定使用 JWT_Selector 策略
options.DefaultAuthenticateScheme = "JWT_Selector";
options.DefaultChallengeScheme = "JWT_Selector";
})
.AddPolicyScheme("JWT_Selector", "选择验证方案", options =>
{
// 动态选择验证方案
options.ForwardDefaultSelector = context =>
{
// 1. 取出 bearer token 待处理
var authorization = context.Request.Headers.Authorization.ToString();
// 2. 如果 token 中包含 authelia_,则使用非透明 token 验证
if (!string.IsNullOrWhiteSpace(authorization) && authorization.Contains("authelia_"))
{
// 这里使用 authelia 的非透明 token 方案
return "Authelia_Introspection";
}
// 3. 如果 token 中包含 something_special_for_ms,则使用微软文件验证, 这里未完成,以后再说
if (!string.IsNullOrWhiteSpace(authorization) && authorization.Contains("something_special_for_ms"))
{
// 这里选择微软文件,(未完成)
return "Microsoft";
}
// 4. 其它默认走 Authelia 验证
return "Authelia";
};
})
// 2.1 自省模式, 这里使用 Authelia 的非透明 token 方案,调用函数时 bearer token 中的 token 为非 jwt 格式,
// 接收到 token 后,根据配置到 Authority 中的服务器获取 jwt 用户信息
// 要引用 `Duende.AspNetCore.Authentication.OAuth2Introspection` nuget 包
// 需要在 authelia 中配置一个 odic client 节点,并指定 client_id 与 client_secret
.AddOAuth2Introspection("Authelia_Introspection", options =>
{
options.Authority = "https://auth.xapi.fun"; // 验证服务器
options.ClientId = "iox-webserver-app";
options.ClientSecret = "xxxx";
options.ClientCredentialStyle = ClientCredentialStyle.AuthorizationHeader; # 指定验证方式使用 Basic, authelia 支持该模式
options.NameClaimType = "preferred_username"; # user 中的 name 使用 preferred_username
options.RoleClaimType = "groups"; # user 中的 role 使用 groups
// options.AuthenticationType = "oidc"; // 重写默认的验证方案名称
options.CacheDuration = TimeSpan.FromSeconds(60); // 程序默认带缓存功能, 这里重写缓存时长, 默认 5 分钟
// 不幸的时,authelia 验证成功后,返回的 token 中没有 preferred_username 与 groups
// 这里添加自定义处理逻辑, 从 userinfo 接口中获取 preferred_username 与 groups
options.Events = new OAuth2IntrospectionEvents
{
// 在验证成功后,添加自定义处理逻辑
OnTokenValidated = async context =>
{
var accessToken = context.SecurityToken;
var cacheKey = $"UserInfo_{accessToken.GetHashCode()}";
var cache = context.HttpContext.RequestServices.GetRequiredService<HybridCache>();
// 这里使用了并发缓存
var userInfo = await cache.GetOrCreateAsync(
cacheKey,
(Token: accessToken, options.Authority),
static async (state, token) =>
{
// 注意:这里使用了 static,确保不会捕获任何外部变量
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", state.Token);
var url = $"{state.Authority?.TrimEnd('/')}/api/oidc/userinfo";
return await client.GetFromJsonAsync<JsonElement>(url, token);
}
);
if (context.Principal?.Identity is ClaimsIdentity identity)
{
identity.AddClaim(new Claim("preferred_username", userInfo.GetProperty("preferred_username").GetString()!));
if (userInfo.TryGetProperty("groups", out var groups))
{
foreach (var g in groups.EnumerateArray())
{
identity?.AddClaim(new Claim("groups", g.GetString()!));
}
}
}
await Task.CompletedTask;
},
// 当出错的时候,调用此函数在返回头中加入错误信息, 自己调试时使用
OnAuthenticationFailed = context =>
{
// 这里可以查看或记录获取的性的错误 错误信息
context.Response.Headers.Append("x-auth-failed-reason", context.Error);
return Task.CompletedTask;
},
};
})
// 4. 这里使用 jwt token 方案,调用函数时 bearer token 中的 token 为 jwt 格式, 可以为 jwt access_token 或是 identity_token
.AddJwtBearer("Authelia", options =>
{
options.Authority = "https://auth.xapi.fun"; // 验证服务器地址
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = "https://auth.xapi.fun", // token 中的签发地址
ValidateIssuer = true,
ValidAudience = "iox-linkedin-app", // token 中的 aud (client_id)
ValidateAudience = false, // authelia 返回的 jwt accesstoken 默认 aud 是空的,这是配置为不校验, 如果使用 identity_token 里面有 aud, 可配置为 true
ValidateLifetime = true, // 验证 token 的有效期
NameClaimType = "preferred_username", # user 中的 name 使用 preferred_username
RoleClaimType = "groups", // # user 中的 role 使用 groups
};
// 当出错的时候,调用此函数在返回头中加入错误信息, 自己调试时使用
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
// 返回值中带入 错误信息
context.Response.Headers.Append("x-auth-failed-reason", context.Exception.Message);
return Task.CompletedTask;
},
};
});
authelia 配置
自省模式
为 api 服务器配置一个 oidc client, 然后填写到 api 服务器上
- client_id: 'iox-webserver-app'
client_name: 'iox web server app'
claims_policy: 'default'
# docker run --rm authelia/authelia:latest authelia crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 --random.charset rfc3986
client_secret: 'xxx' # 生成 token 的 hash 值, 客户端填写原值
public: false # 必须设置为 false
authorization_policy: 'one_factor' # 验证模式为一次验证
scopes: ['openid', 'profile', 'appinfo_scope' ] # 客户端的访问权限
options.Authority = "https://auth.xapi.fun"; // 验证服务器
options.ClientId = "iox-webserver-app";
options.ClientSecret = "xxxx";
options.ClientCredentialStyle = ClientCredentialStyle.AuthorizationHeader; # 指定验证方式使用 Basic, authelia 支持该模式
options.NameClaimType = "preferred_username"; # user 中的 name 使用 preferred_username
options.RoleClaimType = "groups"; # authelia 不带 role , 这里将 user 中的 role 使用 groups替代
jwt access_token 模式
authelia 默认返回非透明 (非 jwt 格式) access_token, 如果要返回 jwt access_token, 需要配置如下
clients:
- client_id: 'iox-linkedin-app'
response_types: 'code'
access_token_signed_response_alg: 'RS256' # 填写 access_token 的签名算法, 这样就强制 authelia 返回 jwt 格式的 access_token