Spring?Security登錄表單配置示例詳解
Spring Security登錄表單配置
1.引入pom依賴
? 創(chuàng)建一個Spring Boot工程,引入Web和Spring Security依賴:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
</parent>
<groupId>com.kapcb.ccc</groupId>
<artifactId>springsecurity-helloworld</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
</project>2.bootstrap.yml添加配置
? 工程創(chuàng)建完成之后,為了方便測試,需要在bootstrap.yml配置文件中添加如下配置,將登錄用戶名和密碼固定下來:
spring:
security:
user:
name: kapcb
password: 1234563.創(chuàng)建login.html
? 在resources/static目錄下創(chuàng)建login.html頁面,這個就是自定義登錄頁面:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form action="/loginSystem" method="post">
<label>username: <input type="text" name="username"/></label></br>
<label>password: <input type="password" name="password"/></label></br>
<input type="submit" name="submit" value="login">
</form>
</body>
</html>
? 這個login.html核心內容就是一個登陸表單,登陸表單中有三個需要注意的地方:
form標簽中的action熟悉,這里是/loginSystem,表示表單要提交請求到/loginSystem接口上。- 用戶名框的
name屬性值為username。 - 密碼框的
name屬性為password。
4.創(chuàng)建配置類
? 定義好login.html之后,接下來就定義兩個測試接口,作為受保護資源。當用戶登陸成功后,就可以訪問受保護的資源。接口定義如下:
package com.kapcb.security.helloworld.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <a>Title: HelloWorldController </a>
* <a>Author: Kapcb <a>
* <a>Description: HelloWorldController <a>
*
* @author Kapcb
* @version 1.0
* @date 2022/6/12 22:05
* @since 1.0
*/
@RestController
public class HelloWorldController {
@GetMapping("index")
public String index() {
return "login success!";
}
@GetMapping("hello")
public String hello() {
return "Hello, World!";
}
}? 最后再提供Spring Security的配置類,需要注意的是在Spring Security 5.7版本中已經廢棄WebSecurityConfigurerAdapter:
package com.kapcb.security.helloworld.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
/**
* <a>Title: SecurityConfiguration </a>
* <a>Author: Kapcb <a>
* <a>Description: SecurityConfiguration <a>
*
* @author Kapcb
* @version 1.0
* @date 2022/6/13 22:23
* @since 1.0
*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/loginSystem")
.defaultSuccessUrl("/index")
.failureUrl("/login.html")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.csrf().disable().build();
}
}
? 這里直接使用聲明SecurityFilterChain類型的Bean的方式即可配置Spring Security登陸表單。
- authorizeHttpRequests()方法表示開啟權限配置。
- anyRequest().authenticated():表示所有請求都需要經過認證。
- and():方法會返回httpSecurityBuilder對象的一個子類,實際上就是HttpSecurity。所以and()方法相當于返回一個HttpSecurity實例,重新開啟新一輪配置。
- formLogin():表示開啟表單登陸配置。
- loginPage("/login.html"):用于配置默認登錄頁的請求地址。
- loginProcessingUrl("/loginSystem"):用于配置登錄請求接口地址。
- defaultSuccessUrl("/index"):表示登錄請求處理成功后的跳轉地址。
- failureUrl("/login.html"):表示登陸失敗跳轉的地址。
- usernameParameter("username"):表示登陸用戶名的參數名稱。
- passwordParameter("password"):表示登錄密碼的參數名稱。
- permitAll():表示跟登錄相關的頁面和接口不做攔截,直接允許訪問。
- csrf().disable():表示禁用CSRF防御功能。Spring Security自帶了CSRF防御機制。為了測試方便,這里先關閉了。
? 需要注意的是,上面的loginPage()、loginProcessingUrl()、defaultSuccessUrl()、usernameParameter()、passwordParameter()需要和之前創(chuàng)建的login.html中登錄表單的配置一致。
? 完成上述配置之后,啟動工程,訪問http://localhost:9096/index,會自動跳轉到http://localhost:9096/login.html頁面。輸入之前設置好的用戶名密碼,登陸成功之后,就可以訪問到/index頁面了。
5.配置細節(jié)
? 在前面的配置中,使用defaultSuccessUrl()方法配置了用戶登陸成功之后的跳轉地址。使用failureUrl()方法配置了用戶登錄失敗之后的跳轉地址。關于用戶登錄成功與登陸失敗,除了這兩個方法可以進行配置之外,還有另外兩個方法也可配置。
6.登陸成功
? 當用戶登錄成功之后,除了使用defaultSuccessUrl()方法可以實現登錄成功后的跳轉之外,successForwardUrl()方法也可以配置實現登錄成功之后的跳轉,配置代碼如下:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/loginSystem")
.successForwardUrl("/index")
.failureUrl("/login.html")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.csrf().disable().build();
}
}
? defaultSuccessUrl()和successForwardUrl()方法配置的區(qū)別如下:
- defaultSuccessUrl()方法表示當用戶登陸成功之后,會自動重定向到登錄之前用戶訪問的地址上。如果用戶本身就是直接訪問的登陸頁面,則登錄成功之后就會重定向到defaultSuccessUrl()指定的頁面。
- successForwardUrl()方法則不會考慮用戶登錄之前訪問的地址,只要用戶登陸成功,就會通過服務器端跳轉到successForwardUrl()所指定的頁面。
- defaultSuccessUrl()方法有一個重載方法,如果重載方法的第二個參數傳入true,則defaultSuccessUrl()方法的效果與successForwardUrl()方法類似,即不考慮用戶之前的訪問地址,只要登陸成功,就重定向到defaultSuccessUrl()所指定的頁面。不同之處在于,defaultSuccessUrl()方法是通過重定向實現的客戶端跳轉,successForwardUrl()則是通過服務端跳轉實現的。
? 無論是defaultSuccessUrl()還是successForwardUrl()方法配置登錄成功跳轉頁面,最終所配置的都是AuthenticationSuccessHandler接口的實例。
? 在Spring Security中專門提供了AuthenticationSuccessHandler接口來處理用戶登陸成功事項,AuthenticationSuccessHandler接口源碼如下:
public interface AuthenticationSuccessHandler {
default void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
this.onAuthenticationSuccess(request, response, authentication);
chain.doFilter(request, response);
}
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException;
}
? AuthenticationSuccessHandler接口中定義了兩個方法,其中一個是default方法,此方法是Spring Security 5.2版本中添加進來的,在處理特定的認證請求Authentication Filter中會用到。另外一個非default方法,則用來處理登陸成功的具體事項,其中Authentication參數保存了登陸成功的用戶信息。
? AuthenticationSuccessHandler接口在Spring Security源碼中一共有三個實現類:
- SimpleUrlAuthenticationSuccessHandler繼承自AbstractAuthenticationTargetUrlRequestHandler。通過AbstractAuthenticationTargetUrlRequestHandler中的handler()方法實現請求重定向。
- SavedRequestAwareAuthenticationSuccessHandler在SimpleUrlAuthenticationSuccessHandler的基礎上增加了請求緩存的功能,可以記錄用戶登陸之前請求的地址,進而在登陸成功之后重定向到一開始訪問的地址。
- ForwardAuthenticationSuccessHandler的實現比較簡單,就是一個服務端跳轉。
? 通過defaultSuccessUrl()方法來配置登陸成功之后重定向的請求地址時,實際上對應的實現類就是SavedRequestAwareAuthenticationSuccessHandler。SavedRequestAwareAuthenticationSuccessHandler源碼如下:
public class SavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private RequestCache requestCache = new HttpSessionRequestCache();
public SavedRequestAwareAuthenticationSuccessHandler() {
}
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
} else {
String targetUrlParameter = this.getTargetUrlParameter();
if (!this.isAlwaysUseDefaultTargetUrl() && (targetUrlParameter == null || !StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
this.clearAuthenticationAttributes(request);
String targetUrl = savedRequest.getRedirectUrl();
this.getRedirectStrategy().sendRedirect(request, response, targetUrl);
} else {
this.requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
public void setRequestCache(RequestCache requestCache) {
this.requestCache = requestCache;
}
}
? SavedRequestAwareAuthenticationSuccessHandler中的核心方法就是onAuthenticationSuccess()。
- 從RequestCache中獲取緩存下來的請求,如果沒有獲取到緩存的請求地址,就代表用戶在登錄之前并沒有訪問其它頁面。此時直接調用父類的onAuthenticationSuccess()方法來處理,最終重定向到defaultSuccessUrl()方法指定的地址。
- 接下里會獲取一個targetUrlParameter,這個地址是用戶顯示指定的、希望登錄成功之后重定向的地址。例如用戶發(fā)送的登錄請求是http://127.0.0.1:9096/loginSystem?target=/hello,這就表示當用戶登陸成功之后,希望主動重定向到/hello接口。targetUrlParameter()方法就是獲取重定向地址參數的key,就是上面舉例中的target。獲取到target之后就可以獲取到重定向的地址了。
- 如果targetUrlParameter存在,或者開發(fā)者設置了isAlwaysUseDefaultTargetUrl()為true,此時緩存下來的請求就失去了意義。此時會直接移除掉緩存的請求地址,直接調用父類的onAuthenticationSuccess()方法完成重定向。targetUrlParameter存在,則直重定向到targetUrlParameter指定的地址。isAlwaysUseDefaultTargetUrl為true,則直接重定向到defaultSuccessUrl指定的地址。如果targetUrlParameter存在并且isAlwaysUseDefaultTargetUrl為true,則直接重定向到defaultSuccessUrl指定的地址。
- 如果上述條件均不滿足,那么最終會從緩存請求SavedRequest對象中獲取重定向地址,然后進行重定向操作。
? 這就是SavedRequestAwareAuthenticationSuccessHandler的實現邏輯,開發(fā)者也可配置自己的SavedRequestAwareAuthenticationSuccessHandler,如下:
package com.kapcb.security.helloworld.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
/**
* <a>Title: SecurityConfiguration </a>
* <a>Author: Kapcb <a>
* <a>Description: SecurityConfiguration <a>
*
* @author Kapcb
* @version 1.0
* @date 2022/6/13 22:23
* @since 1.0
*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/loginSystem")
.successHandler(successHandler())
.failureUrl("/login.html")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.csrf().disable().build();
}
protected SavedRequestAwareAuthenticationSuccessHandler successHandler() {
SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setDefaultTargetUrl("/index");
successHandler.setTargetUrlParameter("target");
return successHandler;
}
}
? 在上面的配置代碼中,制定了targetUrlParameter為target,這樣用戶就可以在登錄請求中,通過target來指定跳轉地址。修改一下上面的login.html中的form表單:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form action="/loginSystem?target=/hello" method="post">
<label>username: <input type="text" name="username"/></label></br>
<label>password: <input type="password" name="password"/></label></br>
<input type="submit" name="submit" value="login">
</form>
</body>
</html>
? 修改form表單的action屬性為/loginSystem?target=/hello。當用戶登陸成功之后,就始終會跳轉到/hello接口了。
? 在使用successForwardUrl()方法設置登陸成功后重定向的地址時,實際上對應的實現類是ForwardAuthenticationSuccessHandler,ForwardAuthenticationSuccessHandler類的源碼非常簡單,就是一個服務端轉發(fā),ForwardAuthenticationSuccessHandler源碼如下:
public class ForwardAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final String forwardUrl;
public ForwardAuthenticationSuccessHandler(String forwardUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), () -> {
return "'" + forwardUrl + "' is not a valid forward URL";
});
this.forwardUrl = forwardUrl;
}
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
request.getRequestDispatcher(this.forwardUrl).forward(request, response);
}
}? 主要功能就是調用request.getRequestDispatcher(this.forwardUrl).forward(request, response)方法實現服務端請求轉發(fā)。
? 啟動服務之后訪問登錄頁面,登錄成功后即可自動重定向到/hello接口。
? AuthenticationSuccessHandler默認的三個實現類,無論是哪一種,都是用來處理頁面跳轉的。隨著前后端分離架構的盛行,頁面跳轉并不能滿足我們的業(yè)務需求,用戶登錄成功后,后端返回JSON數據給前端即可。告訴前端當前用戶是登陸成功還是失敗即可,前端拿到后端響應結果后自行處理即可。像這種需求,可以使用自定義AuthenticationSuccessHandler的實現類完成。
package com.kapcb.security.helloworld.handler;
import com.alibaba.fastjson.JSON;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* <a>Title: CustomizeAuthenticationSuccessHandler </a>
* <a>Author: Kapcb <a>
* <a>Description: CustomizeAuthenticationSuccessHandler <a>
*
* @author Kapcb
* @version 1.0
* @date 2022/6/16 23:47
* @since 1.0
*/
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
Map<String, Object> resultMap = new HashMap<>(4);
resultMap.put("code", 200);
resultMap.put("msg", null);
resultMap.put("data", "1111111");
String jsonResult = JSON.toJSONString(resultMap);
response.getWriter().write(jsonResult);
response.getWriter().close();
}
}
? 修改配置類:
package com.kapcb.security.helloworld.configuration;
import com.kapcb.security.helloworld.handler.CustomizeAuthenticationSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
/**
* <a>Title: SecurityConfiguration </a>
* <a>Author: Kapcb <a>
* <a>Description: SecurityConfiguration <a>
*
* @author Kapcb
* @version 1.0
* @date 2022/6/13 22:23
* @since 1.0
*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureUrl("/login.html")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
}
? 在使用自定義AuthenticationSuccessHandler的實現類來完成登錄成功之后的處理邏輯之后,還需要同步修改login.html。
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form action="/loginSystem" method="post">
<label>username: <input type="text" name="username"/></label></br>
<label>password: <input type="password" name="password"/></label></br>
<input type="submit" name="submit" value="login">
</form>
</body>
</html>
? 將form表單中的action屬性修改為之前的/loginSystem。因為此時使用的是自定義的AuthenticationSuccessHandler。邏輯與SavedRequestAwareAuthenticationSuccessHandler是不一樣的。
? 所有的改動完成之后,重啟工程。當用戶登錄成功之后,就不會在進行頁面跳轉了,而是返回了一段JSON字符串到頁面上。
7.登陸失敗
? Spring Security登陸失敗的處理邏輯。為了方便在前端頁面展示登錄失敗的異常信息,首先在項目的pom.xml文件中引入thymeleaf模板引擎的依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
? 在resources目錄下創(chuàng)建templates文件夾,新建loginTemplate.html文件:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>loginTemplate</title>
</head>
<body>
<form action="/loginSystem" method="post">
<div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
<label>username: <input type="text" name="username"/></label></br>
<label>password: <input type="password" name="password"/></label></br>
<input type="submit" name="submit" value="login">
</form>
</body>
</html>
? loginTemplate.html文件和前面的login.html文件基本類似。login.html在static目錄下,屬于靜態(tài)頁面,loginTemplate.html是template模板頁面。loginTemplate.html在form標簽內新增了<div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>標簽,用于展示Spring Security在處理登陸失敗時的異常信息。在Spring Security中,用戶登陸失敗時,異常信息會放在request中返回給前端,開發(fā)者可將其直接提取出來展示。
? loginTemplate.html屬于動態(tài)頁面,所以就不能像訪問static/login.html那樣直接訪問,需要后端為其提供訪問控制器:
package com.kapcb.security.helloworld.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* <a>Title: RouterController </a>
* <a>Author: Kapcb <a>
* <a>Description: RouterController <a>
*
* @author Kapcb
* @version 1.0
* @date 2022/6/17 23:32
* @since 1.0
*/
@Controller
@RequestMapping("/router")
public class RouterController {
@RequestMapping("loginTemplate")
public String loginTemplate() {
return "loginTemplate";
}
}
? 最后在Spring Security配置類中配置登陸頁面:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.antMatchers("/loginFail")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/router/loginTemplate")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureUrl("/router/loginTemplate")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
}? failureUrl()表示登陸失敗后重定向到/router/loginTemplate接口請求地址,也就是loginTemplate.html頁面。重定向是一種客戶端跳轉,重定向不方便攜帶請求失敗的異常信息,只能放在URL中。
? 如果希望在前端展示請求失敗的異常信息,可以使用下面這種方式:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.antMatchers("/loginFail")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/router/loginTemplate")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureForwardUrl("/router/loginTemplate")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
}
? failureForwardUrl()方法從名字上就可以看出,這種跳轉是一種服務器端跳轉。服務器端跳轉的好處是可以攜帶登錄異常信息。如果用戶登陸失敗,自動跳轉回登錄頁面后,就可以將錯誤信息展示出來。

