Spring Security登錄表單配置(3)

来源:https://www.cnblogs.com/liwenruo/archive/2022/08/02/16543929.html
-Advertisement-
Play Games

1. 登錄表單配置 1.1 快速入門 理解了入門案例之後,接下來我們再來看一下登錄表單的詳細配置,首先創建一個新的Spring Boot項目,引入Web和Spring Security依賴,代碼如下: <dependency> <groupId>org.springframework.boot</g ...


1. 登錄表單配置

1.1 快速入門

  理解了入門案例之後,接下來我們再來看一下登錄表單的詳細配置,首先創建一個新的Spring Boot項目,引入Web和Spring Security依賴,代碼如下:

<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>

  項目創建好之後,為了方便測試,需要在application.yml中添加如下配置,將登錄用戶名和密碼固定下來:

spring:
  security:
    user:
      name: buretuzi
      password: 123456

  接下來,我們在resources/static目錄下創建一個login.html頁而,這個是我們自定義的登錄頁面:

查看代碼
 <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登錄</title>
    <link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<style>
    #login .container #login-row #login-column #login-box {
        border: 1px solid #9c9c9c;
        background-color: #EAEAEA;
    }
</style>
<body>
    <div id="login">
        <div class="container">
            <div id="login-row" class="row justify-content-center align-items-center">
                <div id="login-column" class="col-md-6">
                    <div id="login-box" class="col-md-12">
                        <form id="login-form" class="form" action="/dologin" method="post">
                            <h3 class="text-center text-info">登錄</h3>
                            <div class="form-group">
                                <label for="username" class="text-info">用戶名:</label><br>
                                <input type="text" name="uname" id="username" class="form-control">
                            </div>
                            <div class="form-group">
                                <label for="password" class="text-info">密碼:</label><br>
                                <input type="text" name="passwd" id="password" class="form-control">
                            </div>
                            <div class="form-group">
                                <input type="submit" name="submit" class="btn btn-info btn-md" value="登錄">
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

  這個logmt.html中的核心內容就是一個登錄表單,登錄表單中有三個需要註意的地方,

  • formaction,這裡給的是/doLogin,表示表單要提交到/doLogin介面上。
  • 用戶名輸入框的name屬性值為uname,當然這個值是可以自定義的,這裡採用了uname
  • 密碼輸入框的name屬性值為passwd, passwd也是可以自定義的。

login.html定義好之後,接下來定義兩個測試介面,作為受保護的資源。當用戶登錄成功 後,就可以訪問到受保護的資源。介面定義如下:

package com.intehel.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LoginController {
    @RequestMapping("/index")
    public String index(){
        return "login";
    }
    @RequestMapping("/hello")
    public String hello(){
        return "hello";
    }
}

  最後再提供一個Spring Security的配置類:

