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 启动时各组件之间的初始化顺序与依赖关系:
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 验证授权服务器是否就绪
启动成功后,通过以下端点验证授权服务器核心功能是否正常:
- 获取授权端点元数据:
GET /.well-known/oauth-authorization-server,返回 JSON 格式的 issuer、authorization_endpoint、token_endpoint 等信息。 - 检查数据库表结构:连接 MySQL 或 PostgreSQL,确认
oauth2_registered_client表已创建且包含测试客户端记录。 - 模拟授权码请求(浏览器访问):
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 导入错误。