1. 程式人生 > >spring security 5.x 入門及分析

spring security 5.x 入門及分析

Java Web專案的許可權管理框架,目前有兩個比較成熟且使用較多的框架,Shiro 和 Spring Security ,Shiro 比 Spring Security更加輕量級,但是需要手動配置的東西較多,Spring Security 和 Spring 整合更好,甚至直接適配了Spring Boot。

一、最簡單的使用:

要使用Spring Security 首先要引入依賴,Spring-boot 已經有了整合,直接引入spring-boot-statr-security依賴包即可:

        <!-- spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

演示專案使用的是Spring Cloud 2.0.3版本,所以加入依賴後,會自動獲取Spring Security 5.0.6版本的Jar包:

然後,就沒有然後了,一個基本的Spring Security已經有了,然後開啟瀏覽器,訪問http://localhost:8080,神奇的出來了一個登入頁面,但是並沒有建立任何的html檔案,resouces資料夾下除了兩個配置檔案,別的什麼都沒有了;

Spring Security已經預設做了一些配置,並且建立一個簡單的登入頁面,那這個頁面事怎麼來的?通過檢視相關文件和原始碼來一探究竟。

那什麼都沒有配置,從哪開始開始看呢?

在專案啟動的日誌中,可以發現有這樣一條資訊:

可以看到,自動配置類是UserDetailsServiceAutoConfiguration,密碼是cf73184c-e8f2-48d8-9ce3-4413e3943f19,現在知道了密碼,那使用者名稱是什麼還不知道,進入到UserDetailsServiceAutoConfiguration中去看看。

在這個 UserDetailsServiceAutoConfiguration 類的描述中可以知道,這個類是設定一些 Spring Security 相關預設的自動配置,把InMemoryUserDetailsManager 中得user 和 password 資訊設定為預設得使用者和密碼,可以通過提供的AuthenticationManager、AuthenticationProvider 或者 UserDetailsService 的 bean 來覆蓋預設的自動配置資訊。

可以看到,日誌中那句密碼列印的是從圖片中圈出來的這條語句列印的,在這個方法上面有一個inMemoryUserDetailsManager()方法,返回一個新的帶有UserDetials資訊引數構造的InMemoryUSerDetailsManager物件:

可以看到,第一個引數是User.withUsername(user.getName()),這個user.getName()的user 物件是上面SecurityProperties.User型別的,通過SecurityProperties 物件中獲取的,首先看下SecurityProperties類:

通過配置檔案中的,字首為spring.security 的配置可以改變預設配置資訊,再看看SecurityProperties 的 getUser()方法:

=>=>

通過一步步的跟蹤,發現預設的使用者名稱是user。現在需要定義一個成功之後返回資訊的Controller,並寫一個方法:

    @RequestMapping("/login")
    @ResponseBody
    public Boolean login() {
        
        return false;
    }

去預設的登入頁面嘗試一下,輸入使用者名稱:user ,密碼:(每次啟動都會重新生成一個UUID的字串),登入成功了,跳轉到了RequestMapping("/login")的controller 方法中,並返回結果,上面的提示資訊說XML解析的問題,查了資料,在IE中是沒有問題的,可以直接顯示結果false,而谷歌顯示XML型別的資訊

二、自定義配置(初階):

自定義的配置,就要修改一些預設配置的資訊,從那開始入手呢?

1、第一步:建立Spring Security 的Java配置,改配置建立一個名為springSecurityFilterChain的servlet過濾器,它負責應用程式的安全(保護程式URL,驗證提交的使用者名稱密碼,重定向到登入表單等)······

按照第一步,建立一個配置類並實現WebMvcConfigurer介面(User.withDefaultPasswordEncoder()方法以經不推薦使用了,但是文件還是用了這個方法):

@EnableWebSecurity
public class WebSecurityConfig implements WebMvcConfigurer {

    @Bean
    public UserDetailsService userDetailsService() throws Exception {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build());
        return manager;
    }
}

然後我們在重新啟動一下工程,在日誌中已經沒有列印密碼的資訊了,輸入配置中設定的使用者名稱和密碼,登入成功了。

