【spring boot 系列】spring security 實踐 + 源碼分析

前言

本文將從示例、原理、應用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

  • 支持 OAuth 2.0
  • 支持 Spring WebFlux
  • 可使用 Reactor 的 StepVerifier 進行測試

示例

pom配置

<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 的引入是爲了構建頁面,便於演示數據庫

application.properties 配置

spring.thymeleaf.cache=false
spring.security.user.name=user
spring.security.user.password=password
spring.security.user.roles=USER

一樣很簡單,禁用thymeleaf的緩存功能,另外配置了一個角色爲 USER 的用戶,用戶名/密碼:user/password編程

security config 配置

@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 的配置很簡單,能夠繼承WebSecurityConfigurerAdapterWebSecurityConfigurerAdapter是默認狀況下 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

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 對身份信息封裝的一個接口。

Authentication

先看下接口定義

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 中是最高級別的身份/認證的抽象。

由這個頂級接口,咱們能夠獲得用戶擁有的權限信息列表,密碼,用戶細節信息,用戶身份信息,認證信息。接口詳細解讀以下:

  • getAuthorities(),權限信息列表,默認是 GrantedAuthority 接口的一些實現類,一般是表明權限信息的一系列字符串。
  • getCredentials(),密碼信息,用戶輸入的密碼字符串,在認證事後一般會被移除,用於保障安全。
  • getDetails(),細節信息,web 應用中的實現接口一般爲 WebAuthenticationDetails,它記錄了訪問者的ip地址和sessionId的值。
  • getPrincipal(),最重要的身份信息,大部分狀況下返回的是 UserDetails 接口的實現類,也是框架中的經常使用接口之一。

AuthenticationManager

初次接觸 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 接口的具體實現。

DaoAuthenticationProvider

AuthenticationProvider 最最最經常使用的一個實現即是 DaoAuthenticationProvider。顧名思義,Dao 正是數據訪問層的縮寫,也暗示了這個身份認證器的實現思路。按照咱們最直觀的思路,怎麼去認證一個用戶呢?用戶前臺提交了用戶名和密碼,而數據庫中保存了用戶名和密碼,認證即是負責比對同一個用戶名,提交的密碼和保存的密碼是否相同即是了。在 Spring Security 中。提交的用戶名和密碼,被封裝成了 UsernamePasswordAuthenticationToken,而根據用戶名加載用戶的任務則是交給了 UserDetailsService,在 DaoAuthenticationProvider 中,對應的方法即是 retrieveUser,返回一個 UserDetails。還須要完成 UsernamePasswordAuthenticationToken 和 UserDetails密碼的比對,這即是交給 additionalAuthenticationChecks 方法完成的,若是這個 void 方法沒有拋異常,則認爲比對成功。比對密碼的過程,用到了 PasswordEncoder 和 SaltSource,密碼加密和鹽的概念相信不用我贅述了,它們爲保障安全而設計,都是比較基礎的概念。

DaoAuthenticationProvider:它獲取用戶提交的用戶名和密碼,比對其正確性,若是正確,返回一個數據庫中的用戶信息(假設用戶信息被保存在數據庫中)。

UserDetails與UserDetailsService

上面不斷提到了 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及所需的所有權限,而後根據所配的策略(有:一票決定,一票否認,少數服從多數等),若是權限足夠,則返回,權限不夠則報錯並調用權限不足頁面。

參考文檔

相關文章
相關標籤/搜索