Spring Boot整合Spring Security實現權限控制

學習Spring Security過後進行總結和梳理,首先說一下關於認證授權的基本概念。

基本概念

什麼是認證?

​ 進入移動互聯網時代,大家每天都在刷手機,常用的軟件有微信、支付寶、頭條等,下邊拿微信來舉例子說明認證 相關的基本概念,在初次使用微信前需要註冊成爲微信用戶,然後輸入賬號和密碼即可登錄微信,輸入賬號和密碼 登錄微信的過程就是認證。

系統爲什麼要認證

​ 認證是爲了保護系統的隱私數據與資源,用戶的身份合法方可訪問該系統的資源。

認證:用戶認證就是判斷一個用戶的身份是否合法的過程,用戶去訪問系統資源時系統要求驗證用戶的身份信 息,身份合法方可繼續訪問,不合法則拒絕訪問。常見的用戶身份認證方式有:用戶名密碼登錄,二維碼登錄,手 機短信登錄*,*指紋認證等方式。

什麼是會話?

​ 用戶認證通過後,爲了避免用戶的每次操作都進行認證可將用戶的信息保證在會話中。會話就是系統爲了保持當前用戶的登錄狀態所提供的機制,常見的有基於session方式、基於token方式等。

基於session的認證方式如下:
它的交互流程是,用戶認證成功後,在服務端生成用戶相關的數據保存在session(當前會話)中,發給客戶端的sesssion_id 存放到 cookie 中,這樣用戶客戶端請求時帶上 session_id 就可以驗證服務器端是否存在 session 數據,以此完成用戶的合法校驗,當用戶退出系統或session過期銷燬時,客戶端的session_id也就無效了。

基於token方式如下:

​ 它的交互流程是,用戶認證成功後,服務端生成一個token發給客戶端,客戶端可以放到 cookie 或 localStorage等存儲中,每次請求時帶上 token,服務端收到token通過驗證後即可確認用戶身份。

基於session的認證方式由Servlet規範定製,服務端要存儲session信息需要佔用內存資源,客戶端需要支持cookie。

基於token的方式則一般不需要服務端存儲token,並且不限制客戶端的存儲方式。如今移動互聯網時代,更多類型的客戶端需要接入系統,系統多是採用前後端分離的架構進行實現,所以基於token的方式更適合。

什麼是授權?

​ 比如:微信登錄成功後用戶即可使用微信的功能,如:發紅包、發朋友圈、添加好友等,如未綁定銀行卡用戶則無法發送紅包,綁定銀行卡的用戶纔可以發紅包,發紅包功能、發朋友圈功能都是微信的資源即功能資源,用戶擁有發紅包功能的權限纔可以正常使用發送紅包功能,這個根據用戶的權限來控制用戶使用資源的過程就是授權。

爲什麼要授權?

​ 認證是爲了保證用戶身份的合法性,授權則是爲了更細粒度的對隱私數據進行劃分,授權是在認證通過後發生的,控制不同的用戶能夠訪問不同的資源。

授權: 授權是用戶認證通過根據用戶的權限來控制用戶訪問資源的過程,擁有資源的訪問權限則正常訪問,沒有權限則拒絕訪問。


Spring Security介紹

​ Spring Security是一個能夠爲基於Spring的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。由於它是Spring生態系統中的一員,因此它伴隨着整個Spring生態系統不斷修正、升級,在spring boot項目中加入spring security更是十分簡單,使用Spring Security 減少了爲企業系統安全控制編寫大量重複代碼的工作。

特徵
  • 對身份驗證和授權的全面和可擴展的支持
  • 防止會話固定,點擊劫持,跨站點請求僞造等攻擊
  • Servlet API集成
  • 可選與Spring Web MVC集成
  • Much more

下面我們來實現一個簡單認證授權的demo,首先引入pom.xml

 <!-- 以下是>spring boot依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <!-- 以下是>spring security依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
            <version>3.0.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- Spring Boot熱部署 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>javax.persistence-api</artifactId>
            <version>2.2</version>
        </dependency>
編寫SpringSecurity的配置類,該類需要繼承WebSecurityConfigurerAdapter

​ spring security提供的用戶名密碼登錄、退出、會話管理等認證功能,只需要配置即可使用,安全配置的內容包括:用戶信息、密碼編碼器、安全攔截機制。

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //密碼編碼器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //安全攔截機制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/r1").hasAuthority("p2")
                .antMatchers("/r/r2").hasAuthority("p3")
                .antMatchers("/r/**").authenticated()//所有/r/**的請求必須認證通過
                .anyRequest().permitAll()//除了/r/**,其它的請求可以訪問
                .and()
                //開啓自動配置的登陸功能,如果沒有登錄,則會來到登錄頁面
                .formLogin()//允許表單登錄
                //.loginPage("/login-view")//自定義登錄頁面
                .loginProcessingUrl("/login")
                .successForwardUrl("/login-success")
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)//自定義登錄成功的頁面地址
                .and()
                .logout()
                .logoutUrl("/logout")//訪問/logout表示用戶註銷,清空session
                .logoutSuccessUrl("/");//註銷以後來到首頁

    }
}

