Skip to main content

OAuth2 使用教程 微信授权登录 接入第三方api

本文是这个示例项目的实操教程,目标是让你从 0 到 1 跑通 OAuth2 常见流程,并理解每一步在做什么。

1. 项目说明

本项目同时扮演两个角色:

  • 授权服务器(Authorization Server):负责签发令牌(access token / refresh token)
  • 资源服务器(Resource Server):负责校验令牌并保护业务接口

已经覆盖的常见功能:

  • 授权码模式(Authorization Code)
  • 客户端凭证模式(Client Credentials)
  • 刷新令牌(Refresh Token)
  • JWT 签发与校验
  • 基于 scope 的接口权限控制
  • 令牌撤销、令牌自省(端点已具备)

2. 启动项目

启动后访问:

  • http://localhost:9000/:项目信息
  • http://localhost:9000/health:健康检查

3. 默认账号与客户端

3.1 用户账号(用于登录授权)

  • admin / 123456
  • user / 123456

3.2 OAuth2 客户端

  1. 授权码客户端(适合“用户登录第三方应用”)
  • client_id: demo-client-auth-code
  • client_secret: demo-secret
  • 支持:authorization_coderefresh_token
  1. 机器客户端(适合“服务与服务通信”)
  • client_id: demo-client-machine
  • client_secret: machine-secret
  • 支持:client_credentials

4. 快速跑通

### ============================================
### OAuth2 示例请求集合(按顺序执行)
### 适配:Spring Boot OAuth2 Demo (localhost:9000)
### ============================================

@host = http://localhost:9000
@machineClientId = demo-client-machine
@machineClientSecret = machine-secret
@authCodeClientId = demo-client-auth-code
@authCodeClientSecret = demo-secret
@redirectUri = http://localhost:9000/oauth2/callback

### 0) 健康检查(服务是否已启动)
GET {{host}}/health

### 1) 获取 JWKS(查看 JWT 公钥)
GET {{host}}/oauth2/jwks

### 2) 客户端凭证模式:获取 access_token
POST {{host}}/oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic ZGVtby1jbGllbnQtbWFjaGluZTptYWNoaW5lLXNlY3JldA==

grant_type=client_credentials&scope=message.read message.write

> {%
client.test("Client Credentials 获取成功", function () {
client.assert(response.status === 200, "HTTP 状态码不是 200");
client.assert(!!response.body.access_token, "access_token 为空");
});
client.global.set("access_token", response.body.access_token);
%}

### 3) 使用 access_token 访问 /api/me(查看当前认证主体和 claims)
GET {{host}}/api/me
Authorization: Bearer {{access_token}}

### 4) 读接口(需要 scope: message.read)
GET {{host}}/api/messages/read
Authorization: Bearer {{access_token}}

### 5) 写接口(需要 scope: message.write)
POST {{host}}/api/messages/write
Authorization: Bearer {{access_token}}

### 6) 错误示例:不带 token 访问受保护接口(应返回 401)
GET {{host}}/api/messages/read

### 7) 令牌自省(查看 token 是否 active)
POST {{host}}/oauth2/introspect
Content-Type: application/x-www-form-urlencoded
Authorization: Basic ZGVtby1jbGllbnQtbWFjaGluZTptYWNoaW5lLXNlY3JldA==

token={{access_token}}

### 8) 撤销令牌(revoke)
POST {{host}}/oauth2/revoke
Content-Type: application/x-www-form-urlencoded
Authorization: Basic ZGVtby1jbGllbnQtbWFjaGluZTptYWNoaW5lLXNlY3JldA==

token={{access_token}}

### 9) 撤销后再次调用(通常应失败:401)
GET {{host}}/api/messages/read
Authorization: Bearer {{access_token}}

### ----------------------------------------------------------------
### 10) 授权码模式(手动步骤)
### 第一步:浏览器打开下面 URL 登录并授权,拿到 code 后粘贴到 @authCode 变量
{{host}}/oauth2/authorize?response_type=code&client_id={{authCodeClientId}}&scope=message.read%20message.write&redirect_uri={{redirectUri}}
### ----------------------------------------------------------------
@authCode = VwATVSbY6pdJvQ5nkfcLMaUBi3Rjbs-h7-ihu_WK1c1zdkBk6yiP9U0hJSTjplIVh65zOHajXVg7A2FxauNqQW5461YR8eDBeiExHcyCMbw9-53DE66MJXl8qwtVQSWM

### 11) 授权码换取 access_token + refresh_token
POST {{host}}/oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic ZGVtby1jbGllbnQtYXV0aC1jb2RlOmRlbW8tc2VjcmV0

