Skip to content

授权类型

涵盖 Identity Server 支持的所有授权模式——密码、授权码、客户端凭证、刷新令牌、扩展授权,以及各自的校验器实现和适用场景。

适用场景

  • 需要为不同客户端选择最合适的授权模式
  • 需要实现自定义授权类型(如短信验证码、邮箱验证码登录)
  • 需要理解授权码流程中登录页和同意页的作用
  • 需要对接需要刷新令牌的客户端

前置条件

授权类型的使用依赖 Identity Server 核心注册和 Client.allowedGrantTypes 配置,详见 快速开始

授权类型是否必须实现校验器是否需要登录页
password必须:IResourceOwnerPasswordValidator
authorization_code否(内置)
client_credentials否(内置)
refresh_token否(内置)
扩展授权必须:IExtensionGrantValidator

快速开始

每种授权类型对应一个 grant_type 参数值。客户端向 /connect/token(或 /connect/authorize)发起请求并指定 grant_type,Identity Server 根据类型调用对应的校验器。

先看密码模式的完整请求——这是最简单的上手方式,只需一个 HTTP POST:

POST http://localhost:5000/connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=admin&password=1024&client_id=client&client_secret=secret

成功响应:

json
{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600
}

核心概念

密码模式(password)

密码模式信任客户端能安全收集用户凭证。客户端直接将用户名和密码与令牌请求一起发送给 Identity Server。

必须实现 IResourceOwnerPasswordValidator

cangjie
import soulsoft_identity_server.validation.*

class MyPasswordValidator <: IResourceOwnerPasswordValidator {
    func validate(context: ResourceOwnerPasswordValidationContext): Unit {
        if (context.username != "admin" || context.password != "1024") {
            throw ValidationException("invalid_grant", "用户名或密码错误")
        }
    }
}

// 注册
builder.services.addIdentityServer()
    .addPasswordGrantValidator<MyPasswordValidator>()

校验通过后,Identity Server 自动调用 IProfileService.getProfileData() 获取 claims 并签发 access_token。

适用场景:受信任的应用(如官方移动端、内部工具)。不适合 SPA——客户端代码中处理用户凭证不安全。

授权码模式(authorization_code)

授权码模式是 OAuth 2.0 最安全的授权方式。用户凭据只在 Identity Server 的登录页提交,客户端永远接触不到密码。

流程分三步:

text
① 客户端重定向到 /connect/authorize
   └── 未登录 → 重定向到 loginUrl(你实现的登录页)
        └── 登录成功后 → Cookie 写入登录态 → 重定向回 /connect/authorize

② 未同意 → 重定向到 consentUrl(你实现的同意页)
   └── 用户确认作用域 → 记录同意 → 重定向回 /connect/authorize

③ 生成授权码 → 重定向到客户端的 redirect_uri?code=xxx
   └── 客户端用 code 向 /connect/token 换取 access_token

客户端配置要求

cangjie
Client("interactive.public", []).also { c =>
    c.requireSecret = false       // 公开客户端,不要求密钥
    c.requireConsent = true       // 需要用户同意
    c.allowOfflineAccess = true   // 允许刷新令牌
    c.requirePkce = true          // 强制 PKCE
    c.allowedGrantTypes = [GrantTypes.AuthorizationCode, GrantTypes.RefreshToken]
    c.allowedRedirectUris = ["http://127.0.0.1:3000/callback.html"]
    c.allowedScopes = ["openid", "profile", "api"]
}

重要配置

  • requirePkce = true — 公开客户端应强制要求 PKCE,防止授权码拦截攻击
  • allowedRedirectUris — 白名单校验,授权码只会重定向到此处列出的地址
  • requireConsent = true — 是否要求用户确认授权的作用域

适用场景:SPA、移动端、第三方应用。有 PKCE 加持的授权码模式是当前安全标准推荐的方式。

客户端凭证模式(client_credentials)

客户端凭证模式用于服务间通信——没有用户参与,客户端以自己的身份获取 access_token。

client_credentials 模式虽然不涉及用户(subject 为空主体),但令牌生成流程仍会统一调用 IProfileService.getProfileData()——你仍需注册 IProfileService 实现。客户端通过 client_id + client_secret 证明身份:

POST http://localhost:5000/connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=service-a&client_secret=service-secret&scope=api

适用场景:微服务间调用、后台任务、CI/CD 工具。不需要用户上下文。

刷新令牌模式(refresh_token)

access_token 过期后,客户端用 refresh_token 获取新的 access_token,无需用户重新登录:

POST http://localhost:5000/connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=xxx&client_id=client&client_secret=secret

刷新令牌行为由 Client 的三个参数控制

参数默认值说明
refreshTokenExpirationAbsoluteAbsolute:绝对过期(30 天),Sliding:滑动过期(每次刷新重置 15 天)
refreshTokenUsageReUseReUse:刷新后旧 token 仍可用,Once:刷新后旧 token 立即失效
absoluteRefreshTokenLifetime2592000(30 天)刷新令牌的绝对生命周期,超期后必须重新登录

适用场景:需要长期访问的应用,配合授权码模式使用。

扩展授权类型