? 無論是failureUrl()還是failureForwardUrl()方法,最終所配置的都是AuthenticationFailureHandler接口的實現類。Spring Security中提供了AuthenticationFailureHandler接口,為登陸失敗下的處理方式提供頂級拓展。AuthenticationFailureHandler源碼如下:
public interface AuthenticationFailureHandler {
void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException;
}
? AuthenticationFailureHandler接口中只定義了一個onAuthenticationFailure()方法,用于處理登陸失敗的請求。AuthenticationException表示登陸失敗的異常信息。Spring Security中為AuthenticationFailureHandler一共提供了五個實現類:
- SimpleUrlAuthenticationFailureHandler:默認的處理邏輯就是通過重定向跳轉到登陸頁面,當然也可以通過配置forwardToDestination屬性將重定向改為服務器端跳轉,failureUrl()方法底層實現邏輯就是SimpleUrlAuthenticationFailureHandler。
- ExceptionMappingAuthenticationFailureHandler:可以實現根據不同異常類型,映射到不同路徑。
- ForwardAuthenticationFailureHandler:表示通過服務器端跳轉來重新回到登陸頁面,failureForwardUrl()方法底層實現邏輯就是ForwardAuthenticationFailureHandler。
- AuthenticationEntryPointFailureHandler:是Spring Security 5.2新引入的處理類,可以通過AuthenticationEntryPoint來處理登陸異常。
- DelegatingAuthenticationFailureHandler:可以實現為不同的異常類型配置不同的登錄失敗處理回調。
? 舉個簡單的例子。假設不使用failureForwardUrl()方法進行登陸失敗的處理邏輯配置,同時又想在登陸失敗后通過服務器端跳轉回到登陸頁面,那么可以自定義SimpleUrlAuthenticationFailureHandler配置,并將forwardToDestination屬性設置為true,代碼如下:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.antMatchers("/loginFail")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/router/loginTemplate")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureHandler(failureHandler())
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
protected SimpleUrlAuthenticationFailureHandler failureHandler() {
SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
failureHandler.setDefaultFailureUrl("/loginFail");
failureHandler.setUseForward(true);
return failureHandler;
}
}
? 這樣配置之后,用戶再次登陸失敗,就會通過服務端跳轉重新回到登陸頁面,同時頁面上也會展示相應的錯誤信息,效果和failureForwardUrl()一樣。
? SimpleUrlAuthenticationFailureHandler的源碼也很簡單:
public class SimpleUrlAuthenticationFailureHandler implements AuthenticationFailureHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private String defaultFailureUrl;
private boolean forwardToDestination = false;
private boolean allowSessionCreation = true;
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
public SimpleUrlAuthenticationFailureHandler() {
}
public SimpleUrlAuthenticationFailureHandler(String defaultFailureUrl) {
this.setDefaultFailureUrl(defaultFailureUrl);
}
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
if (this.defaultFailureUrl == null) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Sending 401 Unauthorized error since no failure URL is set");
} else {
this.logger.debug("Sending 401 Unauthorized error");
}
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
} else {
this.saveException(request, exception);
if (this.forwardToDestination) {
this.logger.debug("Forwarding to " + this.defaultFailureUrl);
request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);
} else {
this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);
}
}
}
protected final void saveException(HttpServletRequest request, AuthenticationException exception) {
if (this.forwardToDestination) {
request.setAttribute("SPRING_SECURITY_LAST_EXCEPTION", exception);
} else {
HttpSession session = request.getSession(false);
if (session != null || this.allowSessionCreation) {
request.getSession().setAttribute("SPRING_SECURITY_LAST_EXCEPTION", exception);
}
}
}
public void setDefaultFailureUrl(String defaultFailureUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultFailureUrl), () -> {
return "'" + defaultFailureUrl + "' is not a valid redirect URL";
});
this.defaultFailureUrl = defaultFailureUrl;
}
protected boolean isUseForward() {
return this.forwardToDestination;
}
public void setUseForward(boolean forwardToDestination) {
this.forwardToDestination = forwardToDestination;
}
public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
this.redirectStrategy = redirectStrategy;
}
protected RedirectStrategy getRedirectStrategy() {
return this.redirectStrategy;
}
protected boolean isAllowSessionCreation() {
return this.allowSessionCreation;
}
public void setAllowSessionCreation(boolean allowSessionCreation) {
this.allowSessionCreation = allowSessionCreation;
}
}
? SimpleUrlAuthenticationFailureHandler提供了無參和有參構造器。使用有參構造器構造SimpleUrlAuthenticationFailureHandler對象時,可傳入defaultFailureUrl。defaultFailureUrl也就是登陸失敗時要跳轉的地址。
? onAuthenticationFailure()方法中,首先會判斷defaultFailureUrl是否為null,如果為null則直接通過response返回401狀態(tài)碼Unauthorized。
? 如果defaultFailureUrl不為空,則調用saveException()方。在saveException()方法中,如果forwardToDestination屬性為true,表示此時需要通過服務器端跳轉回登錄首頁,此時就將異常信息放到request中。在回到onAuthenticationFailure()方法中,如果forwardToDestination屬性為true,就通過服務器端跳轉回到登錄頁面,否則通過重定向回到登陸頁面。
? 如果是前后端分離開發(fā),登陸失敗時就不需要進行頁面跳轉了,只需要返回登錄失敗的JSON響應給前端即可。這種場景下通過AuthenticationFailureHandler的實現類來完成,實現代碼如下:
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
Map<String, Object> resultMap = new HashMap<>(4);
resultMap.put("code", 500);
resultMap.put("msg", exception.getMessage());
resultMap.put("data", null);
String jsonResult = JSON.toJSONString(resultMap);
response.getWriter().write(jsonResult);
response.getWriter().close();
}
}
? 在Spring Security配置類中配置自定義登陸失敗處理器:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.antMatchers("/loginFail")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/router/loginTemplate")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureHandler(customizeFailureHandler())
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
protected CustomizeAuthenticationFailureHandler customizeFailureHandler() {
return new CustomizeAuthenticationFailureHandler();
}
}
? 配置完成后,當用戶再次登陸失敗,就不會進行頁面跳轉了,而是直接返回JSON字符串。
8.注銷登錄
? Spring Security中提供了默認的注銷頁面,開發(fā)者也可以根據自己的需求對注銷登錄進行定制。
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.antMatchers("/loginFail")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/router/loginTemplate")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureHandler(customizeFailureHandler())
.usernameParameter("username")
.passwordParameter("password")
.and()
.logout()
.logoutUrl("/logout")
.invalidateHttpSession(true)
.clearAuthentication(true)
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
protected CustomizeAuthenticationFailureHandler customizeFailureHandler() {
return new CustomizeAuthenticationFailureHandler();
}
}
- logout():表示開啟注銷登錄配置。
- logoutUrl():表示指定了注銷登錄請求地址,默認是GET請求,路徑為.logout。
- invalidateHttpSession():表示是否使session失效,默認為true。
- clearAuthentication():表示是否清除認真信息,默認為true。
- logoutSuccessUrl():表示注銷登錄后的跳轉地址。
? 配置完成后,再次啟動工程,登陸成功后,在瀏覽器中輸入http://127.0.0.1:9096/logout就可以發(fā)起注銷登錄請求了。注銷成功后,會自動跳轉到loginTemplate.html頁面。
? 開發(fā)者也可以配置多個注銷登錄的請求,同時還可以指定請求的方法:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.antMatchers("/loginFail")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/router/loginTemplate")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureHandler(customizeFailureHandler())
.usernameParameter("username")
.passwordParameter("password")
.and()
.logout()
.logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/logout1", HttpMethod.GET.name()),
new AntPathRequestMatcher("/logout2", HttpMethod.POST.name())
))
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/router/loginTemplate")
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
protected CustomizeAuthenticationFailureHandler customizeFailureHandler() {
return new CustomizeAuthenticationFailureHandler();
}
}
? 在logoutRequestMatcher()的配置表示注銷請求路徑,分別有兩個:
- 第一個是
/logout1,請求方法是GET。 - 第二個是
/logout2,請求方法是POST
? 使用其中的任意一個請求都可以完成登陸注銷。
? 如果項目采用的是前后端分離架構,注銷成功后就需要頁面跳轉了。只需要將注銷成功的信息返回給前端即可,此時可以自定義返回內容:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.antMatchers("/loginFail")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/router/loginTemplate")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureHandler(customizeFailureHandler())
.usernameParameter("username")
.passwordParameter("password")
.and()
.logout()
.logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/logout1", HttpMethod.GET.name()),
new AntPathRequestMatcher("/logout2", HttpMethod.POST.name())
))
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessHandler((request, response, auth) -> {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
Map<String, Object> resultMap = new HashMap<>(4);
resultMap.put("code", 200);
resultMap.put("msg", "logout success!");
resultMap.put("data", null);
String jsonResult = JSON.toJSONString(resultMap);
response.getWriter().write(jsonResult);
response.getWriter().close();
})
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
protected CustomizeAuthenticationFailureHandler customizeFailureHandler() {
return new CustomizeAuthenticationFailureHandler();
}
}
? 配置logoutSuccessHandler()和logoutSuccessUrl()類似于之前的successHandler和defaultSuccessUrl之間的關系,只是類不同。
? 配置完成之后,重啟項目,登陸成功后再注銷登錄。無論是/logout1還是/logout2進行注銷,只要注銷成功,就會返回JSON。
? 如果開發(fā)者希望為不同的注銷地址返回不同的結果,如下配置即可:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.antMatchers("/loginFail")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/router/loginTemplate")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureHandler(customizeFailureHandler())
.usernameParameter("username")
.passwordParameter("password")
.and()
.logout()
.logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/logout1", HttpMethod.GET.name()),
new AntPathRequestMatcher("/logout2", HttpMethod.POST.name())
))
.invalidateHttpSession(true)
.clearAuthentication(true)
.defaultLogoutSuccessHandlerFor((request, response, auth) -> {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
Map<String, Object> resultMap = new HashMap<>(4);
resultMap.put("code", 200);
resultMap.put("msg", "logout1 success!");
resultMap.put("data", null);
String jsonResult = JSON.toJSONString(resultMap);
response.getWriter().write(jsonResult);
response.getWriter().close();
}, new AntPathRequestMatcher("/logout1", HttpMethod.GET.name()))
.defaultLogoutSuccessHandlerFor((request, response, auth) -> {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
Map<String, Object> resultMap = new HashMap<>(4);
resultMap.put("code", 200);
resultMap.put("msg", "logout2 success!");
resultMap.put("data", null);
String jsonResult = JSON.toJSONString(resultMap);
response.getWriter().write(jsonResult);
response.getWriter().close();
}, new AntPathRequestMatcher("/logout2", HttpMethod.POST.name()))
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
protected CustomizeAuthenticationFailureHandler customizeFailureHandler() {
return new CustomizeAuthenticationFailureHandler();
}
}
? 通過defaultLogoutSuccessHandlerFor()方法可以注冊多個不同的注銷成功回調函數,該方法第一個參數是注銷成功回調,第二個參數則是具體的注銷登錄請求。當用戶注銷成功之后,請求哪個注銷地址,就會返回對應的響應信息。
到此這篇關于Spring Security登錄表單配置的文章就介紹到這了,更多相關Spring Security登錄表單內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Springboot如何實現Web系統(tǒng)License授權認證
這篇文章主要介紹了Springboot如何實現Web系統(tǒng)License授權認證,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-05-05
Springboot使用@RefreshScope注解實現配置文件的動態(tài)加載
本文主要介紹了Springboot使用@RefreshScope注解實現配置文件的動態(tài)加載,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-09-09
SpringBoot?Redis?發(fā)布訂閱模式(Pub/Sub)的具體使用
本文主要介紹了SpringBoot Redis 發(fā)布訂閱模式(Pub/Sub)的具體使用,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12

