Spring Boot:如何用Client/Secret实现无表单登录的服务间API保护
服务到服务场景下的API保护,最适合用OAuth2的客户端凭证模式(Client Credentials Grant),完全不需要用户交互,直接通过Client ID/Secret完成身份验证。下面是基于自定义数据库存储客户端信息的Spring Boot实现示例:
一、依赖配置
在pom.xml中添加必要依赖:
<dependencies> <!-- Spring Security 核心 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- OAuth2 授权服务器 --> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.5.2.RELEASE</version> </dependency> <!-- OAuth2 资源服务器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <!-- JPA 操作数据库 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- MySQL 驱动(根据你的数据库调整) --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies>
二、自定义数据库存储客户端信息
1. 客户端实体类
创建对应数据库表的实体类,存储客户端认证信息:
import javax.persistence.*; @Entity @Table(name = "oauth_clients") public class ClientEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String clientId; @Column(nullable = false) private String clientSecret; private String scope; @Column(name = "authorized_grant_types") private String authorizedGrantTypes; // getter和setter省略 }
2. 客户端Repository
import org.springframework.data.jpa.repository.JpaRepository; public interface ClientRepository extends JpaRepository<ClientEntity, Long> { ClientEntity findByClientId(String clientId); }
3. 实现ClientDetailsService
从自定义数据库加载客户端信息,适配Spring Security的ClientDetails接口:
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.ClientRegistrationException; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @Service public class CustomClientDetailsService implements ClientDetailsService { private final ClientRepository clientRepository; public CustomClientDetailsService(ClientRepository clientRepository) { this.clientRepository = clientRepository; } @Override public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException { ClientEntity client = clientRepository.findByClientId(clientId); if (client == null) { throw new ClientRegistrationException("Client not found: " + clientId); } BaseClientDetails clientDetails = new BaseClientDetails(); clientDetails.setClientId(client.getClientId()); clientDetails.setClientSecret(client.getClientSecret()); // 解析授权类型、权限范围 clientDetails.setAuthorizedGrantTypes(Arrays.asList(client.getAuthorizedGrantTypes().split(","))); clientDetails.setScope(Arrays.asList(client.getScope().split(","))); // 服务到服务场景下可设置基础权限 List<SimpleGrantedAuthority> authorities = Arrays.stream(new String[]{"ROLE_CLIENT"}) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); clientDetails.setAuthorities(authorities); return clientDetails; } }
4. 密码加密配置
确保客户端密码加密存储,配置BCrypt加密器:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class PasswordEncoderConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
注意:存储clientSecret时,必须用
passwordEncoder().encode("your-secret")生成加密后的字符串存入数据库。
三、配置授权服务器
提供token获取端点,验证客户端身份:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { private final CustomClientDetailsService clientDetailsService; private final AuthenticationManager authenticationManager; private final PasswordEncoder passwordEncoder; @Autowired public AuthorizationServerConfig(CustomClientDetailsService clientDetailsService, AuthenticationManager authenticationManager, PasswordEncoder passwordEncoder) { this.clientDetailsService = clientDetailsService; this.authenticationManager = authenticationManager; this.passwordEncoder = passwordEncoder; } @Bean public TokenStore tokenStore() { // 生产环境建议使用JdbcTokenStore或JWT TokenStore return new InMemoryTokenStore(); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { // 允许客户端访问/oauth/token端点,验证clientId/secret security.tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()") .allowFormAuthenticationForClients(); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetailsService) .passwordEncoder(passwordEncoder); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager) .tokenStore(tokenStore()) .userDetailsService(clientDetailsService); } }
四、配置资源服务器
保护API接口,只允许携带有效token的请求访问:
import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler; @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { private static final String RESOURCE_ID = "service-api"; @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId(RESOURCE_ID) .stateless(true); } @Override public void configure(org.springframework.security.config.annotation.web.builders.HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/api/**").authenticated() .and() .exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler()); } }
五、客户端服务调用示例
编写工具类,用于获取token并调用受保护的API:
import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; public class ServiceClient { private static final String TOKEN_URL = "http://localhost:8080/oauth/token"; private static final String API_URL = "http://localhost:8080/api/hello"; private static final String CLIENT_ID = "service-client"; private static final String CLIENT_SECRET = "service-secret"; public static void main(String[] args) { RestTemplate restTemplate = new RestTemplate(); // 1. 获取Access Token HttpHeaders tokenHeaders = new HttpHeaders(); tokenHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap<String, String> tokenParams = new LinkedMultiValueMap<>(); tokenParams.add("grant_type", "client_credentials"); tokenParams.add("client_id", CLIENT_ID); tokenParams.add("client_secret", CLIENT_SECRET); HttpEntity<MultiValueMap<String, String>> tokenRequest = new HttpEntity<>(tokenParams, tokenHeaders); TokenResponse tokenResponse = restTemplate.postForObject(TOKEN_URL, tokenRequest, TokenResponse.class); String accessToken = tokenResponse.getAccess_token(); // 2. 调用受保护的API HttpHeaders apiHeaders = new HttpHeaders(); apiHeaders.set("Authorization", "Bearer " + accessToken); HttpEntity<String> apiRequest = new HttpEntity<>(apiHeaders); String result = restTemplate.postForObject(API_URL, apiRequest, String.class); System.out.println("API响应:" + result); } // 用于解析token响应的实体类 static class TokenResponse { private String access_token; private String token_type; private long expires_in; // getter和setter省略 } }
六、测试步骤
- 在数据库中插入一条客户端数据,clientSecret用BCrypt加密后的字符串:
INSERT INTO oauth_clients (client_id, client_secret, scope, authorized_grant_types) VALUES ('service-client', '$2a$10$xxxxxx...', 'read,write', 'client_credentials');
- 启动服务,运行上述客户端代码,即可无交互完成服务间API调用。
内容的提问来源于stack exchange,提问作者Y A
相关产品推荐
相关产品推荐