grant_type=authorization_code&code={{authCode}}&redirect_uri={{redirectUri}}

> {%
client.test("Authorization Code 换 token 成功", function () {
client.assert(response.status === 200, "HTTP 状态码不是 200");
client.assert(!!response.body.access_token, "access_token 为空");
client.assert(!!response.body.refresh_token, "refresh_token 为空");
});
client.global.set("auth_code_access_token", response.body.access_token);
client.global.set("refresh_token", response.body.refresh_token);
%}

### 12) 刷新令牌(refresh_token -> 新 access_token)
POST {{host}}/oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic ZGVtby1jbGllbnQtYXV0aC1jb2RlOmRlbW8tc2VjcmV0

grant_type=refresh_token&refresh_token={{refresh_token}}

> {%
client.test("Refresh Token 刷新成功", function () {
client.assert(response.status === 200, "HTTP 状态码不是 200");
client.assert(!!response.body.access_token, "新的 access_token 为空");
});
client.global.set("refreshed_access_token", response.body.access_token);
%}

### 13) 用刷新后的 token 访问受保护接口
GET {{host}}/api/messages/read
Authorization: Bearer {{refreshed_access_token}}


7. 常用 OAuth2 端点说明

  • /oauth2/authorize:授权端点(拿 code)
  • /oauth2/token:令牌端点(换 token、刷新 token)
  • /oauth2/jwks:公钥端点(资源服务器可据此验证 JWT)
  • /oauth2/revoke:令牌撤销
  • /oauth2/introspect:令牌自省(适用于 opaque token 或中控校验场景)

8. 关键代码说明(重点)

这部分是你提到的“关键代码说明”,帮助你把“会用”升级到“看懂实现”。

<!-- Web 相关能力(用于提供 REST 接口) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring Security 基础能力 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- OAuth2 资源服务器能力(校验 Bearer Token) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

<!-- OAuth2 授权服务器核心依赖 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>1.5.1</version>
</dependency>

<!-- 便于调试时观察 JSON 输出 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

8.1 AuthServerConfig:授权服务器核心配置


/**
* 授权服务器配置:
* 1) 暴露 /oauth2/authorize、/oauth2/token、/oauth2/jwks、/oauth2/revoke、/oauth2/introspect 等标准端点
* 2) 配置客户端、Token 策略、JWK 密钥
* 3) 为 JWT 增加自定义声明(常见企业需求:写入租户、角色、用户名等)
*/
@Configuration
public class AuthServerConfig {

/**
* 授权服务器过滤器链优先级最高,确保 OAuth2 端点先被匹配。
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();

http.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
// 显式启用 OIDC 1.0(否则请求 openid scope 会被拒绝)
.with(authorizationServerConfigurer, authorizationServer ->
authorizationServer.oidc(Customizer.withDefaults()))
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.exceptionHandling(exceptions -> exceptions
// 浏览器访问未登录时跳转登录页,API 客户端则返回 401
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
);

return http.build();
}

/**
* 注册 OAuth2 客户端(示例给出两个常见客户端):
* - demo-client-auth-code:用于授权码模式(支持 refresh token、openid)
* - demo-client-machine:用于客户端凭证模式(机器对机器)
*/
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient authCodeClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("demo-client-auth-code")
.clientSecret("{noop}demo-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://localhost:9000/oauth2/callback")
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/demo-client-auth-code")
.redirectUri("https://www.oauth.com/playground/authorization-code.html")
.scope(OidcScopes.OPENID)
.scope("profile")
.scope("message.read")
.scope("message.write")
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(true)
.build())
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(30))
.refreshTokenTimeToLive(Duration.ofHours(8))
.reuseRefreshTokens(false)
.build())
.build();

RegisteredClient machineClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("demo-client-machine")
.clientSecret("{noop}machine-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("message.read")
.scope("message.write")
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(20))
.build())
.build();

return new InMemoryRegisteredClientRepository(authCodeClient, machineClient);
}

/**
* 生成并提供 JWK(JSON Web Key),资源服务器会使用公钥校验 JWT。
* 生产环境建议从安全存储加载固定密钥,而不是每次启动重新生成。
*/
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();

RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();

JWKSet jwkSet = new JWKSet(rsaKey);
return (selector, securityContext) -> selector.select(jwkSet);
}

/**
* 授权服务器元数据配置:issuer 会写入 JWT 的 iss 字段。
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
.issuer("http://localhost:9000")
.build();
}

/**
* 自定义 JWT 声明:在 access_token 中追加常见业务字段。
*/
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
return context -> {
if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
return;
}

