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 / 123456user / 123456
3.2 OAuth2 客户端
- 授权码客户端(适合“用户登录第三方应用”)
client_id:demo-client-auth-codeclient_secret:demo-secret- 支持:
authorization_code、refresh_token
- 机器客户端(适合“服务与服务通信”)
client_id:demo-client-machineclient_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);
}
}
}
authorizationServerSecurityFilterChain(...)
- 作用:注册 OAuth2 授权服务器端点的安全过滤器链
- 关键点:
@Order(Ordered.HIGHEST_PRECEDENCE),保证 OAuth2 端点优先匹配 - 结果:
/oauth2/authorize、/oauth2/token等请求先走这条链
registeredClientRepository()
- 作用:注册 OAuth2 客户端(这里用内存存储)
- 关键点:
demo-client-auth-code支持authorization_code + refresh_tokendemo-client-machine支持client_credentials.scope("message.read")/.scope("message.write")决定客户端可申请的权限范围TokenSettings控制 token 有效期和 refresh token 重用策略
jwkSource()
- 作用:提供 JWT 签名密钥(JWK)
- 关键点:
- 项目启动时生成 RSA 密钥对
- 私钥用于签发 JWT,公钥通过
/oauth2/jwks暴露给资源服务器校验
authorizationServerSettings()
- 作用:定义授权服务器元数据
- 关键点:
issuer("http://localhost:9000")会写入 token 的iss字段 - 注意:资源服务器校验时,
iss必须一致
tokenCustomizer()
- 作用:给 access token 注入业务自定义声明
- 当前注入:
client_id、username、authorities - 应用场景:下游服务可直接从 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);
}
}
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权限
.oauth2ResourceServer(...jwt(...))
- 作用:开启 Bearer Token 校验(JWT 模式)
- 流程:提取 token -> 验签 -> 校验过期时间/issuer -> 构建认证对象
userDetailsService()
- 作用:提供登录用户(示例用内存)
- 当前用户:
admin/123456、user/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()
);
}
}
/api/me
- 作用:返回当前登录主体及 token 关键声明
- 关键代码:
authentication.getPrincipal() instanceof Jwt - 用法:调试时快速确认 token 是否包含你自定义的 claims
/api/messages/read与/api/messages/write
- 作用:演示 scope 驱动的读写权限隔离
- 现象:
- 只有
message.read可读 - 只有
message.write可写
- 只有
8.4 端到端请求流(建议记住)
- 客户端请求
/oauth2/authorize或/oauth2/token - 授权服务器校验客户端与用户
- 生成 JWT(附带标准 + 自定义 claims)
- 客户端携带 Bearer Token 调用业务接口
- 资源服务器校验 JWT,并把 scope 映射为
SCOPE_xxx权限 - 控制器按权限返回数据或拒绝访问(401/403)
9. 如何看懂 Token(建议)
- 把
access_token放到 JWT 解析网站(仅测试环境) - 重点关注:
iss:签发者(当前是http://localhost:9000)sub:主体(用户或客户端)scope:权限范围- 自定义声明:
username、client_id、authorities
注意:生产环境不要把真实生产 token 贴到外部网站。
10. 常见错误与排查
401 Unauthorized
常见原因:
- 没带
Authorization: Bearer ... - token 过期
- token 不合法(签名错误、issuer 不匹配)
403 Forbidden
常见原因:
- token 有效,但 scope 不够(例如访问写接口却只有读权限)
invalid_client
常见原因:
client_id或client_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 请求集合说明。