1、概述
安全性在REST API開(kāi)發(fā)中扮演著重要的角色。一個(gè)不安全的REST API可以直接訪(fǎng)問(wèn)到后臺(tái)系統(tǒng)中的敏感數(shù)據(jù)。因此,企業(yè)組織需要關(guān)注API安全性。
Spring Security 提供了各種機(jī)制來(lái)保護(hù)我們的 REST API。其中之一是 API 密鑰。API 密鑰是客戶(hù)端在調(diào)用 API 調(diào)用時(shí)提供的令牌。
在本教程中,我們將討論如何在Spring Security中實(shí)現(xiàn)基于A(yíng)PI密鑰的身份驗(yàn)證。
2、REST API Security
Spring Security可以用來(lái)保護(hù)REST API的安全性。REST API是無(wú)狀態(tài)的,因此不應(yīng)該使用會(huì)話(huà)或cookie。相反,應(yīng)該使用Basic authentication,API Keys,JWT或OAuth2-based tokens來(lái)確保其安全性。
2.1. Basic Authentication
Basic authentication是一種簡(jiǎn)單的認(rèn)證方案??蛻?hù)端發(fā)送HTTP請(qǐng)求,其中包含Authorization標(biāo)頭的值為Basic base64_url編碼的用戶(hù)名:密碼。Basic authentication僅在HTTPS / SSL等其他安全機(jī)制下才被認(rèn)為是安全的。
2.2. OAuth2
OAuth2是REST API安全的行業(yè)標(biāo)準(zhǔn)。它是一種開(kāi)放的認(rèn)證和授權(quán)標(biāo)準(zhǔn),允許資源所有者通過(guò)訪(fǎng)問(wèn)令牌將授權(quán)委托給客戶(hù)端,以獲得對(duì)私有數(shù)據(jù)的訪(fǎng)問(wèn)權(quán)限。
2.3. API Keys
一些REST API使用API密鑰進(jìn)行身份驗(yàn)證。API密鑰是一個(gè)標(biāo)記,用于向API客戶(hù)端標(biāo)識(shí)API,而無(wú)需引用實(shí)際用戶(hù)。標(biāo)記可以作為查詢(xún)字符串或在請(qǐng)求頭中發(fā)送。
3、用API Keys保護(hù)REST API
3.1 ?添加Maven 依賴(lài)
讓我們首先在我們的pom.xml中聲明spring-boot-starter-security依賴(lài)關(guān)系:
???? org.springframework.boot ????spring-boot-starter-security
3.2 創(chuàng)建自定義過(guò)濾器(Filter)
實(shí)現(xiàn)思路是從請(qǐng)求頭中獲取API Key,然后使用我們的配置檢查秘鑰。在這種情況下,我們需要在Spring Security 配置類(lèi)中添加一個(gè)自定義的Filter。
我們將從實(shí)現(xiàn)GenericFilterBean開(kāi)始。GenericFilterBean是一個(gè)基于javax.servlet.Filter接口的簡(jiǎn)單Spring實(shí)現(xiàn)。
讓我們創(chuàng)建AuthenticationFilter類(lèi):
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); ????} }
我們只需要實(shí)現(xiàn)doFilter()方法,在這個(gè)方法中我們從請(qǐng)求頭中獲取API Key,并將生成的Authentication對(duì)象設(shè)置到當(dāng)前的SecurityContext實(shí)例中。
然后請(qǐng)求被傳遞給其余的過(guò)濾器處理,接著轉(zhuǎn)發(fā)給DispatcherServlet最后到達(dá)我們的控制器。
在A(yíng)uthenticationService類(lèi)中,實(shí)現(xiàn)從Header中獲取API Key并構(gòu)造Authentication對(duì)象,代碼如下:
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); ????} }
在這里,我們檢查請(qǐng)求頭是否包含 API Key,如果為空 或者Key值不等于密鑰,那么就拋出一個(gè) BadCredentialsException。如果請(qǐng)求頭包含 API Key,并且驗(yàn)證通過(guò),則將密鑰添加到安全上下文中,然后調(diào)用下一個(gè)安全過(guò)濾器。getAuthentication 方法非常簡(jiǎn)單,我們只是比較 API Key 頭部和密鑰是否相等。
為了構(gòu)建 Authentication 對(duì)象,我們必須使用 Spring Security 為了標(biāo)準(zhǔn)身份驗(yàn)證而構(gòu)建對(duì)象時(shí)使用的相同方法。所以,需要擴(kuò)展 AbstractAuthenticationToken 類(lèi)并手動(dòng)觸發(fā)身份驗(yàn)證。
3.3. 擴(kuò)展AbstractAuthenticationToken
為了成功地實(shí)現(xiàn)我們應(yīng)用的身份驗(yàn)證功能,我們需要將傳入的API Key轉(zhuǎn)換為AbstractAuthenticationToken類(lèi)型的身份驗(yàn)證對(duì)象。AbstractAuthenticationToken類(lèi)實(shí)現(xiàn)了Authentication接口,表示一個(gè)認(rèn)證請(qǐng)求的主體和認(rèn)證信息。
讓我們創(chuàng)建ApiKeyAuthentication類(lèi):
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 類(lèi)是類(lèi)型為 AbstractAuthenticationToken 的對(duì)象,其中包含從 HTTP 請(qǐng)求中獲取的 apiKey 信息。在構(gòu)造方法中使用 setAuthenticated(true) 方法。因此,Authentication對(duì)象包含 apiKey 和authenticated字段:
3.4. Security Config
通過(guò)創(chuàng)建建一個(gè)SecurityFilterChain bean,可以通過(guò)編程方式把我們上面編寫(xiě)的自定義過(guò)濾器(Filter)進(jìn)行注冊(cè)。
我們需要在 HttpSecurity 實(shí)例上使用 addFilterBefore() 方法在 UsernamePasswordAuthenticationFilter 類(lèi)之前添加 AuthenticationFilter。
創(chuàng)建SecurityConfig 類(lèi):
@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(); ????} }
此外注意代碼中我們吧繪畫(huà)策略(session policy)設(shè)置為無(wú)狀態(tài)(STATELESS),因?yàn)槲覀兪褂玫氖荝EST。
3.5. ResourceController
最后,我們創(chuàng)建ResourceController,實(shí)現(xiàn)一個(gè)Get請(qǐng)求 ?/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. 測(cè)試
我們先不提供API Key進(jìn)行測(cè)試
curl?--location?--request?GET?'http://localhost:8080/home'
返回 401 未經(jīng)授權(quán)錯(cuò)誤。
請(qǐng)求頭中加上API Key后,再次請(qǐng)求
curl?--location?--request?GET?'http://localhost:8080/home'? --header?'X-API-KEY:?Baeldung'
請(qǐng)求返回狀態(tài)200
編輯:黃飛
評(píng)論