4. 授权服务器环境搭建:基于 Spring Authorization Server 或 Keycloak 的快速启动与数据库初始化

授权服务器是 OAuth2.0 授权码模式的核心枢纽,负责客户端注册、用户认证、授权码发放与令牌签发。本章将分别基于 Spring Authorization Server (SAS) 和 Keycloak 搭建开发环境,并完成数据库初始化,为后续实战提供可运行的授权端点。

4.1 环境选型对比

两种方案在集成深度、运维成本和功能丰富度上存在显著差异,下表从实战角度进行对比:

对比维度 Spring Authorization Server Keycloak
集成方式 嵌入 Spring Boot 应用,作为 starter 依赖 独立部署的 Java 进程,通过反向代理接入
配置复杂度 低,纯代码 + yaml 配置 中,依赖管理控制台或启动脚本
数据库支持 需手动配置数据源与 schema 脚本 内置 Liquibase 自动建表,开箱即用
用户存储 自定义 UserDetailsService 内置用户联邦或 LDAP 集成
管理界面 无,需自行开发管理端点 内置 Admin Console
生产推荐 适合微服务架构、定制化要求高的团队 适合快速交付、需要可视化管理的场景
选型建议: 本课程后续章节以 Spring Authorization Server 为主线,因为它与 Spring Security 生态无缝衔接,更贴近后端开发者的定制需求。Keycloak 方案作为对比参考,适合需要快速搭建完整 IAM 平台的场景。

4.2 基于 Spring Authorization Server 的快速启动

4.2.1 项目初始化与依赖配置

创建一个 Spring Boot 3.x 项目,引入核心依赖:

<!-- pom.xml -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
</parent>

<dependencies>
    <!-- 授权服务器核心 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
    </dependency>
    <!-- Web 支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 数据库驱动与连接池 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- 测试 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

4.2.2 数据库初始化脚本

SAS 使用 JDBC 存储已注册客户端、授权码和令牌。在 src/main/resources/schema.sql 中准备建表语句:

-- oauth2_registered_client:已注册客户端表
CREATE TABLE IF NOT EXISTS oauth2_registered_client (
    id VARCHAR(100) NOT NULL PRIMARY KEY,
    client_id VARCHAR(100) NOT NULL,
    client_id_issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    client_secret VARCHAR(200) DEFAULT NULL,
    client_secret_expires_at TIMESTAMP DEFAULT NULL,
    client_name VARCHAR(200) NOT NULL,
    client_authentication_methods VARCHAR(1000) NOT NULL,
    authorization_grant_types VARCHAR(1000) NOT NULL,
    redirect_uris VARCHAR(1000) DEFAULT NULL,
    post_logout_redirect_uris VARCHAR(1000) DEFAULT NULL,
    scopes VARCHAR(1000) NOT NULL,
    client_settings VARCHAR(2000) NOT NULL,
    token_settings VARCHAR(2000) NOT NULL
);

-- oauth2_authorization:授权码与令牌存储表
CREATE TABLE IF NOT EXISTS oauth2_authorization (
    id VARCHAR(100) NOT NULL PRIMARY KEY,
    registered_client_id VARCHAR(100) NOT NULL,
    principal_name VARCHAR(200) NOT NULL,
    authorization_grant_type VARCHAR(100) NOT NULL,
    authorized_scopes VARCHAR(1000) DEFAULT NULL,
    attributes BLOB DEFAULT NULL,
    state VARCHAR(500) DEFAULT NULL,
    authorization_code_value BLOB DEFAULT NULL,
    authorization_code_issued_at TIMESTAMP DEFAULT NULL,
    authorization_code_expires_at TIMESTAMP DEFAULT NULL,
    authorization_code_metadata BLOB DEFAULT NULL,
    access_token_value BLOB DEFAULT NULL,
    access_token_issued_at TIMESTAMP DEFAULT NULL,
    access_token_expires_at TIMESTAMP DEFAULT NULL,
    access_token_metadata BLOB DEFAULT NULL,
    access_token_type VARCHAR(100) DEFAULT NULL,
    access_token_scopes VARCHAR(1000) DEFAULT NULL,
    refresh_token_value BLOB DEFAULT NULL,
    refresh_token_issued_at TIMESTAMP DEFAULT NULL,
    refresh_token_expires_at TIMESTAMP DEFAULT NULL,
    refresh_token_metadata BLOB DEFAULT NULL,
    oidc_id_token_value BLOB DEFAULT NULL,
    oidc_id_token_issued_at TIMESTAMP DEFAULT NULL,
    oidc_id_token_expires_at TIMESTAMP DEFAULT NULL,
    oidc_id_token_metadata BLOB DEFAULT NULL,
    oidc_id_token_claims VARCHAR(2000) DEFAULT NULL,
    user_code_value BLOB DEFAULT NULL,
    user_code_issued_at TIMESTAMP DEFAULT NULL,
    user_code_expires_at TIMESTAMP DEFAULT NULL,
    user_code_metadata BLOB DEFAULT NULL,
    device_code_value BLOB DEFAULT NULL,
    device_code_issued_at TIMESTAMP DEFAULT NULL,
    device_code_expires_at TIMESTAMP DEFAULT NULL,
    device_code_metadata BLOB DEFAULT NULL
);

-- oauth2_authorization_consent:用户授权记录表
CREATE TABLE IF NOT EXISTS oauth2_authorization_consent (
    registered_client_id VARCHAR(100) NOT NULL,
    principal_name VARCHAR(200) NOT NULL,
    authorities VARCHAR(1000) NOT NULL,
    PRIMARY KEY (registered_client_id, principal_name)
);
注意: 生产环境请务必使用数据库迁移工具(如 Flyway 或 Liquibase)管理 schema 版本,避免直接执行原始 SQL。本课程为快速启动,采用 spring.sql.init.mode=always 自动执行 schema.sql。

4.2.3 核心配置类

创建 AuthorizationServerConfig 配置类,注册客户端信息并启用 JDBC 存储:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig {

    @Bean
    public RegisteredClientRepository registeredClientRepository(
            JdbcTemplate jdbcTemplate) {
        return new JdbcRegisteredClientRepository(jdbcTemplate);
    }

    @Bean
    public OAuth2AuthorizationService authorizationService(
            JdbcTemplate jdbcTemplate,
            RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(
                jdbcTemplate, registeredClientRepository);
    }

    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(
            JdbcTemplate jdbcTemplate,
            RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(
                jdbcTemplate, registeredClientRepository);
    }

    // 初始化一个测试客户端(仅用于开发环境)
    @Bean
    public CommandLineRunner initTestClient(
            RegisteredClientRepository clientRepository) {
        return args -> {
            RegisteredClient client = RegisteredClient.withId("test-client")
                    .clientId("messaging-client")
                    .clientSecret("{noop}secret")
                    .clientAuthenticationMethod(
                            ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                    .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                    .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                    .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client")
                    .scope("message.read")
                    .scope("message.write")
                    .tokenSettings(TokenSettings.builder()
                            .accessTokenTimeToLive(Duration.ofMinutes(30))
                            .refreshTokenTimeToLive(Duration.ofDays(1))
                            .build())
                    .build();
            clientRepository.save(client);
        };
    }
}

4.2.4 application.yml 配置

spring:
  application:
    name: authorization-server
  datasource:
    url: jdbc:mysql://localhost:3306/oauth2_db?useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: root123
    driver-class-name: com.mysql.cj.jdbc.Driver
  sql:
    init:
      mode: always
      schema-locations: classpath:schema.sql

server:
  port: 9000

4.3 授权服务器启动流程架构

下图展示了 SAS 启动时各组件之间的初始化顺序与依赖关系:

Spring Authorization Server 启动流程 Spring Boot 启动 数据源初始化 执行 schema.sql 注册 Repository Bean CommandLineRunner 初始化客户端 关键说明 • schema.sql 仅在首次启动时执行 • 测试客户端仅用于开发环境 • 生产环境需移除 CommandLineRunner

4.4 基于 Keycloak 的快速启动

4.4.1 Docker Compose 一键部署

Keycloak 22+ 版本已内置 PostgreSQL 支持,推荐使用 Docker Compose 启动:

# docker-compose.yml
version: '3.8'
services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: keycloak123
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - keycloak_net

  keycloak:
    image: quay.io/keycloak/keycloak:22.0.5
    command: start-dev --import-realm
    environment:
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: keycloak123
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin123
    ports:
      - "8080:8080"
    volumes:
      - ./realm-export.json:/opt/keycloak/data/import/realm-export.json
    depends_on:
      - postgres
    networks:
      - keycloak_net

volumes:
  postgres_data:

networks:
  keycloak_net:
    driver: bridge

4.4.2 预导入 Realm 配置

创建 realm-export.json 文件,定义 OAuth2 客户端和用户:

{
  "realm": "oauth2-lab",
  "enabled": true,
  "clients": [
    {
      "clientId": "web-app",
      "enabled": true,
      "protocol": "openid-connect",
      "publicClient": false,
      "secret": "web-app-secret",
      "redirectUris": ["http://localhost:3000/*"],
      "standardFlowEnabled": true,
      "directAccessGrantsEnabled": false
    }
  ],
  "users": [
    {
      "username": "alice",
      "enabled": true,
      "credentials": [
        {
          "type": "password",
          "value": "alice123"
        }
      ],
      "realmRoles": ["user"]
    }
  ]
}
启动命令: 在 docker-compose.yml 所在目录执行 docker compose up -d,等待约 30 秒后访问 http://localhost:8080,使用 admin/admin123 登录管理控制台。

4.5 数据库初始化对比总结

初始化方式 SAS 方案 Keycloak 方案
建表机制 手动编写 schema.sql,通过 spring.sql.init 执行 Liquibase 自动检测并执行变更集
初始数据 CommandLineRunner 或 data.sql realm-export.json 预导入
版本管理 需自行集成 Flyway 内置 changelog 追踪
回滚支持 需手动编写回滚脚本 Liquibase 支持回滚标签
适用阶段 开发/测试环境快速验证 开发/演示环境一键部署

4.6 验证授权服务器是否就绪

启动成功后,通过以下端点验证授权服务器核心功能是否正常:

  1. 获取授权端点元数据:GET /.well-known/oauth-authorization-server,返回 JSON 格式的 issuer、authorization_endpoint、token_endpoint 等信息。
  2. 检查数据库表结构:连接 MySQL 或 PostgreSQL,确认 oauth2_registered_client 表已创建且包含测试客户端记录。
  3. 模拟授权码请求(浏览器访问):
    http://localhost:9000/oauth2/authorize?response_type=code&client_id=messaging-client&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client&scope=message.read
    应跳转到登录页面,输入测试用户凭证后返回授权码。
常见问题排查: 如果启动报错“Table 'oauth2_authorization' doesn't exist”,请检查 spring.sql.init.mode 是否设置为 always,并确认 schema.sql 位于 classpath 根目录。Keycloak 启动失败时,查看容器日志 docker logs <container_id>,重点关注数据库连接和 realm 导入错误。