java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot接口安全的访问控制

SpringBoot中接口安全的5种访问控制方法详解

作者:风象南

在项目开发种,API接口安全已成为系统设计中不可忽视的关键环节,本文将介绍SpringBoot应用中实现接口安全的5种访问控制方法,有需要的小伙伴可以了解下

在项目开发种,API接口安全已成为系统设计中不可忽视的关键环节。

一个不安全的API可能导致数据泄露、未授权访问、身份冒用等严重安全事件。

本文将介绍SpringBoot应用中实现接口安全的5种访问控制方法。

方法一:基于Spring Security的认证与授权

原理

Spring Security是Spring生态系统中负责安全控制的核心框架,它通过一系列过滤器链实现认证和授权功能。认证(Authentication)确认用户身份的真实性,而授权(Authorization)则控制已认证用户可以访问的资源范围。

实现方式

1. 添加依赖

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

2. 配置安全规则

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .antMatchers("/api/user/**").hasRole("USER")
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginProcessingUrl("/api/login")
                .successHandler(new SimpleUrlAuthenticationSuccessHandler())
                .failureHandler(new SimpleUrlAuthenticationFailureHandler())
            .and()
            .logout()
                .logoutUrl("/api/logout")
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
            .and()
            .exceptionHandling()
                .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
    }
}

3. 实现UserDetailsService

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
        
        return new org.springframework.security.core.userdetails.User(
            user.getUsername(),
            user.getPassword(),
            user.isEnabled(),
            true, true, true,
            getAuthorities(user.getRoles())
        );
    }
    
    private Collection<? extends GrantedAuthority> getAuthorities(Collection<Role> roles) {
        return roles.stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
            .collect(Collectors.toList());
    }
}

4. 创建安全控制器

@RestController
@RequestMapping("/api")
public class SecuredController {

    @GetMapping("/public/data")
    public ResponseEntity<String> publicData() {
        return ResponseEntity.ok("This is public data");
    }
    
    @GetMapping("/user/data")
    public ResponseEntity<String> userData() {
        return ResponseEntity.ok("This is user data");
    }
    
    @GetMapping("/admin/data")
    public ResponseEntity<String> adminData() {
        return ResponseEntity.ok("This is admin data");
    }
    
    @GetMapping("/current-user")
    public ResponseEntity<UserInfo> getCurrentUser(Authentication authentication) {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        return ResponseEntity.ok(new UserInfo(userDetails.getUsername(), userDetails.getAuthorities()));
    }
}

优缺点分析

优点:

• 功能全面,提供完整的认证和授权体系

• 与Spring生态系统无缝集成

• 高度可定制,支持多种认证方式

• 内置防护机制,如防止CSRF攻击、会话固定等

缺点:

• 配置相对复杂,学习曲线较陡

• 默认基于session,在分布式系统中需要额外配置

• 过滤器链执行可能影响性能

适用场景

• 传统Web应用,特别是有复杂权限模型的系统

• 需要细粒度权限控制的企业应用

• 对安全性要求较高的内部系统

方法二:基于JWT的无状态认证

原理

JWT(JSON Web Token)是一种紧凑的、自包含的方式,用于在网络应用环境间安全地传输信息。由于JWT包含了认证所需的所有信息,服务器无需保存会话状态,实现了真正的无状态认证。

实现方式

1. 添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

2. JWT工具类

@Component
public class JwtTokenProvider {

    @Value("${app.jwt.secret}")
    private String jwtSecret;
    
    @Value("${app.jwt.expiration}")
    private long jwtExpiration;
    
    private Key key;
    
    @PostConstruct
    public void init() {
        byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }
    
    public String generateToken(Authentication authentication) {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpiration);
        
        return Jwts.builder()
                .setSubject(userDetails.getUsername())
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .claim("authorities", getAuthorities(userDetails))
                .signWith(key)
                .compact();
    }
    
    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
        
        return claims.getSubject();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
    
    private List<String> getAuthorities(UserDetails userDetails) {
        return userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());
    }
}

3. JWT认证过滤器

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenProvider tokenProvider;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        
        String jwt = getJwtFromRequest(request);
        
        if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
            String username = tokenProvider.getUsernameFromToken(jwt);
            
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

4. 配置JWT安全

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }
    
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .cors().and()
            .csrf().disable()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
                .antMatchers("/api/auth/**").permitAll()
                .antMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated();
        
        // 添加JWT过滤器
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

