本文將從示例、原理、應用3個方面介紹 spring security。html
如下分析基於spring boot 2.0 + spring 5.0.4版本源碼java
示例源碼:請戳這裏git
Spring Security 是一個可以爲基於 Spring 的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組能夠在 Spring 應用上下文中配置的 Bean,充分利用了Spring IoC,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和 AOP(面向切面編程)功能,爲應用系統提供聲明式的安全訪問控制功能,減小了爲企業系統安全控制編寫大量重複代碼的工做。當前版本爲 5.0.5。web
Spring Security 5 相比 4,主要有如下幾點升級:spring
<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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
配置很是簡單,和 spring security 有關的就是 spring-boot-starter-security,web 和 thymeleaf 的引入是爲了構建頁面,便於演示數據庫
spring.thymeleaf.cache=false spring.security.user.name=user spring.security.user.password=password spring.security.user.roles=USER
一樣很簡單,禁用thymeleaf的緩存功能,另外配置了一個角色爲 USER 的用戶,用戶名/密碼:user/password編程
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off http.authorizeRequests() .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() .anyRequest().fullyAuthenticated() .and() .formLogin().loginPage("/login").failureUrl("/login?error").permitAll() .and() .logout().permitAll(); // @formatter:on } }
security 的配置很簡單,能夠繼承WebSecurityConfigurerAdapter
,WebSecurityConfigurerAdapter
是默認狀況下 spring security 的 http 配置。一般狀況下,都會存在部分 url 請求不須要過安全驗證,此時能夠經過configure()
方法將不須要進行權限校驗的 url 排除掉。上面的例子,指定了 靜態資源、login 連接不須要過安全驗證,其他 url 均須要segmentfault
至此,整個 security 最簡單的功能就已經實現了,是否是很是簡單。下面咱們用一個例子來試驗下。定義一個 HomeController
緩存
@Controller public class HomeController implements WebMvcConfigurer { @GetMapping("/") public String home(Map<String, Object> model) { model.put("message", "Hello World"); model.put("title", "Hello Home"); model.put("date", new Date()); return "home"; } @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/login").setViewName("login"); } }
Spring 的 WebMvcConfigurer 接口提供了不少方法讓咱們來定製 SpringMVC 的配置,這裏經過 addViewControllers 將 /login 請求映射到了資源 login.html安全
附上 WebMvcConfigurer 提供的配置方法
好了,啓動 web 應用,能夠體驗安全驗證的效果了。
上面最簡單的示例,用戶權限信息是直接再配置文件中寫死的,那麼如何實現多個用戶呢?多個角色呢?
經過自定義 UserDetailsService 實現,這裏列舉使用內存存放用戶信息的方式。在上述的SecurityConfig
中增長配置:
@Bean public InMemoryUserDetailsManager inMemoryUserDetailsManager() throws Exception { return new InMemoryUserDetailsManager( User.withDefaultPasswordEncoder().username("admin").password("admin") .roles("ADMIN", "USER", "ACTUATOR").build(), User.withDefaultPasswordEncoder().username("user").password("user") .roles("USER").build()); }
上述配置添加了2個用戶,admin 和 user
答案是也很方便,只要加上一個註解配置便可。在SecurityConfig
類上增長以下配置
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
開啓註解配置的方式,開啓方法執行先後的安全校驗
寫個簡單的 service 作測試:
@Service public class SimpleSecureService { @Secured("ROLE_USER") public String secure() { return "Hello User Security"; } @PreAuthorize("hasRole('ADMIN')") public String authorized() { return "Hello Admin Security"; } }
經過配置,即實現了方法級別的安全校驗,@Secured 和 @PreAuthorize 最大區別是後者支持 spring EL,前者不支持,故後者比前者功能更強大
像上面的例子 admin 只能訪問 admin 受權的接口,而不能訪問 user 的接口,而咱們的業務場景每每是 admin 擁有最高權限,可訪問其餘全部用戶的資源,故這裏涉及到一個權限繼承的問題(固然你能夠在全部方法上都標記 admin 可訪問)。
spring 提供了 RoleHierarchy 接口來實現權限的級聯。
假設須要的級聯關係是
A > B B > C C > D D > E D > F
那麼對應的一級map配置
A --> [B] B --> [C] C --> [D] D --> [E,F]
構造完以後的關係
A --> [B,C,D,E,F] B --> [C,D,E,F] C --> [D,E,F] D --> [E,F]
SecurityContextHolder 用於存儲安全上下文(security context)的信息。當前操做的用戶是誰,該用戶是否已經被認證,他擁有哪些角色權限,這些都被保存在 SecurityContextHolder 中。SecurityContextHolder 默認使用ThreadLocal 策略來存儲認證信息。看到ThreadLocal 也就意味着,這是一種與線程綁定的策略。Spring Security 在用戶登陸時自動綁定認證信息到當前線程,在用戶退出時,自動清除當前線程的認證信息。
如何獲取當前用戶的信息?
由於身份信息是與線程綁定的,因此能夠在程序的任何地方使用靜態方法獲取用戶信息。一個典型的獲取當前登陸用戶的姓名的例子以下所示:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); }
getAuthentication()
返回了認證信息,getPrincipal()
返回了身份信息,UserDetails 即是 Spring 對身份信息封裝的一個接口。
先看下接口定義
public interface Authentication extends Principal, Serializable { Collection<? extends GrantedAuthority> getAuthorities(); Object getCredentials(); Object getDetails(); Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean var1) throws IllegalArgumentException; }
Authentication 是 spring security 包中的接口,直接繼承自 Principal 類,而 Principal 是位於 java.security 包中的。能夠見得,Authentication 在 spring security 中是最高級別的身份/認證的抽象。
由這個頂級接口,咱們能夠獲得用戶擁有的權限信息列表,密碼,用戶細節信息,用戶身份信息,認證信息。接口詳細解讀以下:
初次接觸 Spring Securit y的朋友相信會被 AuthenticationManager,ProviderManager ,AuthenticationProvider,這麼多類似的 Spring 認證類搞得暈頭轉向,但只要稍微梳理一下就能夠理解清楚它們的聯繫和設計者的用意。AuthenticationManager(接口)是認證相關的核心接口,也是發起認證的出發點,由於在實際需求中,咱們可能會容許用戶使用用戶名+密碼登陸,同時容許用戶使用郵箱+密碼,手機號碼+密碼登陸,甚至,可能容許用戶使用指紋登陸),因此說 AuthenticationManager 通常不直接認證,AuthenticationManager 接口的經常使用實現類 ProviderManager 內部會維護一個 List<AuthenticationProvider> 列表,存放多種認證方式,實際上這是委託者模式的應用(Delegate)。
核心的認證入口始終只有一個:AuthenticationManager,不一樣的認證方式:用戶名+密碼(UsernamePasswordAuthenticationToken),郵箱+密碼,手機號碼+密碼登陸則對應了三個 AuthenticationProvider。在默認策略下,只須要經過一個 AuthenticationProvider 的認證,便可被認爲是登陸成功。
ProviderManager 中的 List,會依照次序去認證,認證成功則當即返回,若認證失敗則返回 null,下一個AuthenticationProvider 會繼續嘗試認證,若是全部認證器都沒法認證成功,則 ProviderManager 會拋出一個 ProviderNotFoundException 異常。
到這裏,若是不糾結於 AuthenticationProvider 的實現細節以及安全相關的過濾器,認證相關的核心類其實都已經介紹完畢了:身份信息的存放容器 SecurityContextHolder,身份信息的抽象 Authentication,身份認證器 AuthenticationManager 及其認證流程。姑且在這裏作一個分隔線。下面來介紹下 AuthenticationProvider 接口的具體實現。
AuthenticationProvider 最最最經常使用的一個實現即是 DaoAuthenticationProvider。顧名思義,Dao 正是數據訪問層的縮寫,也暗示了這個身份認證器的實現思路。按照咱們最直觀的思路,怎麼去認證一個用戶呢?用戶前臺提交了用戶名和密碼,而數據庫中保存了用戶名和密碼,認證即是負責比對同一個用戶名,提交的密碼和保存的密碼是否相同即是了。在 Spring Security 中。提交的用戶名和密碼,被封裝成了 UsernamePasswordAuthenticationToken,而根據用戶名加載用戶的任務則是交給了 UserDetailsService,在 DaoAuthenticationProvider 中,對應的方法即是 retrieveUser,返回一個 UserDetails。還須要完成 UsernamePasswordAuthenticationToken 和 UserDetails密碼的比對,這即是交給 additionalAuthenticationChecks 方法完成的,若是這個 void 方法沒有拋異常,則認爲比對成功。比對密碼的過程,用到了 PasswordEncoder 和 SaltSource,密碼加密和鹽的概念相信不用我贅述了,它們爲保障安全而設計,都是比較基礎的概念。
DaoAuthenticationProvider:它獲取用戶提交的用戶名和密碼,比對其正確性,若是正確,返回一個數據庫中的用戶信息(假設用戶信息被保存在數據庫中)。
上面不斷提到了 UserDetails 這個接口,它表明了最詳細的用戶信息,這個接口涵蓋了一些必要的用戶信息字段,具體的實現類對它進行了擴展。
它和 Authentication 接口很相似,好比它們都擁有 username,authorities,區分他們也是本文的重點內容之一。Authentication 的getCredentials()
與 UserDetails 中的getPassword()
須要被區分對待,前者是用戶提交的密碼憑證,後者是用戶正確的密碼,認證器其實就是對這二者的比對。Authentication 中的getAuthorities()
實際是由 UserDetails 的getAuthorities()
傳遞而造成的。還記得Authentication 接口中的getUserDetails()
方法嗎?其中的 UserDetails 用戶詳細信息即是通過了 AuthenticationProvider 以後被填充的。
UserDetailsService 只負責從特定的地方(一般是數據庫)加載用戶信息,僅此而已。UserDetailsService 常見的實現類有 JdbcDaoImpl,InMemoryUserDetailsManager,前者從數據庫加載用戶,後者從內存中加載用戶,也能夠本身實現 UserDetailsService,一般這更加靈活。
用戶登錄,會被 AuthenticationProcessingFilter 攔截,調用 AuthenticationManager 的實現,AuthenticationManager 會調用ProviderManager來獲取用戶驗證信息(不一樣的 Provider 調用的服務不一樣,由於這些信息能夠是在數據庫上,能夠是xml配置文件上等),若是驗證經過後會將用戶的權限信息封裝一個User放到spring的全局緩存SecurityContextHolder中,以備後面訪問資源時使用。
訪問資源(即受權管理)時,會經過 AbstractSecurityInterceptor 攔截器攔截,其中會調用 FilterInvocationSecurityMetadataSource 的方法來獲取被攔截 url 所需的所有權限,在調用受權管理器 AccessDecisionManager,這個受權管理器會經過 spring 的全局緩存 SecurityContextHolder 獲取用戶的權限信息,還會獲取被攔截的url及所需的所有權限,而後根據所配的策略(有:一票決定,一票否認,少數服從多數等),若是權限足夠,則返回,權限不夠則報錯並調用權限不足頁面。