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

如何将Spring Boot微服务Basic Auth改造为OAuth2 JWT认证通信?

没问题!咱们一步步把你的Basic Auth改造为OAuth2 JWT认证模式——核心是让第一个微服务同时扮演授权服务器(提供/access-token端点发令牌)和资源服务器(保护/products接口),第二个微服务先拿令牌再调用接口。话不多说,直接上代码和步骤:

改造步骤:从Basic Auth到OAuth2 JWT认证

一、改造微服务1(REST API:授权服务器+资源服务器)

首先要添加Spring Security OAuth2相关依赖,然后替换掉原来的Basic Auth配置,新增令牌生成和接口保护的逻辑。

1. 添加依赖(Maven为例)

<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>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

2. 替换原有Security配置,新增OAuth2相关配置

(1)授权服务器配置(生成JWT令牌)

负责处理/access-token请求,生成并返回JWT:

@Configuration
public class AuthorizationServerConfig {

    @Value("${jwt.signing.key}")
    private String signingKey;

    // 配置客户端信息(内存存储,生产建议用数据库)
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient demoClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("demo-client")
                .clientSecret("{bcrypt}" + new BCryptPasswordEncoder().encode("demo-secret"))
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.PASSWORD) // 采用密码模式,匹配你的需求
                .scope("read")
                .tokenSettings(tokenSettings())
                .build();

        return new InMemoryRegisteredClientRepository(demoClient);
    }

    // 令牌配置:有效期、格式等
    @Bean
    public TokenSettings tokenSettings() {
        return TokenSettings.builder()
                .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
                .accessTokenTimeToLive(Duration.ofHours(1)) // 令牌有效期1小时
                .build();
    }

    // JWT编码器:用对称密钥签名
    @Bean
    public JwtEncoder jwtEncoder() {
        SecretKeySpec secretKey = new SecretKeySpec(signingKey.getBytes(), "HmacSHA256");
        return new NimbusJwtEncoder(new JWSSignerConfiguration(secretKey));
    }

    // JWT解码器:验证令牌合法性
    @Bean
    public JwtDecoder jwtDecoder() {
        SecretKey secretKey = new SecretKeySpec(signingKey.getBytes(), "HmacSHA256");
        return NimbusJwtDecoder.withSecretKey(secretKey).build();
    }
}

(2)资源服务器配置(保护API接口)

负责验证JWT令牌,保护/products接口:

@Configuration
@RequiredArgsConstructor
public class ResourceServerConfig {

    private final JwtDecoder jwtDecoder;
    private final DemoApiConfiguration apiConfig;

    @Bean
    public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeHttpRequests(auth -> auth
                        .anyRequest().authenticated()
                )
                .oauth2ResourceServer(oauth2 -> oauth2
                        .jwt(jwt -> jwt.decoder(jwtDecoder))
                );
        return http.build();
    }

    // 保留用户信息配置(从配置文件读取用户名密码)
    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
        UserDetails systemUser = User.withUsername(apiConfig.getUsername())
                .password(passwordEncoder.encode(apiConfig.getPassword()))
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(systemUser);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

(3)自定义/access-token端点

Spring Authorization Server默认的令牌端点是/oauth2/token,我们转发到/access-token以匹配你的需求:

@RestController
@RequestMapping("/access-token")
public class TokenForwardController {

    private final OAuth2TokenEndpointFilter tokenEndpointFilter;

    public TokenForwardController(OAuth2TokenEndpointFilter tokenEndpointFilter) {
        this.tokenEndpointFilter = tokenEndpointFilter;
    }

    @PostMapping
    public void forwardToTokenEndpoint(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.getRequestDispatcher("/oauth2/token").forward(request, response);
    }
}

3. 修改微服务1的application.yml

server:
  port: 8080

# JWT签名密钥(生产环境建议从环境变量/密钥管理服务读取)
jwt:
  signing-key: "your-strong-256bit-signing-key-here-123456789"

demo:
  api:
    credentials:
      username: ${demo_api_username:john}
      password: ${demo_api_password:test}

二、改造微服务2(REST Consumer)

需要改造客户端逻辑,先请求令牌,再携带JWT调用接口。

1. 添加OAuth2客户端依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

2. 修改配置类与客户端逻辑

(1)更新DemoApiConfiguration,添加OAuth2客户端信息