package com.intehel.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/loginNew.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index")
                .failureUrl("/loginNew.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
}

  在Spring Security中,如果我們需要自定義配置,基本上都是繼承自WebSecurityConfigurerAdapter來實現的,當然WebSecurityConfigurerAdapter本身的配置還是比較複雜,同時也是比較豐富的,這裡先不做過多的展開,僅就結合上面的代碼來解釋,在下節中將會對這裡的配置再做更加詳細的介紹。

  • 首先configure方法中是一個鏈式配置,當然也可以不用鏈式配置,每一個屬性配置完畢後再從重新開始寫起
  • authorizeRequests()方法表示開啟許可權配置(該方法的含義其實比較複雜,在後面還會再次介紹該方法),.anyRequest().authenticated()表示所有的請求都要認證之後才能訪問.
  • 有的讀者會對and()方法表示疑惑,and()方法會返回HttpSecurityBuilder對象的一個 子類(實際上就是HttpSecurity),所以and()方法相當於又回到HttpSecurity實例,重新開啟 新一輪的配置。如果覺得and()方法很難理解,也可以不用and()方法, 在.anyRequest().authenticated。配置完成後直接用分號(;)結束,然後通過http.formLogin()繼續配置表單登錄。
  • formLogin()表示開啟表單登錄配置,loginPage用來配置登錄頁面地址; loginProcessingUrl用來配置登錄介面地址;defaultSuccessUrl表示登錄成功後的跳轉地址; failureUrl表示登錄失敗後的跳轉地址;usernameParameter表示登錄用戶名的參數名稱; passwordParameter表示登錄密碼的參數名稱;permitAll表示跟登錄相關的頁面和介面不做攔截, 直接通過。需要註意的是,loginProcessingUrl、usernameParameter、passwordParameter 需要和login-html中登錄表單的配置一致。
  • 最後的csrf().disable()表示禁用CSRF防禦功能,Spring Security自帶了 CSRF防禦機制,但是我們這裡為了測試方便,先將CSRF防禦機制關閉,在後面將會詳細介紹CSRF攻擊與防禦問題。

  配置完成後,啟動Spring Boot項目,瀏覽器地址欄中輸入http://localhost:8080/index,會自動跳轉到http://localhost:8080/loginNew.html頁面,如圖2-5所示。輸入用戶名和密碼進行登錄(用 戶名為buretuzi,密碼為123456),登錄成功之後,就可以訪問到index頁面了,如圖2-6所示。

  

圖2-5

圖2-6

  經過上面的配置,我們已經成功自定義了一個登錄頁面出來,用戶在登錄成功之後,就可以訪問受保護的資源了。

1.2 配置細節

  當然,前面的配置比較粗糙,這裡還有一些配置的細節需要和讀者分享一下。

  在前面的配置中,我們用defaultSuccessUrl表示用戶登錄成功後的跳轉地址,用failureUrl 表示用戶登錄失敗後的跳轉地址。關於登錄成功和登錄失敗,除了這兩個方法可以配置之外, 還有另外兩個方法也可以配置。

  1.2.1 登錄成功

  當用戶登錄成功之後,除了 defaultSuccessUrl方法可以實現登錄成功後的跳轉之外, successForwardUrl也可以實現登錄成功後的跳轉,代碼如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/loginNew.html")
                .loginProcessingUrl("/doLogin")
                .successForwardUrl("/index")
                .failureUrl("/loginNew.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
}

defaultSuccessUrl 和 successForwardUrl 的區別如下:

  • defaultSuccessUrl表示當用戶登錄成功之後,會自動重定向到登錄之前的地址上, 如果用戶本身就是直接訪問的登錄頁面,則登錄成功後就會重定向到defaultSuccessUrl指定的頁面中。例如,用戶在未認證的情況下,訪問了/hello頁面,此時會自動重定向到登錄頁面, 當用戶登錄成功後,就會自動重定向到/hello頁面;而用戶如果一開始就訪問登錄頁面,則登錄成功後就會自動重定向到defaultSuccessUrl所指定的頁面中,
  • successForwardUrl則不會考慮用戶之前的訪問地址,只要用戶登錄成功,就會通過伺服器端跳轉到successForwardUrl所指定的頁面
  • defaultSuccessUrl有一個重載方法,如果重載方法的第二個參數傳入true,則 defaultSuccessUrl的效果與successForwardUrl類似,即不考慮用戶之前的訪問地址,只要登錄成功,就重定向到defaultSuccessUrl所指定的頁面。不同之處在於,defaultSuccessUrl是通過重定向實現的跳轉(客戶端跳轉),successForwardUrl則是通過伺服器端跳轉實現的。

無論是 defaultSuccessUrl 還是 successForwardUrl,最終所配置的都是 AuthenticationSuccessHandler介面的實例。

Spring Security中專門提供了 AuthenticationSuccessHandler介面用來處理登錄成功事項:

