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

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省略
    }
}

六、测试步骤

  1. 在数据库中插入一条客户端数据,clientSecret用BCrypt加密后的字符串:
INSERT INTO oauth_clients (client_id, client_secret, scope, authorized_grant_types)
VALUES ('service-client', '$2a$10$xxxxxx...', 'read,write', 'client_credentials');
  1. 启动服务,运行上述客户端代码,即可无交互完成服务间API调用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 14:16:47