如何将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(8080端口)和微服务2(8090端口)
- 访问
http://localhost:8090/products-client,会自动完成“拿令牌→调用接口”的流程,返回These are products! - 也可以直接测试令牌端点: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
相关产品推荐
相关产品推荐

