1. 概述
在本快速教程中,我們將探討如何在 Spring Security 應用程序中定義多個入口點。
這主要涉及在 XML 配置文件中定義多個 http 塊,或者通過創建 SecurityFilterChain 豆類多次來創建多個 HttpSecurity 實例。
2. Maven 依賴
為了開發,我們需要以下依賴項:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>5.4.0</version>
</dependency>最新版本的 spring-boot-starter-security、spring-boot-starter-web、spring-boot-starter-thymeleaf、spring-boot-starter-test、spring-security-test 可從 Maven Central 下載。
3. 多個入口點
3.1. 多個入口點與多個HTTP元素
讓我們定義一個主要配置類,用於存儲用户信息:
@Configuration
@EnableWebSecurity
public class MultipleEntryPointsSecurityConfig {
@Bean
public UserDetailsService userDetailsService() throws Exception {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User
.withUsername("user")
.password(encoder().encode("userPass"))
.roles("USER").build());
manager.createUser(User
.withUsername("admin")
.password(encoder().encode("adminPass"))
.roles("ADMIN").build());
return manager;
}
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
}現在,讓我們來看一下如何在我們的安全配置中定義多個入口點。
我們將使用基於基本身份驗證的示例,充分利用 Spring Security 支持在配置中定義多個 HTTP 元素的事實。
當使用 Java 配置時,定義多個安全領域的方式是擁有多個 @Configuration 類,每個類都具有自己的安全配置。這些類可以是靜態的,並放置在主配置中。
在一個應用程序中擁有多個入口點的主要動機是,如果存在不同類型的用户可以訪問應用程序的不同部分。
讓我們定義一個具有三個入口點,每個入口點具有不同的權限和身份驗證模式的配置:
- 一個用於管理用户,使用 HTTP Basic 身份驗證
- 一個用於普通用户,使用表單身份驗證
- 以及一個用於訪客用户,不需要身份驗證
對於管理用户入口點,使用 HTTP Basic 身份驗證,它將保護以 /admin/** 形式的 URL,只允許具有 ADMIN 角色的用户,並使用 BasicAuthenticationEntryPoint 類型入口點,通過 authenticationEntryPoint() 方法進行設置。
@Configuration
@Order(1)
public static class App1ConfigurationAdapter {
@Bean
public SecurityFilterChain filterChainApp1(HttpSecurity http) throws Exception {
http.antMatcher("/admin/**")
.authorizeRequests().anyRequest().hasRole("ADMIN")
.and().httpBasic().authenticationEntryPoint(authenticationEntryPoint())
.and().exceptionHandling().accessDeniedPage("/403");
return http.build();
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint(){
BasicAuthenticationEntryPoint entryPoint = new BasicAuthenticationEntryPoint();
entryPoint.setRealmName("admin realm");
return entryPoint;
}
}每個靜態類上的@Order註解指示配置項將被按照該值順序考慮,以找到與請求 URL 匹配的配置項。 每個類的order值必須是唯一的。
類型為BasicAuthenticationEntryPoint的 Bean 需要設置 realName 屬性。
3.2. 多個入口點,相同 HTTP 元素
接下來,讓我們定義能夠使用用户角色(USER)通過表單身份驗證訪問的,格式為 /user/** 的 URL 配置:
@Configuration
@Order(2)
public static class App2ConfigurationAdapter {
@Bean
public SecurityFilterChain filterChainApp2(HttpSecurity http) throws Exception {
http.antMatcher("/user/**")
.authorizeRequests().anyRequest().hasRole("USER")
.and().formLogin().loginProcessingUrl("/user/login")
.failureUrl("/userLogin?error=loginError").defaultSuccessUrl("/user/myUserPage")
.and().logout().logoutUrl("/user/logout").logoutSuccessUrl("/multipleHttpLinks")
.deleteCookies("JSESSIONID")
.and().exceptionHandling()
.defaultAuthenticationEntryPointFor(loginUrlauthenticationEntryPointWithWarning(), new AntPathRequestMatcher("/user/private/**"))
.defaultAuthenticationEntryPointFor(loginUrlauthenticationEntryPoint(), new AntPathRequestMatcher("/user/general/**"))
.accessDeniedPage("/403")
.and().csrf().disable();
return http.build();
}
}如我們所見,除了 authenticationEntryPoint() 方法之外,另一種定義入口點的做法是使用 defaultAuthenticationEntryPointFor() 方法。該方法可以定義多個入口點,這些入口點基於 RequestMatcher 對象,並匹配不同的條件。
RequestMatcher 接口有基於不同條件的實現,例如路徑匹配、媒體類型匹配或正則表達式匹配。在我們的示例中,我們使用了 AntPathRequestMatch 來為 /user/private/** 和 /user/general/** 這樣的 URL 路徑設置兩個不同的入口點。
接下來,我們需要在同一靜態配置類中定義入口點 Bean。
@Bean
public AuthenticationEntryPoint loginUrlauthenticationEntryPoint(){
return new LoginUrlAuthenticationEntryPoint("/userLogin");
}
@Bean
public AuthenticationEntryPoint loginUrlauthenticationEntryPointWithWarning(){
return new LoginUrlAuthenticationEntryPoint("/userLoginWithWarning");
}主要目標是瞭解如何設置這些多個入口點——這並不一定涉及每個入口點的具體實現細節。
在這種情況下,入口點都是 LoginUrlAuthenticationEntryPoint 類型,並使用不同的登錄頁面 URL:/userLogin 用於簡單的登錄頁面,/userLoginWithWarning 用於同時嘗試訪問 /user/ 私有 URL 時還會顯示警告的登錄頁面。
此配置還需要定義 /userLogin 和 /userLoginWithWarning 兩個 MVC 映射以及包含標準登錄表單的兩個頁面。
對於表單身份驗證,務必記住,用於配置的任何 URL,例如登錄處理 URL 也需要遵循 /user/** 格式或以其他方式進行配置,使其可訪問。
上述所有配置都將重定向到 /403 URL,如果用户沒有適當的角色嘗試訪問受保護的 URL,則會發生這種情況。
即使在不同的靜態類中,也應使用唯一的名稱來命名 Bean,否則一個 Bean 將覆蓋另一個 Bean。
3.3. 新 HTTP 元素,無入口點
最後,讓我們定義用於 /guest/** 形式 URL 的第三種配置,該配置將允許所有類型的用户,包括未身份驗證的用户:
@Configuration
@Order(3)
public static class App3ConfigurationAdapter {
@Bean
public SecurityFilterChain filterChainApp3(HttpSecurity http) throws Exception {
http.antMatcher("/guest/**")
.authorizeRequests()
.anyRequest()
.permitAll();
return http.build();
}
}3.4. XML 配置
讓我們來看一下前一節中三個 <i>HttpSecurity</i> 實例的等效 XML 配置。
正如預期的那樣,這個配置將包含三個獨立的 XML <http> 塊。
對於 /admin/** URL,XML 配置將使用 <http-basic> 元素的 entry-point-ref 屬性:
<security:http pattern="/admin/**" use-expressions="true" auto-config="true">
<security:intercept-url pattern="/**" access="hasRole('ROLE_ADMIN')"/>
<security:http-basic entry-point-ref="authenticationEntryPoint" />
</security:http>
<bean id="authenticationEntryPoint"
class="org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint">
<property name="realmName" value="admin realm" />
</bean>需要注意的是,如果使用 XML 配置,角色必須採用 ROLE_<ROLE_NAME> 的形式。
對於 /user/** URL 的配置,由於沒有直接對應的 defaultAuthenticationEntryPointFor() 方法,因此需要在 XML 中將其分解為兩個 http 塊。
對於 URL /user/general/** 的配置,如下所示:
<security:http pattern="/user/general/**" use-expressions="true" auto-config="true"
entry-point-ref="loginUrlAuthenticationEntryPoint">
<security:intercept-url pattern="/**" access="hasRole('ROLE_USER')" />
//form-login configuration
</security:http>
<bean id="loginUrlAuthenticationEntryPoint"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<constructor-arg name="loginFormUrl" value="/userLogin" />
</bean>對於 /user/private/** 類型的 URL,我們可以定義類似的配置:
<security:http pattern="/user/private/**" use-expressions="true" auto-config="true"
entry-point-ref="loginUrlAuthenticationEntryPointWithWarning">
<security:intercept-url pattern="/**" access="hasRole('ROLE_USER')"/>
//form-login configuration
</security:http>
<bean id="loginUrlAuthenticationEntryPointWithWarning"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<constructor-arg name="loginFormUrl" value="/userLoginWithWarning" />
</bean>對於 /guest/** URL,我們將使用 http 元素:
<security:http pattern="/**" use-expressions="true" auto-config="true">
<security:intercept-url pattern="/guest/**" access="permitAll()"/>
</security:http>至少一個XML<em>http</em>塊必須與“/**”模式匹配也很重要。
4. 訪問受保護的 URL
This section describes how to access URLs that require authentication or authorization. This is common for accessing sensitive data or restricted areas within an application.
Authentication Methods
There are several methods for authenticating users before they can access protected URLs:
- Basic Authentication: This is the simplest method, using a username and password to identify the user. The browser automatically sends these credentials in the
Authorizationheader. - OAuth 2.0: A more secure protocol that allows users to grant limited access to their resources without sharing their credentials directly. This typically involves obtaining an access token.
- API Keys: Similar to API keys, these are used to identify the application making the request.
Example (Basic Authentication Header)
Authorization: Basic <base64 encoded username:password>
Explanation:
The Authorization header is crucial for informing the server that the request requires authentication. The value of the header is typically a Base64 encoded string containing the username and password.
4.1. MVC 配置
讓我們創建請求映射,以匹配我們已保護的 URL 模式:
@Controller
public class PagesController {
@GetMapping("/admin/myAdminPage")
public String getAdminPage() {
return "multipleHttpElems/myAdminPage";
}
@GetMapping("/user/general/myUserPage")
public String getUserPage() {
return "multipleHttpElems/myUserPage";
}
@GetMapping("/user/private/myPrivateUserPage")
public String getPrivateUserPage() {
return "multipleHttpElems/myPrivateUserPage";
}
@GetMapping("/guest/myGuestPage")
public String getGuestPage() {
return "multipleHttpElems/myGuestPage";
}
@GetMapping("/multipleHttpLinks")
public String getMultipleHttpLinksPage() {
return "multipleHttpElems/multipleHttpLinks";
}
}/multipleHttpLinks映射將返回一個簡單的HTML頁面,其中包含指向受保護URL的鏈接:
<a th:href="@{/admin/myAdminPage}">Admin page</a>
<a th:href="@{/user/general/myUserPage}">User page</a>
<a th:href="@{/user/private/myPrivateUserPage}">Private user page</a>
<a th:href="@{/guest/myGuestPage}">Guest page</a>每個對應受保護 URL 的 HTML 頁面都將包含一段簡單的文本和一個回鏈:
Welcome admin!
<a th:href="@{/multipleHttpLinks}" >Back to links</a>4.2. 初始化應用程序
我們將以 Spring Boot 應用程序的方式運行我們的示例,因此讓我們定義一個包含主方法的類:
@SpringBootApplication
public class MultipleEntryPointsApplication {
public static void main(String[] args) {
SpringApplication.run(MultipleEntryPointsApplication.class, args);
}
}如果我們要使用 XML 配置,還需要在我們的主類中添加 @ImportResource({"classpath*:spring-security-multiple-entry.xml"})</em/> 標註。
4.3. 測試安全配置
讓我們設置一個 JUnit 測試類,用於測試我們的受保護 URL:
@RunWith(SpringRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = MultipleEntryPointsApplication.class)
public class MultipleEntryPointsTest {
@Autowired
private WebApplicationContext wac;
@Autowired
private FilterChainProxy springSecurityFilterChain;
private MockMvc mockMvc;
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
.addFilter(springSecurityFilterChain).build();
}
}接下來,讓我們使用 admin 用户測試 URL。
當請求 URL /admin/adminPage 時,且未包含 HTTP Basic 身份驗證,我們應該收到 401 Unauthorized 狀態碼,而在添加身份驗證後,狀態碼應為 200 OK。
如果嘗試使用 admin 用户訪問 URL /user/userPage,我們應該收到 403 Forbidden 狀態碼:
@Test
public void whenTestAdminCredentials_thenOk() throws Exception {
mockMvc.perform(get("/admin/myAdminPage")).andExpect(status().isUnauthorized());
mockMvc.perform(get("/admin/myAdminPage")
.with(httpBasic("admin", "adminPass"))).andExpect(status().isOk());
mockMvc.perform(get("/user/myUserPage")
.with(user("admin").password("adminPass").roles("ADMIN")))
.andExpect(status().isForbidden());
}讓我們使用常規用户憑據訪問 URL 創建一個類似的測試:
@Test
public void whenTestUserCredentials_thenOk() throws Exception {
mockMvc.perform(get("/user/general/myUserPage")).andExpect(status().isFound());
mockMvc.perform(get("/user/general/myUserPage")
.with(user("user").password("userPass").roles("USER")))
.andExpect(status().isOk());
mockMvc.perform(get("/admin/myAdminPage")
.with(user("user").password("userPass").roles("USER")))
.andExpect(status().isForbidden());
}在第二個測試中,我們可以看到缺少表單身份驗證會導致狀態碼為 302 Found,而不是 Unauthorized,因為 Spring Security 會將請求重定向到登錄表單。
最後,我們創建一個測試,訪問 /guest/guestPage URL,並驗證我們收到狀態碼 200 OK:
@Test
public void givenAnyUser_whenGetGuestPage_thenOk() throws Exception {
mockMvc.perform(get("/guest/myGuestPage")).andExpect(status().isOk());
mockMvc.perform(get("/guest/myGuestPage")
.with(user("user").password("userPass").roles("USER")))
.andExpect(status().isOk());
mockMvc.perform(get("/guest/myGuestPage")
.with(httpBasic("admin", "adminPass")))
.andExpect(status().isOk());
}5. 結論
在本教程中,我們演示瞭如何配置多入口點,當使用 Spring Security 時。
請注意,使用 HTTP Basic 身份驗證時無法註銷,因此您需要關閉並重新打開瀏覽器以清除身份驗證。
要運行 JUnit 測試,請使用定義的 Maven 配置文件 entryPoints,如下所示:
mvn clean install -PentryPoints