当内置的 4 种授权不够用时,可以实现 IExtensionGrantValidator 添加自定义授权。

下面是一个邮箱验证码授权的完整示例:

cangjie
import soulsoft_identity_server.validation.*

class EmailExtensionGrantValidator <: IExtensionGrantValidator {
    var grantType = "email"

    func validate(context: ExtensionGrantValidationContext): Unit {
        let email = context.parameters.get("email") ?? ""
        let code = context.parameters.get("code") ?? ""
        if (email.isEmpty()) {
            throw ValidationException("invalid_grant", "邮箱不能为空")
        }
        if (code != "123456") {
            throw ValidationException("invalid_grant", "验证码错误")
        }
    }
}

// 注册
builder.services.addIdentityServer()
    .addExtensionGrantValidator<EmailExtensionGrantValidator>("email")

客户端请求时指定自定义的 grant_type

POST http://localhost:5000/connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=email&email=test@example.com&code=123456&client_id=client&client_secret=secret

context.parameters 包含了请求体中的所有原始参数(包括 grant_typeclient_idclient_secret),你可以按业务需要读取任意字段。框架不自动剔除任何参数。

适用场景:短信验证码登录、邮箱验证码登录、第三方社交登录(微信/支付宝)、生物识别认证等。

常见用法

授权类型与客户端组合

同一个 Identity Server 可以同时支持多种授权类型,不同客户端使用不同的授权方式:

cangjie
builder.services.addIdentityServer() { options =>
    options.issuer = "spire-idp"
    options.issuerUri = "http://localhost:5000"
}
    // 密码模式客户端——内部工具使用
    .addInMemoryClients([
        Client("internal-tool", [Secret("secret".sha256())]).also { c =>
            c.allowedGrantTypes = [GrantTypes.Password]
            c.allowedScopes = ["api"]
        },
        // 授权码模式客户端——SPA 使用
        Client("spa-app", []).also { c =>
            c.requireSecret = false
            c.requirePkce = true
            c.allowedGrantTypes = [GrantTypes.AuthorizationCode, GrantTypes.RefreshToken]
            c.allowedScopes = ["openid", "profile", "api"]
            c.allowedRedirectUris = ["http://localhost:3000/callback.html"]
        },
        // 客户端凭证模式——微服务使用
        Client("service-a", [Secret("service-secret".sha256())]).also { c =>
            c.allowedGrantTypes = [GrantTypes.ClientCredentials]
            c.allowedScopes = ["api"]
        }
    ])

授权码模式的端点配置

interaction 配置控制授权码流程中各个跳转页面的地址:

cangjie
builder.services.addIdentityServer() { options =>
    options.interaction.loginUrl = "/account/login"       // 未登录时跳转
    options.interaction.consentUrl = "/account/consent"    // 未同意时跳转
    options.interaction.errorUrl = "/error"                // 出错时跳转
    options.interaction.returnUrlParameter = "returnUrl"   // 回跳参数名
}

returnUrlParameter 的值(默认 "returnUrl")会被自动拼接为 query string 参数,携带完整的授权请求 URL。你的登录页需在登录成功后读取这个参数并重定向回去。

生产建议

  • 公开客户端(SPA、移动端)必须使用授权码模式 + PKCE,不允许用密码模式。 密码模式要求客户端持有用户凭证,对公开客户端而言这是不可接受的安全风险。requirePkce = true 可有效防止授权码拦截攻击。
  • refreshTokenUsage = Once 更安全。 每次刷新后旧 token 立即失效——如果旧 token 被泄露,持有者在下一次刷新时会因为 token 已被消费而无法刷新,攻击被暴露。
  • 扩展授权中不要信任 context.parameters 的原始值。 校验器应验证所有输入的有效性(非空、格式、长度),并通过 ValidationException 报告错误——框架会自动返回 400 和对应的 error 字段。

常见问题

密码模式和授权码模式怎么选

密码模式适合你控制的应用——官方移动端、内部管理工具,客户端代码在你的控制范围内。授权码模式适合你不控制的应用——第三方应用、SPA,用户凭证永远不应离开 Identity Server 的登录页。简单判断:如果需要 client_secret 能安全保管就用密码模式,否则用授权码模式 + PKCE。

客户端凭证模式是否调用 IProfileService

客户端凭证模式没有用户参与——客户端以自己的身份(而非用户身份)请求令牌。但由于令牌生成流程统一调用 IProfileService.getProfileData(),你仍需注册 IProfileService 实现。可以在 getProfileData() 中通过 request.subject 是否为空主体来判断是否为客户端凭证模式,从而返回空声明集合或不返回用户相关声明。令牌中的 claims 会包括客户端 allowedClaims、scope 声明以及 IProfileService 返回的声明。

扩展授权和密码验证器有什么区别

密码验证器 IResourceOwnerPasswordValidator 绑定固定的 grant_type=password,只接收 usernamepassword 两个字段。扩展授权 IExtensionGrantValidator 可以自定义 grant_type 名称(如 "email")和任意参数(如 emailcode),更灵活。如果你的"密码模式"需要额外字段(如图形验证码),用扩展授权替代。

相关专题