基于Spring Security与JWT在同一应用中实现授权与资源服务器
Spring Boot 3 + Java 17 单应用整合授权服务器与资源服务器(JWT)
问题描述
我是Spring Security新手,想在同一应用中用JWT实现Authorization Server(授权服务器)和Resource Server(资源服务器),当前使用Java 17、Spring Boot 3。找到的资料要么是拆分到不同应用,要么大多过时,也不清楚完整实现流程。想获取相关参考资料、详细代码及配置。
我试过相关配置,但不确定是否正确,也不知道怎么在Login和SignUp API中使用这些配置。之前用的@EnableAuthorizationServer和@EnableResourceServer注解已经不能用了,求可用示例。
以下是我的SecurityConfig类:
@Configuration @EnableWebSecurity public class SecurityConfig{ @Bean @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, CorsConfigurationSource corsConfigurationSource) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults()); http.exceptionHandling((exceptions) -> exceptions.authenticationEntryPoint( new LoginUrlAuthenticationEntryPoint("/login")) ) .oauth2ResourceServer((resourceServer)-> resourceServer.jwt(Customizer.withDefaults())); http.cors(customizer -> customizer.configurationSource(corsConfigurationSource)); return http.build(); } @Bean @Order(2) public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize .anyRequest().authenticated() ) //.oauth2ResourceServer(OAuth2AuthorizationServerConfigurer::jwt) //jwt method is not resolved .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()))//https://github.com/spring-projects/spring-security/issues/13446 // Form login handles the redirect to the login page from the // authorization server filter chain .formLogin(Customizer.withDefaults()); return http.build(); } @Bean public UserDetailsService userDetailsService() { UserDetails userDetails = User.withDefaultPasswordEncoder() .username("user") .password("password") .roles("USER") .build(); return new InMemoryUserDetailsManager(userDetails); } @Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("oidc-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/oidc-client") .postLogoutRedirectUri("http://127.0.0.1:8080/") .scope(OidcScopes.OPENID) .scope(OidcScopes.PROFILE) .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) .build(); return new InMemoryRegisteredClientRepository(oidcClient); } @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 new ImmutableJWKSet<>(jwkSet); } private static KeyPair generateRsaKey() { KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } return keyPair; } @Bean public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } @Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().build(); } }
使用的依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId> </dependency>
解答
一、现有配置的正确性评估
你的基础配置方向符合Spring Boot 3 + Spring Security 6的新范式(用SecurityFilterChain替代旧注解),但存在几个需要调整的点:
- 授权服务器过滤器链冗余配置:在
authorizationServerSecurityFilterChain中添加oauth2ResourceServer是多余的,授权服务器的默认安全配置已覆盖自身端点保护,资源服务器配置只需在默认过滤器链中处理。 - 缺少密码授权模式:当前仅配置了授权码和刷新令牌模式,若要实现登录API直接获取JWT,需添加
AuthorizationGrantType.PASSWORD并开启客户端的密码模式支持。 - 密码编码器不适合生产:
User.withDefaultPasswordEncoder()仅用于测试,生产环境需配置独立的PasswordEncoderBean。
二、补充与修正后的完整配置
1. 修正后的SecurityConfig
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, CorsConfigurationSource corsConfigurationSource) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .oidc(Customizer.withDefaults()); // 开启OIDC标准支持 // 未认证时跳转至登录页 http.exceptionHandling(exceptions -> exceptions .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))) .cors(customizer -> customizer.configurationSource(corsConfigurationSource)); return http.build(); } @Bean @Order(2) public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/api/auth/**").permitAll() // 放行登录、注册API .anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) .formLogin(Customizer.withDefaults()); // 表单登录用于授权码模式的用户认证 return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // 生产环境推荐的密码哈希编码器 } @Bean public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) { UserDetails userDetails = User.builder() .username("user") .password(passwordEncoder.encode("password")) .roles("USER") .build(); return new InMemoryUserDetailsManager(userDetails); } @Bean public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) { RegisteredClient backendClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("backend-client") .clientSecret(passwordEncoder.encode("secret")) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .authorizationGrantType(AuthorizationGrantType.PASSWORD) // 添加密码模式,用于登录API直接获取令牌 .redirectUri("http://127.0.0.1:8080/login/oauth2/code/backend-client") .postLogoutRedirectUri("http://127.0.0.1:8080/") .scope(OidcScopes.OPENID) .scope(OidcScopes.PROFILE) .scope("read") // 自定义权限范围 .scope("write") .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build()) // 测试环境关闭授权确认页 .tokenSettings(TokenSettings.builder() .accessTokenTimeToLive(Duration.ofHours(1)) // 设置访问令牌有效期 .refreshTokenTimeToLive(Duration.ofDays(7)) // 设置刷新令牌有效期 .build()) .build(); return new InMemoryRegisteredClientRepository(backendClient); } @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 new ImmutableJWKSet<>(jwkSet); } private static KeyPair generateRsaKey() { try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); return keyPairGenerator.generateKeyPair(); } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException(ex); } } @Bean public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } @Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().build(); } }
2. 登录与注册API实现
@RestController @RequestMapping("/api/auth") public class AuthController { private final OAuth2TokenEndpointFilter tokenEndpointFilter; private final UserDetailsManager userDetailsManager; private final PasswordEncoder passwordEncoder; public AuthController(OAuth2TokenEndpointFilter tokenEndpointFilter, UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) { this.tokenEndpointFilter = tokenEndpointFilter; this.userDetailsManager = userDetailsManager; this.passwordEncoder = passwordEncoder; } // 用户注册接口 @PostMapping("/signup") public ResponseEntity<String> signup(@RequestBody UserRequest userRequest) { if (userDetailsManager.userExists(userRequest.getUsername())) { return ResponseEntity.badRequest().body("用户名已存在"); } UserDetails user = User.builder() .username(userRequest.getUsername()) .password(passwordEncoder.encode(userRequest.getPassword())) .roles("USER") .build(); userDetailsManager.createUser(user); return ResponseEntity.ok("注册成功"); } // 用户登录接口(封装授权服务器令牌端点调用) @PostMapping("/login") public void login(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置密码模式请求参数 request.setParameter("grant_type", "password"); request.setParameter("username", request.getParameter("username")); request.setParameter("password", request.getParameter("password")); request.setParameter("client_id", "backend-client"); request.setParameter("client_secret", "secret"); request.setParameter("scope", "read write"); // 转发请求到授权服务器令牌端点 tokenEndpointFilter.doFilter(request, response, servletRequest -> {}); } // 资源接口示例:获取当前用户信息 @GetMapping("/profile") public ResponseEntity<Map<String, Object>> getProfile(@AuthenticationPrincipal Jwt jwt) { Map<String, Object> profile = new HashMap<>(); profile.put("username", jwt.getClaim("sub")); profile.put("roles", jwt.getClaim("authorities")); return ResponseEntity.ok(profile); } // 内部请求DTO public static class UserRequest { private String username; private String password; // getter和setter public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } }
三、关键说明
- 过滤器链顺序:
@Order(1)的过滤器链专门处理授权服务器核心端点(如/oauth2/token、/oauth2/authorize),@Order(2)的过滤器链处理应用业务请求。 - 密码模式使用:添加
AuthorizationGrantType.PASSWORD后,可通过POST请求/oauth2/token直接获取JWT,登录API本质是封装了该端点的调用逻辑。 - JWT解析:资源服务器通过
JwtDecoder自动验证JWT的签名与有效性,@AuthenticationPrincipal Jwt可直接获取当前用户的令牌声明信息。 - 生产环境优化建议:
- 替换
InMemoryUserDetailsManager和InMemoryRegisteredClientRepository为数据库存储实现。 - 从配置文件或密钥管理服务加载RSA密钥对,避免硬编码生成。
- 强制开启HTTPS,保障令牌传输安全。
- 替换
四、参考资料
- Spring官方文档:Spring Security OAuth 2.0 Authorization Server章节
- Spring Boot官方指南:OAuth 2.0 Resource Server章节
内容的提问来源于stack exchange,提问作者Sam Brooks
相关产品推荐
相关产品推荐