public interface AuthenticationSuccessHandler {
	default void onAuthenticationSuccess(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authentication)
			throws IOException, ServletException{
		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開始加入進來的,在處理特定的認證請求 AuthenticationFilter中會用到;另外一個非default方法,則用來處理登錄成功的具體事項,其 中request和response參數好理解,authentication參數保存了登錄成功的用戶信息。我們將在後面的章節中詳細介紹authentication參數。

  AuthenticationSuccessHandler介面共有三個實現類,如圖2-7所示。

  

圖2-7

(1) SimpleUrlAuthenticationSuccessHandler繼承自 AbstractAuthenticationTargetUrlRequestHandler,通過 AbstractAuthenticationTargetUrlRequestHandler 中的 handle 方法實現請求重定向。

 (2)SavedRequestAwareAuthenticationSuccessHandler在 SimpleUrlAuthenticationSuccessHandler的基礎上增加了請求緩存的功能,可以記錄之前請求的地址,進而在登錄成功後重定向到一開始訪問的地址。

 (3) ForwardAuthenticationSuccessHandler的實現則比較容易,就是一個服務端跳轉。

  我們來重點分析 SavedRequestAwareAuthenticationSuccessHandler和ForwardAuthenticationSuccessHandler的實現。

  當通過defaultSuccessUrl來設置登錄成功後重定向的地址時,實際上對應的實現類就是 SavedRequestAwareAuthenticationSuccessHandler。

public class SavedRequestAwareAuthenticationSuccessHandler extends
		SimpleUrlAuthenticationSuccessHandler {
	protected final Log logger = LogFactory.getLog(this.getClass());
	private RequestCache requestCache = new HttpSessionRequestCache();
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication authentication)
			throws ServletException, IOException {
		SavedRequest savedRequest = requestCache.getRequest(request, response);
		if (savedRequest == null) {
			super.onAuthenticationSuccess(request, response, authentication);
			return;
		}
		String targetUrlParameter = getTargetUrlParameter();
		if (isAlwaysUseDefaultTargetUrl()
				|| (targetUrlParameter != null && StringUtils.hasText(request
						.getParameter(targetUrlParameter)))) {
			requestCache.removeRequest(request, response);
			super.onAuthenticationSuccess(request, response, authentication);

			return;
		}
		clearAuthenticationAttributes(request);
		// Use the DefaultSavedRequest URL
		String targetUrl = savedRequest.getRedirectUrl();
		logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
		getRedirectStrategy().sendRedirect(request, response, targetUrl);
	}
	public void setRequestCache(RequestCache requestCache) {
		this.requestCache = requestCache;
	}
}

  

這裡的核心方法就是onAuthenticationSuccess:

  • 首先從requestcache中獲取緩存下來的請求,如果沒有獲取到緩存請求,就說明用戶在訪問登錄頁面之前並沒有訪問其他頁面,此時直接調用父類的onAuthenticationSuccess方法來處理最終會重定向到defaultSuccessUrl指定的地址。
  • 接下來會獲取一個targetUrlParameter,這個是用戶顯式指定的、希望登錄成功後重定向的地址,例如用戶發送的登錄請求是http://localhost:8080/doLogin?target=/hello,這就表示當用戶登錄成功之後,希望自動重定向到/hello這個介面,getTargetUrlParameter就是要獲取重定向地址參數的key,也就是上面的target,拿到target之後,就可以獲取到重定向地址了。
  • 如果 targetUrlParameter 存在,或者用戶設置了 alwaysUseDefaultTargetUrl 為 true, 這個時候緩存下來的請求就沒有意義了。此時會直接調用父類的onAuthenticationSuccess方法完成重定向口 targetUrlParameter存在,則直接重定向到targetUrlParameter指定的地址;alwaysUseDefaultTargetUrl 為 true,則直接重定向到 defaultSuccessUrl 指定的地址;如果 targetUrlParameter 存在並且 alwaysUseDefaultTargetUrl 為 true,則重定向到 defaultSuccessUrl 指定的地址。
  • 如果前面的條件都不滿足,那麼最終會從緩存請求savedRequest中獲取重定向地址, 然後進行重定向操作。