5. 认证控制器

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private JwtTokenProvider tokenProvider;
    
    @PostMapping("/login")
    public ResponseEntity<JwtResponse> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                loginRequest.getUsername(),
                loginRequest.getPassword()
            )
        );
        
        SecurityContextHolder.getContext().setAuthentication(authentication);
        
        String jwt = tokenProvider.generateToken(authentication);
        return ResponseEntity.ok(new JwtResponse(jwt));
    }
}

优缺点分析

优点:

• 无状态,适合分布式系统和微服务架构

• 减轻服务器负担,无需存储会话

• 跨域支持良好,适合前后端分离应用

• 可包含丰富的用户信息,减少数据库查询

缺点:

• Token一旦签发,在过期前无法撤销(可通过黑名单机制缓解)

• 需要妥善保护密钥

• Token体积可能较大,增加网络传输开销

• 敏感信息不应存储在Token中(虽然签名,但非加密)

适用场景

• 前后端分离的单页应用(SPA)

• 微服务架构

• 移动应用后端

• 跨域API调用

方法三:OAuth 2.0第三方授权

原理

OAuth 2.0是一个授权框架,允许第三方应用在不获取用户凭证的情况下,获得对用户资源的有限访问权限。它通过将认证委托给可信的认证服务器,实现了应用间的安全授权。

实现方式

1. 添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.6.8</version>
</dependency>

2. 配置OAuth2资源服务器

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .antMatchers("/api/user/**").hasRole("USER")
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated();
    }
}

3. 配置OAuth2授权服务器

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Value("${oauth2.client-id}")
    private String clientId;
    
    @Value("${oauth2.client-secret}")
    private String clientSecret;
    
    @Value("${oauth2.jwt.signing-key}")
    private String signingKey;
    
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients();
    }
    
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient(clientId)
                .secret(passwordEncoder.encode(clientSecret))
                .scopes("read", "write")
                .authorizedGrantTypes("password", "refresh_token")
                .accessTokenValiditySeconds(3600)
                .refreshTokenValiditySeconds(86400);
    }
    
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                .tokenStore(tokenStore())
                .accessTokenConverter(accessTokenConverter());
    }
    
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }
    
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(signingKey);
        return converter;
    }
}

4. 安全配置

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                .anyRequest().authenticated()
            .and()
            .httpBasic();
    }
}

5. 资源访问控制器

@RestController
@RequestMapping("/api")
public class ResourceController {

    @GetMapping("/public/data")
    public ResponseEntity<String> publicData() {
        return ResponseEntity.ok("This is public data");
    }
    
    @GetMapping("/user/data")
    public ResponseEntity<String> userData(Principal principal) {
        return ResponseEntity.ok("User data for: " + principal.getName());
    }
    
    @GetMapping("/admin/data")
    public ResponseEntity<String> adminData() {
        return ResponseEntity.ok("This is admin data");
    }
    
    @GetMapping("/me")
    public ResponseEntity<UserInfo> getUserInfo(OAuth2Authentication authentication) {
        return ResponseEntity.ok(new UserInfo(authentication.getName(), authentication.getAuthorities()));
    }
}

6. 配置第三方登录

@Configuration
@EnableOAuth2Client
public class OAuth2ClientConfig {

    @Bean
    public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext,
                                                OAuth2ProtectedResourceDetails details) {
        return new OAuth2RestTemplate(details, oauth2ClientContext);
    }
    
    @Bean
    @ConfigurationProperties("github.client")
    public AuthorizationCodeResourceDetails github() {
        return new AuthorizationCodeResourceDetails();
    }
    
    @Bean
    @ConfigurationProperties("github.resource")
    public ResourceServerProperties githubResource() {
        return new ResourceServerProperties();
    }
}

在application.yml中配置:

github:
  client:
    clientId: your-github-client-id
    clientSecret: your-github-client-secret
    accessTokenUri: https://github.com/login/oauth/access_token
    userAuthorizationUri: https://github.com/login/oauth/authorize
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://api.github.com/user

优缺点分析

优点:

• 标准化的授权协议,广泛支持

• 支持多种授权模式(授权码、密码、客户端凭证等)

• 可实现第三方应用登录(如Google、Facebook、GitHub等)

• 细粒度的权限范围控制

缺点:

• 配置较为复杂

• 理解和实现完整流程有一定门槛

• 在某些场景下可能过于重量级

适用场景

• 需要支持第三方登录的应用

• 企业内多系统间的授权

• 开放平台API授权

• 移动应用与服务端交互

方法四:接口签名验证