Authentication principal = context.getPrincipal();
JwtClaimsSet.Builder claims = context.getClaims();
claims.claim("token_type", "access_token")
.claim("client_id", context.getRegisteredClient().getClientId())
.claim("username", principal.getName())
.claim("authorities", principal.getAuthorities().stream().map(Object::toString).toList());
};
}

private static KeyPair generateRsaKey() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException("Failed to generate RSA key", ex);
}
}
}

  1. authorizationServerSecurityFilterChain(...)
  • 作用:注册 OAuth2 授权服务器端点的安全过滤器链
  • 关键点:@Order(Ordered.HIGHEST_PRECEDENCE),保证 OAuth2 端点优先匹配
  • 结果:/oauth2/authorize/oauth2/token 等请求先走这条链
  1. registeredClientRepository()
  • 作用:注册 OAuth2 客户端(这里用内存存储)
  • 关键点:
    • demo-client-auth-code 支持 authorization_code + refresh_token
    • demo-client-machine 支持 client_credentials
    • .scope("message.read")/.scope("message.write") 决定客户端可申请的权限范围
    • TokenSettings 控制 token 有效期和 refresh token 重用策略
  1. jwkSource()
  • 作用:提供 JWT 签名密钥(JWK)
  • 关键点:
    • 项目启动时生成 RSA 密钥对
    • 私钥用于签发 JWT,公钥通过 /oauth2/jwks 暴露给资源服务器校验
  1. authorizationServerSettings()
  • 作用:定义授权服务器元数据
  • 关键点:issuer("http://localhost:9000") 会写入 token 的 iss 字段
  • 注意:资源服务器校验时,iss 必须一致
  1. tokenCustomizer()
  • 作用:给 access token 注入业务自定义声明
  • 当前注入:client_idusernameauthorities
  • 应用场景:下游服务可直接从 JWT 中读取业务上下文,减少重复查库

8.2 DefaultSecurityConfig:资源服务器和权限控制

/**
* 应用默认安全配置:
* - 处理普通业务接口安全策略
* - 提供演示用户
* - 同时启用资源服务器 JWT 验签能力
*/
@Configuration
public class DefaultSecurityConfig {

/**
* 第二条过滤器链:处理除授权服务器端点外的其余请求。
*/
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/health", "/error", "/oauth2/callback").permitAll()
.requestMatchers("/api/messages/read").hasAuthority("SCOPE_message.read")
.requestMatchers("/api/messages/write").hasAuthority("SCOPE_message.write")
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.oauth2ResourceServer(resourceServer -> resourceServer
// 为了让“撤销令牌”立即生效,这里使用 introspection 模式而非本地 JWT 验签。
// JWT 本地验签不会主动查询授权服务器,因此默认感知不到 revoke 结果。
.opaqueToken(opaqueToken -> opaqueToken
.introspectionUri("http://localhost:9000/oauth2/introspect")
.introspectionClientCredentials("demo-client-machine", "machine-secret")
));

return http.build();
}

/**
* 演示用户(内存模式):
* - admin / 123456(ROLE_ADMIN)
* - user / 123456(ROLE_USER)
*/
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails admin = User.withUsername("admin")
.password("{noop}123456")
.roles("ADMIN")
.build();

UserDetails user = User.withUsername("user")
.password("{noop}123456")
.roles("USER")
.build();

return new InMemoryUserDetailsManager(admin, user);
}
}

  1. defaultSecurityFilterChain(...)
  • 作用:处理除 OAuth2 端点外的普通业务请求
  • 关键点:
    • requestMatchers("/api/messages/read").hasAuthority("SCOPE_message.read")
    • requestMatchers("/api/messages/write").hasAuthority("SCOPE_message.write")
  • 解释:Spring Security 会把 token 中的 scope=message.read 映射成 SCOPE_message.read 权限
  1. .oauth2ResourceServer(...jwt(...))
  • 作用:开启 Bearer Token 校验(JWT 模式)
  • 流程:提取 token -> 验签 -> 校验过期时间/issuer -> 构建认证对象
  1. userDetailsService()
  • 作用:提供登录用户(示例用内存)
  • 当前用户:admin/123456user/123456
  • 注意:生产环境应改为数据库 + 密码加密

8.3 DemoController:受保护资源接口示例

@RestController
@RequestMapping
public class DemoController {

@GetMapping("/")
public Map<String, Object> index() {
return Map.of(
"project", "spring-boot-oauth2-demo",
"description", "OAuth2 Authorization Server + Resource Server 示例项目",
"time", Instant.now().toString()
);
}

@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "UP");
}

