You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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替代旧注解),但存在几个需要调整的点:

  1. 授权服务器过滤器链冗余配置:在authorizationServerSecurityFilterChain中添加oauth2ResourceServer是多余的,授权服务器的默认安全配置已覆盖自身端点保护,资源服务器配置只需在默认过滤器链中处理。
  2. 缺少密码授权模式:当前仅配置了授权码和刷新令牌模式,若要实现登录API直接获取JWT,需添加AuthorizationGrantType.PASSWORD并开启客户端的密码模式支持。
  3. 密码编码器不适合生产:User.withDefaultPasswordEncoder()仅用于测试,生产环境需配置独立的PasswordEncoder Bean。

二、补充与修正后的完整配置

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; }
    }
}

三、关键说明

  1. 过滤器链顺序:@Order(1)的过滤器链专门处理授权服务器核心端点(如/oauth2/token、/oauth2/authorize),@Order(2)的过滤器链处理应用业务请求。
  2. 密码模式使用:添加AuthorizationGrantType.PASSWORD后,可通过POST请求/oauth2/token直接获取JWT,登录API本质是封装了该端点的调用逻辑。
  3. JWT解析:资源服务器通过JwtDecoder自动验证JWT的签名与有效性,@AuthenticationPrincipal Jwt可直接获取当前用户的令牌声明信息。
  4. 生产环境优化建议:
    • 替换InMemoryUserDetailsManager和InMemoryRegisteredClientRepository为数据库存储实现。
    • 从配置文件或密钥管理服务加载RSA密钥对,避免硬编码生成。
    • 强制开启HTTPS,保障令牌传输安全。

四、参考资料

  • Spring官方文档:Spring Security OAuth 2.0 Authorization Server章节
  • Spring Boot官方指南:OAuth 2.0 Resource Server章节

内容的提问来源于stack exchange,提问作者Sam Brooks

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 08:22:05