原理

接口签名验证通过对请求参数、时间戳等信息进行加密签名,服务端验证签名的有效性来确保请求的合法性和完整性。这种方式常用于开放API的安全防护。

实现方式

1. 签名生成工具类

@Component
public class SignatureUtils {

    /**
     * 生成签名
     * @param params 参数Map
     * @param secretKey 密钥
     * @return 签名字符串
     */
    public static String generateSignature(Map<String, String> params, String secretKey) {
        // 1. 参数按字典序排序
        TreeMap<String, String> sortedParams = new TreeMap<>(params);
        
        // 2. 构建签名字符串
        StringBuilder stringBuilder = new StringBuilder();
        for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
            if (!"sign".equals(entry.getKey()) && StringUtils.hasText(entry.getValue())) {
                stringBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
            }
        }
        
        // 3. 添加密钥
        stringBuilder.append("key=").append(secretKey);
        
        // 4. MD5加密并转大写
        String signStr = stringBuilder.toString();
        return DigestUtils.md5DigestAsHex(signStr.getBytes()).toUpperCase();
    }
    
    /**
     * 验证签名
     * @param params 包含签名的参数Map
     * @param secretKey 密钥
     * @return 是否验证通过
     */
    public static boolean verifySignature(Map<String, String> params, String secretKey) {
        String providedSign = params.get("sign");
        if (!StringUtils.hasText(providedSign)) {
            return false;
        }
        
        // 生成签名进行比对
        String generatedSign = generateSignature(params, secretKey);
        return providedSign.equals(generatedSign);
    }
    
    /**
     * 验证时间戳是否有效(防重放攻击)
     * @param timestamp 时间戳
     * @param timeWindow 有效时间窗口(毫秒)
     * @return 是否在有效期内
     */
    public static boolean isTimestampValid(String timestamp, long timeWindow) {
        try {
            long requestTime = Long.parseLong(timestamp);
            long currentTime = System.currentTimeMillis();
            return Math.abs(currentTime - requestTime) <= timeWindow;
        } catch (NumberFormatException e) {
            return false;
        }
    }
}

2. 签名验证拦截器

@Component
public class SignatureVerificationInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(SignatureVerificationInterceptor.class);
    
    @Value("${api.signature.secret}")
    private String secretKey;
    
    @Value("${api.signature.time-window}")
    private long timeWindow;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 检查是否需要验证签名的接口
        if (isSignatureRequired(request, handler)) {
            Map<String, String> params = getAllParameters(request);
            
            // 验证时间戳
            String timestamp = params.get("timestamp");
            if (!SignatureUtils.isTimestampValid(timestamp, timeWindow)) {
                logger.warn("Invalid timestamp: {}", timestamp);
                response.setStatus(HttpStatus.FORBIDDEN.value());
                response.getWriter().write("{"code":403,"message":"Invalid timestamp"}");
                return false;
            }
            
            // 验证签名
            if (!SignatureUtils.verifySignature(params, secretKey)) {
                logger.warn("Invalid signature for request: {}", request.getRequestURI());
                response.setStatus(HttpStatus.FORBIDDEN.value());
                response.getWriter().write("{"code":403,"message":"Invalid signature"}");
                return false;
            }
        }
        
        return true;
    }
    
    private boolean isSignatureRequired(HttpServletRequest request, Object handler) {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            // 检查是否有SignatureRequired注解
            return handlerMethod.getMethod().isAnnotationPresent(SignatureRequired.class);
        }
        return false;
    }
    
    private Map<String, String> getAllParameters(HttpServletRequest request) {
        Map<String, String> params = new HashMap<>();
        
        // 获取URL参数
        request.getParameterMap().forEach((key, values) -> {
            if (values != null && values.length > 0) {
                params.put(key, values[0]);
            }
        });
        
        // 如果是POST请求且Content-Type为application/json,解析请求体
        if ("POST".equals(request.getMethod()) && 
            request.getContentType() != null && 
            request.getContentType().contains("application/json")) {
            try {
                String body = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8);
                if (StringUtils.hasText(body)) {
                    ObjectMapper mapper = new ObjectMapper();
                    JsonNode jsonNode = mapper.readTree(body);
                    // 将JSON转换为平铺的Map
                    flattenJsonToMap("", jsonNode, params);
                }
            } catch (IOException e) {
                logger.error("Failed to parse request body", e);
            }
        }
        
        return params;
    }
    
    private void flattenJsonToMap(String prefix, JsonNode jsonNode, Map<String, String> map) {
        if (jsonNode.isObject()) {
            Iterator<Map.Entry<String, JsonNode>> fields = jsonNode.fields();
            while (fields.hasNext()) {
                Map.Entry<String, JsonNode> entry = fields.next();
                String newPrefix = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
                flattenJsonToMap(newPrefix, entry.getValue(), map);
            }
        } else if (jsonNode.isArray()) {
            // 简单处理:数组转为逗号分隔字符串
            StringBuilder sb = new StringBuilder();
            for (JsonNode element : jsonNode) {
                if (sb.length() > 0) {
                    sb.append(",");
                }
                sb.append(element.asText());
            }
            map.put(prefix, sb.toString());
        } else {
            map.put(prefix, jsonNode.asText());
        }
    }
}

