第一次接触OAuth2授权码模式时,我被那一堆专业术语绕得头晕——client_id、redirect_uri、authorization_code...直到自己动手实现了一遍企业微信登录功能,才发现这套流程设计得确实精妙。想象你正在开发一个内部管理系统,需要接入GitHub账号登录,整个过程就像去游乐场玩项目:
这个模式最安全之处在于:敏感信息全程不经过浏览器。授权码只在URL中出现一次,真正的access_token永远通过后端通道交换。我去年重构某金融系统登录模块时,审计人员特别赞赏这种设计,有效避免了token被恶意截获的风险。
先引入关键依赖(以Spring Boot 2.7为例):
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.7.0</version>
</dependency>
基础安全配置要特别注意过滤器链顺序。有次线上事故就是因为资源服务器配置覆盖了认证配置,导致/oauth/authorize接口返回404:
java复制@Configuration
@Order(1) // 关键!必须高于资源服务器配置
public class AuthServerConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/oauth/**")
.authorizeRequests()
.antMatchers("/oauth/token").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
}
OAuth2标准需要5张核心表,但实际开发中我常简化成3张:
建表SQL示例(MySQL):
sql复制CREATE TABLE `oauth_client_details` (
`client_id` varchar(128) NOT NULL COMMENT '相当于AppID',
`client_secret` varchar(256) NOT NULL COMMENT '相当于AppSecret',
`redirect_uri` varchar(2048) DEFAULT NULL COMMENT '授权回调地址',
`scope` varchar(256) DEFAULT 'read' COMMENT '权限范围',
`access_token_validity` int DEFAULT 3600 COMMENT 'token有效期(秒)',
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
默认的/oauth/authorize接口会返回丑陋的Basic认证页面,我们可以用自定义登录页覆盖:
java复制@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()
.antMatchers("/login", "/oauth/authorize")
.and()
.formLogin()
.loginPage("/custom-login") // 自定义登录页路径
.loginProcessingUrl("/do-login");
}
登录页模板示例(Thymeleaf):
html复制<form action="/do-login" method="post">
<input type="text" name="username" placeholder="工号">
<input type="password" name="password" placeholder="密码">
<input type="hidden" name="_csrf" th:value="${_csrf.token}">
<button type="submit">进入系统</button>
</form>
默认的confirm_access页面就像90年代的风格,改造方法分三步:
java复制endpoints.pathMapping("/oauth/confirm_access", "/custom-confirm");
java复制@Controller
@SessionAttributes("authorizationRequest")
public class CustomApprovalController {
@GetMapping("/custom-confirm")
public String showForm(Map<String, Object> model) {
AuthorizationRequest request = (AuthorizationRequest) model.get("authorizationRequest");
model.put("appName", request.getClientId());
model.put("scopes", request.getScope());
return "approval-page";
}
}
html复制<div class="auth-container">
<h3>[[${appName}]]请求以下权限:</h3>
<ul>
<li th:each="scope : ${scopes}" th:text="${scope}"></li>
</ul>
<form method="post" action="/oauth/authorize">
<input type="hidden" name="user_oauth_approval" value="true"/>
<button type="submit" class="btn-confirm">确认授权</button>
</form>
</div>
默认的授权码是随机字符串,但金融行业往往需要包含业务信息。我们改造JdbcAuthorizationCodeServices:
java复制public class BizAuthorizationCodeServices extends JdbcAuthorizationCodeServices {
@Override
public String createAuthorizationCode(OAuth2Authentication authentication) {
User principal = (User) authentication.getUserAuthentication().getPrincipal();
return principal.getDeptId() + "-" +
System.currentTimeMillis() + "-" +
RandomStringUtils.randomAlphanumeric(6);
}
}
配置方式:
java复制@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
return new BizAuthorizationCodeServices(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authorizationCodeServices(authorizationCodeServices);
}
标准的access_token只包含基础信息,我们可以通过TokenEnhancer添加用户详情:
java复制public class CustomTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
User user = (User) authentication.getPrincipal();
Map<String, Object> info = new HashMap<>();
info.put("staffId", user.getUsername());
info.put("department", user.getDepartment());
token.setAdditionalInformation(info);
return token;
}
}
配置令牌服务时注入增强器:
java复制@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
services.setTokenStore(tokenStore);
services.setTokenEnhancer(new CustomTokenEnhancer());
return services;
}
在银行项目里我们实施了这些防护措施:
java复制http.csrf()
.requireCsrfProtectionMatcher(
new AntPathRequestMatcher("/oauth/authorize")
)
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
java复制@Bean
public RedirectResolver redirectResolver() {
return new ExactMatchRedirectResolver() {
@Override
public String resolveRedirect(String requestedRedirect,
ClientDetails client) {
// 严格校验redirect_uri与注册信息一致
if(!client.getRegisteredRedirectUri().contains(requestedRedirect)) {
throw new InvalidRequestException("非法回调地址");
}
return requestedRedirect;
}
};
}
当QPS超过2000时,基于JDBC的令牌存储会成为瓶颈。我们的优化方案:
java复制@Bean
public TokenStore tokenStore(RedisConnectionFactory factory) {
return new RedisTokenStore(factory);
}
java复制public class CachedTokenStore implements TokenStore {
private final TokenStore delegate;
private final Cache cache;
@Override
public OAuth2AccessToken readAccessToken(String tokenValue) {
OAuth2AccessToken token = cache.get(tokenValue);
if(token == null) {
token = delegate.readAccessToken(tokenValue);
cache.put(tokenValue, token);
}
return token;
}
}
java复制// 短时效access_token + 长时效refresh_token
services.setAccessTokenValiditySeconds(1800); // 30分钟
services.setRefreshTokenValiditySeconds(2592000); // 30天