Spring Security 之基本概念


Spring Security 是一個安全框架, 可以簡單地認為 Spring Security 是放在用戶和 Spring 應用之間的一個安全屏障, 每一個 web 請求都先要經過 Spring Security 進行 Authenticate 和 Authoration 驗證.

不得不說, Spring Security 是一個非常龐大的框架, 非常值得花時間好好學習.

===================================
參考文檔
===================================
Spring Security 4 官方文檔翻譯 
Spring Security 架構概述 
Spring Security JWT Authentication architecture 

 

===================================
核心類
===================================

--------------------------------------
SecurityContextHolder
--------------------------------------
為了方便我們訪問 SecurityContext 對象, Spring Security 提供了 SecurityContextHolder 類, 通過 SecurityContextHolder.getContext() 即可獲取 SecurityContext 對象. SecurityContextHolder 采用 ThreadLocal 方式存儲 SecurityContext 對象, 這樣就能保證我們調用 SecurityContextHolder.getContext() 得到的永遠是當前用戶的 SecurityContext.


--------------------------------------
SecurityContext
--------------------------------------
SecurityContext 是 Spring Security 的核心, 保存着當前用戶是誰, 該用戶是否被認證, 具有哪些角色.

獲取 SecurityContext 對象是代碼為:
SecurityContext context= SecurityContextHolder.getContext();

 


--------------------------------------
Authentication
--------------------------------------
Authentication 接口是 SecurityContext 中的核心, 包含着 sessionId/IP 、用戶 UserDetails 信息、用戶的角色等等, 它有很多實現類(主要是 AbstractAuthenticationToken 的子類), 每種類都對應着一個認證方式.

獲取 Authentication 對象是代碼為:
Authentication auth=SecurityContextHolder.getContext().getAuthentication();

Authentication 類成員有:
Collection<? extends GrantedAuthority> getAuthorities() 用來獲取操作權限清單
Object getCredentials() 用來獲取密碼信息. 為了防止密碼泄漏, 在認證通過后, 密碼通常會被移除.
Object getPrincipal() 獲取用戶身份信息, 大部分返回的是 UserDetails 接口類型.
boolean isAuthenticated() 判斷是否已經通過驗證.
Object getDetails() 細節信息, 比如, 對於web應用, 返回類型通常是 WebAuthenticationDetails 接口類型, 包含 IP 和 sessionId.

 

--------------------------------------
AbstractAuthenticationToken 的子類
--------------------------------------
AnonymousAuthenticationToken 和 AbstractAuthenticationToken 的很多子類(類名都已Token) 都實現了 Authentication 接口, 每一個子類都代表着一個具體的認證方式, 主要的子類有:
UsernamePasswordAuthenticationToken
RunAsUserToken
RememberMeAuthenticationToken
JwtAuthenticationToken
OAuth2LoginAuthenticationToken
CasAuthenticationToken

 

--------------------------------------
GenericFilterBean Filter 類
--------------------------------------
用來攔截認證的 filter, 可以在這個filter上注冊認證成功的handler, 認證失敗的handler.
子類有 OncePerRequestFilter, CasAuthenticationFilter, OpenIDAuthenticationFilter, UsernamePasswordAuthenticationFilter, LogoutFilter 等等.

一般情況下, 我們不需要為 http 請求增加新的filter, 直接基於已有的 filter 做一些定制化既能滿足絕大多數需求(比如定制化 Handler). 增加 filter 的方式是, 在 Security Config 類的 configure(HttpSecurity http) 方法加入, 如下代碼:

@override
protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore(myAuthFilter(), UsernamePasswordAuthenticationFilter.class)
}

 

--------------------------------------
AuthenticationManager 和 AuthenticationProvider
--------------------------------------
用戶訪問網頁, Spring Security 將通過 SecurityFilter 來調用 AuthenticationManager 進行驗證, 缺省情況下, AuthenticationManager 將驗證工作交給 ProviderManager 去做, 而 ProviderManager 會通過一系列 AuthenticationProvider 完成具體的驗證.

調用鏈是:
AuthenticationManager --委托--> ProviderManager --委托--> 幾個 AuthenticationProvider ----> 調用 AbstractAuthenticationToken.Authenticate()

AuthenticationProvider 和 AbstractAuthenticationToken 子類是一一對應的, 每種 AuthenticationProvider 都需要 一個 AbstractAuthenticationToken 來支持, AuthenticationProvider 接口是對 AbstractAuthenticationToken 類的一個二次封裝, 保留了 AbstractAuthenticationToken.Authenticate() 方法, 另外增加了一個檢測函數用來反映是否支持傳入的認證token.

和 AbstractAuthenticationToken 一樣, AuthenticationProvider 也有很多很多實現類, 比如:
DaoAuthenticationProvider
RunAsImplAuthenticationProvider
LdapAuthenticationProvider
ActiveDirectoryLdapAuthenticationProvider
CasAuthenticationProvider

ProviderManager 可以設置1個或0個 Parent Provider, 當所有成員 Provider 都不支持當前 Authentication 請求對象, 由 Parent Provider 提供缺省驗證.

Authentication authenticate() 函數的執行過程是:
ProviderManager 會依次讓成員 AuthenticationProvider 去驗證, 如果第一個 AuthenticationProvider 不支持這個 Authentication 具體對象, 將使用第二個 AuthenticationProvider 驗證, 如果全部的 AuthenticationProvider 都不支持, 再看是否設置了 Parent Provider, 如果 Parent Provider 的驗證方法返回了 null, 最終會拋出 AuthenticationException 異常.

向 ProviderManager 注冊一個 AuthenticationProvider 的方式是, 在 SecurityConfig 配置類的 configure(AuthenticationManagerBuilder auth) 函數中, 使用 AuthenticationManagerBuilder 對象的 authenticationProvider() 方法注冊.

@EnableWebSecurity
public class SecurityConfig  extends WebSecurityConfigurerAdapter {
    @Autowired
    CustomAuthenticationProvider customAuthProvider;
 
    @Override
    public void configure(AuthenticationManagerBuilder auth) 
      throws Exception {
 
        //注冊一個自定義的 AuthenticationProvider
        auth.authenticationProvider(customAuthProvider);
        
        //再注冊一個基於內存的 AuthenticationProvider
        auth.inMemoryAuthentication()
            .withUser("memuser")
            .password(encoder().encode("pass"))
            .roles("USER");
    }


--------------------------------------
UserDetails 和 UserDetailsService
--------------------------------------
UserDetails 包含着 Authentication 需要的用戶信息(用戶名/密碼/操作權限), 這些信息往往是從 Jdbc 或其他數據源獲取到的.

UserDetailsService 是用來獲取指定username 對應的 UserDetails 的接口.

 

--------------------------------------
常用 Java 代碼
--------------------------------------
在 controller 接口函數中, 獲取用戶身份信息

1. 使用 @AuthenticationPrincipal 注解參數的方式:

@RequestMapping("/foo")
public String foo(@AuthenticationPrincipal User user) {
  ... // do stuff with user
}


2. 使用 HttpServletRequest 中 Principal 對象的方式:
注意這時的 Principal 對應的是 Spring Security 的 Authentication 對象.

@RequestMapping("/foo")
public String foo(Principal principal) {
  Authentication authentication = (Authentication) principal;
  User = (User) authentication.getPrincipal();
  ... // do stuff with user
}

3. 注銷的寫法

@RequestMapping(value="/logout", method = RequestMethod.GET)
public String logoutPage (HttpServletRequest request, HttpServletResponse response) {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (auth != null){    
        new SecurityContextLogoutHandler().logout(request, response, auth);
    }
    return "redirect:/login?logout";
}

4. 使用 SecurityContextHolder 獲取 Authentication 對象

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();

5. 獲取 principal 的通用方法

private String getPrincipal(){
    String userName = null;
    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

    if (principal instanceof UserDetails) {
        userName = ((UserDetails)principal).getUsername();
    } else {
        userName = principal.toString();
    }
    return userName;
}

===================================
認證過程中的 Handler
===================================
一: AuthenticationSuccessHandler 接口, 用來設置驗證成功后的處理動作, 有下面幾個實現類, 分別是:
ForwardAuthenticationSuccessHandler, SimpleUrlAuthenticationSuccessHandler, SavedRequestAwareAuthenticationSuccessHandler

二: LogoutHandler 接口,設置 logout 過程中必須處理動作, logout后的重定向建議使用 LogoutSuccessHandler, 有下面幾個實現類:
AbstractRememberMeServices, CompositeLogoutHandler, CookieClearingLogoutHandler, CsrfLogoutHandler, PersistentTokenBasedRememberMeServices, SecurityContextLogoutHandler, TokenBasedRememberMeServices

三: LogoutSuccessHandler 接口, 設置 logout完成后需要處理動作, LogoutSuccessHandler 是在 LogoutHandler 之后被執行, LogoutHandler 完成必要的動作(該過程不應該拋異常), LogoutSuccessHandler 定位是處理后續更多的步驟, 比如重定向等, 它有下面幾個實現類:
DelegatingLogoutSuccessHandler, HttpStatusReturningLogoutSuccessHandler, SimpleUrlLogoutSuccessHandler

四: AuthenticationFailureHandler 接口, 用來設置用戶驗證失敗后的處理動作, 有下面幾個實現類, 分別是:
1. SimpleUrlAuthenticationFailureHandler, Spring Security 缺省使用該Handler, 處理的機制是: 如果指定了 failureUrl 則跳轉到該url, 如果未指定, 則返回 401 錯誤代碼
2. ForwardAuthenticationFailureHandler, 不管是報哪種 AuthenticationException 類型, 總是重定向到指定的 url.
3. DelegatingAuthenticationFailureHandler, 這是一個代理類, 可以根據不同的AuthenticationException 類型, 設置不同的 AuthenticationFailureHandlers
4. ExceptionMappingAuthenticationFailureHandler, 可以根據不同的AuthenticationException 類型,設置不同的跳轉 url.

五: AccessDeniedHandler 接口, 用來設置訪問拒絕后的處理動作, 有下面幾個實現類, 分別是:
AccessDeniedHandlerImpl, DelegatingAccessDeniedHandler, InvalidSessionAccessDeniedHandler
   參考 http://www.mkyong.com/spring-security/customize-http-403-access-denied-page-in-spring-security/

 


===================================
Granted Authority 和 Role 的概念
===================================
在Spring security 中, 有 Granted Authority 和 role 兩個概念. 我們既可以使用 Granted Authority 控制權限, 也可以通過 role 控制權限.

Role 是 coarse-grained 的權限管控機制, 比較典型的角色有 ROLE_ADMIN/ROLE_USER 等.
Granted Authority 是 fine-grained 的權限管控機制, 比較典型的權限有: OP_DELETE_ACCOUNT/OP_CREATE_USER/OP_RUN_BATCH_JOB 等.
從另一個角度看, Role 應該是面向業務的, 而 Granted Authority 應該是面向實現的.

需要注意的是, 在Spring security 中, 如果在 hasRole 等函數中使用到 role 名稱, 不能加上 ROLE_ 前綴, spring security 會自動強制加上該前綴. 而對於 hasAuthority 等函數, spring security 並不會為權限名加任何前綴.

在一般的項目中, 我認為沒有必要區分 role 和 Granted Authority, 可以將它們認為等同起來, 統一認為它們都是角色, 以減少概念的混淆.

@PreAuthorize("hasAuthority('OP_DELETE_ACCOUNT')")
@PreAuthorize("hasRole('ADMIN')")

https://stackoverflow.com/questions/19525380/difference-between-role-and-grantedauthority-in-spring-security
https://www.baeldung.com/spring-security-granted-authority-vs-role

 


===================================
SpringBoot 集成 spring security
===================================
項目需加上 security starter 包,

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

SpringBoot 使用 SecurityAutoConfiguration 導入 DefaultConfigurerAdapter 配置類, 該配置類完成了缺省的用戶認證/和鑒權工作, 這包括:
1. 自動配置一個內存用戶, 賬號為 user, 密碼在程序啟動后動態生成並打印出來.
2. 忽略 /css/**, /js/**, /images/**, **/favicon.ico 靜態文件的攔截
3. 向 servletContext 注冊 securityFilterChain
另外, 在application.properties 文件中, 可以設置相關的配置項

ecurity.user.name=user  #默認用戶的賬號名
security.user.password=  #默認用戶的密碼, 不設定的話就動態生成
security.user.role=USER  #默認的用戶的角色
security.enable-csrf=false #是否開啟"跨站請求偽造", 默認關閉
security.ignored= #用逗號隔開的無需攔截的路徑

SpringBoot 已經為我們做了很多與Spring Security的集成工作, 在實際項目中, DefaultConfigurerAdapter 肯定是不夠的, 需要我們做的是, 像 DefaultConfigurerAdapter 一樣開發一個配置類(比如名為 WebSecurityConfig), 該類也繼承自 WebSecurityConfigurerAdapter 即可, 並分別實現兩個configure 方法, 分別完成用戶級的認證和http請求級的驗證.


下面就是一個空的 WebSecurityConfigurerAdapter 框架.

@Configuration
// @EnableWebSecurity //@EnableWebSecurity 可以省略
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
    @Override
    //設置用戶級的認證機制
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    }
    
    @Override
    //設置http請求級的驗證
    protected void configure(HttpSecurity http) throws Exception {
    }
}


===================================
Authentication 用戶認證
===================================
用戶認證的方法簽名如下:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
}

 

------------------------

1.1 內存用戶驗證
------------------------

@Configuration
// @EnableWebSecurity //@EnableWebSecurity 可以省略
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //鏈式寫法
        auth.inMemoryAuthentication()
        .withUser("user1").password("password1").roles("ADMIN")
        .and()
        .withUser("user2").password("password2").roles("USER");

        //常規寫法
        auth.inMemoryAuthentication()
        .withUser("user3").password("password3").roles("ADMIN");
        auth.inMemoryAuthentication()
        .withUser("user4").password("password4").roles("USER");
}

    @SuppressWarnings("deprecation")
    @Bean
    public NoOpPasswordEncoder passwordEncoder() {
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }

//    @Bean
//    public BCryptPasswordEncoder passwordEncoder() {
//        return new BCryptPasswordEncoder();
//    }
}  //end of class

auth.inMemoryAuthentication()用來創建一個基於內存的用戶清單(包括賬號名/密碼/角色), 兩個用戶之間使用 .add() 鏈式方法連接.
需要注意的是:
1. spring security 角色名稱會自動在角色名前加上 ROLE_ 前綴, 所以代碼中的角色名不能以 ROLE_ 開頭.
2. 推薦的角色名全部為大寫字母.
3. 內存用戶清單必須再提供一個 PasswordEncoder bean, 程序將使用該bean 進行密碼驗證, 這里使用了 NoOpPasswordEncoder, 還有 Md5PasswordEncoder, 在生產環境推薦使用 BCryptPasswordEncoder.
使用BCryptPasswordEncoder后, 如果代碼中是明文密碼, 需要先做一下encode, 因為 Spring security 將使用encoding 后的密碼進行驗證. auth.inMemoryAuthentication().withUser("user1").password(passwordEncoder().encode("user1Pass")).roles("USER")


------------------------
1.2 JDBC用戶驗證
------------------------

@Configuration
// @EnableWebSecurity //@EnableWebSecurity 可以省略
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
    @Autowired
    DataSource dataSource;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        String userQuery = "select username, password, enable from "
                + "( select 'user1' username, 'password1' password, true enable ) t " + "where username=?";

        String roleQuery = "select username, rolename authority from"
                + "(select 'user1' username,'ADMIN' rolename ) t" + " where username=?";
        auth.jdbcAuthentication().dataSource(dataSource).usersByUsernameQuery(userQuery)
                .authoritiesByUsernameQuery(roleQuery);
    }
    
    @SuppressWarnings("deprecation")
    @Bean
    public NoOpPasswordEncoder passwordEncoder() {
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }                
} //end of class

auth.jdbcAuthentication()用來創建一個基於JDBC數據庫用戶驗證, 需要提供兩個查詢, 一個是賬號/密碼查詢(使用 usersByUsernameQuery 方法), 另一個是角色查詢(使用 authoritiesByUsernameQuery 方法).


------------------------
1.3 通用用戶驗證
------------------------
實際項目中基本上是采用通用用戶驗證, 這種方式需要定義一個能完成用戶檢索的類, 該類需實現 UserDetailsService 接口, 只要實現一個 loadUserByUsername() 方法即可, 該方法的返回類型是 UserDetails 接口, 我們沒有必要再定義一個專門的類去實現 UserDetails 接口, Spring Securtiy 已經提供了一個這樣的類, 類名全稱為 org.springframework.security.core.userdetails.User.

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用自定義的 UserDetailsService 接口認證
        auth.userDetailsService(new MyUserDetailsService()); 
    }

    @SuppressWarnings("deprecation")
    @Bean
    public NoOpPasswordEncoder passwordEncoder() {
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }
} //end of class

class MyUserDetailsService implements UserDetailsService {
    private Map<String, User> userRepository = new HashMap<String, User>();
    
    //模擬真實的用戶清單    
    public MyUserDetailsService() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("Admin"));
        authorities.add(new SimpleGrantedAuthority("User"));
        User user1 = new User("user1", "password1", authorities);
        userRepository.put("user1", user1);
    }

    // 需要實現的方法  
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.get(username);
        return user;
    }
}

===================================
理解http 請求級驗證代碼中的鏈式調用
===================================

// @formatter:off
protected void configure(HttpSecurity http) throws Exception {
    /*
     * HttpSecurity對象支持來復合鏈式調用寫法.
     * 比如 http.authorizeRequests() 可以加任意多二級 antMatchers()鏈式調用, 每一個 antMatchers() 以授權函數結尾,
     *                               二級 antMatchers() 之間無需 and() 連接.
     * 和 authorizeRequests() 同層次的還有 httpBasic()/sessionManagement()等, 多個一級函數的鏈式調用需使用 and() 連接.
     * */

    //一級函數
    //http.authorizeRequests();  //url 模式賦權
    //http.formLogin();          //登陸 form
    //http.httpBasic();          //Basic Authentication 設置
    //http.sessionManagement();  //session 管理
    //http.cors();               //cors 跨源資源共享
    //http.csrf()                //Cross Site Request Forgery 跨站域請求偽造
    
    //關於路徑的配置,
    //應該是先配置具體的路徑, 然后再配置寬泛的路徑
    //antMatchers()

    http
       .authorizeRequests()
           // 對於/api 路徑下的訪問需要有 ROLE_ADMIN 的權限
          .antMatchers("/api/**").hasRole("ADMIN")
          // 對於/api2 路徑下的訪問需要有 ROLE_ADMIN或ROLE_DBA或ROLE_USER 的權限
          .antMatchers("/api2/**").access("hasRole('USER') or hasRole('ADMIN') or hasRole('DBA')")
           // 對於/guest 路徑開放訪問
          .antMatchers("/guest/**").permitAll()
           // 其他url路徑之需要登陸即可.
           .anyRequest().authenticated()
           .and()
       //啟用 basic authentication
      .httpBasic().realmName(REALM).authenticationEntryPoint(getBasicAuthenticationEntryPoint())
           .and()
       //不創建 session
      .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
// @formatter:on

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM