基于Spring Security 5.7实现OAuth2密码与授权码双登录流程
解决方案:Spring Security 同时支持同一OAuth2提供商的授权码与密码流程
核心解决思路
- 基于Spring Security原生组件扩展,为密码流程单独注册一个不参与登录页面展示的客户端实例
- 通过自定义授权客户端管理器,适配两种流程的令牌获取逻辑,无需重复配置认证提供商信息
具体实现步骤
1. 配置授权码流程客户端(显示在/login页面)
在application.yml中配置常规授权码流程的客户端,该配置会自动生成/login页面的登录选项:
spring: security: oauth2: client: registration: my-provider-code: client-id: your-client-id client-secret: your-client-secret authorization-grant-type: authorization_code redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" scope: openid, profile, email provider: my-provider: authorization-uri: https://your-provider.com/oauth2/authorize token-uri: https://your-provider.com/oauth2/token user-info-uri: https://your-provider.com/oauth2/userinfo user-name-attribute: sub
2. 注册密码流程专用客户端(不显示在/login页面)
通过Java配置创建密码流程的客户端实例,加入自定义的注册仓库,避免被登录页面扫描:
@Configuration public class OAuth2AdditionalConfig { // 创建密码流程客户端,可复用同一客户端ID(需认证提供商支持) @Bean public ClientRegistration passwordFlowClientRegistration() { return ClientRegistration.withRegistrationId("my-provider-password") .clientId("your-client-id") .clientSecret("your-client-secret") .authorizationGrantType(AuthorizationGrantType.PASSWORD) .tokenUri("https://your-provider.com/oauth2/token") .scope("openid", "profile", "email") .build(); } // 整合默认授权码客户端和自定义密码客户端的注册仓库 @Bean public ClientRegistrationRepository customClientRegistrationRepository( ClientRegistrationRepository defaultRepo, ClientRegistration passwordFlowClient) { Map<String, ClientRegistration> registrations = new HashMap<>(); // 导入自动配置的授权码客户端 defaultRepo.findByRegistrationId("my-provider-code") .ifPresent(reg -> registrations.put(reg.getRegistrationId(), reg)); // 添加密码流程客户端 registrations.put(passwordFlowClient.getRegistrationId(), passwordFlowClient); return new InMemoryClientRegistrationRepository(registrations); } }
3. 配置支持密码流程的授权客户端管理器
构建专门处理密码流程的令牌获取逻辑,支持用户名密码凭证传入:
@Configuration public class OAuth2AuthorizedClientConfig { @Bean public OAuth2AuthorizedClientManager passwordFlowAuthorizedClientManager( ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService authorizedClientService) { // 启用密码流程及可选的令牌刷新支持 OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() .password() .refreshToken() .build(); DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService); authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); // 自定义上下文属性解析,传递用户名密码 authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()); return authorizedClientManager; } private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() { return authorizeRequest -> { Map<String, Object> contextAttributes = new HashMap<>(); String username = (String) authorizeRequest.getAttribute("username"); String password = (String) authorizeRequest.getAttribute("password"); if (username != null && password != null) { contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username); contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password); } return contextAttributes; }; } }
4. 提供遗留应用调用的密码流程令牌接口
编写REST接口供遗留应用获取令牌,调用上述管理器完成认证:
@RestController @RequestMapping("/auth") public class LegacyAuthController { private final OAuth2AuthorizedClientManager authorizedClientManager; public LegacyAuthController(OAuth2AuthorizedClientManager authorizedClientManager) { this.authorizedClientManager = authorizedClientManager; } @PostMapping("/token/password") public ResponseEntity<OAuth2AccessToken> getTokenByPassword( @RequestParam String username, @RequestParam String password) { OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("my-provider-password") .principal(username) .attribute("username", username) .attribute("password", password) .build(); OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest); if (authorizedClient == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } return ResponseEntity.ok(authorizedClient.getAccessToken()); } }
5. 验证密码客户端的隐藏效果
由于密码流程客户端是通过自定义代码注册的,不会被Spring Security的登录页自动扫描展示,因此默认不会出现在/login页面中,仅能通过上述REST接口调用。
关键注意事项
- 确保你的认证提供商允许同一客户端同时开启授权码和密码两种授权流程
- 密码流程仅作为过渡方案使用,需规划遗留应用的OAuth2升级路线
- 所有实现均基于Spring Security原生组件,无自定义底层认证逻辑
内容的提问来源于stack exchange,提问作者Jeff Walker
相关产品推荐
相关产品推荐

