Spring Security 學(xué)習(xí)
Spring Security是一種基于Spring AOP和Servlet規(guī)范中的FIlter實現(xiàn)的安全框架
是為給予Spring應(yīng)用程序提供聲明式安全保護(hù)的安全性框架甚疟,它能夠在Web請求級別和方法調(diào)用級別處理身份認(rèn)證和授權(quán)稻爬,并且因為基于Spring所以Spring Securitychongfenliyongle依賴注入和面向切面的技術(shù)。
Spring Security從兩個方面解決問題
- 它使用servlet規(guī)范中的Filter保護(hù)Web請求并限制URL級別的訪問。
- Spring Security還能夠使用Spring AOP保護(hù)方法的調(diào)用——借助于對象代理和適用通知涩咖,能夠確保只有具備適當(dāng)權(quán)限的用戶才能訪問安全保護(hù)的方法
Spring Security 命名空間的引入可以簡化我們的開發(fā)苞也,它涵蓋了大部分 Spring Security 常用的功能。它的設(shè)計是基于框架內(nèi)大范圍的依賴的棒厘,可以被劃分為以下幾塊纵穿。
- Web/Http 安全:這是最復(fù)雜的部分。通過建立 filter 和相關(guān)的 service bean 來實現(xiàn)框架的認(rèn)證機(jī)制奢人。當(dāng)訪問受保護(hù)的 URL 時會將用戶引入登錄界面或者是錯誤提示界面谓媒。
業(yè)務(wù)對象或者方法的安全:控制方法訪問權(quán)限的。 - AuthenticationManager:處理來自于框架其他部分的認(rèn)證請求何乎。
- AccessDecisionManager:為 Web 或方法的安全提供訪問決策句惯。會注冊一個默認(rèn)的,但是我們也可以通過普通 bean 注冊的方式使用自定義的 AccessDecisionManager支救。
- AuthenticationProvider:AuthenticationManager 是通過它來認(rèn)證用戶的抢野。
- UserDetailsService:跟 AuthenticationProvider 關(guān)系密切,用來獲取用戶信息的各墨。
通過Spring Security使用Spring MVC Web應(yīng)用程序集成指孤,只是在web.xml聲明 DelegatingFilterProxy 作為一個Servlet過濾器來攔截任何傳入的請求。
DelegatingFilterProxy是一個特殊的Servlet Filter贬堵,他本身做的工作并不多恃轩,只是將工作委托給一個javax.servlet.Filter實現(xiàn)類,這個實現(xiàn)類作為一個<bean>注冊在Spring的應(yīng)用上下文中
傳統(tǒng)配置DelegatingFilterProxy過濾器
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>
編寫簡單的安全性配置
@Configuration
@EnableWebSecurity
public class SecurityConfig extend WebSecurityConfigurerAdapter {
}
顧名思義@EnableWebSecurity注解將會啟用Web安全功能黎做。但是它本身并沒有什么用處叉跛,Spring Security必須配置在一個實現(xiàn)了WebSecurityConfigurerAdapter的bean中,或者拓展WebSecurityConfigurerAdapter蒸殿。
All-Security項目——Maven管理
項目分為5個Model分別為主模塊筷厘,APP安全模塊,瀏覽器安全模塊宏所,安全模塊核心酥艳,安全模塊的Demo
<modules>
<module>../SecurityApp</module>
<module>../SecurityBrowser</module>
<module>../SecurityCore</module>
<module>../SecurityDemo</module>
</modules>
我們可以看到在主模塊的pom.xml的文件中,管理了剩余的4個Model并且將其作為自己的子Model
使用Maven的dependencyManagement管理統(tǒng)一版本號
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Cairo-SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
可以看到其中的兩個依賴都是從Spring 官網(wǎng)的項目中引下來的管理整個項目版本號的兩個依賴楣铁,分別為Spring IO和Spring Cloud
在導(dǎo)入Spring Cloud的時候需要注意每個版本的Spring Cloud管理的Spring Boot項目的版本不同玖雁,可能會因為與別的其他依賴產(chǎn)生版本沖突
接著我們舉其中的一個例子來看
現(xiàn)在我們來看Demo的Model中的pom.xml
<parent>
<artifactId>bsb-security</artifactId>
<groupId>com.bsb.security</groupId>
<version>1.0-SNAPSHOT</version>
<relativePath>../Security/pom.xml</relativePath>
</parent>
其中有這些結(jié)點,這些節(jié)點的意思就是該Model作為主模塊的子Model進(jìn)行管理盖腕,并且引用主模塊的pom中的依賴
項目的逐步搭建
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Bean
public static PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.and()
.authorizeRequests()
.antMatchers("/authentication/require",
securityProperties.getBrowser().getLoginPage()).permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable();
}
}
上面這個SpringSecurity的配置類首先繼承WebSecurityConfigurerAdapter并且重寫參數(shù)為HttpSecurity的方法赫冬,可以看到這一整個方法都是由一系列的鏈?zhǔn)秸{(diào)用來重寫的這個configure方法浓镜,下面我們來淺淺地解讀一下這個configure方法
- 使用formLogin方法使得整個配置了SpringScurity的Rest服務(wù)開啟表單登錄認(rèn)證
- 接著調(diào)用loginPage指定登錄頁,這里使用一個url來表示劲厌,并且通過一個Controller去接收這個登錄認(rèn)證請求
@RestController
public class BrowserSecurityController {
private Logger logger = LoggerFactory.getLogger(getClass());
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private SecurityProperties securityProperties;
/**
* 當(dāng)需要身份認(rèn)證時跳轉(zhuǎn)到這里
* @param request
* @param response
* @return
*/
@RequestMapping("/authentication/require")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
logger.info("引發(fā)跳轉(zhuǎn)的請求是 " + targetUrl);
if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
logger.info(securityProperties.getBrowser().getLoginPage());
redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
}
}
return new SimpleResponse("訪問服務(wù)需要身份認(rèn)證膛薛,請引導(dǎo)用戶到登錄頁面");
}
}
- 通過ResquestMapping映射到這個請求的url上,這個Controller的作用是补鼻,通過判斷是對數(shù)據(jù)的請求還是對html的靜態(tài)頁面的請求哄啄,對應(yīng)使用不用的登陸頁
讀取.yml文件中的屬性
如何讀取.yml這種配置文件中的屬性呢,Spring為我們提供了一個解決策略
因為我在項目中使用的是.yml作為項目的配置文件风范,這種配置文件在我看來咨跌,有幾個好處,層次比較清晰硼婿,并且結(jié)構(gòu)清晰锌半,配置使用的是K-V形式的配置,看一下我的SpringBoot項目中的.yml配置
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
name: root
password: xxxxxx
url: jdbc:mysql://localhost:3306/securityDemo?useSSL=false
session:
store-type: none
output:
ansi:
enabled: always
server:
port: 8060
bsb:
security:
browser:
loginPage: /demo-signIn.html
可以看到.yml這種配置文件有天然的樹狀結(jié)構(gòu)寇漫,并且通過類似父子結(jié)點能夠更好地去尋找配置的結(jié)點進(jìn)行修改或者查找
現(xiàn)在刊殉,我們就要來為上面安全配置類通過不同的條件,分配不同的認(rèn)證頁面州胳,我們來回顧一下上面的安全配置類
http.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.and()
.authorizeRequests()
.antMatchers("/authentication/require",
securityProperties.getBrowser().getLoginPage()).permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable();
現(xiàn)在有兩個身份認(rèn)證的表單
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>signIn</title>
</head>
<body>
<h2>標(biāo)準(zhǔn)登錄頁面</h2>
<h2>表單登錄</h2>
<form action="/authentication/form" method="post">
<table>
<tr>
<td>用戶名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密碼:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td colspan="2"><button type="submit">登錄</button> </td>
</tr>
</table>
</form>
</body>
</html>
標(biāo)準(zhǔn)登錄頁
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>demo-signIn</title>
</head>
<body>
<h1>demo-signIn</h1>
</body>
</html>
demo登錄頁(為了簡單簡寫一下)
現(xiàn)在我們希望一切對html靜態(tài)頁面請求的身份驗證頁面都展示為demo登錄頁记焊,一切對數(shù)據(jù)請求的認(rèn)證頁面都展示為標(biāo)準(zhǔn)登錄頁
現(xiàn)在我們希望由.yml來配置不同的登錄頁,首先我們來封裝幾個類
public class BrowserProperties {
private String loginPage = "/signIn.html";
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
}
首先封裝BrowserProperties類栓撞,其中只有一個成員變量就是loginPage并且添加getter/setter方法遍膜,并且為loginPage指定默認(rèn)的值為/signIn.html 這個就是我們的標(biāo)準(zhǔn)登錄頁
@ConfigurationProperties(prefix = "bsb.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browserProperties) {
this.browser = browserProperties;
}
}
其次我們封裝的這個類是SecurityProperties類,其中引用一個BrowserProperties對象腐缤,并且這個對象的名稱為browser捌归,并且在這個類上我們使用Spring的注解 @ConfigurationProperties指定它是一個Spring的配置文件的讀取類,并且前綴為bsb.security看到這里大家或許能理解為什么要這么寫了岭粤,當(dāng)然如果只是封裝這兩個類,那么這個相當(dāng)于工具類的配置文件讀取的工作是完成不了的
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}
最后一個類特笋,這個類使用Spring支持的兩個注解
- @Configuration 告訴Spring這個類是一個Java配置類剃浇,其中可能會配置一些Bean進(jìn)行注入
- @EnableConfigurationProperties(SecurityProperties.class) 開啟Spring的配置讀取并指定配置讀取類也就是我們剛才配置的SecurityProperties類
這個時候我們再回來看一下我們的Controller
@RequestMapping("/authentication/require")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
logger.info("引發(fā)跳轉(zhuǎn)的請求是 " + targetUrl);
if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
logger.info(securityProperties.getBrowser().getLoginPage());
redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
}
}
return new SimpleResponse("訪問服務(wù)需要身份認(rèn)證,請引導(dǎo)用戶到登錄頁面");
}
這個Controller中的一個映射請求url的方法通過判斷請求的url后綴是否為.html猎物,重定向到不同的登錄頁虎囚,因為我們在SecurityCoreConfig類上指定了讀取配置的類并且指定其為Java配置類,所以我們可以通過使用@AutoWired方式注入進(jìn)來并且成功讀取配置類
@Autowired
private SecurityProperties securityProperties;
securityProperties.getBrowser().getLoginPage();
bsb:
security:
browser:
loginPage: /demo-signIn.html
可以看出來這其實就是按照yml這種樹狀結(jié)構(gòu)一級一級進(jìn)行讀取蔫磨,并且獲取到我們在配置類中設(shè)定的loginPage并且通過Controller的判斷成功重定向到不同的身份認(rèn)證頁
認(rèn)證步驟
看到了上面的一些簡單配置淘讥,我們現(xiàn)在來分析一下Spring Security的認(rèn)證步驟
如果我們不像上面那樣為SpringBoot創(chuàng)建的服務(wù)配置一個我們需要的安全配置類的話,就是說當(dāng)Spring Boot只是存在于我們的依賴中堤如,這個時候訪問我們的服務(wù)會有什么效果呢
這個時候我們能夠看到在url上Spring Security 為我們重定向到了localhost:8060/login頁面蒲列,并且這個頁面很丑窒朋,沒錯這就是Spring Security默認(rèn)的認(rèn)證頁面,如果需要進(jìn)一步地去訪問我們的服務(wù)蝗岖,就必須通過這一關(guān)默認(rèn)的身份驗證
接下來我們還可以看到侥猩,開啟服務(wù)之后在idea的控制臺打印了這樣一句之前沒有過的話
Using generated security password: 135610b7-f01a-49c9-b11f-1e987da36f0c
這句話就是告訴我們本次服務(wù)開啟的時候,需要通過認(rèn)證的密碼是這一串密碼抵赢,接下來我們試一下(默認(rèn)的認(rèn)證用戶為user)
我們可以看到在通過了Spring Security的默認(rèn)安全認(rèn)證之后我們順利地訪問到了我們的服務(wù)并且成功地返回了我們的響應(yīng)
如何通過自己的配置讓Spring Security使用我們自己的安全配置
通過繼承WebSecurityConfigurerAdapter 類重寫其中的configure方法欺劳,并且在其中通過鏈?zhǔn)秸{(diào)用進(jìn)行身份認(rèn)證,經(jīng)過上面模塊的說明铅鲤,我們可以看到使用自己的配置類進(jìn)行配置之后的安全模塊的啟用
Spring Security的工作原理(過濾器鏈)
我們可以看到前面的兩個過濾器
-
UsernamePasswordAuthticationFilter 表單登錄
這個過濾器使用用戶表單登錄提交的username/password進(jìn)行校驗划提,如果提交了用戶名密碼,這個過濾器就會嘗試著用過濾到的username/password進(jìn)行校驗邢享,如果這個過濾器攔截到的請求沒有攜帶username/password參數(shù)腔剂,那么這個過濾器就會將請求移交給下一個過濾器進(jìn)行處理
BasicAuthenticationFilter 默認(rèn)的basic登錄
FilterSecurityInterceptor 這個攔截器作為Spring Security安全認(rèn)證的最后一環(huán)守門人,他會進(jìn)行最終的身份驗證去判斷是否能夠訪問Rest的服務(wù)
ExceptionTranslationFilter 用來捕獲FilterSecurityInterceptor根據(jù)認(rèn)證結(jié)果拋出的異常驼仪,并且做出相應(yīng)處理
如上圖掸犬,其中綠色的過濾器我們可以通過代碼的控制來控制其是否啟用,但是藍(lán)色绪爸,橙色這種攔截去和過濾器我們沒有辦法進(jìn)行控制湾碎,這些攔截器和過濾器會一直存在于過濾器鏈上進(jìn)行他們的工作
我們可以通過在每個過濾器源碼打斷點debug來觀察一次完整的安全認(rèn)證是怎么被處理的
如果我們直接通過瀏覽器去訪問Rest服務(wù)的話,這個時候會直接進(jìn)入到最后的橙色FilterSecurityInterceptor 攔截器奠货,因為在這個過程中我們沒有攜帶任何關(guān)于username以及password的數(shù)據(jù)介褥,所以自然前面的綠色攔截器就沒有了作用
并且這個時候拋出一個異常,異常拋出之后由ExceptionTranslationFilter 過濾器递惋,并且對這個異常進(jìn)行處理柔滔,實際上就是一個重定向到Spring Security默認(rèn)的認(rèn)證頁上進(jìn)行身份認(rèn)證
這個時候可以看到調(diào)試的斷點到了UsernamePasswordAuthticationFilter 中,因為這個時候已經(jīng)使用了默認(rèn)的登錄認(rèn)證頁萍虽,并且通過UsernamePasswordAuthticationFilter 來認(rèn)證用戶的登錄請求
-
最終還是到了FilterSecurityInterceptor 攔截器睛廊,這個時候,這個攔截器攔截到的已經(jīng)不是對認(rèn)證的請求了杉编,已經(jīng)是對Rest服務(wù)的請求了超全,這個時候我們可以看一下FilterSecurityInterceptor 的源碼
InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); }
這個時候如果身份認(rèn)證通過的話,就會這個攔截器就會調(diào)用下一個鏈進(jìn)行真正的對Rest服務(wù)的訪問
自定義的用戶認(rèn)證邏輯實現(xiàn)
上面我們說到的所有的認(rèn)證邏輯都是基于Spring Security的默認(rèn)實現(xiàn)邓馒,那么我們?nèi)绾瓮ㄟ^自定義的認(rèn)證邏輯實現(xiàn)用戶的認(rèn)證呢
UserDetailsService接口
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
我們可以看到在這個Spring官方提供的接口中只有一個方法嘶朱,接收一個var1的String變量作為參數(shù)(并且作為用戶名),并且可能會拋出UsernameNotFoundException異常
我們看一下UserDetails接口
package org.springframework.security.core.userdetails;
import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
其中包括了一些我們對平時項目的一些數(shù)據(jù)的封裝,包括用戶名密碼光酣,用戶是否被鎖住疏遏,是否解凍是否可以使用
實現(xiàn)UserDetails接口
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根據(jù)用戶名查找用戶信息
logger.info(username);
String passwordEn = passwordEncoder.encode("123456");
logger.info("密碼為 " + passwordEn);
return new User(username, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
我們可以通過使用@AutoWired注解注入一些Mybaits或者JPA的DAO對象來實現(xiàn)根據(jù)數(shù)據(jù)庫中已有的記錄實現(xiàn)我們自己邏輯的功能
我們可以看到上述代碼的最后返回了一個User對象,這個User對象不是我們自己封裝的pojo對象,而是Spring官方提供的一個User類财异,大概看一下
public class User implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = 500L;
private static final Log logger = LogFactory.getLog(User.class);
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
…………
}
其中也有很多的用戶信息的封裝倘零,并且重要的是實現(xiàn)了UserDetails接口
我們可以看一下這個User類的其中一個構(gòu)造器
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
這個構(gòu)造器提供了三個參數(shù),用戶名密碼以及該用戶的授權(quán)宝当,一旦返回該User實例视事,也就說明我們自己實現(xiàn)的自定義的用戶認(rèn)證邏輯成功,并且我們可以通過我們自己的安全配置來進(jìn)行對用戶授權(quán)的驗證