自定義UserDetailsService
@Service
public class SpringDataUserDetailsService implements UserDetailsService {

    @Autowired
    private UserDao userDao;

    //根據 賬號查詢用戶信息
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //將來連接數據庫根據賬號查詢用戶信息
        TUser user = userDao.getUserByUsername(username);
        if (user == null) {
            //如果用戶查不到,返回null,由provider來拋出異常
            return null;
        }
//      根據用戶的id查詢用戶的權限
        List<String> permissions = userDao.findPermissionsByUserId(user.getId());
//        //將permissions轉成數組
        String[] permissionArray = new String[permissions.size()];
        permissions.toArray(permissionArray);
        UserDetails userDetails = User.withUsername(user.getUsername()).password(user.getPassword()).authorities(permissionArray).build();
        return userDetails;
    }
}

​ UserDetailsService 接口只有一個方法,loadUserByUsername(String username),一般需要我們實現此接口方法,根據用戶名加載登錄認證和訪問授權所需要的信息,並返回一個 UserDetails的實現類,後面登錄認證和訪問授權都需要用到此中的信息。

​ UserDetails 提供了一個默認實現 User,主要包含用戶名(username)、密碼(password)、權限(authorities)和一些賬號或密碼狀態的標識。如果默認實現滿足不了你的需求,可以根據需求定製自己的 UserDetails,然後在 UserDetailsService 的 loadUserByUsername 中返回即可。


LoginController內容

@RestController
public class LoginController {

    @RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
    public String loginSuccess(){
        //提示具體用戶名稱登錄成功
        return getUsername()+" 登錄成功";
    }

    /**
     * 測試資源1
     * @return
     */
    @GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
//  要使用註解限制方法訪問權限需要開啓WebSecurityConfig中@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
//    @PreAuthorize("hasAuthority('p1')")//擁有p1權限纔可以訪問,方法調用的權限控制。
    public String r1(){
        return getUsername()+" 訪問資源1";
    }

    /**
     * 測試資源2
     * @return
     */
    @GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
//    @PreAuthorize("hasAnyAuthority('p1','p3')")//擁有p2權限纔可以訪問
//    @PreAuthorize("hasAuthority('p2')")//擁有p1權限纔可以訪問
    public String r2(){
        return getUsername()+" 訪問資源2";
    }

    //獲取當前用戶信息
    private String getUsername(){
        String username = null;
        //當前認證通過的用戶身份
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //用戶身份
        Object principal = authentication.getPrincipal();
        if(principal == null){
            username = "匿名";
        }
        if(principal instanceof UserDetails){
            UserDetails userDetails = (UserDetails) principal;
            username = userDetails.getUsername();
        }else{
            username = principal.toString();
        }
        return username;
    }
}

工作原理

1、結構總覽

​ Spring Security所解決的問題就是安全訪問控制,而安全訪問控制功能其實就是對所有進入系統的請求進行攔截,校驗每個請求是否能夠訪問它所期望的資源。根據前邊知識的學習,可以通過Filter或AOP等技術來實現,SpringSecurity對Web資源的保護是靠Filter實現的,所以從這個Filter來入手,逐步深入Spring Security原理。

​ 當初始化Spring Security時,會創建一個名爲 SpringSecurityFilterChain 的Servlet過濾器,類型爲
org.springframework.security.web.FilterChainProxy,它實現了javax.servlet.Filter,因此外部的請求會經過此類,下圖是Spring Security過慮器鏈結構圖:

在這裏插入圖片描述

FilterChainProxy是一個代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各個Filter,同時這些Filter作爲Bean被Spring管理,它們是Spring Security核心,各有各的職責,但他們並不直接處理用戶的認證,也不直接處理用戶的授權,而是把它們交給了認證管理器(AuthenticationManager)和決策管理器
(AccessDecisionManager)進行處理,下圖是FilterChainProxy相關類的UML圖示。
在這裏插入圖片描述

spring Security功能的實現主要是由一系列過濾器鏈相互配合完成。
在這裏插入圖片描述

下面介紹過濾器鏈中主要的幾個過濾器及其作用:

  1. SecurityContextPersistenceFilter這個Filter是整個攔截過程的入口和出口(也就是第一個和最後一個攔截
    器),會在請求開始時從配置好的 SecurityContextRepository 中獲取 SecurityContext,然後把它設置給
    SecurityContextHolder。在請求完成後將 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同時清除 securityContextHolder 所持有的 SecurityContext;
  2. UsernamePasswordAuthenticationFilter 用於處理來自表單提交的認證。該表單必須提供對應的用戶名和密碼,其內部還有登錄成功或失敗後進行處理的 AuthenticationSuccessHandler 和
  3. AuthenticationFailureHandler,這些都可以根據需求做相關改變;
  4. FilterSecurityInterceptor 是用於保護web資源的,使用AccessDecisionManager對當前用戶進行授權訪問,前面已經詳細介紹過了;
  5. ExceptionTranslationFilter 能夠捕獲來自 FilterChain 所有的異常,並進行處理。但是它只會處理兩類異常:AuthenticationException 和 AccessDeniedException,其它的異常它會繼續拋出。
2、認證流程

在這裏插入圖片描述

讓我們仔細分析認證過程:

  1. 用戶提交用戶名、密碼被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 過濾器獲取到,
    封裝爲請求Authentication,通常情況下是UsernamePasswordAuthenticationToken這個實現類。
  2. 然後過濾器將Authentication提交至認證管理器(AuthenticationManager)進行認證
  3. 認證成功後, AuthenticationManager 身份管理器返回一個被填充滿了信息的(包括上面提到的權限信息,身份信息,細節信息,但密碼通常會被移除) Authentication 實例。
  4. SecurityContextHolder 安全上下文容器將第3步填充了信息的 Authentication ,通過SecurityContextHolder.getContext().setAuthentication(…)方法,設置到其中。
    可以看出AuthenticationManager接口(認證管理器)是認證相關的核心接口,也是發起認證的出發點,它的實現類爲ProviderManager。而Spring Security支持多種認證方式,因此ProviderManager維護着一個
    List 列表,存放多種認證方式,最終實際的認證工作是由
    AuthenticationProvider 完成的。咱們知道web表單的對應的AuthenticationProvider 實現類爲 DaoAuthenticationProvider,它的內部又維護着一個UserDetailsService 負責UserDetails的獲取。最終AuthenticationProvider將UserDetails填充至Authentication。

我們來看一下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;
}
  1. Authentication是spring security包中的接口,直接繼承自Principal類,而Principal是位於 java.security
    包中的。它是表示着一個抽象主體身份,任何主體都有一個名稱,因此包含一個getName()方法。
  2. getAuthorities(),權限信息列表,默認是GrantedAuthority接口的一些實現類,通常是代表權限信息的一系列字符串。
  3. getCredentials(),憑證信息,用戶輸入的密碼字符串,在認證過後通常會被移除,用於保障安全。
  4. getDetails(),細節信息,web應用中的實現接口通常爲 WebAuthenticationDetails,它記錄了訪問者的ip地址和sessionId的值。
  5. getPrincipal(),身份信息,大部分情況下返回的是UserDetails接口的實現類,UserDetails代表用戶的詳細信息,那從Authentication中取出來的UserDetails就是當前登錄用戶信息,它也是框架中的常用接口之一。
3、授權流程

​ Spring Security可以通過 http.authorizeRequests() 對web請求進行授權保護。SpringSecurity使用標準Filter建立了對web請求的攔截,最終實現對資源的授權訪問。

Spring Security的授權流程如下:

在這裏插入圖片描述

分析授權流程:
  1. 攔截請求,已認證用戶訪問受保護的web資源將被SecurityFilterChain中的 FilterSecurityInterceptor 的子
    類攔截。

  2. 獲取資源訪問策略,FilterSecurityInterceptor會從 SecurityMetadataSource 的子類
    DefaultFilterInvocationSecurityMetadataSource 獲取要訪問當前資源所需要的權限
    Collection 。
    SecurityMetadataSource其實就是讀取訪問策略的抽象,而讀取的內容,其實就是我們配置的訪問規則, 讀取訪問策略如:

    http
    .authorizeRequests()
    .antMatchers("/r/r1").hasAuthority("p1")
    .antMatchers("/r/r2").hasAuthority("p2")
    
  3. 最後,FilterSecurityInterceptor會調用 AccessDecisionManager 進行授權決策,若決策通過,則允許訪問資源,否則將禁止訪問

最終效果:

1、測試認證

在這裏插入圖片描述

2、測試授權

登錄成功之後訪問需要授權頁面,未擁有權限時返回狀態403

在這裏插入圖片描述

有該權限時則直接訪問資源

在這裏插入圖片描述

到此使用Spring Boot整合Spring Security實現簡單的權限控制完成,寫這個demo只是爲了熟悉流程,還有更多的知識等着我們去學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章