@Configuration
@Getter
public class DemoApiConfiguration {
    @Value("${demo.api.credentials.username}")
    private String username;
    @Value("${demo.api.credentials.password}")
    private String password;
    @Value("${demo.api.credentials.basePath}")
    private String basePath;
    @Value("${demo.oauth2.client-id}")
    private String clientId;
    @Value("${demo.oauth2.client-secret}")
    private String clientSecret;
    @Value("${demo.oauth2.token-url}")
    private String tokenUrl;
}

(2)改造WebConfigurer,新增获取令牌的方法

@Configuration
@RequiredArgsConstructor
public class WebConfigurer {
    private final DemoApiConfiguration apiConfig;

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Bean
    public ApiClient apiClient(RestTemplate restTemplate) {
        ApiClient apiClient = new ApiClient(restTemplate);
        apiClient.setBasePath(apiConfig.getBasePath());
        return apiClient;
    }

    // 获取JWT令牌
    public String getAccessToken() {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "password");
        params.add("username", apiConfig.getUsername());
        params.add("password", apiConfig.getPassword());
        params.add("scope", "read");

        // 客户端认证:用Basic Auth携带clientId和clientSecret
        String clientAuth = apiConfig.getClientId() + ":" + apiConfig.getClientSecret();
        String authHeader = "Basic " + Base64Utils.encodeToString(clientAuth.getBytes());

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.set(HttpHeaders.AUTHORIZATION, authHeader);

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);

        try {
            ResponseEntity<OAuth2AccessTokenResponse> response = restTemplate().exchange(
                    apiConfig.getTokenUrl(),
                    HttpMethod.POST,
                    request,
                    new ParameterizedTypeReference<>() {}
            );
            return response.getBody().getAccessToken().getTokenValue();
        } catch (Exception e) {
            throw new RuntimeException("Failed to fetch access token", e);
        }
    }
}

(3)改造ApiClient,使用Bearer Token认证

@Getter
@RequiredArgsConstructor
@Slf4j
public class ApiClient {
    private static final String AUTHORIZATION_HEADER = "Authorization";
    private final RestTemplate restTemplate;
    private String basePath;

    public ApiClient setBasePath(String basePath) {
        this.basePath = basePath;
        return this;
    }

    public String invokeApi(String path, String accessToken) {
        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(basePath).path(path);
        RequestEntity.BodyBuilder requestBuilder = RequestEntity.method(HttpMethod.GET, builder.build().toUri());
        requestBuilder.contentType(MediaType.APPLICATION_JSON);
        // 使用Bearer Token认证
        requestBuilder.header(AUTHORIZATION_HEADER, "Bearer " + accessToken);
        RequestEntity<Object> requestEntity = requestBuilder.body(null);
        return restTemplate.exchange(requestEntity, String.class).getBody();
    }
}

(4)更新ConsumeController,先拿令牌再调用接口

@RestController
@RequiredArgsConstructor
public class ConsumeController {
    private static final String PATH = "/rest/api/v1/products";
    private final WebConfigurer webConfigurer;
    private final ApiClient apiClient;

    @GetMapping(value = "/products-client")
    public String getProductList() {
        // 第一步:获取JWT令牌
        String accessToken = webConfigurer.getAccessToken();
        // 第二步:携带令牌调用接口
        return apiClient.invokeApi(PATH, accessToken);
    }
}

3. 修改微服务2的application.yml

server:
  port: 8090

demo:
  api:
    credentials:
      username: ${demo_api_username:john}
      password: ${demo_api_password:test}
      basePath: ${demo_api_path:http://localhost:8080}
  oauth2:
    client-id: demo-client
    client-secret: demo-secret
    token-url: ${demo_api_path:http://localhost:8080}/access-token

三、测试流程

  1. 启动微服务1(8080端口)和微服务2(8090端口)
  2. 访问http://localhost:8090/products-client,会自动完成“拿令牌→调用接口”的流程,返回These are products!
  3. 也可以直接测试令牌端点:POST请求http://localhost:8080/access-token,携带参数grant_type=password&username=john&password=test,请求头添加Authorization: Basic ZGVtby1jbGllbnQ6ZGVtby1zZWNyZXQ=(base64编码的demo-client:demo-secret),会返回包含access_token的JSON响应。

生产环境注意事项

  • 不要用内存存储客户端和用户信息,改用JdbcRegisteredClientRepository和JdbcUserDetailsManager连接数据库
  • JWT签名密钥要使用更安全的存储方式(比如环境变量、AWS Secrets Manager等),禁止硬编码
  • 可以添加令牌刷新逻辑,避免令牌过期后重新输入用户名密码
  • 客户端认证方式可以根据需求调整(比如CLIENT_SECRET_POST)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 18:32:29