3. 自定义注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SignatureRequired {
}

4. 配置拦截器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private SignatureVerificationInterceptor signatureVerificationInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(signatureVerificationInterceptor)
                .addPathPatterns("/api/**");
    }
}

5. 接口使用示例

@RestController
@RequestMapping("/api/open")
public class OpenApiController {

    @GetMapping("/public-data")
    public ResponseEntity<String> publicData() {
        return ResponseEntity.ok("This is public data without signature verification");
    }
    
    @SignatureRequired
    @GetMapping("/protected-data")
    public ResponseEntity<String> protectedData() {
        return ResponseEntity.ok("This is protected data with signature verification");
    }
}

优缺点分析

优点:

• 无需用户交互,适合系统间API调用

• 可防止请求篡改和重放攻击

• 不依赖于会话和Cookie

• 可与其他安全机制结合使用

缺点:

• 接口调用较为复杂,需客户端配合实现签名逻辑

• 时钟不同步可能导致验证失败

• 密钥泄露风险高,需妥善保管

适用场景

• 开放API平台

• 支付和金融相关接口

• 系统间后台通信

• 对安全性要求高的数据交换接口

方法五:限流与防刷机制

原理

限流与防刷机制通过控制API访问频率,防止恶意用户通过高频请求对系统进行攻击或爬取数据。常见的限流算法包括固定窗口计数器、滑动窗口、漏桶和令牌桶等。

实现方式

1. 添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

2. 基于Redis的限流实现

@Component
public class RedisRateLimiter {

    private final StringRedisTemplate redisTemplate;
    
    @Autowired
    public RedisRateLimiter(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    /**
     * 固定窗口限流算法
     * @param key 限流键
     * @param period 窗口期(秒)
     * @param maxRequests 最大请求数
     * @return 是否允许请求
     */
    public boolean isAllowed(String key, int period, int maxRequests) {
        String finalKey = "rate:limiter:" + key;
        
        // 获取当前计数
        Long count = redisTemplate.opsForValue().increment(finalKey, 1);
        
        // 如果是第一次请求,设置过期时间
        if (count != null && count == 1) {
            redisTemplate.expire(finalKey, period, TimeUnit.SECONDS);
        }
        
        return count != null && count <= maxRequests;
    }
}

3. 基于Guava的令牌桶限流器

@Component
public class GuavaRateLimiter {

    private final ConcurrentMap<String, RateLimiter> limiters = new ConcurrentHashMap<>();
    
    /**
     * 获取指定key的限流器
     * @param key 限流键
     * @param permitsPerSecond 每秒允许的请求数
     * @return RateLimiter实例
     */
    public RateLimiter getRateLimiter(String key, double permitsPerSecond) {
        return limiters.computeIfAbsent(key, k -> RateLimiter.create(permitsPerSecond));
    }
    
    /**
     * 尝试获取令牌
     * @param key 限流键
     * @param permitsPerSecond 每秒允许的请求数
     * @return 是否获取成功
     */
    public boolean tryAcquire(String key, double permitsPerSecond) {
        RateLimiter limiter = getRateLimiter(key, permitsPerSecond);
        return limiter.tryAcquire();
    }
    
    /**
     * 尝试在指定时间内获取令牌
     * @param key 限流键
     * @param permitsPerSecond 每秒允许的请求数
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return 是否获取成功
     */
    public boolean tryAcquire(String key, double permitsPerSecond, long timeout, TimeUnit unit) {
        RateLimiter limiter = getRateLimiter(key, permitsPerSecond);
        return limiter.tryAcquire(1, timeout, unit);
    }
}

4. IP限流拦截器

@Component
public class IpRateLimitInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(IpRateLimitInterceptor.class);
    