  這就是SavedRequestAwareAuthenticationSuccessHandler的實現邏輯,升發者也可以配置 自己的 SavedRequestAwareAuthenticationSuccessHandler,代碼如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/loginNew.html")
                .loginProcessingUrl("/doLogin")
                .successForwardUrl("/index")
                .failureUrl("/loginNew.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
    SavedRequestAwareAuthenticationSuccessHandler successHandler(){
        SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();
        handler.setDefaultTargetUrl("/index");
        handler.setTargetUrlParameter("target");
        return handler;
    }
}

  註意在配置時指定了 targetUrlParametertarget,這樣用戶就可以在登錄請求中,通過 target來指定跳轉地址了,然後我們修改一下前面login.html中的form表單:

<form id="login-form" class="form" action="/doLogin?target=/hello" method="post">
    <h3 class="text-center text-info">登錄</h3>
    <div class="form-group">
        <label for="username" class="text-info">用戶名:</label><br>
        <input type="text" name="uname" id="username" class="form-control">
    </div>
    <div class="form-group">
        <label for="password" class="text-info">密碼:</label><br>
        <input type="text" name="passwd" id="password" class="form-control">
    </div>
    <div class="form-group">
        <input type="submit" name="submit" class="btn btn-info btn-md" value="登錄">
    </div>
</form>

  在form表單中,action修改/doLogin?target=/hello,這樣當用戶登錄成功之後,就始終跳轉到/hello介面了。

  當我們通過successForwardUrl來設置登錄成功後重定向的地址時,實際上對應的實現類 就是 ForwardAuthenticationSuccessHandler,ForwardAuthenticationSuccessHandler 的源碼特別簡單,就是一個服務端轉發,代碼如下:

public class ForwardAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
	private final String forwardUrl;
	public ForwardAuthenticationSuccessHandler(String forwardUrl) {
		Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl),
				() -> "'" + forwardUrl + "' is not a valid forward URL");
		this.forwardUrl = forwardUrl;
	}
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
		request.getRequestDispatcher(forwardUrl).forward(request, response);
	}
}

  由上述代碼可以看到,主要功能就是調用getRequestDispatcher方法進行服務端轉發。 AuthenticationSuccessHandler預設的三個實現類,無論是哪一個,都是用來處理頁面跳轉的,有時候頁面跳轉並不能滿足我們的需求,特別是現在流行的前後端分離開發中,用戶登錄成功後,就不再需要頁面跳轉了,只需要給前端返回一個JSON數據即可,告訴前端登錄成功還是登錄失敗,前端收到消息之後自行處理。像這樣的需求,我們可以通過自定義 AuthenticationSuccessHandler 的實現類來完成:

package com.intehel.demo.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
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;

public class MyAuthenticationSuccessHandler 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> resp = new HashMap<String,Object>();
        resp.put("status",200);
        resp.put("msg","登錄成功");
        ObjectMapper om = new ObjectMapper();
        String s = om.writeValueAsString(resp);
        response.getWriter().write(s);
    }
}

  在自定義的 MyAuthenticationSuccessHandler中,重寫 onAuthenticationSuccess方法,在該方法中,通過HttpServletResponse對象返回一段登錄成功的JSON字元串給前端即可。最後, 在 SecurityConfig中配置自定義的 MyAuthenticationSuccessHandler,代碼如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/loginNew.html")
                .loginProcessingUrl("/doLogin")
                .successHandler(new MyAuthenticationSuccessHandler())
                .failureUrl("/loginNew.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
}

  配置完成後,重啟項目,此時,當用戶成功登錄之後,就不會進行頁面跳轉了,而是返回一段JSON字元串。

  1.2.2 登錄失敗

  接下來看登錄失敗的處理邏輯。為了方便在前端頁面展示登錄失敗的異常信息,我們首先在項目的pom.xml文件中引入thymeleaf依賴,代碼如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>2.0.7.RELEASE</version>