2、第二步:註冊springSecurityFilterChain,這可以用Servlet 3.0 以後版本的Spring's WebApplicationInitializer support在Java 配置中實現。Spring Security 提供了一個基礎類 AbstractSecurityWebApplicationInitializer,這個類能夠幫你實現註冊springSecurityFilterChain,使用這個基礎類的方式取決於專案中是否使用Spring,這裡按照使用了Spring的方式配置。

public class MvcWebApplicationInitializer extends
		AbstractAnnotationConfigDispatcherServletInitializer {

	@Override
	protected Class<?>[] getRootConfigClasses() {
        //這裡的WebSecurityConfig.class 就是前面定義的配置類
		return new Class[] { WebSecurityConfig.class };
	}

	// ... other overrides ...
}

這樣就實現了為工程中的每個URL實現通過springSecurityFilterChain攔截 ,那登入頁面事怎麼出來的呢?

繼續看文件,在WebSecurityConfigurerAdapter 類中,有一個方法 configure(HttpSecurity http):

     /**
	 * Override this method to configure the {@link HttpSecurity}. Typically subclasses
	 * should not invoke this method by calling super as it may override their
	 * configuration. The default configuration is:
	 *
	 * <pre>
	 * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
	 * </pre>
	 *
	 * @param http the {@link HttpSecurity} to modify
	 * @throws Exception if an error occurs
	 */
	// @formatter:off
	protected void configure(HttpSecurity http) throws Exception {
		logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

		http
			.authorizeRequests()
				.anyRequest().authenticated()
				.and()
			.formLogin().and()
			.httpBasic();
	}

這個方法攔截所有路徑並跳轉到基礎登入頁,就是預設的登入頁。點選formLogin()方法,進入到HttpSecurity類中,會看到:

提示看FormLoginConfigurer中的loginPage()方法,但是這裡並沒有什麼特別有用的東西,可以看到的是,這個方法返回一個FormLoginConfigurer型別的資料,進入FormLoginConfigurer類中,在這個類的描述有這樣一段話:

可以看出,一個預設的login頁在這裡被建立,在這個類最後有一個方法:

這個方法,初始化一個預設登入頁的過濾器,可以看到第一句程式碼,預設的過濾器是DefaultLoginPageGeneratingFilter,下面是設定一些必要的引數,進入到這個過濾器中:

在描述中可以看到,如果沒有配置login頁,這個過濾器會被建立,然後看doFilter()方法:

登入頁面的配置是通過generateLoginPageHtml()方法建立的,再來看看這個方法內容:

