五種方案!保護(hù)Spring Boot API接口安全
環(huán)境:SpringBoot3.4.2
1. 簡(jiǎn)介
API是系統(tǒng)核心數(shù)據(jù)的出入口,必須防止未授權(quán)訪問(wèn)、數(shù)據(jù)泄露、惡意攻擊等風(fēng)險(xiǎn)。例如:
- 防止游客查看敏感數(shù)據(jù)(如用戶信息)
- 阻止越權(quán)操作(如普通用戶刪除管理員數(shù)據(jù))
- 抵御惡意刷接口或注入攻擊
合理的權(quán)限控制能確保數(shù)據(jù)安全、系統(tǒng)穩(wěn)定,并符合合規(guī)要求。
接下來(lái)我將詳細(xì)的介紹 5 種簡(jiǎn)單好用的保護(hù)方案,讓你能輕松搞定 API 接口安全。
以下是本篇文章要實(shí)現(xiàn)的5種方案:
- 通過(guò)Filter實(shí)現(xiàn)
- 使用Interceptor攔截器實(shí)現(xiàn)
- 基于AOP+Filter實(shí)現(xiàn)
- 使用Spring Security實(shí)現(xiàn)
- 整合OAuth2認(rèn)證
2.實(shí)戰(zhàn)案例
準(zhǔn)備如下Controller接口
@RestController
public class ApiController {
@GetMapping("/api/query")
public ResponseEntity<?> query() {
System.err.println(SecurityContext.get()) ;
return ResponseEntity.ok("query...") ;
}
@GetMapping("/create")
public ResponseEntity<?> create() {
return ResponseEntity.ok("create...") ;
}
}
這里提供了2個(gè)接口。其中,/api/query是需要認(rèn)證的接口,/create無(wú)需認(rèn)證,可直接訪問(wèn)。
2.1 通過(guò)Filter實(shí)現(xiàn)
Filter 是 Java Servlet 規(guī)范中的一個(gè)組件,可以在請(qǐng)求到達(dá) Servlet 之前或響應(yīng)返回客戶端之前對(duì)請(qǐng)求和響應(yīng)進(jìn)行攔截和處理。通過(guò)在 Filter 中實(shí)現(xiàn)權(quán)限認(rèn)證邏輯,可以有效地控制用戶對(duì) API 接口的訪問(wèn)權(quán)限。
代碼實(shí)現(xiàn):
@WebFilter("/api/*")
public class JwtAuthFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req ;
HttpServletResponse response = (HttpServletResponse) resp ;
try {
// 從請(qǐng)求header中讀取token
String token = JwtUtil.extractToken(request) ;
// 驗(yàn)證token & 將從token中解析出的數(shù)據(jù)保存到線程上下文中
if (token != null && JwtUtil.validateToken(token)) {
Map<String, Object> data = JwtUtil.parseToken(token) ;
SecurityContext.set(data) ;
filterChain.doFilter(request, resp) ;
} else {
// 認(rèn)證失敗,則直接輸出錯(cuò)誤內(nèi)容
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(new ObjectMapper()
.writeValueAsString(Map.of("code", -1, "msg", "無(wú)效token"))) ;
return ;
}
} finally {
SecurityContext.clear() ;
}
}
}
此過(guò)濾器將只攔截 /api 開(kāi)頭的請(qǐng)求。
運(yùn)行結(jié)果
圖片
圖片
提供正確的token再次訪問(wèn)
圖片
2.2 使用Interceptor攔截器
HandlerInterceptor 是 Spring MVC 提供的一個(gè)接口,可以在請(qǐng)求處理前后進(jìn)行攔截。通過(guò)實(shí)現(xiàn)其 preHandle 方法,可以在請(qǐng)求到達(dá)控制器之前進(jìn)行權(quán)限驗(yàn)證,決定是否允許請(qǐng)求繼續(xù)執(zhí)行。
代碼實(shí)現(xiàn):
@Component
public class AuthInterceptor implements HandlerInterceptor {
private final ObjectMapper objectMapper ;
public AuthInterceptor(ObjectMapper objectMapper) {
this.objectMapper = objectMapper ;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = JwtUtil.extractToken(request) ;
if (token != null && JwtUtil.validateToken(token)) {
Map<String, Object> data = JwtUtil.parseToken(token) ;
SecurityContext.set(data) ;
return true ;
} else {
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(objectMapper
.writeValueAsString(Map.of("code", -1, "msg", "無(wú)效token"))) ;
return false ;
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
SecurityContext.clear() ;
}
}
注冊(cè)攔截器
@Component
public class WebConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor ;
public WebConfig(AuthInterceptor authInterceptor) {
this.authInterceptor = authInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this.authInterceptor).addPathPatterns("/api/**") ;
}
}
注意:不要忘記在攔截器的 afterCompletion 中進(jìn)行上下文的清理。
2.3 使用AOP + Filter實(shí)現(xiàn)
AOP用于定義權(quán)限驗(yàn)證的切面邏輯,F(xiàn)ilter 則用于攔截請(qǐng)求并解析處理Token。這種組合方式能夠在請(qǐng)求處理的不同階段進(jìn)行權(quán)限控制,既靈活又高效,適合在需要復(fù)雜權(quán)限管理。
代碼實(shí)現(xiàn):
Filter實(shí)現(xiàn)
@WebFilter("/*")
public class TokenFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req ;
try {
String token = JwtUtil.extractToken(request) ;
if (token != null && JwtUtil.validateToken(token)) {
Map<String, Object> data = JwtUtil.parseToken(token) ;
SecurityContext.set(data) ;
}
filterChain.doFilter(request, resp) ;
} finally {
SecurityContext.clear() ;
}
}
}
AOP切面實(shí)現(xiàn)
// 自定義注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionCheck {
String[] value() default {} ;
}
// 切面定義
@Aspect
@Component
public class PermissionAspect {
@SuppressWarnings("unchecked")
@Around("@annotation(permissionCheck)")
public Object checkPermission(ProceedingJoinPoint joinPoint, PermissionCheck permissionCheck) throws Throwable {
try {
Map<String, Object> data = SecurityContext.get() ;
if (data == null) {
throw new PermissionException("無(wú)權(quán)限訪問(wèn)", 403) ;
}
// 獲取所需角色
String[] requiredRoles = permissionCheck.value() ;
// 用戶所擁有的角色
List<String> assignedRoles = (List<String>) data.get("roles") ;
// 判斷2個(gè)集合如果沒(méi)有共同元素則返回true
if (Collections.disjoint(List.of(requiredRoles), assignedRoles)) {
throw new PermissionException("無(wú)權(quán)限訪問(wèn)", 403) ;
}
// 繼續(xù)執(zhí)行方法
return joinPoint.proceed();
} catch (Exception e) {
e.printStackTrace() ;
throw new PermissionException("Token無(wú)效或過(guò)期", 401) ;
}
}
}
修改Controller接口
@GetMapping("/api/query")
@PermissionCheck({"api:query"})
public ResponseEntity<?> query() {
return ResponseEntity.ok("query...") ;
}
2.4 使用Spring Security
Spring Security 是 Spring 生態(tài)中的安全框架,提供了豐富的權(quán)限控制功能,包括身份認(rèn)證、授權(quán)、密碼加密等。通過(guò)配置安全規(guī)則和過(guò)濾器鏈,可以輕松實(shí)現(xiàn)細(xì)粒度的權(quán)限控制。
代碼實(shí)現(xiàn):
首先,引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
最后,安全配置
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain apiSecurity(HttpSecurity http) throws Throwable {
http.csrf(csrf -> csrf.disable()) ;
http.securityMatcher("/api/**", "/login") ;
http.formLogin(Customizer.withDefaults()) ;
http.authorizeHttpRequests(registry -> {
registry.anyRequest().authenticated() ;
}) ;
return http.build() ;
}
// 實(shí)際我們應(yīng)該從數(shù)據(jù)庫(kù)中查詢,這里為了簡(jiǎn)單直接固定一個(gè)內(nèi)存用戶
@Bean
UserDetailsService userDetailsService() {
UserDetails user = User.withUsername("pack")
.password("{noop}123123")
.roles("ADMIN")
.build() ;
return new InMemoryUserDetailsManager(user) ;
}
}
在上面的SecurityFilterChain配置中,我們將只會(huì)對(duì)/api/*,/login接口進(jìn)行攔截。如果沒(méi)有權(quán)限將會(huì)自動(dòng)定向到登錄頁(yè)面。
當(dāng)訪問(wèn)/api/query接口時(shí),自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面:
圖片
輸入正確的用戶名密碼后,自動(dòng)跳回之前的接口地址
圖片