</dependency>

  然後在resources/templates目錄下新建mylogin.html,代碼如下:

  

查看代碼
 <!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登錄</title>
    <link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<style>
    #login .container #login-row #login-column #login-box {
        border: 1px solid #9c9c9c;
        background-color: #EAEAEA;
    }
</style>
<body>
<div id="login">
    <div class="container">
        <div id="login-row" class="row justify-content-center align-items-center">
            <div id="login-column" class="col-md-6">
                <div id="login-box" class="col-md-12">
                    <form id="login-form" class="form" action="/doLogin?target=/hello" method="post">
                        <h3 class="text-center text-info">登錄</h3>
                        <div th:text="${SPRING SECURITY LAST EXCEPTION}"></div>
                        <div class="form-group">
                            <label for="username" class="text-info">用戶名:</label><br>
                            <input type="text" name="uname" id="username" class="form-control">
                        </div>
                        <div class="form-group">
                            <label for="password" class="text-info">密碼:</label><br>
                            <input type="text" name="passwd" id="password" class="form-control">
                        </div>
                        <div class="form-group">
                            <input type="submit" name="submit" class="btn btn-info btn-md" value="登錄">
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

  mylogin.html和前面的login.html基本類似,前面的login.html是靜態頁面,這裡的 mylogin.html是thymeleaf模板頁面,mylogin.html頁面在form中多了一個div,用來展示登錄失敗時候的異常信息,登錄失敗的異常信息會放在request中返回到前端,開發者可以將其直接提取岀來展示。

  既然mylogm.html是動態頁面,就不能像靜態頁面那樣直接訪問了,需要我們給mylogin.html頁面提供一個訪問控制器:

package com.intehel.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class MyLoginController {
    @RequestMapping("/mylogin.html")
    public String myLogin(){
        return "mylogin";
    }
}

  最後再在SecurityConfig中配置登錄頁面,代碼如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureUrl("/mylogin.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
}

  failureUrl表示登錄失敗後重定向到mylogin.html頁面。重定向是一種客戶端跳轉,重定向不方便攜帶請求失敗的異常信息(只能放在URL中)。

  如果希望能夠在前端展示請求失敗的異常信息,可以使用下麵這種方式:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureForwardUrl("/mylogin.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
}

  failureForwardUrl方法從名字上就可以看出,這種跳轉是一種伺服器端跳轉,伺服器端跳轉的好處是可以攜帶登錄異常信息,如果登錄失敗,自動跳轉回登錄頁面後,就可以將錯誤信息展示出來,如圖2-8所示。

  

圖 2-8

  無論是 failureUrl 還是 failureForwardUrl,最終所配置的都是 AuthenticationFailureHandler 介面的實現。Spring Security中提供了 AuthenticationFailureHandler 介面,用來規範登錄失敗的 實現:

public interface AuthenticationFailureHandler {
	void onAuthenticationFailure(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException exception)
			throws IOException, ServletException;
}

  AuthenticationFailureHandler 介面中只有一個 onAuthenticationFailure 方法,用來處理登錄 失敗請求,request和response參數很好理解,最後的exception則表示登錄失敗的異常信息。 Spring Security 中為 AuthenticationFailureHandler 一共提供了五個實現類如圖 2-9 所示

圖2-9

  • SimpleUrlAuthenticationFailureHandler預設的處理邏輯就是通過重定向跳轉到登錄頁 面,當然也可以通過配置forwardToDestination屬性將重定向改為伺服器端跳轉,failureUrl方法的底層實現邏輯就是 SimpleUrlAuthenticationFailureHandler。
  • ExceptionMappingAuthenticationFailureHandler可以實現根據不同的異常類型,映射到不同的路徑
  • ForwardAuthenticationFailureHandler表示通過伺服器端跳轉來重新回到登錄頁面, failureForwardUrl 方法的底層實現邏輯就是 ForwardAuthenticationFailureHandler。
  • AuthenticationEntryPointFailureHandler是 Spring Security 5.2 新引進的處理類,可以 通過AuthenticationEntryPoint來處理登錄異常。
  • DelegatingAuthenticationFailureHandler可以實現為不同的異常類型配置不同的登錄失敗處理回調。

  這裡舉一個簡單的例子。假如不使用failureForwardUrl 方法,同時又想在登錄失敗後通過伺服器端跳轉回到登錄頁面,那麼可以自定義SimpleUrlAuthenticationFailureHandler配置,並將forwardToDestination屬性設置為true,代碼如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureHandler(failureHandler())
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
    SimpleUrlAuthenticationFailureHandler failureHandler(){
        SimpleUrlAuthenticationFailureHandler handler = 
                new SimpleUrlAuthenticationFailureHandler("/mylogin.html");
        handler.setUseForward(true);
        return handler;
    }
}

  這樣配置之後,如果用戶再次登錄失敗,就會通過服務端跳轉重新回到登錄頁面,登錄頁而也會展示相應的錯誤信息,效果和failureForwardUrl 一致。

SimpleUrlAuthenticationFailureHandler的源碼也很簡單,我們一起來看一下實現邏輯(源碼比較長,這裡列出來核心部分):

public class SimpleUrlAuthenticationFailureHandler implements
		AuthenticationFailureHandler {
	protected final Log logger = LogFactory.getLog(getClass());
	private String defaultFailureUrl;
	private boolean forwardToDestination = false;
	private boolean allowSessionCreation = true;
	private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
	public SimpleUrlAuthenticationFailureHandler() {
	}
	public SimpleUrlAuthenticationFailureHandler(String defaultFailureUrl) {
		setDefaultFailureUrl(defaultFailureUrl);
	}
	public void onAuthenticationFailure(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException exception)
			throws IOException, ServletException {

		if (defaultFailureUrl == null) {
			logger.debug("No failure URL set, sending 401 Unauthorized error");

			response.sendError(HttpStatus.UNAUTHORIZED.value(),
				HttpStatus.UNAUTHORIZED.getReasonPhrase());
		}
		else {
			saveException(request, exception);

			if (forwardToDestination) {
				logger.debug("Forwarding to " + defaultFailureUrl);

				request.getRequestDispatcher(defaultFailureUrl)
						.forward(request, response);
			}
			else {
				logger.debug("Redirecting to " + defaultFailureUrl);
				redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
			}
		}
	}
	protected final void saveException(HttpServletRequest request,
			AuthenticationException exception) {
		if (forwardToDestination) {
			request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
		}
		else {
			HttpSession session = request.getSession(false);

			if (session != null || allowSessionCreation) {
				request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,
						exception);
			}
		}
	}
	public void setDefaultFailureUrl(String defaultFailureUrl) {
		Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultFailureUrl),
				() -> "'" + defaultFailureUrl + "' is not a valid redirect URL");
		this.defaultFailureUrl = defaultFailureUrl;
	}
	protected boolean isUseForward() {
		return forwardToDestination;
	}
	public void setUseForward(boolean forwardToDestination) {
		this.forwardToDestination = forwardToDestination;
	}
}

  從這段源碼中可以看到,當用戶構造SimpleUrlAuthenticationFailureHandler對象的時候, 就傳入了 defaultFailureUrl也就是登錄失敗時要跳轉的地址。在onAuthenticationFailure方法中,如果發現defaultFailureUrl為null,則直接通過response返回異常信息,否則調用 saveException 方法。在 saveException 方法中,如果 fowardToDestination 屬性設置為ture,表示通過伺服器端跳轉回到登錄頁面,此時就把異常信息放到request中。再回到 onAuthenticationFailure方法中,如果用戶設置fowardToDestination 為 true,就通過伺服器 端跳轉回到登錄頁面,否則通過重定向回到登錄頁面。

  如果是前後端分離開發,登錄失敗時就不需要頁面跳轉了,只需要返回JSON字元串給前端即可,此時可以通過自定義AuthenticationFailureHandler的實現類來完成,代碼如下

