SpringBoot中接口安全的5種訪問控制方法詳解
在項目開發(fā)種,API接口安全已成為系統(tǒng)設(shè)計中不可忽視的關(guān)鍵環(huán)節(jié)。
一個不安全的API可能導(dǎo)致數(shù)據(jù)泄露、未授權(quán)訪問、身份冒用等嚴重安全事件。
本文將介紹SpringBoot應(yīng)用中實現(xiàn)接口安全的5種訪問控制方法。
方法一:基于Spring Security的認證與授權(quán)
原理
Spring Security是Spring生態(tài)系統(tǒng)中負責(zé)安全控制的核心框架,它通過一系列過濾器鏈實現(xiàn)認證和授權(quán)功能。認證(Authentication)確認用戶身份的真實性,而授權(quán)(Authorization)則控制已認證用戶可以訪問的資源范圍。
實現(xiàn)方式
1. 添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>2. 配置安全規(guī)則
@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. 實現(xiàn)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. 創(chuàng)建安全控制器
@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()));
}
}優(yōu)缺點分析
優(yōu)點:
• 功能全面,提供完整的認證和授權(quán)體系
• 與Spring生態(tài)系統(tǒng)無縫集成
• 高度可定制,支持多種認證方式
• 內(nèi)置防護機制,如防止CSRF攻擊、會話固定等
缺點:
• 配置相對復(fù)雜,學(xué)習(xí)曲線較陡
• 默認基于session,在分布式系統(tǒng)中需要額外配置
• 過濾器鏈執(zhí)行可能影響性能
適用場景
• 傳統(tǒng)Web應(yīng)用,特別是有復(fù)雜權(quán)限模型的系統(tǒng)
• 需要細粒度權(quán)限控制的企業(yè)應(yīng)用
• 對安全性要求較高的內(nèi)部系統(tǒng)
方法二:基于JWT的無狀態(tài)認證
原理
JWT(JSON Web Token)是一種緊湊的、自包含的方式,用于在網(wǎng)絡(luò)應(yīng)用環(huán)境間安全地傳輸信息。由于JWT包含了認證所需的所有信息,服務(wù)器無需保存會話狀態(tài),實現(xiàn)了真正的無狀態(tài)認證。
實現(xiàn)方式
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));
}
}優(yōu)缺點分析
優(yōu)點:
• 無狀態(tài),適合分布式系統(tǒng)和微服務(wù)架構(gòu)
• 減輕服務(wù)器負擔,無需存儲會話
• 跨域支持良好,適合前后端分離應(yīng)用
• 可包含豐富的用戶信息,減少數(shù)據(jù)庫查詢
缺點:
• Token一旦簽發(fā),在過期前無法撤銷(可通過黑名單機制緩解)
• 需要妥善保護密鑰
• Token體積可能較大,增加網(wǎng)絡(luò)傳輸開銷
• 敏感信息不應(yīng)存儲在Token中(雖然簽名,但非加密)
適用場景
• 前后端分離的單頁應(yīng)用(SPA)
• 微服務(wù)架構(gòu)
• 移動應(yīng)用后端
• 跨域API調(diào)用
方法三:OAuth 2.0第三方授權(quán)
原理
OAuth 2.0是一個授權(quán)框架,允許第三方應(yīng)用在不獲取用戶憑證的情況下,獲得對用戶資源的有限訪問權(quán)限。它通過將認證委托給可信的認證服務(wù)器,實現(xiàn)了應(yīng)用間的安全授權(quán)。
實現(xiàn)方式
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資源服務(wù)器
@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授權(quán)服務(wù)器
@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優(yōu)缺點分析
優(yōu)點:
• 標準化的授權(quán)協(xié)議,廣泛支持
• 支持多種授權(quán)模式(授權(quán)碼、密碼、客戶端憑證等)
• 可實現(xiàn)第三方應(yīng)用登錄(如Google、Facebook、GitHub等)
• 細粒度的權(quán)限范圍控制
缺點:
• 配置較為復(fù)雜
• 理解和實現(xiàn)完整流程有一定門檻
• 在某些場景下可能過于重量級
適用場景
• 需要支持第三方登錄的應(yīng)用
• 企業(yè)內(nèi)多系統(tǒng)間的授權(quán)
• 開放平臺API授權(quán)
• 移動應(yīng)用與服務(wù)端交互
方法四:接口簽名驗證
原理
接口簽名驗證通過對請求參數(shù)、時間戳等信息進行加密簽名,服務(wù)端驗證簽名的有效性來確保請求的合法性和完整性。這種方式常用于開放API的安全防護。
實現(xiàn)方式
1. 簽名生成工具類
@Component
public class SignatureUtils {
/**
* 生成簽名
* @param params 參數(shù)Map
* @param secretKey 密鑰
* @return 簽名字符串
*/
public static String generateSignature(Map<String, String> params, String secretKey) {
// 1. 參數(shù)按字典序排序
TreeMap<String, String> sortedParams = new TreeMap<>(params);
// 2. 構(gòu)建簽名字符串
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加密并轉(zhuǎn)大寫
String signStr = stringBuilder.toString();
return DigestUtils.md5DigestAsHex(signStr.getBytes()).toUpperCase();
}
/**
* 驗證簽名
* @param params 包含簽名的參數(shù)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 是否在有效期內(nèi)
*/
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參數(shù)
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轉(zhuǎn)換為平鋪的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()) {
// 簡單處理:數(shù)組轉(zhuǎn)為逗號分隔字符串
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");
}
}優(yōu)缺點分析
優(yōu)點:
• 無需用戶交互,適合系統(tǒng)間API調(diào)用
• 可防止請求篡改和重放攻擊
• 不依賴于會話和Cookie
• 可與其他安全機制結(jié)合使用
缺點:
• 接口調(diào)用較為復(fù)雜,需客戶端配合實現(xiàn)簽名邏輯
• 時鐘不同步可能導(dǎo)致驗證失敗
• 密鑰泄露風(fēng)險高,需妥善保管
適用場景
• 開放API平臺
• 支付和金融相關(guān)接口
• 系統(tǒng)間后臺通信
• 對安全性要求高的數(shù)據(jù)交換接口
方法五:限流與防刷機制
原理
限流與防刷機制通過控制API訪問頻率,防止惡意用戶通過高頻請求對系統(tǒng)進行攻擊或爬取數(shù)據(jù)。常見的限流算法包括固定窗口計數(shù)器、滑動窗口、漏桶和令牌桶等。
實現(xiàn)方式
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的限流實現(xiàn)
@Component
public class RedisRateLimiter {
private final StringRedisTemplate redisTemplate;
@Autowired
public RedisRateLimiter(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 固定窗口限流算法
* @param key 限流鍵
* @param period 窗口期(秒)
* @param maxRequests 最大請求數(shù)
* @return 是否允許請求
*/
public boolean isAllowed(String key, int period, int maxRequests) {
String finalKey = "rate:limiter:" + key;
// 獲取當前計數(shù)
Long count = redisTemplate.opsForValue().increment(finalKey, 1);
// 如果是第一次請求,設(shè)置過期時間
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 每秒允許的請求數(shù)
* @return RateLimiter實例
*/
public RateLimiter getRateLimiter(String key, double permitsPerSecond) {
return limiters.computeIfAbsent(key, k -> RateLimiter.create(permitsPerSecond));
}
/**
* 嘗試獲取令牌
* @param key 限流鍵
* @param permitsPerSecond 每秒允許的請求數(shù)
* @return 是否獲取成功
*/
public boolean tryAcquire(String key, double permitsPerSecond) {
RateLimiter limiter = getRateLimiter(key, permitsPerSecond);
return limiter.tryAcquire();
}
/**
* 嘗試在指定時間內(nèi)獲取令牌
* @param key 限流鍵
* @param permitsPerSecond 每秒允許的請求數(shù)
* @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;
/**
* 窗口期內(nèi)最大請求數(shù)
*/
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");
}
}優(yōu)缺點分析
優(yōu)點:
• 有效防止惡意攻擊和刷接口行為
• 保護系統(tǒng)資源,防止過載
• 可針對不同用戶、IP或接口設(shè)置不同限制
• 結(jié)合業(yè)務(wù)場景靈活配置
缺點:
• 分布式環(huán)境下需要集中存儲計數(shù)器(如Redis)
• 時間窗口設(shè)置不當可能影響正常用戶體驗
• 固定限流策略難以應(yīng)對突發(fā)流量
適用場景
• 防止接口被惡意調(diào)用或爬取
• 保護高消耗資源的接口
• 付費API的訪問頻率控制
• 防止用戶批量操作(如注冊、評論等)
方案比較
| 方法 | 復(fù)雜度 | 安全性 | 適用場景 | 主要優(yōu)勢 | 主要劣勢 |
| Spring Security認證授權(quán) | 高 | 高 | 企業(yè)應(yīng)用、內(nèi)部系統(tǒng) | 全面的安全框架,細粒度權(quán)限控制 | 配置復(fù)雜,學(xué)習(xí)曲線陡 |
| JWT無狀態(tài)認證 | 中 | 中 | 前后端分離、微服務(wù) | 無狀態(tài),易于擴展 | Token撤銷困難 |
| OAuth 2.0授權(quán) | 高 | 高 | 第三方登錄、API平臺 | 標準化授權(quán)協(xié)議,支持多種授權(quán)模式 | 實現(xiàn)復(fù)雜 |
| 接口簽名驗證 | 中 | 高 | 開放API、支付接口 | 防篡改,適合系統(tǒng)間調(diào)用 | 需客戶端配合實現(xiàn) |
| 限流防刷機制 | 低 | 中 | 防爬蟲、資源保護 | 簡單有效,保護系統(tǒng)資源 | 可能影響正常用戶 |
總結(jié)
在實際應(yīng)用中,往往需要根據(jù)具體場景組合使用這些方法,構(gòu)建多層次的安全防護體系。
同時,安全是一個持續(xù)的過程,除了技術(shù)手段外,還需要定期的安全審計、漏洞掃描和安全意識培訓(xùn)。
到此這篇關(guān)于SpringBoot中接口安全的5種訪問控制方法詳解的文章就介紹到這了,更多相關(guān)SpringBoot接口安全的訪問控制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
spring-cloud入門之eureka-client(服務(wù)注冊)
本篇文章主要介紹了spring-cloud入門之eureka-client(服務(wù)注冊),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-01-01
關(guān)于SpringMVC請求域?qū)ο蟮臄?shù)據(jù)共享問題
這篇文章主要介紹了SpringMVC請求域?qū)ο蟮臄?shù)據(jù)共享問題,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-02-02
Java如何接收并解析HL7協(xié)議數(shù)據(jù)
文章主要介紹了HL7協(xié)議及其在醫(yī)療行業(yè)中的應(yīng)用,詳細描述了如何配置環(huán)境、接收和解析數(shù)據(jù),以及與前端進行交互的實現(xiàn)方法,文章還分享了使用7Edit工具進行調(diào)試的經(jīng)驗,并記錄了一個常見的解析問題及其解決方法2024-12-12
java中建立0-10m的消息(字符串)實現(xiàn)方法
下面小編就為大家?guī)硪黄猨ava中建立0-10m的消息(字符串)實現(xiàn)方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-05-05
Java實現(xiàn)調(diào)用MySQL存儲過程詳解
相信大家都知道存儲過程是在大型數(shù)據(jù)庫系統(tǒng)中,一組為了完成特定功能的SQL語句集。存儲過程是數(shù)據(jù)庫中的一個重要對象,任何一個設(shè)計良好的數(shù)據(jù)庫應(yīng)用程序都應(yīng)該用到存儲過程。Java調(diào)用mysql存儲過程,實現(xiàn)如下,有需要的朋友們可以參考借鑒,下面來一起看看吧。2016-11-11
springboot集成websocket的四種方式小結(jié)
本文主要介紹了springboot集成websocket的四種方式小結(jié),文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12

