天天看點

SpringSecurity學習筆記(三)自定義資源攔截規則以及登入界面跳轉

參考視訊,程式設計不良人

由前面的學習可以知道,SS的預設的攔截規則很簡單,我們在項目中實際使用的時候往往需要更加複雜的攔截規則,這個時候就需要自定義一些攔截規則。

自定義攔截規則

在我們的項目中,資源往往是需要不同的權限才能操作的,可以分為下面幾種:

  • 公共資源:可以随意通路
  • 認證通路:隻有登入了之後的使用者才能通路。
  • 授權通路:登入的使用者必須具有響應的權限才能夠通路。

我們想要自定義認證邏輯,就需要建立一些原來不存在的bean,這個時候就可以使

@ConditionalOnMissingBean

注解發現建立預設的實作類失效。

測試環境搭建
@RequestMapping("/public/test")
    public String justatest(){
        return "just a test,這個是公共資源!";
    }

    @RequestMapping("/private/t1")
    public String t1(){
        return "通路受限資源!";
    }
           

下面我們重寫一個配置類去替換内部預設的配置類

@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
       //ss裡面要求放行的資源要寫在任何請求的前面
        http.authorizeRequests()//開啟請求的權限管理
                .mvcMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin();//表單驗證的方式
    }
}
           

下面測試,通路公共資源

SpringSecurity學習筆記(三)自定義資源攔截規則以及登入界面跳轉

通路/private/t1跳轉到

SpringSecurity學習筆記(三)自定義資源攔截規則以及登入界面跳轉

輸入賬号密碼之後通路到

SpringSecurity學習筆記(三)自定義資源攔截規則以及登入界面跳轉

自定義登入界面

在前面的學習中我們知道了預設的登入界面是在過濾器

DefaultLoginPageGeneratingFilter

裡面實作的,現在我們想要自定義一個登入界面。

  • 首先引入thymeleaf依賴
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
           
  • 在templates目錄下面建立一個login的html頁面
    SpringSecurity學習筆記(三)自定義資源攔截規則以及登入界面跳轉
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>冬木自定義使用者登入</title>

</head>
<body>
    <form th:action="@{/login}" method="post">
        使用者名:<input type="text" name="username"><br>
        密碼:<input type="text" name="password"><br>
        <input type="submit" name="登入">

    </form>
</body>
</html>
           

編寫一個controller接口用于跳轉到我們自己寫的登入頁面,

SpringSecurity學習筆記(三)自定義資源攔截規則以及登入界面跳轉
SpringSecurity學習筆記(三)自定義資源攔截規則以及登入界面跳轉

這裡的字首預設就是在templates下面是以我下面直接

return login

package com.dongmu.controller;

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

@Controller
public class LoginController {

    @RequestMapping("/login.html")
    public String login(){
        return "login";
    }
}

           

添加配置路徑,

spring:
  thymeleaf:
    cache: false #可以讓我們的修改立即生效
           

另外把認證相關的接口放行

@Override
    protected void configure(HttpSecurity http) throws Exception {
        //ss裡面要求放行的資源要寫在任何請求的前面
        http.authorizeRequests()//開啟請求的權限管理
                .mvcMatchers("/public/**").permitAll()
                .mvcMatchers("/login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login");//表單驗證的方式,同時指定預設的登入界面
    }
           

這個時候再去通路頁面就會跳轉到下面這個頁面

SpringSecurity學習筆記(三)自定義資源攔截規則以及登入界面跳轉

這個時候登入會發現還是條狀到登入頁面,這裡要注意,一旦指自定義了登入頁面就需要指定登入的url,是以我們在接口裡面添加下面的代碼

//ss裡面要求放行的資源要寫在任何請求的前面
        http.authorizeRequests()//開啟請求的權限管理
                .mvcMatchers("/public/**").permitAll()
                .mvcMatchers("/login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login.html")//表單驗證的方式,同時指定預設的登入界面
                //一旦自定義登入界面必須指定登入url
                .loginProcessingUrl("/login")
                .and()
                .csrf().disable();
           

這個時候就可以登入成功了。

但是這時候要注意源碼中指定了登入的參數名,隻能是username和password。

SpringSecurity學習筆記(三)自定義資源攔截規則以及登入界面跳轉

這個時候可以進行修改如下

http.authorizeRequests()//開啟請求的權限管理
                .mvcMatchers("/public/**").permitAll()
                .mvcMatchers("/login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login.html")//表單驗證的方式,同時指定預設的登入界面
                //一旦自定義登入界面必須指定登入url
                .loginProcessingUrl("/login")
                .usernameParameter("uname")//指定登入的參數
                .passwordParameter("pwd")