package com.intehel.demo.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
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;

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(
            HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        Map<String,Object> resp = new HashMap<String,Object>();
        resp.put("status",500);
        resp.put("msg","登錄失敗"+exception.getMessage());
        ObjectMapper om = new ObjectMapper();
        String s = om.writeValueAsString(resp);
        response.getWriter().write(s);
    }
}

  然後在SecurityConfig中進行配置即可:

package com.intehel.demo.config;

import com.intehel.demo.handler.MyAuthenticationFailureHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureHandler(new MyAuthenticationFailureHandler())
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
}

  配置完成後,當用戶再次登錄失敗,就不會進行頁而跳轉了,而是直接返回JSON字元串, 如圖2-10所示。

圖 2-10

  1.2.3 註銷登錄

  Spring Security中提供了預設的註銷頁面,當然開發者也可以根據自己的需求對註銷登錄進行定製。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureHandler(new MyAuthenticationFailureHandler())
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout")
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutSuccessUrl("/mylogin.html")
                .and()
                .csrf().disable();
    }
}
  • 通過.logout()方法開啟註銷登錄配置。
  • logoutUrl指定了註銷登錄請求地址,預設是GET請求,路徑為/logout。
  • invalidateHttpSession 表示是否使 session 失效,預設為 true。
  • clearAuthentication表示是否清除認證信息,預設為true。
  • logoutSuccessUrl表示註銷登錄後的跳轉地址。

  配置完成後,再次啟動項目登錄成功後,在瀏覽器中輸入http://localhost:8080/logout就可以發起註銷登錄請求了,註銷成功後,會自動跳轉到mylogin.html頁面。

  如果項目有需要,開發者也可以配置多個註銷登錄的請求,同時還可以指定請求的方法。

package com.intehel.demo.config;

import com.intehel.demo.handler.MyAuthenticationFailureHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureHandler(new MyAuthenticationFailureHandler())
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .logout()
                .logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET"),
                        new AntPathRequestMatcher("/logout2","POST")))
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutSuccessUrl("/mylogin.html")
                .and()
                .csrf().disable();
    }
}

上面這個配置表示註銷請求路徑有兩個:

  • 第一個是/logout1,請求方法是GET。
  • 第二個是/logout2,請求方法是POST。

使用任意一個請求都可以完成登錄註銷。

如果項目是前後端分離的架構,註銷成功後就不需要頁面跳轉了,只需將註銷成功的信息返回給前端即可,此時我們可以自定義返回內容:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureHandler(new MyAuthenticationFailureHandler())
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .logout()
                .logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET"),
                        new AntPathRequestMatcher("/logout2","POST")))
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutSuccessHandler((req,resp,auth)->{
                    resp.setContentType("application/json;charset=UTF-8");
                    Map<String,Object> result = new HashMap<String,Object>();
                    result.put("status",200);
                    result.put("msg","註銷成功!");
                    ObjectMapper om = new ObjectMapper();
                    String s = om.writeValueAsString(result);
                    resp.getWriter().write(s);
                })
                .and()
                .csrf().disable();
    }
}

  

  配置 logoutSuccessHandler 和 logoutSuccessUrl 類似於前面所介紹的 successHandler 和defaultSuccessUrl之間的關係,只是類不同而已,因此這裡不再贅述,讀者可以按照我們前面的分析思路自行分析。

  配置完成後,重啟項目,登錄成功後再去註銷登錄,無論是使用/logout1還是/logout2進行註銷,只要註銷成功後,就會返回一段JSON字元串。

  如果開發者希望為不同的註銷地址返回不同的結果,也是可以的,配置如下:

查看代碼
 @Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureHandler(new MyAuthenticationFailureHandler())
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .logout()
                .logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET"),
                        new AntPathRequestMatcher("/logout2","POST")))
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .defaultLogoutSuccessHandlerFor((req,resp,auth)->{
                    resp.setContentType("application/json;charset=UTF-8");
                    Map<String,Object> result = new HashMap<String,Object>();
                    result.put("status",200);
                    result.put("msg","使用logout1註銷成功!");
                    ObjectMapper om = new ObjectMapper();
                    String s = om.writeValueAsString(result);
                    resp.getWriter().write(s);
                },new AntPathRequestMatcher("/logout1","GET"))
                .defaultLogoutSuccessHandlerFor((req,resp,auth)->{
                    resp.setContentType("application/json;charset=UTF-8");
                    Map<String,Object> result = new HashMap<String,Object>();
                    result.put("status",200);
                    result.put("msg","使用logout2註銷成功!");
                    ObjectMapper om = new ObjectMapper();
                    String s = om.writeValueAsString(result);
                    resp.getWriter().write(s);
                },new AntPathRequestMatcher("/logout1","GET"))
                .and()
                .csrf().disable();
    }
}

  通過defaultLogoutSuccessHandlerFor方法可以註冊多個不同的註銷成功回調函數,該方法第一個參數是註銷成功回調,第二個參數則是具體的註銷請求。當用戶註銷成功後,使用了哪個註銷請求,就給出對應的響應信息。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. 登錄用戶數據獲取 登錄成功之後,在後續的業務邏輯中,開發者可能還需要獲取登錄成功的用戶對象,如果不使用任何安全管理框架,那麼可以將用戶信息保存在HttpSession中,以後需要的時候直接從HttpSession中獲取數據。在Spring Security中,用戶登錄信息本質上還是保存在 Ht ...
  • 24 類型標註 24.1 Python中的數據類型 在Python中有很多數據類型,比較常見如下所示: |整型 | 浮點型|字元串 | 列表|元組|字典|集合|布爾| | | | | | | | | | |int| float|str|list|tuple|dict|set|bool| 因Pytho ...
  • 目錄 一.簡介 二.效果演示 三.源碼下載 四.猜你喜歡 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 基礎 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 轉場 零基礎 O ...
  • 大家好,我是“良工說技術”。 今天給大家帶來的是springboot中的@ConditionalOnClass註解的用法。上次的@ConditionalOnBean註解還記得嗎? 一、@ConditionalOnClass註解初始 看下@CodidtionalOnClass註解的定義, 需要註意的有 ...
  • 一、緩存機制的原理 一個系統在面向用戶使用的時候,當用戶的數量不斷增多,那麼請求次數也會不斷增多,當請求次數增多的時候,就會造成請求壓力,而我們當前的所有數據查詢都是從資料庫MySQL中直接查詢的,那麼就可能會產生如下問題 ==頻繁訪問資料庫,資料庫訪問壓力大,系統性能下降,用戶體驗差== 解決問題 ...
  • 今天我們來講解leetcode案例分析,如何動態規劃的解題套路,態規劃的核心思想,以前經常會遇到動態規劃類型題目。 ...
  • SpringBoot 2.7.2 學習系列,本節內容快速體驗Spring Boot,帶大家瞭解它的基本使用、運行和打包。 Spring Boot 基於 Spring 框架,底層離不開 IoC、AoP 等核心思想。Spring 4.0 提供了基於 Java Config 的開發方式,Spring Bo ...
  • 一、問題復現 在實際的軟體系統開發過程中,隨著使用的用戶群體越來越多,表數據也會隨著時間的推移,單表的數據量會越來越大。 以訂單表為例,假如每天的訂單量在 4 萬左右,那麼一個月的訂單量就是 120 多萬,一年就是 1400 多萬,隨著年數的增加和單日下單量的增加,訂單表的數據量會越來越龐大,訂單數 ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...