/**
* 授权码模式回调演示端点:
* - 授权成功时返回 code/state,便于直接复制 code 做 token 交换
* - 授权失败时返回 error 信息,便于定位问题
*/
@GetMapping("/oauth2/callback")
public Map<String, String> oauth2Callback(String code, String state, String error, String error_description) {
Map<String, String> result = new LinkedHashMap<>();
result.put("code", code == null ? "" : code);
result.put("state", state == null ? "" : state);
result.put("error", error == null ? "" : error);
result.put("error_description", error_description == null ? "" : error_description);
return result;
}

/**
* 受保护接口:返回当前认证主体与 token 关键信息。
*/
@GetMapping("/api/me")
public Map<String, Object> currentUser(Authentication authentication) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("name", authentication.getName());
result.put("authorities", authentication.getAuthorities());

if (authentication.getPrincipal() instanceof Jwt jwt) {
result.put("token_issuer", jwt.getIssuer());
result.put("token_subject", jwt.getSubject());
result.put("token_scopes", jwt.getClaimAsString("scope"));
result.put("custom_username", jwt.getClaimAsString("username"));
result.put("custom_client_id", jwt.getClaimAsString("client_id"));
} else if (authentication.getPrincipal() instanceof DefaultOAuth2AuthenticatedPrincipal principal) {
// introspection 模式下,principal 不是 Jwt,而是 OAuth2AuthenticatedPrincipal
result.put("token_subject", principal.getAttribute("sub"));
result.put("token_scopes", principal.getAttribute("scope"));
result.put("principal_attributes", principal.getAttributes());
}
return result;
}

/**
* 需要 scope=message.read 才可访问(在安全配置中已声明)。
*/
@GetMapping("/api/messages/read")
public Map<String, Object> readMessage() {
return Map.of(
"action", "read",
"message", "读取消息成功(需要 message.read)",
"time", Instant.now().toString()
);
}

/**
* 需要 scope=message.write 才可访问(在安全配置中已声明)。
*/
@PostMapping("/api/messages/write")
public Map<String, Object> writeMessage() {
return Map.of(
"action", "write",
"message", "写入消息成功(需要 message.write)",
"time", Instant.now().toString()
);
}
}
  1. /api/me
  • 作用:返回当前登录主体及 token 关键声明
  • 关键代码:authentication.getPrincipal() instanceof Jwt
  • 用法:调试时快速确认 token 是否包含你自定义的 claims
  1. /api/messages/read/api/messages/write
  • 作用:演示 scope 驱动的读写权限隔离
  • 现象:
    • 只有 message.read 可读
    • 只有 message.write 可写

8.4 端到端请求流(建议记住)

  1. 客户端请求 /oauth2/authorize/oauth2/token
  2. 授权服务器校验客户端与用户
  3. 生成 JWT(附带标准 + 自定义 claims)
  4. 客户端携带 Bearer Token 调用业务接口
  5. 资源服务器校验 JWT,并把 scope 映射为 SCOPE_xxx 权限
  6. 控制器按权限返回数据或拒绝访问(401/403)

9. 如何看懂 Token(建议)

  1. access_token 放到 JWT 解析网站(仅测试环境)
  2. 重点关注:
  • iss:签发者(当前是 http://localhost:9000
  • sub:主体(用户或客户端)
  • scope:权限范围
  • 自定义声明:usernameclient_idauthorities

注意:生产环境不要把真实生产 token 贴到外部网站。


10. 常见错误与排查

401 Unauthorized

常见原因:

  • 没带 Authorization: Bearer ...
  • token 过期
  • token 不合法(签名错误、issuer 不匹配)

403 Forbidden

常见原因:

  • token 有效,但 scope 不够(例如访问写接口却只有读权限)

invalid_client

常见原因:

  • client_idclient_secret 错误
  • 客户端认证方式不匹配

invalid_grant

常见原因:

  • authorization_code 过期或已使用
  • redirect_uri 与申请 code 时不一致
  • refresh_token 已失效

11. 关键源码位置

  • 授权服务器:src/main/java/com/hua/oauth2/config/AuthServerConfig.java
  • 资源服务器和权限:src/main/java/com/hua/oauth2/config/DefaultSecurityConfig.java
  • 业务接口:src/main/java/com/hua/oauth2/controller/DemoController.java
  • 配置文件:src/main/resources/application.properties

12. 生产化改造建议

  • 使用数据库持久化用户、客户端、授权信息
  • 密码改为 BCryptPasswordEncoder
  • JWT 签名密钥改成固定安全托管(KMS/HSM)
  • 强制 HTTPS
  • 关闭 TRACE 日志,开启审计日志
  • 加入限流、防刷、异常告警

如果你希望,我可以继续给你补一版 “前后端分离(Vue/React)接入本项目 OAuth2” 的实战教程,并配套 Postman 请求集合说明。