//                .successForwardUrl("")//預設驗證成功之後的跳轉,這個是請求轉發, 登入成功之後
				//直接跳轉到這個指定的位址,原來的位址不跳轉了。
                .defaultSuccessUrl("")//這個也是成功之後的跳轉路徑,預設是請求重定向。 登入成功之
                //後會記住原來通路的路徑,也可以再傳遞一個boolean參數指定位址預設false
                .and()
                .csrf().disable();
           

前後端分離項目路徑跳轉

前面介紹了前後端不分離項目的登入認證成功之後的路徑跳轉,但是針對于前後端分離項目,比如有的時候可能會發送AJAX請求,這個時候怎麼處理呢?

SpringSecurity學習筆記(三)自定義資源攔截規則以及登入界面跳轉

我們可以自定義一個類實作

AuthenticationSuccessHandler

接口即可。

package com.dongmu.config;

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;

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        HashMap<String,Object> hashMap = new HashMap<>();
        hashMap.put("msg","登入成功");
        hashMap.put("code",200);
        hashMap.put("auth",authentication);
        response.setContentType("application/json;charset=utf-8");
        String s = new ObjectMapper().writeValueAsString(hashMap);
        response.getWriter().write(s);

    }
}

           

在successHandler裡面配置即可

//ss裡面要求放行的資源要寫在任何請求的前面
        http.authorizeRequests()//開啟請求的權限管理
                .mvcMatchers("/public/**").permitAll()
                .mvcMatchers("/login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login.html")//表單驗證的方式,同時指定預設的登入界面
                //一旦自定義登入界面必須指定登入url
                .loginProcessingUrl("/login")
//                .usernameParameter("uname")
//                .passwordParameter("pwd")
//                .successForwardUrl("")//預設驗證成功之後的跳轉,這個是請求轉發, 登入成功之後直接跳轉到這個指定的位址,原來的位址不跳轉了。
//                .defaultSuccessUrl("")//這個也是成功之後的跳轉路徑,預設是請求重定向。 登入成功之後會記住原來通路的路徑
                .successHandler(new MyAuthenticationSuccessHandler())//前後端分離的處理方案
                .and()
                .csrf().disable();
           

這個時候登入成功傳回的是一個json字元串。

SpringSecurity學習筆記(三)自定義資源攔截規則以及登入界面跳轉

身份驗證失敗跳轉

首先點進

SpringSecurity學習筆記(三)自定義資源攔截規則以及登入界面跳轉
SpringSecurity學習筆記(三)自定義資源攔截規則以及登入界面跳轉
SpringSecurity學習筆記(三)自定義資源攔截規則以及登入界面跳轉

UsernamePasswordAuthenticationFilter

這個類裡面由一個方法

attemptAuthentication

進行身份的驗證

public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}

		String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}

		if (password == null) {
			password = "";
		}

		username = username.trim();

		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);

		return this.getAuthenticationManager().authenticate(authRequest);
	}
           

然後最後一句代碼

authenticate(authRequest)

會進入

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();

		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			} catch (AuthenticationException e) {
				lastException = e;
			}
		}

		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
				lastException = parentException = e;
			}
		}

		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}

			// If the parent AuthenticationManager was attempted and successful then it will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
			if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).

		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		// If the parent AuthenticationManager was attempted and failed then it will publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}

		throw lastException;
	}
           

上面代碼中

try {
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
           

這一塊會進入

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));

		// Determine username
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();

		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);

		if (user == null) {
			cacheWasUsed = false;

			try {
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
           

這裡面

user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);

的實作

protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}
           

可以發現這裡就是去一開始我們學習的map裡面找到對應使用者名和密碼,這裡面應該會報出異常。這個異常後面會被這個方法接收

private void doAuthenticate(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
		Authentication authResult;

		Object principal = getPreAuthenticatedPrincipal(request);
		Object credentials = getPreAuthenticatedCredentials(request);

		if (principal == null) {
			if (logger.isDebugEnabled()) {
				logger.debug("No pre-authenticated principal found in request");
			}

			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("preAuthenticatedPrincipal = " + principal
					+ ", trying to authenticate");
		}

		try {
			PreAuthenticatedAuthenticationToken authRequest = new PreAuthenticatedAuthenticationToken(
					principal, credentials);
			authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
			authResult = authenticationManager.authenticate(authRequest);
			successfulAuthentication(request, response, authResult);
		}
		catch (AuthenticationException failed) {
			unsuccessfulAuthentication(request, response, failed);

			if (!continueFilterChainOnUnsuccessfulAuthentication) {
				throw failed;
			}
		}
	}
           

