應用Spring Security
前面介紹了在項目開發時為什么選擇Spring Security,還介紹了它的原理。本節開始動手實踐Spring Security的相關技術。
實戰:Spring Security入門
現在開始搭建一個新項目,實踐一個Spring Security的入門程序。
(1)新建一個spring-security-demo模塊,添加項目依賴,在pom.xml中添加如下依賴:
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--thymeleaf對security5的支持依賴-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<!--<version>3.0.4.RELEASE</version>-->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
(2)在application.properties中添加Spring Security配置,配置當前登錄的用戶名和密碼,配置內容如下:
#登錄的用戶名
spring.security.user.name=admin
#登錄的密碼
spring.security.user.password=123456
(3)在resources文件夾下創建頁面add.html,表示添加頁面,代碼如下:
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
title></title>
</head>
<body>
add 頁面
</body>
</html>
(4)添加主頁home.html,代碼如下:
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>主頁</title>
</head>
<body>
你已經登錄成功!
<form th:action="@{/logout}" action="/login" method="post">
<input type="submit" value="退出系統"/>
</form>
</body>
</html>
(5)添加login.html登錄頁,用于用戶的登錄,代碼如下:
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>請登錄</title>
</head>
<body>
<div>
<form th:action="@{/login}" method="post" action="/login">
<p>
<span>請輸入用戶名:</span>
<input type="text" id="username" name="username">
</p>
<p>
<span>請輸入密碼:</span>
<input type="password" id="password"
name="password">
</p>
<input type="submit" value="登錄"/>
</form>
</div>
</body>
</html>
(6)在resources文件夾下創建一個css文件夾,新建一個my.css文件,內容如下:
my css file
(7)新建一個Controller包,再新建如下3個Controller。
AddController類,用于返回add頁面,代碼如下:
package com.example.springsecuritydemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class AddController {
@GetMapping("/add")
public String ad(){
return "add";
}
}
omeController類用于訪問home頁面,代碼如下:
package com.example.springsecuritydemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/home")
public String home(){
return "home";
}
}
LoginController類用于用戶登錄,代碼如下:
package com.example.springsecuritydemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
@GetMapping("/login")
public String login(){
return "login";
}
}
(8)新建一個config包,添加Spring Security配置文件
WebSecurityConfig:
package com.example.springsecuritydemo.config;
import com.example.springsecuritydemo.service.LoginSuccessHandler;
import org.springframework.context.annotation.Configuration;
import
org.springframework.security.config.annotation.web.builders.HttpSecuri
ty;
import
org.springframework.security.config.annotation.web.configuration.WebSe
curityConfigurerAdapter;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 關閉csrf校驗
http.csrf().disable();
// 配置登錄頁面,用戶名和密碼已在配置文件中
http.formLogin().loginPage("/login").permitAll();
// 配置登錄成功后的操作
http.formLogin().successHandler(new LoginSuccessHandler());
// 登錄授權
http.logout().permitAll();
// 授權配置
http.authorizeRequests()
/* 所有的靜態文件可以訪問 */
.antMatchers("/js/**","/css/**","/images/**").permitAll()
/* 所有的以/add 開頭的 add頁面可以訪問 */
.antMatchers("/add/**").permitAll()
.anyRequest().fullyAuthenticated();
}
}
9)新建一個登錄成功后的業務處理服務類LoginSuccessHandler,代碼如下:
package com.example.springsecuritydemo.service;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.Authentication
SuccessHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 登錄成功后的業務處理類
*/
public class LoginSuccessHandler implements
AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws
IOException {
System.out.println("登錄成功");
//重定向到home.html頁面
response.sendRedirect("/home");
}
}
(10)添加當前項目的啟動類
SpringSecurityDemoApplication,使用注解@EnableWeb- Security啟動Spring Security功能:
package com.example.springsecuritydemo;
import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;
@EnableWebSecurity
@SpringBootApplication
public class SpringSecurityDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityDemoApplication.class,
args);
}
}
(11)執行
SpringSecurityDemoApplication啟動當前項目,訪問localhost:8080,因為沒有登錄,所以跳轉到登錄頁,如圖5.1所示。再訪問localhost:8080/home,還是會自動跳轉到登錄頁面,因為沒有登錄。前兩次訪問Spring Security時會自動判斷用戶還未登錄,直接跳轉到登錄頁面,提示用戶登錄。
再訪問localhost:8080/add,可以看到add頁面,如圖5.2所示。之所以能夠在沒有登錄的情況下看到add頁面,是因為Spring Security配置了未登錄時可以訪問/add這個鏈接。配置在WebSecurityConfig.java的代碼如下:
.antMatchers("/add/**").permitAll()
這個代碼的含義是所有以/add開頭的鏈接都允許訪問,因此可以看到add頁面。
同理,訪問localhost:8080/css/my.css會返回項目的靜態文件my.css,因為在WebSecurity-Config中配置了靜態文件的訪問權限。
/* 所有的靜態文件可以訪問 */
.antMatchers("/js/**","/css/**","/images/**").permitAll()
所以,js、css和images文件夾下的所有文件可以直接獲取,不會有任何校驗,訪問結果如圖5.3所示。
現在輸入用戶名admin和密碼123456登錄系統,登錄成功后的頁面如圖5.4所示,因為在LoginSuccessHandler中配置了登錄成功后的跳轉頁面代碼,即response.sendRedirect ("/home"),所以登錄成功后直接跳轉到了home頁面。
Spring Security適配器
Spring大量使用適配器模式,適配器的好處是當選擇性地修改一部分配置時不用覆蓋其他不相關的配置,Spring Security常用的適配器有
WebSecurityConfigurerAdapter。在開發中,可以選擇覆蓋部分自定義的配置,從而快速完成開發。
設計模式中適配器模式的結構如圖5.5所示。
適配器模式有3個類,分別是Adapter適配者類、Target目標類和ObjectAdapter適配器,可以通過這3個類實現適配器的相關功能。
在Spring Security框架中,
WebSecurityConfigurerAdapter類圖如圖5.6所示。
在圖5.6中,SecurityBuilder、SecurityConfigurer和SecurityBuilder這3個類非常重要,它們是用來構建過濾器鏈的,在用HttpSecurity實現SecurityBuilder時,傳入的泛型是
DefaultSecurityFilterChain,因此SecurityBuilder.build()用來構建過濾器鏈,而WebSecurity- Configurer用來配置WebSecurity。
在
WebSecurityConfigurerAdapter中有兩個方法非常重要,下面分別介紹。
(1)第一個是init()方法,其部分源碼如下:
public void init(final WebSecurity web) throws Exception {
final HttpSecurity http = getHttp();
web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
FilterSecurityInterceptor securityInterceptor = http .getSharedObject(FilterSecurityInterceptor.class);
web.securityInterceptor(securityInterceptor);
});
}
首先init()方法調用了getHttp()方法,其作用是進行HttpSecurity的初始化,其部分源碼如下:
@SuppressWarnings({ "rawtypes", "unchecked" })
protected final HttpSecurity getHttp() throws Exception {
if (http != null) {
return http;
}
AuthenticationEventPublisher eventPublisher =
getAuthenticationEventPublisher();
localConfigureAuthenticationBldr.authenticationEventPublisher(eventPub
lisher);
AuthenticationManager authenticationManager =
authenticationManager();
authenticationBuilder.parentAuthenticationManager(authenticationManage
r);
Map<Class<?>, Object> sharedObjects = createSharedObjects();
http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
sharedObjects);
if (!disableDefaults) {
// @formatter:off
http
.csrf().and()
.addFilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and()
.headers().and()
.sessionManagement().and() .securityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi().and()
.apply(new DefaultLoginPageConfigurer<>()).and()
.logout();
// @formatter:on
ClassLoader classLoader = this.context.getClassLoader();
List<AbstractHttpConfigurer> defaultHttpConfigurers =
SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class,
classLoader);
for (AbstractHttpConfigurer configurer : defaultHttpConfigurers)
{
http.apply(configurer);
}
}
configure(http);
return http;
}
在初始化完成后,init()方法調用了configure()方法配置默認的攔截器,當完成HttpSecurity初始化后,將HttpSecurity放入WebSecurity中,最終保存在WebSecurity的
securityFilterChainBuilders集合中。configure()方法的部分源碼如下:
/**
* 覆蓋此方法以配置{@link HttpSecurity}。
通常子類不建議通過調用super來調用此方法,因為它可能會覆蓋它們的配置。默認配置如下:
*
* <pre>
*
http.authorizeRequests().anyRequest().authenticated().and().formLogin(
).and().httpBasic();
* </pre> *
* 任何需要防御常見漏洞的端點都可以在這里指定,包括公共的端點
* See {@link HttpSecurity#authorizeRequests} and the
`permitAll()`authorization rule
* 更多關于公共端點的詳細信息
*
* @param http the {@link HttpSecurity} to modify
* @throws Exception if an error occurs
*/
// @formatter:off
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and()
.formLogin().and().httpBasic();
}
(2)另外一個非常重要的方法是前面提到的configure()方法。可以看到,抽象類
WebSecurityConfigurerAdapter中的configure是個protect()方法,開發者可以新建類或繼承此類后實現該方法,從而實現業務邏輯。
在當前項目中,自定義的WebSecurityConfig類繼承了
WebSecurityConfigurerAdapter()方法,實現了空的configure()方法,并配置了當前項目的登錄和攔截信息。當前方法的入參是HttpSecurity,可以使用HttpSecurity的builder構建方式來靈活制定訪問策略。Http- Security的常用方法參見表5.1。
表5.1 HttpSecurity的常用方法
實戰:用戶授權
在Spring Security中可以設置不同的用戶擁有不同的角色,同時不同的角色有不同的權限。下面舉例說明。
修改HomeController.java文件,增加一個/home2方法,增加的代碼如下:
@GetMapping("/home2")
public String home2(){ return "home2";
}
修改WebSecurityConfig.java文件,增加配置屬性:
/**
* 授權,賦予用戶角色,基于內存授權
*/
@Override
protected UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new
InMemoryUserDetailsManager();
manager.createUser(User.withUsername("admin").password("123456").
roles("admin").build());
return manager;
}
修改WebSecurityConfig.java中的configure()方法,增加/home2連接的
角色權限配置:
// 授權配置
http.authorizeRequests()
//可以訪問所有靜態文件
.antMatchers("/js/**","/css/**","/images/**").permitAll()
//可以訪問所有以/add開頭的add頁面
.antMatchers("/add/**").permitAll()
.antMatchers("/home2").hasRole("user")
.anyRequest().fullyAuthenticated();
重啟項目,登錄之后訪問localhost:8080/home2,結果如圖5.7所示。因為當前用戶沒有權限,所以訪問報錯。
Spring Security核心類
Spring Security框架中最核心的接口類是AuthenticationManager,其部分源碼如下:
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
AuthenticationManager是用來處理認證(Authentication)請求的基本接口。這個接口定義了方法authenticate(),此方法只接收一個代表認證請求的Authentication對象作為參數,如果認證成功,則會返回一個封裝了當前用戶權限等信息的Authentication對象,否則認證無法通過。
AuthenticationManager接口有兩個重要的實現。
AuthenticationManagerDelegator是一個委托類,由SecurityBuilder接口的子類來配置生成一個身份管理器;另外一個實現類是ProviderManager,此類的部分源碼如下:
public class ProviderManager implements AuthenticationManager, Message
SourceAware,
InitializingBean {
private List<AuthenticationProvider> providers =
Collections.emptyList();
private AuthenticationManager parent;public ProviderManager(AuthenticationProvider... providers) {
this(Arrays.asList(providers), null);
}
/**
* 使用給定的{@link AuthenticationProvider}構造一個{@link
ProviderManager}
*
* @param providers the {@link AuthenticationProvider}s to use
*/
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, null);
}
/**
* 使用給定的參考構造一個{@link ProviderManager}
*
* @param providers the {@link AuthenticationProvider}s to use
* @param parent a parent {@link AuthenticationManager} to fall back
to
*/
public ProviderManager(List<AuthenticationProvider> providers,
AuthenticationManager parent) {
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
checkState();
}
}
在此類中,構造函數有List<AuthenticationProvider> providers,它的作用是真正地完成認證工作。Spring Security有多種認證方式,如郵箱登錄、手機號登錄和第三方登錄等,只要一個認證成功了,就表示認證成功。
Spring Security的驗證機制
核心類AuthenticationManager調用其他的實現類進行認證。在SpringSecurity中提供認證功能的接口是
org.springframework.security.authentication.AuthenticationProvider。
該接口有兩個方法:authenticate()方法用來認證處理,返回一個authentication的實現類,代表認證成功;supports()方法表示當前身份提供者支持認證什么類型的身份信息,如果支持返回true,才會執行authenticate()方法進行身份認證。該接口的部分源碼如下:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
AuthenticationProvider接口有幾個常用的實現類,用來實現認證類型的具體方式,包括
AbstractUserDetailsAuthenticationProvider、DaoAuthenticationProvider和RememberMe- AuthenticationProvider。
DaoAuthenticationProvider的作用是從數據源中加載身份信息,其類圖如圖5.8所示。
RememberMeAuthenticationFilter的作用是當用戶沒有登錄而直接訪問資源時,首先從cookie中查找用戶信息,如果Spring Security能夠識別出用戶提供的remember me cookie,則不用再輸入用戶名和密碼,表示用戶已經認證成功。如圖5.9所示為RememberMeAuthenticationProvider類圖。
Spring Security的認證流程如下:
(1)從
WebSecurityConfigurerAdapter認證配置的configure(HttpSecurity http)方法進入,并添加攔截器addFilterBefore。
(2)進入
AbstractAuthenticationProcessingFilter攔截器的attemptAuthentication方法,指定認證對象AbstractAuthenticationToken。
(3)執行AuthenticationProvider認證邏輯,根據supports的判斷對認證的目標對象選擇一個攔截器進行認證,進入具體的認證邏輯方法authenticate()。
(4)如果認證成功,則進入攔截器的successfulAuthentication()方法;如果認證失敗,則進入攔截器的
unsuccessfulAuthentication方法()。
(5)對認證結果進行處理。
認證成功的邏輯:進入
SimpleUrlAuthenticationSuccessHandler的onAuthentication-Success()方法。
認證失敗的邏輯:進入
SimpleUrlAuthenticationFailureHandler的onAuthentication-Failure()方法。
(6)將數據封裝在ObjectMapper對象中后即可返回結果。