Spring?Security登錄表單配置示例詳解
Spring Security登錄表單配置
1.引入pom依賴
? 創(chuàng)建一個(gè)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)建完成之后,為了方便測(cè)試,需要在bootstrap.yml
配置文件中添加如下配置,將登錄用戶名和密碼固定下來:
spring: security: user: name: kapcb password: 123456
3.創(chuàng)建login.html
? 在resources/static
目錄下創(chuàng)建login.html
頁(yè)面,這個(gè)就是自定義登錄頁(yè)面:
<!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>
? 這個(gè)login.html
核心內(nèi)容就是一個(gè)登陸表單,登陸表單中有三個(gè)需要注意的地方:
form
標(biāo)簽中的action
熟悉,這里是/loginSystem
,表示表單要提交請(qǐng)求到/loginSystem
接口上。- 用戶名框的
name
屬性值為username
。 - 密碼框的
name
屬性為password
。
4.創(chuàng)建配置類
? 定義好login.html
之后,接下來就定義兩個(gè)測(cè)試接口,作為受保護(hù)資源。當(dāng)用戶登陸成功后,就可以訪問受保護(hù)的資源。接口定義如下:
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
版本中已經(jīng)廢棄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()方法表示開啟權(quán)限配置。
- anyRequest().authenticated():表示所有請(qǐng)求都需要經(jīng)過認(rèn)證。
- and():方法會(huì)返回httpSecurityBuilder對(duì)象的一個(gè)子類,實(shí)際上就是HttpSecurity。所以and()方法相當(dāng)于返回一個(gè)HttpSecurity實(shí)例,重新開啟新一輪配置。
- formLogin():表示開啟表單登陸配置。
- loginPage("/login.html"):用于配置默認(rèn)登錄頁(yè)的請(qǐng)求地址。
- loginProcessingUrl("/loginSystem"):用于配置登錄請(qǐng)求接口地址。
- defaultSuccessUrl("/index"):表示登錄請(qǐng)求處理成功后的跳轉(zhuǎn)地址。
- failureUrl("/login.html"):表示登陸失敗跳轉(zhuǎn)的地址。
- usernameParameter("username"):表示登陸用戶名的參數(shù)名稱。
- passwordParameter("password"):表示登錄密碼的參數(shù)名稱。
- permitAll():表示跟登錄相關(guān)的頁(yè)面和接口不做攔截,直接允許訪問。
- csrf().disable():表示禁用CSRF防御功能。Spring Security自帶了CSRF防御機(jī)制。為了測(cè)試方便,這里先關(guān)閉了。
? 需要注意的是,上面的loginPage()
、loginProcessingUrl()
、defaultSuccessUrl()
、usernameParameter()
、passwordParameter()
需要和之前創(chuàng)建的login.html
中登錄表單的配置一致。
? 完成上述配置之后,啟動(dòng)工程,訪問http://localhost:9096/index
,會(huì)自動(dòng)跳轉(zhuǎn)到http://localhost:9096/login.html
頁(yè)面。輸入之前設(shè)置好的用戶名密碼,登陸成功之后,就可以訪問到/index
頁(yè)面了。
5.配置細(xì)節(jié)
? 在前面的配置中,使用defaultSuccessUrl()
方法配置了用戶登陸成功之后的跳轉(zhuǎn)地址。使用failureUrl()
方法配置了用戶登錄失敗之后的跳轉(zhuǎn)地址。關(guān)于用戶登錄成功與登陸失敗,除了這兩個(gè)方法可以進(jìn)行配置之外,還有另外兩個(gè)方法也可配置。
6.登陸成功
? 當(dāng)用戶登錄成功之后,除了使用defaultSuccessUrl()
方法可以實(shí)現(xiàn)登錄成功后的跳轉(zhuǎn)之外,successForwardUrl()
方法也可以配置實(shí)現(xiàn)登錄成功之后的跳轉(zhuǎn),配置代碼如下:
@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()方法表示當(dāng)用戶登陸成功之后,會(huì)自動(dòng)重定向到登錄之前用戶訪問的地址上。如果用戶本身就是直接訪問的登陸頁(yè)面,則登錄成功之后就會(huì)重定向到defaultSuccessUrl()指定的頁(yè)面。
- successForwardUrl()方法則不會(huì)考慮用戶登錄之前訪問的地址,只要用戶登陸成功,就會(huì)通過服務(wù)器端跳轉(zhuǎn)到successForwardUrl()所指定的頁(yè)面。
- defaultSuccessUrl()方法有一個(gè)重載方法,如果重載方法的第二個(gè)參數(shù)傳入true,則defaultSuccessUrl()方法的效果與successForwardUrl()方法類似,即不考慮用戶之前的訪問地址,只要登陸成功,就重定向到defaultSuccessUrl()所指定的頁(yè)面。不同之處在于,defaultSuccessUrl()方法是通過重定向?qū)崿F(xiàn)的客戶端跳轉(zhuǎn),successForwardUrl()則是通過服務(wù)端跳轉(zhuǎn)實(shí)現(xiàn)的。
? 無論是defaultSuccessUrl()
還是successForwardUrl()
方法配置登錄成功跳轉(zhuǎn)頁(yè)面,最終所配置的都是AuthenticationSuccessHandler
接口的實(shí)例。
? 在Spring Security
中專門提供了AuthenticationSuccessHandler
接口來處理用戶登陸成功事項(xiàng),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
接口中定義了兩個(gè)方法,其中一個(gè)是default
方法,此方法是Spring Security 5.2
版本中添加進(jìn)來的,在處理特定的認(rèn)證請(qǐng)求Authentication Filter
中會(huì)用到。另外一個(gè)非default
方法,則用來處理登陸成功的具體事項(xiàng),其中Authentication
參數(shù)保存了登陸成功的用戶信息。
? AuthenticationSuccessHandler
接口在Spring Security
源碼中一共有三個(gè)實(shí)現(xiàn)類:
- SimpleUrlAuthenticationSuccessHandler繼承自AbstractAuthenticationTargetUrlRequestHandler。通過AbstractAuthenticationTargetUrlRequestHandler中的handler()方法實(shí)現(xiàn)請(qǐng)求重定向。
- SavedRequestAwareAuthenticationSuccessHandler在SimpleUrlAuthenticationSuccessHandler的基礎(chǔ)上增加了請(qǐng)求緩存的功能,可以記錄用戶登陸之前請(qǐng)求的地址,進(jìn)而在登陸成功之后重定向到一開始訪問的地址。
- ForwardAuthenticationSuccessHandler的實(shí)現(xiàn)比較簡(jiǎn)單,就是一個(gè)服務(wù)端跳轉(zhuǎn)。
? 通過defaultSuccessUrl()
方法來配置登陸成功之后重定向的請(qǐng)求地址時(shí),實(shí)際上對(duì)應(yīng)的實(shí)現(xiàn)類就是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中獲取緩存下來的請(qǐng)求,如果沒有獲取到緩存的請(qǐng)求地址,就代表用戶在登錄之前并沒有訪問其它頁(yè)面。此時(shí)直接調(diào)用父類的onAuthenticationSuccess()方法來處理,最終重定向到defaultSuccessUrl()方法指定的地址。
- 接下里會(huì)獲取一個(gè)targetUrlParameter,這個(gè)地址是用戶顯示指定的、希望登錄成功之后重定向的地址。例如用戶發(fā)送的登錄請(qǐng)求是http://127.0.0.1:9096/loginSystem?target=/hello,這就表示當(dāng)用戶登陸成功之后,希望主動(dòng)重定向到/hello接口。targetUrlParameter()方法就是獲取重定向地址參數(shù)的key,就是上面舉例中的target。獲取到target之后就可以獲取到重定向的地址了。
- 如果targetUrlParameter存在,或者開發(fā)者設(shè)置了isAlwaysUseDefaultTargetUrl()為true,此時(shí)緩存下來的請(qǐng)求就失去了意義。此時(shí)會(huì)直接移除掉緩存的請(qǐng)求地址,直接調(diào)用父類的onAuthenticationSuccess()方法完成重定向。targetUrlParameter存在,則直重定向到targetUrlParameter指定的地址。isAlwaysUseDefaultTargetUrl為true,則直接重定向到defaultSuccessUrl指定的地址。如果targetUrlParameter存在并且isAlwaysUseDefaultTargetUrl為true,則直接重定向到defaultSuccessUrl指定的地址。
- 如果上述條件均不滿足,那么最終會(huì)從緩存請(qǐng)求SavedRequest對(duì)象中獲取重定向地址,然后進(jìn)行重定向操作。
? 這就是SavedRequestAwareAuthenticationSuccessHandler
的實(shí)現(xiàn)邏輯,開發(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
,這樣用戶就可以在登錄請(qǐng)求中,通過target
來指定跳轉(zhuǎn)地址。修改一下上面的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
。當(dāng)用戶登陸成功之后,就始終會(huì)跳轉(zhuǎn)到/hello
接口了。
? 在使用successForwardUrl()
方法設(shè)置登陸成功后重定向的地址時(shí),實(shí)際上對(duì)應(yīng)的實(shí)現(xiàn)類是ForwardAuthenticationSuccessHandler
,ForwardAuthenticationSuccessHandler
類的源碼非常簡(jiǎn)單,就是一個(gè)服務(wù)端轉(zhuǎn)發(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); } }
? 主要功能就是調(diào)用request.getRequestDispatcher(this.forwardUrl).forward(request, response)
方法實(shí)現(xiàn)服務(wù)端請(qǐng)求轉(zhuǎn)發(fā)。
? 啟動(dòng)服務(wù)之后訪問登錄頁(yè)面,登錄成功后即可自動(dòng)重定向到/hello
接口。
? AuthenticationSuccessHandler
默認(rèn)的三個(gè)實(shí)現(xiàn)類,無論是哪一種,都是用來處理頁(yè)面跳轉(zhuǎn)的。隨著前后端分離架構(gòu)的盛行,頁(yè)面跳轉(zhuǎn)并不能滿足我們的業(yè)務(wù)需求,用戶登錄成功后,后端返回JSON
數(shù)據(jù)給前端即可。告訴前端當(dāng)前用戶是登陸成功還是失敗即可,前端拿到后端響應(yīng)結(jié)果后自行處理即可。像這種需求,可以使用自定義AuthenticationSuccessHandler
的實(shí)現(xiàn)類完成。
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
的實(shí)現(xiàn)類來完成登錄成功之后的處理邏輯之后,還需要同步修改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
。因?yàn)榇藭r(shí)使用的是自定義的AuthenticationSuccessHandler
。邏輯與SavedRequestAwareAuthenticationSuccessHandler
是不一樣的。
? 所有的改動(dòng)完成之后,重啟工程。當(dāng)用戶登錄成功之后,就不會(huì)在進(jìn)行頁(yè)面跳轉(zhuǎn)了,而是返回了一段JSON
字符串到頁(yè)面上。
7.登陸失敗
? Spring Security
登陸失敗的處理邏輯。為了方便在前端頁(yè)面展示登錄失敗的異常信息,首先在項(xiàng)目的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)頁(yè)面,loginTemplate.html
是template
模板頁(yè)面。loginTemplate.html
在form
標(biāo)簽內(nèi)新增了<div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
標(biāo)簽,用于展示Spring Security
在處理登陸失敗時(shí)的異常信息。在Spring Security
中,用戶登陸失敗時(shí),異常信息會(huì)放在request
中返回給前端,開發(fā)者可將其直接提取出來展示。
? loginTemplate.html
屬于動(dòng)態(tài)頁(yè)面,所以就不能像訪問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
配置類中配置登陸頁(yè)面:
@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
接口請(qǐng)求地址,也就是loginTemplate.html
頁(yè)面。重定向是一種客戶端跳轉(zhuǎn),重定向不方便攜帶請(qǐng)求失敗的異常信息,只能放在URL
中。
? 如果希望在前端展示請(qǐng)求失敗的異常信息,可以使用下面這種方式:
@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()
方法從名字上就可以看出,這種跳轉(zhuǎn)是一種服務(wù)器端跳轉(zhuǎn)。服務(wù)器端跳轉(zhuǎn)的好處是可以攜帶登錄異常信息。如果用戶登陸失敗,自動(dòng)跳轉(zhuǎn)回登錄頁(yè)面后,就可以將錯(cuò)誤信息展示出來。
? 無論是failureUrl()
還是failureForwardUrl()
方法,最終所配置的都是AuthenticationFailureHandler
接口的實(shí)現(xiàn)類。Spring Security
中提供了AuthenticationFailureHandler
接口,為登陸失敗下的處理方式提供頂級(jí)拓展。AuthenticationFailureHandler
源碼如下:
public interface AuthenticationFailureHandler { void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException; }
? AuthenticationFailureHandler
接口中只定義了一個(gè)onAuthenticationFailure()
方法,用于處理登陸失敗的請(qǐng)求。AuthenticationException
表示登陸失敗的異常信息。Spring Security
中為AuthenticationFailureHandler
一共提供了五個(gè)實(shí)現(xiàn)類:
- SimpleUrlAuthenticationFailureHandler:默認(rèn)的處理邏輯就是通過重定向跳轉(zhuǎn)到登陸頁(yè)面,當(dāng)然也可以通過配置forwardToDestination屬性將重定向改為服務(wù)器端跳轉(zhuǎn),failureUrl()方法底層實(shí)現(xiàn)邏輯就是SimpleUrlAuthenticationFailureHandler。
- ExceptionMappingAuthenticationFailureHandler:可以實(shí)現(xiàn)根據(jù)不同異常類型,映射到不同路徑。
- ForwardAuthenticationFailureHandler:表示通過服務(wù)器端跳轉(zhuǎn)來重新回到登陸頁(yè)面,failureForwardUrl()方法底層實(shí)現(xiàn)邏輯就是ForwardAuthenticationFailureHandler。
- AuthenticationEntryPointFailureHandler:是Spring Security 5.2新引入的處理類,可以通過AuthenticationEntryPoint來處理登陸異常。
- DelegatingAuthenticationFailureHandler:可以實(shí)現(xiàn)為不同的異常類型配置不同的登錄失敗處理回調(diào)。
? 舉個(gè)簡(jiǎn)單的例子。假設(shè)不使用failureForwardUrl()
方法進(jìn)行登陸失敗的處理邏輯配置,同時(shí)又想在登陸失敗后通過服務(wù)器端跳轉(zhuǎn)回到登陸頁(yè)面,那么可以自定義SimpleUrlAuthenticationFailureHandler
配置,并將forwardToDestination
屬性設(shè)置為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; } }
? 這樣配置之后,用戶再次登陸失敗,就會(huì)通過服務(wù)端跳轉(zhuǎn)重新回到登陸頁(yè)面,同時(shí)頁(yè)面上也會(huì)展示相應(yīng)的錯(cuò)誤信息,效果和failureForwardUrl()
一樣。
? SimpleUrlAuthenticationFailureHandler
的源碼也很簡(jiǎn)單:
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
提供了無參和有參構(gòu)造器。使用有參構(gòu)造器構(gòu)造SimpleUrlAuthenticationFailureHandler
對(duì)象時(shí),可傳入defaultFailureUrl
。defaultFailureUrl
也就是登陸失敗時(shí)要跳轉(zhuǎn)的地址。
? onAuthenticationFailure()
方法中,首先會(huì)判斷defaultFailureUrl
是否為null
,如果為null
則直接通過response
返回401
狀態(tài)碼Unauthorized
。
? 如果defaultFailureUrl
不為空,則調(diào)用saveException()
方。在saveException()
方法中,如果forwardToDestination
屬性為true
,表示此時(shí)需要通過服務(wù)器端跳轉(zhuǎn)回登錄首頁(yè),此時(shí)就將異常信息放到request
中。在回到onAuthenticationFailure()
方法中,如果forwardToDestination
屬性為true
,就通過服務(wù)器端跳轉(zhuǎn)回到登錄頁(yè)面,否則通過重定向回到登陸頁(yè)面。
? 如果是前后端分離開發(fā),登陸失敗時(shí)就不需要進(jìn)行頁(yè)面跳轉(zhuǎn)了,只需要返回登錄失敗的JSON
響應(yīng)給前端即可。這種場(chǎng)景下通過AuthenticationFailureHandler
的實(shí)現(xiàn)類來完成,實(shí)現(xiàn)代碼如下:
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(); } }
? 配置完成后,當(dāng)用戶再次登陸失敗,就不會(huì)進(jìn)行頁(yè)面跳轉(zhuǎn)了,而是直接返回JSON
字符串。
8.注銷登錄
? Spring Security
中提供了默認(rèn)的注銷頁(yè)面,開發(fā)者也可以根據(jù)自己的需求對(duì)注銷登錄進(jìn)行定制。
@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():表示指定了注銷登錄請(qǐng)求地址,默認(rèn)是GET請(qǐng)求,路徑為.logout。
- invalidateHttpSession():表示是否使session失效,默認(rèn)為true。
- clearAuthentication():表示是否清除認(rèn)真信息,默認(rèn)為true。
- logoutSuccessUrl():表示注銷登錄后的跳轉(zhuǎn)地址。
? 配置完成后,再次啟動(dòng)工程,登陸成功后,在瀏覽器中輸入http://127.0.0.1:9096/logout
就可以發(fā)起注銷登錄請(qǐng)求了。注銷成功后,會(huì)自動(dòng)跳轉(zhuǎn)到loginTemplate.html
頁(yè)面。
? 開發(fā)者也可以配置多個(gè)注銷登錄的請(qǐng)求,同時(shí)還可以指定請(qǐng)求的方法:
@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()
的配置表示注銷請(qǐng)求路徑,分別有兩個(gè):
- 第一個(gè)是
/logout1
,請(qǐng)求方法是GET
。 - 第二個(gè)是
/logout2
,請(qǐng)求方法是POST
? 使用其中的任意一個(gè)請(qǐng)求都可以完成登陸注銷。
? 如果項(xiàng)目采用的是前后端分離架構(gòu),注銷成功后就需要頁(yè)面跳轉(zhuǎn)了。只需要將注銷成功的信息返回給前端即可,此時(shí)可以自定義返回內(nèi)容:
@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
之間的關(guān)系,只是類不同。
? 配置完成之后,重啟項(xiàng)目,登陸成功后再注銷登錄。無論是/logout1
還是/logout2
進(jìn)行注銷,只要注銷成功,就會(huì)返回JSON
。
? 如果開發(fā)者希望為不同的注銷地址返回不同的結(jié)果,如下配置即可:
@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()
方法可以注冊(cè)多個(gè)不同的注銷成功回調(diào)函數(shù),該方法第一個(gè)參數(shù)是注銷成功回調(diào),第二個(gè)參數(shù)則是具體的注銷登錄請(qǐng)求。當(dāng)用戶注銷成功之后,請(qǐng)求哪個(gè)注銷地址,就會(huì)返回對(duì)應(yīng)的響應(yīng)信息。
到此這篇關(guān)于Spring Security登錄表單配置的文章就介紹到這了,更多相關(guān)Spring Security登錄表單內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringSecurity表單配置之登錄成功及頁(yè)面跳轉(zhuǎn)原理解析
- SpringSecurity?表單登錄的實(shí)現(xiàn)
- SpringBoot基于SpringSecurity表單登錄和權(quán)限驗(yàn)證的示例
- SpringSecurity 自定義表單登錄的實(shí)現(xiàn)
- SpringSecurity 默認(rèn)表單登錄頁(yè)展示流程源碼
- Spring Security 表單登錄功能的實(shí)現(xiàn)方法
- Spring Security在標(biāo)準(zhǔn)登錄表單中添加一個(gè)額外的字段
- 最新Spring?Security實(shí)戰(zhàn)教程之表單登錄定制到處理邏輯的深度改造(最新推薦)
相關(guān)文章
SpringMVC+EasyUI實(shí)現(xiàn)頁(yè)面左側(cè)導(dǎo)航菜單功能
這篇文章主要介紹了SpringMVC+EasyUI實(shí)現(xiàn)頁(yè)面左側(cè)導(dǎo)航菜單功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09Mybatis傳遞多個(gè)參數(shù)的解決辦法(三種)
這篇文章主要介紹了Mybatis傳遞多個(gè)參數(shù)的解決辦法(三種),個(gè)人覺得第三種解決辦法比較好用,有需要的朋友一起學(xué)習(xí)吧2016-05-05ShardingSphere JDBC強(qiáng)制路由使用的項(xiàng)目實(shí)踐
在某些特定場(chǎng)景下,可能需要繞過分片規(guī)則直接定位到特定的數(shù)據(jù)庫(kù)或表,這種情況下就可以使用HintRouting,本文就來介紹一下ShardingSphere JDBC強(qiáng)制路由使用的項(xiàng)目實(shí)踐,感興趣的可以了解一下2024-06-06Java中的Semaphore信號(hào)量簡(jiǎn)析
這篇文章主要介紹了Java中的Semaphore信號(hào)量簡(jiǎn)析,Semaphore:信號(hào)量,用來限制能同時(shí)訪問共享資源的線程上限,使用Semaphore實(shí)現(xiàn)簡(jiǎn)單連接池,對(duì)比享元模式下的實(shí)現(xiàn)(用wait和notify),性能和可讀性要更好,需要的朋友可以參考下2023-12-12Java基于Base64實(shí)現(xiàn)編碼解碼圖片文件
這篇文章主要介紹了Java基于Base64實(shí)現(xiàn)編碼解碼圖片文件,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03詳解Spring-boot中讀取config配置文件的兩種方式
這篇文章主要介紹了詳解Spring-boot中讀取config配置文件的兩種方式,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10寧可用Lombok也不把成員設(shè)置為public原理解析
這篇文章主要為大家介紹了寧可用Lombok也不把成員設(shè)置為public原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03