執行

unsuccessfulAuthentication

protected void unsuccessfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
		SecurityContextHolder.clearContext();

		if (logger.isDebugEnabled()) {
			logger.debug("Cleared security context due to exception", failed);
		}
		//這裡會把異常資訊放到request作用域當中
		request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, failed);

		if (authenticationFailureHandler != null) {
			authenticationFailureHandler.onAuthenticationFailure(request, response, failed);
		}
	}
           

這裡配置請求轉發

protected void configure(HttpSecurity http) throws Exception {
        //ss裡面要求放行的資源要寫在任何請求的前面
        http.authorizeRequests()//開啟請求的權限管理
                .mvcMatchers("/public/**").permitAll()
                .mvcMatchers("/login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login.html")//表單驗證的方式,同時指定預設的登入界面
                //一旦自定義登入界面必須指定登入url
                .loginProcessingUrl("/login")
//                .usernameParameter("uname")
//                .passwordParameter("pwd")
//                .successForwardUrl("")//預設驗證成功之後的跳轉,這個是請求轉發, 登入成功之後直接跳轉到這個指定的位址,原來的位址不跳轉了。
//                .defaultSuccessUrl("")//這個也是成功之後的跳轉路徑,預設是請求重定向。 登入成功之後會記住原來通路的路徑
                .successHandler(new MyAuthenticationSuccessHandler())//前後端分離的處理方案
                .failureForwardUrl("/login.html")//登入失敗之後的請求轉發頁面
//                .failureUrl("/login.html")//登入失敗之後的重定向頁面
                .and()
                .csrf().disable();
    }
           

可以直接從request作用域中擷取異常

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>冬木自定義使用者登入</title>

</head>
<h2>
    <div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
</h2>
<body>
    <form th:action="@{/login}" method="post">
        使用者名:<input type="text" name="username"><br>
        密碼:<input type="text" name="password"><br>
        <input type="submit" name="登入">

    </form>
</body>
</html>
           
SpringSecurity學習筆記(三)自定義資源攔截規則以及登入界面跳轉

如果是在重定向就會放在session作用域中。如果是請求轉發就會放到reques作用域中。

前後端分離項目認證失敗處理

實作接口

AuthenticationFailureHandler

package com.dongmu.config;

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

public class MyAuthenticationHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        HashMap<String,Object> hashMap = new HashMap<>();
        hashMap.put("msg","登入成功");
        hashMap.put("code",200);
        hashMap.put("auth",authentication);
        response.setContentType("application/json;charset=utf-8");
        String s = new ObjectMapper().writeValueAsString(hashMap);
        response.getWriter().write(s);
    }


    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        HashMap<String,Object> hashMap = new HashMap<>();
        hashMap.put("code",403);
        hashMap.put("msg",exception.getMessage());
        response.setContentType("application/json;charset=utf-8");
        String s = new ObjectMapper().writeValueAsString(hashMap);
        response.getWriter().write(s);
    }
}

           

配置認證失敗接口實作類

protected void configure(HttpSecurity http) throws Exception {
        //ss裡面要求放行的資源要寫在任何請求的前面
        http.authorizeRequests()//開啟請求的權限管理
                .mvcMatchers("/public/**").permitAll()
                .mvcMatchers("/login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login.html")//表單驗證的方式,同時指定預設的登入界面
                //一旦自定義登入界面必須指定登入url
                .loginProcessingUrl("/login")
//                .usernameParameter("uname")
//                .passwordParameter("pwd")
//                .successForwardUrl("")//預設驗證成功之後的跳轉,這個是請求轉發, 登入成功之後直接跳轉到這個指定的位址,原來的位址不跳轉了。
//                .defaultSuccessUrl("")//這個也是成功之後的跳轉路徑,預設是請求重定向。 登入成功之後會記住原來通路的路徑
//                .successHandler(new MyAuthenticationSuccessHandler())//前後端分離的處理方案
//                .failureForwardUrl("/login.html")//登入失敗之後的請求轉發頁面
                .failureUrl("/login.html")//登入失敗之後的重定向頁面
                .failureHandler(new MyAuthenticationHandler())
                .and()
                .csrf().disable();
    }
           
SpringSecurity學習筆記(三)自定義資源攔截規則以及登入界面跳轉