    @Autowired
    private RedisRateLimiter redisRateLimiter;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            
            // 检查是否有RateLimit注解
            RateLimit rateLimit = handlerMethod.getMethod().getAnnotation(RateLimit.class);
            if (rateLimit != null) {
                String ip = getClientIp(request);
                String uri = request.getRequestURI();
                String key = ip + ":" + uri;
                
                if (!redisRateLimiter.isAllowed(key, rateLimit.period(), rateLimit.maxRequests())) {
                    logger.warn("Rate limit exceeded for IP: {}, URI: {}", ip, uri);
                    response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
                    response.getWriter().write("{"code":429,"message":"Too many requests"}");
                    return false;
                }
            }
        }
        
        return true;
    }
    
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        
        // 如果是多级代理,取第一个IP
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        
        return ip;
    }
}

5. 自定义限流注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    
    /**
     * 窗口期(秒)
     */
    int period() default 60;
    
    /**
     * 窗口期内最大请求数
     */
    int maxRequests() default 10;
}

6. 限流注解全局处理(AOP方式)

@Aspect
@Component
public class RateLimitAspect {

    private static final Logger logger = LoggerFactory.getLogger(RateLimitAspect.class);
    
    @Autowired
    private GuavaRateLimiter guavaRateLimiter;
    
    @Autowired
    private HttpServletRequest request;
    
    @Around("@annotation(rateLimit)")
    public Object rateLimit(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable {
        String methodName = point.getSignature().getName();
        String className = point.getTarget().getClass().getName();
        String ip = getClientIp(request);
        
        // 使用方法名、类名和IP作为限流键
        String key = ip + ":" + className + ":" + methodName;
        
        // 限流速率:maxRequests / period
        double permitsPerSecond = (double) rateLimit.maxRequests() / rateLimit.period();
        
        if (guavaRateLimiter.tryAcquire(key, permitsPerSecond)) {
            return point.proceed();
        } else {
            logger.warn("Rate limit exceeded for method: {}.{}, IP: {}", className, methodName, ip);
            throw new TooManyRequestsException("Too many requests, please try again later.");
        }
    }
    
    private String getClientIp(HttpServletRequest request) {
        // 获取客户端IP的逻辑,同上文
    }
}

7. 配置拦截器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private IpRateLimitInterceptor ipRateLimitInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(ipRateLimitInterceptor)
                .addPathPatterns("/api/**");
    }
}

8. 使用限流注解的控制器

@RestController
@RequestMapping("/api")
public class RateLimitedController {

    @RateLimit(period = 60, maxRequests = 5)
    @GetMapping("/limited-data")
    public ResponseEntity<String> getLimitedData() {
        return ResponseEntity.ok("This is rate-limited data");
    }
    
    @RateLimit(period = 3600, maxRequests = 100)
    @PostMapping("/submit-form")
    public ResponseEntity<String> submitForm(@RequestBody FormData formData) {
        return ResponseEntity.ok("Form submitted successfully");
    }
}

优缺点分析

优点:

• 有效防止恶意攻击和刷接口行为

• 保护系统资源,防止过载

• 可针对不同用户、IP或接口设置不同限制

• 结合业务场景灵活配置

缺点:

• 分布式环境下需要集中存储计数器(如Redis)

• 时间窗口设置不当可能影响正常用户体验

• 固定限流策略难以应对突发流量

适用场景

• 防止接口被恶意调用或爬取

• 保护高消耗资源的接口

• 付费API的访问频率控制

• 防止用户批量操作(如注册、评论等)

方案比较

方法复杂度安全性适用场景主要优势主要劣势
Spring Security认证授权企业应用、内部系统全面的安全框架,细粒度权限控制配置复杂,学习曲线陡
JWT无状态认证前后端分离、微服务无状态,易于扩展Token撤销困难
OAuth 2.0授权第三方登录、API平台标准化授权协议,支持多种授权模式实现复杂
接口签名验证开放API、支付接口防篡改,适合系统间调用需客户端配合实现
限流防刷机制防爬虫、资源保护简单有效,保护系统资源可能影响正常用户

总结

在实际应用中,往往需要根据具体场景组合使用这些方法,构建多层次的安全防护体系。

同时,安全是一个持续的过程,除了技术手段外,还需要定期的安全审计、漏洞扫描和安全意识培训。

到此这篇关于SpringBoot中接口安全的5种访问控制方法详解的文章就介绍到这了,更多相关SpringBoot接口安全的访问控制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文