Identity Server
本章只对应 E:\gitcode\spire\soulsoft_identity_server\samples 里的样例。
样例不是单个程序,而是三个角色一起工作:
samples/server:认证与授权服务器,监听http://127.0.0.1:5000samples/client:前端 OIDC 客户端,运行在http://127.0.0.1:3000samples/api:受保护资源服务器,监听http://127.0.0.1:8080
它们组合起来展示的是一条完整链路:
浏览器客户端
-> Identity Server 登录与授权
-> 拿到 code / token
-> 带 access_token 调用 API样例里到底配了什么
samples/server/src/main.cj 做了四件核心事情:
- 注册 Identity Server,并设置
issuer、issuerUri、登录页和同意页地址。 - 通过内存配置加载客户端、身份资源、API Scope、API Resource。
- 注册签名凭据、ProfileService、密码模式校验器、扩展授权校验器。
- 把 Identity Server 中间件插入 HTTP 管道。
最小骨架就是这样:
import soulsoft_identity_tokens.*
import soulsoft_identity_server.*
import soulsoft_web_hosting.*
import soulsoft_web_routing.*
import soulsoft_web_staticfiles.*
import soulsoft_web_authentication.*
import soulsoft_web_authentication_cookies.*
import soulsoft_web_authorization.*
import soulsoft_extensions_injection.*
import soulsoft_identity_server.authentication.*
let builder = WebHost.createBuilder()
let rsaKey = RsaSecurityKey.fromPemFiles("rsa256_public.pem", "rsa256_private.pem")
builder.services.addIdentityServer { options =>
options.issuer = "soulsoft"
options.issuerUri = "http://localhost:5000"
options.interaction.loginUrl = "/account/login.html"
options.interaction.consentUrl = "/account/consent.html"
}
.addInMemoryClients(builder.configuration.getSection("identityserver:clients"))
.addInMemoryApiScopes(builder.configuration.getSection("identityserver:apiScopes"))
.addInMemoryApiResources(builder.configuration.getSection("identityserver:apiResources"))
.addInMemoryIdentityResources(builder.configuration.getSection("identityserver:identityResources"))
.addInMemorySigningCredentials([SigningCredentials(rsaKey, SecurityAlgorithms.RsaSha256)])
.addProfileService<ProfileService>()
.addExtensionGrantValidator<EmailExtensionGrantValidator>("email")
.addExtensionGrantValidator<PhoneExtensionGrantValidator>("phone")
.addPasswordGrantValidator<ResourceOwnerPasswordValidator>()
builder.services.addRouting()
builder.services.addAuthentication(LocalApiAuthenticationDefaults.Scheme)
.addCookie()
.addLocalApi()
builder.services.addAuthorizationBuilder().addPolicy(CookieAuthenticationDefaults.Scheme) { policy =>
policy.addAuthenticationSchemes(CookieAuthenticationDefaults.Scheme)
policy.requireAuthenticatedUser()
}
let app = builder.build()
app.useIdentityServer()
app.useFileServer()
app.useRouting()
app.useAuthentication()
app.useAuthorization()配置模型
samples/server/appsettings.json 里把 Identity Server 的元数据都放进了 identityserver 节点。
样例里的资源模型是:
- 客户端
interactive.publicauthorization_code + refresh_tokenrequirePkce = trueallowedRedirectUris = ["http://127.0.0.1:3000/callback.html"]allowedScopes = ["profile", "openid", "api"]
- 客户端
localauthorization_code + password- 带
clientSecrets
- 身份资源
openidprofile
- API Scope
apioffline_access
- API Resource
order- 允许的 scope 是
api
这说明样例同时覆盖了两类场景:
- 浏览器前端通过授权码模式登录
- 本地客户端通过密码模式或扩展授权模式拿令牌
协议端点
Identity Server 默认暴露的协议端点可以直接从 ConstantEndpointPaths 看出来:
/connect/authorize/.well-known/openid-configuration/.well-known/openid-configuration/jwks/connect/token/connect/userinfo/connect/revocation/connect/introspect/connect/endsession/connect/endsession-callback
这些端点由 useIdentityServer() 挂进去,不是你手工 mapGet() / mapPost() 出来的。
中间件位置
样例把 useIdentityServer() 放在最前面,这个顺序是有意义的:
IdentityServer -> StaticFiles -> Routing -> Authentication -> Authorization -> Endpoint原因很直接:
- 协议端点先由 Identity Server 中间件接管
- 登录页、同意页再由静态文件中间件提供
- 业务端点继续走普通路由、认证和授权链路
另外 useIdentityServer() 内部还会做两件事:
useCors()- 检查
IdentityServerOptions是否已经注册
所以它不是一个“纯路由映射”扩展,而是真正的协议中间件入口。
授权码流程时序
samples/client 使用 oidc-client-ts,配置如下:
authority = "http://localhost:5000"client_id = "interactive.public"response_type = "code"scope = "openid profile offline_access"redirect_uri = "http://127.0.0.1:3000/callback.html"
整条交互链路如下:
这里有两个页面不是协议端点,而是样例自己提供的交互页面:
/account/login.html/account/consent.html
对应的处理端点则是:
POST /account/loginPOST /account/consent/dataPOST /account/consent
访问受保护 API 的时序
前端拿到 access_token 后,会调用 http://127.0.0.1:8080/order。
samples/api 的配置是:
- JWT Bearer 认证
authority = "http://127.0.0.1:5000"validAudiences = ["order"]- 默认授权策略要求
scope = "api"
对应执行链路如下:
这里要注意两个约束是分开的:
validAudiences = ["order"]解决“这个 token 是不是发给当前 API 的”- 默认授权策略
requireClaim("scope", "api")解决“这个 token 有没有访问当前资源的权限”
样例里的几个扩展点
ProfileService
samples/server/src/services/ProfileService.cj 负责把用户资料转成 claim。样例里返回了:
nameemailpicture
这意味着 userinfo 和身份令牌里的 profile 类声明,最终都可以从这里扩展。
ResourceOwnerPasswordValidator
samples/server/src/validators/ResourceOwnerPasswordValidator.cj 演示了密码模式的接入点。
它接收 username 和 password,验证成功后返回 IdentityUser("1024")。样例里用户名密码是硬编码的:
admin1024
EmailExtensionGrantValidator / PhoneExtensionGrantValidator
这两个类展示的是扩展授权类型,不需要改协议中间件本身,只要注册校验器即可:
grantType = "email"grantType = "phone"
它们分别从请求参数里读取:
email + codephone + code
验证通过后同样返回 IdentityUser("1024")。
这正是这个架构最有意思的地方:协议端点是统一的,但授权方式和用户装载逻辑是可插拔的。
LocalApi 与 Cookie 的边界
样例服务端同时用了两套认证:
CookieLocalApi
它们解决的问题不同:
Cookie用于浏览器登录态和同意页LocalApi用于当前 Identity Server 自己暴露的本地 API 认证
LocalApiAuthenticationHandler 的执行方式是:
- 从请求里解析 token。
- 调用
ITokenValidator.validateAccessToken(token)。 - 把结果 claim 写入
ClaimsIdentity(options.authenticationType)。 - 返回
AuthenticationTicket,认证方案名固定为"local"。
所以 LocalApi 不是外部 API 的 Bearer 认证替代品,而是 Identity Server 宿主进程内部的本地访问方案。
可以直接照着样例理解的几个结论
1. addIdentityServer(...) 注册的是整套协议能力
它不只是一个配置对象,还会继续挂上:
- validators
- default endpoints
- response generators
- consent / refresh_token / authorization_code 的内存存储
2. useIdentityServer() 是协议入口,不是普通业务路由
协议请求先被它接住,只有不是协议路径的请求才会继续落到静态文件或业务端点。
3. 登录页和同意页是普通页面,但它们驱动的是标准 OIDC 流程
也就是说,页面可以换,协议端点不需要换。
4. 资源服务器不需要自己理解授权码流程
samples/api 只需要:
- 信任
authority - 验证
aud - 再做自己的授权策略判断
它只消费 access token,不参与登录交互。