1、概述
安全性在REST API開發中扮演著重要的角色。一個不安全的REST API可以直接訪問到后臺系統中的敏感數據。因此,企業組織需要關注API安全性。
Spring Security 提供了各種機制來保護我們的 REST API。其中之一是 API 密鑰。API 密鑰是客戶端在調用 API 調用時提供的令牌。
在本教程中,我們將討論如何在Spring Security中實現基于API密鑰的身份驗證。
2、REST API Security
Spring Security可以用來保護REST API的安全性。REST API是無狀態的,因此不應該使用會話或cookie。相反,應該使用Basic authentication,API Keys,JWT或OAuth2-based tokens來確保其安全性。
2.1. Basic Authentication
Basic authentication是一種簡單的認證方案。客戶端發送HTTP請求,其中包含Authorization標頭的值為Basic base64_url編碼的用戶名:密碼。Basic authentication僅在HTTPS / SSL等其他安全機制下才被認為是安全的。
2.2. OAuth2
OAuth2是REST API安全的行業標準。它是一種開放的認證和授權標準,允許資源所有者通過訪問令牌將授權委托給客戶端,以獲得對私有數據的訪問權限。
2.3. API Keys
一些REST API使用API密鑰進行身份驗證。API密鑰是一個標記,用于向API客戶端標識API,而無需引用實際用戶。標記可以作為查詢字符串或在請求頭中發送。
3、用API Keys保護REST API
3.1 ?添加Maven 依賴
讓我們首先在我們的pom.xml中聲明spring-boot-starter-security依賴關系:
???? org.springframework.boot ????spring-boot-starter-security
3.2 創建自定義過濾器(Filter)
實現思路是從請求頭中獲取API Key,然后使用我們的配置檢查秘鑰。在這種情況下,我們需要在Spring Security 配置類中添加一個自定義的Filter。
我們將從實現GenericFilterBean開始。GenericFilterBean是一個基于javax.servlet.Filter接口的簡單Spring實現。
讓我們創建AuthenticationFilter類:
public?class?AuthenticationFilter?extends?GenericFilterBean?{
????@Override ????public?void?doFilter(ServletRequest?request,?ServletResponse?response,?FilterChain?filterChain) ??????throws?IOException,?ServletException?{ ????????try?{ ????????????Authentication?authentication?=?AuthenticationService.getAuthentication((HttpServletRequest)?request); ????????????SecurityContextHolder.getContext().setAuthentication(authentication); ????????}?catch?(Exception?exp)?{ ????????????HttpServletResponse?httpResponse?=?(HttpServletResponse)?response; ????????????httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); ????????????httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE); ????????????PrintWriter?writer?=?httpResponse.getWriter(); ????????????writer.print(exp.getMessage()); ????????????writer.flush(); ????????????writer.close(); ????????} ????????filterChain.doFilter(request,?response); ????} }
我們只需要實現doFilter()方法,在這個方法中我們從請求頭中獲取API Key,并將生成的Authentication對象設置到當前的SecurityContext實例中。
然后請求被傳遞給其余的過濾器處理,接著轉發給DispatcherServlet最后到達我們的控制器。
在AuthenticationService類中,實現從Header中獲取API Key并構造Authentication對象,代碼如下:
public?class?AuthenticationService?{
????private?static?final?String?AUTH_TOKEN_HEADER_NAME?=?"X-API-KEY"; ????private?static?final?String?AUTH_TOKEN?=?"Baeldung"; ????public?static?Authentication?getAuthentication(HttpServletRequest?request)?{ ????????String?apiKey?=?request.getHeader(AUTH_TOKEN_HEADER_NAME); ????????if?((apiKey?==?null)?||?!apiKey.equals(AUTH_TOKEN))?{ ????????????throw?new?BadCredentialsException("Invalid?API?Key"); ????????} ????????return?new?ApiKeyAuthentication(apiKey,?AuthorityUtils.NO_AUTHORITIES); ????} }
在這里,我們檢查請求頭是否包含 API Key,如果為空 或者Key值不等于密鑰,那么就拋出一個 BadCredentialsException。如果請求頭包含 API Key,并且驗證通過,則將密鑰添加到安全上下文中,然后調用下一個安全過濾器。getAuthentication 方法非常簡單,我們只是比較 API Key 頭部和密鑰是否相等。
為了構建 Authentication 對象,我們必須使用 Spring Security 為了標準身份驗證而構建對象時使用的相同方法。所以,需要擴展 AbstractAuthenticationToken 類并手動觸發身份驗證。
3.3. 擴展AbstractAuthenticationToken
為了成功地實現我們應用的身份驗證功能,我們需要將傳入的API Key轉換為AbstractAuthenticationToken類型的身份驗證對象。AbstractAuthenticationToken類實現了Authentication接口,表示一個認證請求的主體和認證信息。
讓我們創建ApiKeyAuthentication類:
public?class?ApiKeyAuthentication?extends?AbstractAuthenticationToken?{
????private?final?String?apiKey; ????public?ApiKeyAuthentication(String?apiKey, ????????Collection?authorities)?{ ????????super(authorities); ????????this.apiKey?=?apiKey; ????????setAuthenticated(true); ????} ????@Override ????public?Object?getCredentials()?{ ????????return?null; ????} ????@Override ????public?Object?getPrincipal()?{ ????????return?apiKey; ????} }
ApiKeyAuthentication 類是類型為 AbstractAuthenticationToken 的對象,其中包含從 HTTP 請求中獲取的 apiKey 信息。在構造方法中使用 setAuthenticated(true) 方法。因此,Authentication對象包含 apiKey 和authenticated字段:
3.4. Security Config
通過創建建一個SecurityFilterChain bean,可以通過編程方式把我們上面編寫的自定義過濾器(Filter)進行注冊。
我們需要在 HttpSecurity 實例上使用 addFilterBefore() 方法在 UsernamePasswordAuthenticationFilter 類之前添加 AuthenticationFilter。
創建SecurityConfig 類:
@Configuration @EnableWebSecurity public?class?SecurityConfig?{ ????@Bean ????public?SecurityFilterChain?filterChain(HttpSecurity?http)?throws?Exception?{ ????????http.csrf() ??????????.disable() ??????????.authorizeRequests() ??????????.antMatchers("/**") ??????????.authenticated() ??????????.and() ??????????.httpBasic() ??????????.and() ??????????.sessionManagement() ??????????.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ??????????.and() ??????????.addFilterBefore(new?AuthenticationFilter(),?UsernamePasswordAuthenticationFilter.class); ????????return?http.build(); ????} }
此外注意代碼中我們吧繪畫策略(session policy)設置為無狀態(STATELESS),因為我們使用的是REST。
3.5. ResourceController
最后,我們創建ResourceController,實現一個Get請求 ?/home
@RestController public?class?ResourceController?{ ????@GetMapping("/home") ????public?String?homeEndpoint()?{ ????????return?"Baeldung?!"; ????} }
3.6. 禁用 Auto-Configuration
@SpringBootApplication(exclude?=?{SecurityAutoConfiguration.class,?UserDetailsServiceAutoConfiguration.class}) public?class?ApiKeySecretAuthApplication?{ ????public?static?void?main(String[]?args)?{ ????????SpringApplication.run(ApiKeySecretAuthApplication.class,?args); ????} }
4. 測試
我們先不提供API Key進行測試
curl?--location?--request?GET?'http://localhost:8080/home'
返回 401 未經授權錯誤。
請求頭中加上API Key后,再次請求
curl?--location?--request?GET?'http://localhost:8080/home'? --header?'X-API-KEY:?Baeldung'
請求返回狀態200
編輯:黃飛
評論
查看更多