private String generateLoginPageHtml(HttpServletRequest request, boolean loginError,
			boolean logoutSuccess) {
		String errorMsg = "none";

		if (loginError) {
			HttpSession session = request.getSession(false);

			if (session != null) {
				AuthenticationException ex = (AuthenticationException) session
						.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
				errorMsg = ex != null ? ex.getMessage() : "none";
			}
		}

		StringBuilder sb = new StringBuilder();

		sb.append("<html><head><title>Login Page</title></head>");

		if (formLoginEnabled) {
			sb.append("<body onload='document.f.").append(usernameParameter)
					.append(".focus();'>\n");
		}

		if (loginError) {
			sb.append("<p style='color:red;'>Your login attempt was not successful, try again.<br/><br/>Reason: ");
			sb.append(errorMsg);
			sb.append("</p>");
		}

		if (logoutSuccess) {
			sb.append("<p style='color:green;'>You have been logged out</p>");
		}

		if (formLoginEnabled) {
			sb.append("<h3>Login with Username and Password</h3>");
			sb.append("<form name='f' action='").append(request.getContextPath())
					.append(authenticationUrl).append("' method='POST'>\n");
			sb.append("<table>\n");
			sb.append("	<tr><td>User:</td><td><input type='text' name='");
			sb.append(usernameParameter).append("' value='").append("'></td></tr>\n");
			sb.append("	<tr><td>Password:</td><td><input type='password' name='")
					.append(passwordParameter).append("'/></td></tr>\n");

			if (rememberMeParameter != null) {
				sb.append("	<tr><td><input type='checkbox' name='")
						.append(rememberMeParameter)
						.append("'/></td><td>Remember me on this computer.</td></tr>\n");
			}

			sb.append("	<tr><td colspan='2'><input name=\"submit\" type=\"submit\" value=\"Login\"/></td></tr>\n");
			renderHiddenInputs(sb, request);
			sb.append("</table>\n");
			sb.append("</form>");
		}

		if (openIdEnabled) {
			sb.append("<h3>Login with OpenID Identity</h3>");
			sb.append("<form name='oidf' action='").append(request.getContextPath())
					.append(openIDauthenticationUrl).append("' method='POST'>\n");
			sb.append("<table>\n");
			sb.append("	<tr><td>Identity:</td><td><input type='text' size='30' name='");
			sb.append(openIDusernameParameter).append("'/></td></tr>\n");

			if (openIDrememberMeParameter != null) {
				sb.append("	<tr><td><input type='checkbox' name='")
						.append(openIDrememberMeParameter)
						.append("'></td><td>Remember me on this computer.</td></tr>\n");
			}

			sb.append("	<tr><td colspan='2'><input name=\"submit\" type=\"submit\" value=\"Login\"/></td></tr>\n");
			sb.append("</table>\n");
			renderHiddenInputs(sb, request);
			sb.append("</form>");
		}

		if (oauth2LoginEnabled) {
			sb.append("<h3>Login with OAuth 2.0</h3>");
			sb.append("<table>\n");
			for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {
				sb.append(" <tr><td>");
				sb.append("<a href=\"").append(request.getContextPath()).append(clientAuthenticationUrlToClientName.getKey()).append("\">");
				sb.append(HtmlUtils.htmlEscape(clientAuthenticationUrlToClientName.getValue(), "UTF-8"));
				sb.append("</a>");
				sb.append("</td></tr>\n");
			}
			sb.append("</table>\n");
		}

		sb.append("</body></html>");

		return sb.toString();
	}

至此,預設登入頁及配置,已經可以清楚了。

三、自定義配置(高階):

在專案開發中,會有自己的登入頁,如何加入到Spring Sercurty的攔截中去,接下來進一步分析:

在文件中有這樣的介紹:

根據介紹,需要在自定義的配置類中重寫configure(HttpSercurity http)這個方法,並將登入頁路經配置為工程中的路徑就會覆蓋預設的配置,跟蹤這個方法可以看到自定義的登入路徑會替換掉預設的路徑:

但是有個問題,自定義的路徑還是跟預設的路徑一樣,都是/login,問什麼會調到自定義的頁面,而不是預設的登入頁面???

現在既然開始配置Spring Security,可以看到,使用了一個註解 @EnableWebSecurity ,就從這個註解開始看,到底會發生什麼事情。點選進入 @EnableWebSecurity 可以看到 @Import 了兩個類,一個 WebSecurityConfigutation (WebSecurity的配置類),一個SpringWebMvcImportSelector:

先來看看SpringWebMvcImportSelector:當classpath中存在DispatcherServlet時,有條件的將WebMvcSecurityConfiguration匯入進來 。

再來看 WebSecurityConfigutation:通過WebSecurity建立過濾器鏈代理FilterChainProxy,來實現基於Spring Security的web專案的安全性。它會暴露一些必要的beans。可以通過繼承WebSecurityConfigurerAdapter來建立自定義的WebSecurity,並且作為一個配置暴露出去,也可以通過實現WebSecurityConfigurer介面來實現這一配置。

在WebSecurityConfiguration中有個方法,用來建立過濾器鏈,可以看到,首先進行判斷webSecurityConfigurers是否為null,如果沒有,就通過傳入了一個new WebSecurityConfigurerAdapter()的ObjectPostProcessor來建立一個,如果有則直接呼叫webSecurity的build()方法:

再來看WebSecurityConfigurerAdapter,new 之後會先設定disableDefaults為false,即開啟預設配置:

在啟動工程時debug一下,可以看到: