Spring Security

Spring Security是一個用於保護Java應用程序的強大框架,尤其是Web應用程序。它提供了全面的身份驗證(Authentication)和授權(Authorization)機制,用於確保只有授權的用戶可以訪問應用程序的特定資源。

主要功能和特點:

  1. 身份驗證(Authentication)

    • Spring Security提供了多種身份驗證方式,包括基於表單的身份驗證、基於HTTP基本認證、OAuth 2.0、LDAP身份驗證等。

    • 可以自定義身份驗證邏輯,例如使用自己的用戶數據庫或身份驗證服務。

  2. 授權(Authorization)

    • Spring Security允許你基於角色(Roles)、權限(Permissions)或自定義的規則來授權用戶對特定資源的訪問。

    • 你可以使用注解或XML配置來定義授權規則。

  3. 防止跨站請求偽造(CSRF)

    • Spring Security提供了防止CSRF攻擊的機制,通過生成和驗證CSRF令牌來確保請求的合法性。

  4. 防止跨站腳本攻擊(XSS)

    • Spring Security提供了內建的防止XSS攻擊的機制,可以自動對輸出進行編碼。

  5. 單點登錄(SSO)

    • Spring Security可以集成單點登錄提供者,允許用戶在多個應用程序之間共享身份驗證。

  6. 靈活的配置

    • Spring Security使用Java配置,你可以通過編寫Java類來定制安全性設置。

    • 它還支持XML配置,因此你可以根據項目需求選擇不同的配置方式。

  7. 集成性

    • Spring Security可以輕松集成到Spring應用程序中,利用Spring框架的強大特性,如依賴注入(Dependency Injection)和面向切面編程(Aspect-Oriented Programming)。

實戰練習

預計針對以下項目,練習測試

  • CORS

  • CSRF

  • AuthenticationFilter、ExceptionHandling

  • AccessDecisionManager、AccessDecisionVoter

  • SecurityMetadataSource

SQL Table建置

主要目的還是以實戰為主,所以會建立一些基本使用者、角色、權限、路由,等等基本資料表,來配置用戶權限。DB:我是使用MySQL 8.0,完整SQL 請參閱GitHub完整內容,就不附在這邊了。

Security config 基本配置

/**
 * @author caster.hsu
 * @Since 2023/5/29
 */
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
@EnableAutoConfiguration(exclude = {ErrorMvcAutoConfiguration.class})
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    private final SystemUserDetailsService userDetailsService;
    private final RolesService rolesService;
    private final UserService userService;
    private final JwtProvider jwtProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
//      @formatter:off

        http
                .cors(o -> o.configurationSource(corsConfigurationSource())) // 啟用跨域設定
                .csrf(o -> o.csrfTokenRepository(generateSCRFToken())) // 使用 CSRF 令牌保護
                .addFilterBefore(new AuthenticationFilter(userDetailsService, userService, jwtProvider), UsernamePasswordAuthenticationFilter.class) // 在驗證前添加自定義的身份驗證過濾器
                .exceptionHandling(o -> o.authenticationEntryPoint(new AuthenticationFailEntryPoint())) // 錯誤處理,指定未驗證的請求的處理方式
//                .sessionManagement(o -> o.disable()) // session 機制設定
//                .anonymous(o -> o.disable()) // 匿名訪問設定
                .authorizeRequests(o ->
                                // 這邊設定僅只忽略權限驗證, 還是會進入過濾器
//                        o.antMatchers("/system/info").permitAll()
                                o.anyRequest()
                                        .authenticated()
                                        .accessDecisionManager(accessDecisionManager())// 自定義訪問決策管理器, 可使用自訂義投票方式, 或是自行實作 DecisionManager
                                        .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                                            public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
                                                fsi.setSecurityMetadataSource(new CustomSecurityMetadataSource(rolesService)); // 使用自定義的安全元數據來源
                                                return fsi;
                                            }
                                        })
                )
        ;
        // @formatter:ON
    }
    // 其他配置 .... 詳閱 gitHub 內容
}

測試 CORS

Spring Security中的CORS(跨源資源共享)支援用於處理跨網域HTTP請求。CORS是一個Web安全性標準,允許網頁應用程序在不同源之間進行跨網域HTTP請求,以避免同源政策的限制。Spring Security提供了CORS支援,以確保你的應用程序可以安全地處理跨網域請求。

可以看 webSecurityConfig.corsConfigurationSource 設定,你可以建立一個 Controller 去測試CORS內容。

測試 CSRF

CSRF(Cross-Site Request Forgery)是一種網站應用程式中常見的安全漏洞,攻擊者通過欺騙用戶在不知情的情況下執行非意願的操作。Spring Security提供了內建的CSRF保護機制,以幫助防止這種類型的攻擊。

CSRF保護的核心思想是確保每個由用戶提交的請求都包含一個隨機生成的CSRF令牌。當用戶首次訪問應用程序時,伺服器將生成一個CSRF令牌並將其存儲在用戶的會話中。然後,每次用戶提交表單或執行操作時,應用程序都會將該令牌包含在請求中。伺服器將檢查該令牌是否有效,如果無效,請求將被拒絕。

可以看 webSecurityConfig.generateSCRFToken 設定,在默認的情況下,僅針對 Http Method 是 POSTPUTDELETE 執行 CSRF 保護。

依序請求說明

  1. 首先透過GET請求,當您查看回應的標頭(header)時,您會注意到存在一個名為"Set-Cookie"的標頭,其中包含了CSRF令牌。這個令牌是在伺服器端生成的,然後透過Cookie的方式傳遞給客戶端。

  2. 在下一次GET請求時,如果您仍然查看標頭,您會發現"Set-Cookie"標頭不再出現,這是因為Spring Security已經成功地將CSRF令牌存儲在用戶的Cookie中。因此,伺服器不再需要再次傳遞此令牌。

  3. 首次POST請求時,請確保您將CSRF令牌包含在請求中。即使Cookie中包含了令牌,如果您的POST請求未傳送CSRF令牌,Spring Security的實作將無法在指定的位置找到令牌,並且將視此請求為不合法,因此被攔截。

  4. 再次 POST請求,將CSRF令牌包含在請求中,同時也要確保將令牌放在正確的位置,通常是作為請求標頭(Header)中的一部分,默認使用"X-XSRF-TOKEN"這個標頭名稱。當您完成這些步驟,伺服器將能夠正確驗證請求並提供正確的回應。

當您使用Spring Security實作CookieCsrfTokenRepository時,您可以深入研究這個程式的內部運作方式,特別是關於如何抓取CSRF令牌的部分。這樣做可以幫助您更好地了解安全性的運作原理,同時也能夠進行自訂調整以防範潛在的攻擊。

但是,如果您有特殊需求或對安全性非常關注,您可以自行客製化CookieCsrfTokenRepository的行為。例如,您可以調整令牌的存儲位置、過期時間,或者實現額外的令牌驗證機制,以提高安全性。

總之,深入瞭解CookieCsrfTokenRepository的工作方式是提高您應用程序安全性的重要一步,同時也能夠根據實際需求進行自訂調整,以確保應用程序的安全性。

測試 AuthenticationFilter、ExceptionHandling

AuthenticationFilter 這我是自定義的身分認證過濾器,用於處理身份驗證的關鍵組件之一。它的主要目的是在HTTP請求到達應用程序之前執行身份驗證,確保只有合法的用戶才能訪問受保護的資源。

使用場景:

  1. 身份驗證: AuthenticationFilter用於驗證用戶的身份,確保他們具有訪問特定資源的權限。當用戶嘗試訪問需要身份驗證的資源時,該過濾器會介入處理。

  2. 使用者登入: 在用戶嘗試登入應用程序時,AuthenticationFilter負責處理身份驗證。它將驗證用戶提供的憑證(例如使用者名稱和密碼),並確定是否允許登入。

  3. Token驗證: 在使用令牌進行身份驗證的情況下(例如JWT驗證),AuthenticationFilter可以檢查令牌的有效性,並確保用戶是合法的。

那我這邊是使用 JWT Token 驗證,首先登入成功後,會回應一個JWT Token 給客戶端,客戶端將Token 放置在指定位置( Header Authorization: Bearer {{token}} ) ,後續請求就會針對此Token 進行驗證,方可確認用戶取得當前資源的合法性。

/**
* POST請求處理登入的端點
* @param loginReq
* @return
*/
@PostMapping(value = "/login", name = "登入")
public ResponseEntity authenticateUser(@RequestBody LoginReq loginReq) {
log.trace("current login:{}", loginReq.toString());

// 使用AuthenticationManager進行身份驗證
Authentication authentication =
        authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginReq.getLoginId(), loginReq.getPassword()));

// 從驗證結果中獲取用戶主體
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();

// 設置用戶的身份驗證信息到Spring Security的上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);

// 通過用戶ID查詢用戶信息
User user = userService.lambdaQuery().eq(User::getId, userPrincipal.getId()).one();

// 創建JWT Token
JsonWebToken jwtObj = new JsonWebToken();
Map<String, Object> extra = new HashMap<>();

extra.put("type", TokenType.LOGIN.getType());
jwtObj.setExtra(extra);
jwtObj.setExpireTimeMs(ConfigConstant.jwtExpirationInMs);
jwtObj.setSecretStr(ConfigConstant.jwtSecret);

// 生成用戶的登入Token
String loginToken = jwtProvider.generateUserLoginToken(user.getId().longValue(), jwtObj);
log.trace("current login token:{}", loginToken);

 // 其他配置 .... 詳閱 gitHub 內容

// 返回包含用戶信息和登入Token的回應
return ResponseEntity
        .ok(JSONResult.createResult(SuccessCodeMsg.COMMON_OK)
                .addResult("userInfo", user)
                .addResult("authentication", loginToken));
}

測試 AccessDecisionManager、AccessDecisionVoter

AccessDecisionManager 是 Spring Security 中用於決定是否授予訪問權限的關鍵組件之一。它的主要作用是根據已驗證的用戶、請求的資源和已授權的角色/權限,確定用戶是否被授予對特定資源的訪問權限。

使用時機:

  1. 權限控制:當你需要實現細粒度的權限控制時,可以使用 AccessDecisionManager。例如,你可能希望某些資源只能由特定角色或特定用戶訪問,而其他資源則可以被更廣泛的用戶訪問。

  2. 自定義授權邏輯:當 Spring Security 的內建授權邏輯不足以滿足你的需求時,你可以實現自己的 AccessDecisionManager 來定義自定義的授權邏輯。

  3. 多層次的授權:有些應用可能需要多層次的授權,不僅僅基於角色。這時你可以使用 AccessDecisionManager 來實現更複雜的授權邏輯,例如基於角色和其他條件的授權。

使用場景:

  1. RESTful API 的權限控制:當你構建一個 RESTful API 並需要對不同的端點進行權限控制時,AccessDecisionManager 可以幫助你確保只有授權的用戶可以訪問特定的端點。

  2. Web 應用程式的權限控制:對於傳統的 Web 應用程式,你可能希望根據用戶的角色和權限來控制對頁面或操作的訪問。AccessDecisionManager 可以處理這種情況。

  3. 動態權限管理:有些場景中,權限可能是動態的,需要在運行時進行計算。AccessDecisionManager 可以用於這種情況,讓你能夠根據具體情況調整權限。

AccessDecisionManagerAccessDecisionVoter 是 Spring Security 中用於授權管理的關鍵組件,它們之間有著緊密的交互關係,讓你能夠靈活地定義權限控制邏輯。

  1. AccessDecisionManager

    • AccessDecisionManager 是一個核心介面,用於最終判斷是否授予訪問權限。

    • 它通常包含一個或多個 AccessDecisionVoter 實例,這些投票者用於進行具體的權限投票。

    • 它的 decide 方法接收一個 Authentication 對象(已驗證的用戶)、一個 Object 對象(要訪問的資源,通常是 FilterInvocation 對象),以及一個權限列表(通常是角色或權限列表)。

    • AccessDecisionManager 將根據投票者的結果來決定是否授予訪問權限。通常,如果有一個投票者認為允許訪問,那麼訪問就會被授予。

  2. AccessDecisionVoter

    • AccessDecisionVoter 是用於對單個權限(如角色或權限)進行投票的組件。

    • 它有三種可能的投票結果:同意、反對、棄權。

    • AccessDecisionVoter 的實現類通常根據自己的邏輯來決定是否同意或反對訪問。

    • 通常,每個權限(角色或權限)都有對應的投票者。例如,有一個 RoleVoter 用於處理角色,有一個 AuthenticatedVoter 用於處理是否已驗證,等等。

下面是一個簡單的使用範例,演示了如何配置 AccessDecisionManagerAccessDecisionVoter

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // ... 其他配置
    @Bean
    public AccessDecisionManager accessDecisionManager() {
        /**
         * 可在這邊自訂義決策管理以及投票機制, 方便調整權限控制的顆粒度. 使用default spring boot security 提共的
         * 或是可自行實作 AccessDecisionManager
         * 自定義的投票者,可以根據需要添加更多
         */
        // 使用 AffirmativeBased 方式進行投票決策
        return new AffirmativeBased(
                Arrays.asList(
                        new APIPermissionVoter(userService),    // Api 資源權限檢查
                        new UserTypeVoter(),                    // 使用者類型檢查
                        new PublicResourceVoter()));            // 公共資源檢查
    }

    // 其他配置 .... 
}

在上述範例中,我們配置了自定義的 AccessDecisionManager,並添加了不同的 AccessDecisionVoter,包括 RoleVoterAuthenticatedVoter。你還可以添加自己的投票者,以實現更複雜的權限控制邏輯。這樣,每個投票者都可以對訪問請求進行投票,最終決定是否授予訪問權限。

Spring Security 中的 AffirmativeBased 與其他主要實現方式的比較:

  1. AffirmativeBased:

    • AffirmativeBased 是一個 Affirmative Access Decision Manager。它的工作方式是只要有一個投票者同意授予訪問權限,就會授予權限。如果沒有投票者同意,則被拒絕。 範例一:3個同意、1個棄權、2個反對; 結果:同意。 範例二:0個同意、0個反對、6個棄權; 可額外設定當全部棄權時處理結果,預設反對。

    • 這意味著,如果有一個投票者認為訪問是合法的,即使其他投票者反對,也會被授予訪問權限。

  2. ConsensusBased:

    • ConsensusBased 是一個一致性 Access Decision Manager。它的工作方式是根據所有投票者的結果來決定是否授予權限。

    • 只有當多數投票者同意授予權限時,才會授予權限。 範例一:3個同意、1個棄權、2個反對; 結果:同意。 範例二:3個同意、3個反對; 可額外設定票數相同時的處理結果,預設同意。

  3. UnanimousBased:

    • UnanimousBased 是一個一致性只有當所有投票者同意(忽略棄權)授予權限時,才會授予權限。如果有任何一個投票者反對,則被拒絕。

    • 但與 ConsensusBased 不同,UnanimousBased 要求所有投票者都必須同意,即使一個投票者反對,也會被拒絕。

比較差異:

  • AffirmativeBased 適用於大多數場景,因為它只要有一個投票者同意,就能授予權限,這更具靈活性。

  • ConsensusBasedUnanimousBased 更嚴格,要求所有投票者達成共識,因此在需要嚴格的授權控制場景中使用。

你可以根據你的項目需求選擇適合的 AccessDecisionManager。通常,使用 AffirmativeBased 即可,但在特定情況下,如需要高度安全的場景,你可以考慮使用 ConsensusBasedUnanimousBased 以實現更嚴格的權限控制。

測試 SecurityMetadataSource

SecurityMetadataSource 是 Spring Security 中的一個介面,它用於提供關於應用程式中安全性資源(例如 URL、方法或其他資源)的訪問控制訊息。它通常用於在基於角色的訪問控制(Role-Based Access Control,RBAC)中,幫助 Spring Security 確定特定用戶是否有權限訪問某個資源。當一個用戶嘗試訪問一個受保護的資源時,Spring Security 會使用 SecurityMetadataSource 來查找該資源的相關訪問控制資訊。

@Override
protected void configure(HttpSecurity http) throws Exception {
        http
                .cors(o -> o.configurationSource(corsConfigurationSource())) // 啟用跨域設定
                .csrf(o -> o.csrfTokenRepository(generateSCRFToken())) // 使用 CSRF 令牌保護
                .addFilterBefore(new AuthenticationFilter(userDetailsService, userService, jwtProvider), UsernamePasswordAuthenticationFilter.class) // 在驗證前添加自定義的身份驗證過濾器
                .exceptionHandling(o -> o.authenticationEntryPoint(new AuthenticationFailEntryPoint())) // 錯誤處理,指定未驗證的請求的處理方式
        //                .sessionManagement(o -> o.disable()) // session 機制設定
        //                .anonymous(o -> o.disable()) // 匿名訪問設定
                .authorizeRequests(o ->
                                // 這邊設定僅只忽略權限驗證, 還是會進入過濾器
        //                        o.antMatchers("/system/info").permitAll()
                                o.anyRequest()
                                        .authenticated()
                                        .accessDecisionManager(accessDecisionManager())// 自定義訪問決策管理器, 可使用自訂義投票方式, 或是自行實作 DecisionManager
                                        .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                                            public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
                                                fsi.setSecurityMetadataSource(new CustomSecurityMetadataSource(rolesService)); // 使用自定義的安全元數據來源
                                                return fsi;
                                            }
                                        })
                )
        ;
}
public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    // 其他配置....

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        FilterInvocation fi = (FilterInvocation) object;

        /**
         * 如果需要自訂義路由權限, 需要在這邊設定當前的Path 權限是甚麼, 後續再透過 AccessDecisionManager 進行投票決策.
         */
        log.debug("CustomSecurityMetadataSource incoming url:{}", fi.getHttpRequest().getServletPath());
        // 查找當前路由,所需的角色權限有哪些
        List<Roles> rolesList = rolesService.selectJoinList(Roles.class,
                new MPJLambdaWrapper<>().selectAll(Roles.class)
                        .innerJoin(RolePermission.class, on -> on.eq(RolePermission::getRoleId, Roles::getId))
                        .innerJoin(PermissionApi.class, on -> on.eq(PermissionApi::getPermissionId, RolePermission::getPermissionId))
                        .innerJoin(ApiUrl.class, on ->
                                on.eq(ApiUrl::getId, PermissionApi::getApiId)
                                        .eq(ApiUrl::getApiUrl, fi.getHttpRequest().getServletPath())
                                        .eq(ApiUrl::getApiHttpMethod, fi.getHttpRequest().getMethod())));

        if (Objects.isNull(rolesList) || rolesList.isEmpty())
            return SecurityConfig.createList(SystemConstants.ROLE_ADMIN); // 沒有記錄此api 給予此路徑為 最高管理員權限才可訪問
        else
            return SecurityConfig.createList(rolesList.stream().map(Roles::getName).collect(Collectors.toList()).toArray(String[]::new));

//        return null; // 默認通過不驗證路由權限
    }

    // 其他配置....
}

在這個配置中,我們將 CustomSecurityMetadataSource 設置為 FilterSecurityInterceptorSecurityMetadataSource

這樣,當用戶嘗試訪問受保護的 URL 時,Spring Security 將使用 MySecurityMetadataSource 來確定是否有權限訪問該 URL 亦或者是將其URL賦予一個基本訪問權限,以提高請求資源的安全性。

GitHub:前往Spring Secuirty 基本範例

總之,Spring Security是一個非常強大且廣泛使用的安全性框架,它可以幫助開發人員實現應用程序的身份驗證和授權需求,同時提供了各種保護應用程序免受常見的Web安全性攻擊的機制。

Last updated