十、Spring Boot整合Spring Security之HTTP請求授權

那你为何对我三笑留情發表於2024-12-02

目錄
  • 前言
  • 一、HTTP請求授權工作原理
  • 二、HTTP請求授權配置
    • 1、新增使用者許可權
    • 2、配置ExceptionTranslationFilter自定義異常處理器
    • 3、HTTP請求授權配置
  • 三、測試介面
    • 1、測試類
    • 2、測試
  • 四、總結

前言

本文介紹HTTP請求授權工作原理、配置及適用場景,配合以下內容觀看效果更佳!!!

  • 什麼是授權,授權有哪些流程,Spring Security的授權配置有幾種?請檢視九、Spring Boot整合Spring Security之授權概述
  • HTTP請求授權的實現原理是什麼,如何配置HTTP請求授權?請檢視十、Spring Boot整合Spring Security之HTTP請求授權
  • 方法授權的實現原理是什麼,如何配置方法授權?請檢視十一、Spring Boot整合Spring Security之方法授權
  • 如何實現基於RBAC模型的授權方式?請檢視十二、Spring Boot整合Spring Security之基於RBAC模型的授權

一、HTTP請求授權工作原理

​ 基於Spring Security最新的Http請求授權講解,不再使用舊版的請求授權

  1. 授權過濾器AuthorizationFilter獲取認證資訊
  2. 呼叫RequestMatcherDelegatingAuthorizationManager的check方法驗證該使用者是否具有該請求的授權
  3. RequestMatcherDelegatingAuthorizationManager根據配置的請求和授權關係校驗使用者是否具有當前請求的授權並返回授權結果
  4. AuthorizationFilter處理授權結果,授權成功則繼續呼叫過濾器鏈,否則丟擲AccessDeniedException異常
  5. 認證失敗時,ExceptionTranslationFilter處理AccessDeniedException異常,如果是當前認證是匿名認證或者RememberMe認證則呼叫AuthenticationEntryPoint的commence方法,否則呼叫AccessDeniedHandler的handler方法
  6. 工作原理圖如下

authorizationfilter

二、HTTP請求授權配置

1、新增使用者許可權

package com.yu.demo.spring.impl;

import com.yu.demo.entity.UserDetailsImpl;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    //@Autowired
    //private UserService userService;
    // @Autowired
    //private UserRoleService userRoleService;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //TODO 透過username從資料庫中獲取使用者,將使用者轉UserDetails
        //User user = userService.getByUsername(username);
        //TODO 從資料庫實現查詢許可權並轉化為List<GrantedAuthority>
        //List<String> roleIds = userRoleService.listRoleIdByUsername(username);
        //List<GrantedAuthority> grantedAuthorities = new ArrayList<>(roleIds.size());
        //roleIds.forEach(roleId -> grantedAuthorities.add(new SimpleGrantedAuthority(roleId)));
        //return new User(username, user.getPassword(), user.getEnable(), user.getAccountNonExpired(), user.getCredentialsNonExpired(), user.getAccountNonLocked(), user.getAuthorities());
        //測試使用,指定許可權
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        //與hasXxxRole匹配時新增ROLE_字首
        grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
        //與hasXxxAuthority匹配時原始值
        grantedAuthorities.add(new SimpleGrantedAuthority("OPERATE"));
        //{noop}不使用密碼加密器,密碼123的都可以驗證成功
        UserDetailsImpl userDetails = new UserDetailsImpl(username, "{noop}123", true, true, true, true, grantedAuthorities);
        //userDetails中設定token,該token只是實現認證流程,未使用jwt
        userDetails.setToken(UUID.randomUUID().toString());
        return userDetails;
    }

}

2、配置ExceptionTranslationFilter自定義異常處理器

  • 因AuthorizationFilter授權失敗時會丟擲異常,該異常由ExceptionTranslationFilter處理,所以要配置自定義的異常處理器。

  • 自定義AccessDeniedHandler和AuthenticationEntryPoint異常處理器(可以用一個類實現認證授權相關的所有介面,也可以使用多個類分別實現)。

package com.yu.demo.spring.impl;


import com.yu.demo.entity.ApiResp;
import com.yu.demo.entity.UserDetailsImpl;
import com.yu.demo.util.SpringUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

@Component
public class LoginResultHandler implements AuthenticationSuccessHandler, LogoutSuccessHandler, AuthenticationEntryPoint, AuthenticationFailureHandler, AccessDeniedHandler {

    /**
     * 登入成功
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) authentication;
        UserDetailsImpl userDetailsImpl = (UserDetailsImpl) usernamePasswordAuthenticationToken.getPrincipal();
        //token返回到前端
        SpringUtil.respJson(response, ApiResp.success(userDetailsImpl.getToken()));
    }

    /**
     * 登入失敗
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        SpringUtil.respJson(response, ApiResp.loginFailure());
    }

    /**
     * 登出成功
     */
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        SpringUtil.respJson(response, ApiResp.success());
    }

    /**
     * 未登入呼叫需要登入的介面時
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        SpringUtil.respJson(response, ApiResp.notLogin());
    }

    /**
     * 已登入呼叫未授權的介面時
     */
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        SpringUtil.respJson(response, ApiResp.forbidden());
    }
}

  • 配置異常處理:
                //異常處理配置
                .exceptionHandling(exceptionHandlingCustomizer -> exceptionHandlingCustomizer
                        //授權失敗處理器(登入賬號訪問未授權的資源時)
                        .accessDeniedHandler(loginResultHandler)
                        //登入失敗處理器(匿賬號訪問需要未授權的資源時)
                        .authenticationEntryPoint(loginResultHandler))

3、HTTP請求授權配置

  • 本文使用最新的authorizeHttpRequests(AuthorizationFilter+AuthorizationManager)配置,不在使用authorizeRequests(FilterSecurityInterceptor+AccessDecisionManager+AccessDecisionVoter)
  • 請求和授權配置成對出現,配置在前的優先順序更高
  • 請求種類
    • antMatchers:Ant風格的路徑模式,?(匹配一個字元)、*(匹配零個或多個字元,但不包括目錄分隔符)、**(匹配零個或多個目錄)
    • mvcMatchers:Spring MVC的路徑模式,支援路徑變數和請求引數
    • regexMatchers:正規表示式路徑模式
    • requestMatchers:實現RequestMatcher自定義匹配邏輯
    • anyRequest:未匹配的其他請求,只能有一個且只能放在最後
  • 授權種類
    • permitAll:匿名或登入使用者都允許訪問
    • denyAll:匿名和登入使用者都不允許訪問
    • hasAuthority:有配置的許可權允許訪問,AuthorityAuthorizationManager校驗
    • hasRole:有配置的角色允許訪問,ROLE_{配置角色}與使用者許可權匹配,AuthorityAuthorizationManager校驗
    • hasAnyAuthority:有配置的任意一個許可權的允許訪問,AuthorityAuthorizationManager校驗
    • hasAnyRole:有配置的任意一個角色允許訪問,ROLE_{配置角色}與使用者許可權匹配,AuthorityAuthorizationManager校驗
    • authenticated:已認證(不包括匿名)的允許訪問,AuthenticatedAuthorizationManager校驗
    • access:自定義授權處理
  • 因authorizeHttpRequests不支援使用anonymous()的方式配置匿名訪問,未自定義匿名角色時可以透過hasRole("ANONYMOUS")或者hasAuthority("ROLE_ANONYMOUS")或其他類似的方式實現允許匿名請求的設定

  • http請求授權配置
                //http請求授權
                .authorizeHttpRequests(authorizeHttpRequestsCustomizer -> authorizeHttpRequestsCustomizer
                        //不允許訪問
                        .antMatchers("/test/deny")
                        .denyAll()
                        //允許匿名訪問
                        .antMatchers("/test/anonymous")
                        .hasRole("ANONYMOUS")
                        //允許訪問
                        .antMatchers("/test/permit")
                        .permitAll()
                        //測試使用:擁有ADMIN角色
                        .antMatchers("/test/admin")
                        //擁有ROLE_ADMIN許可權,配置的角色不能以ROLE_作為字首
                        .hasRole("ADMIN")
                        //測試使用:擁有OPERATE許可權
                        .antMatchers("/test/operate")
                        //擁有OPERATE許可權
                        .hasAuthority("OPERATE")
                        //其他的任何請求
                        .anyRequest()
                        //需要認證,且不能是匿名
                        .authenticated())
  • 完整過濾器鏈配置
package com.yu.demo.config;

import com.yu.demo.spring.filter.RestfulLoginConfigurer;
import com.yu.demo.spring.filter.RestfulUsernamePasswordAuthenticationFilter;
import com.yu.demo.spring.impl.LoginResultHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.context.SecurityContextRepository;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    //登入引數使用者名稱
    private static final String LOGIN_ARG_USERNAME = "username";
    //登入引數密碼
    private static final String LOGIN_ARG_PASSWORD = "password";
    //登入請求型別
    private static final String LOGIN_HTTP_METHOD = HttpMethod.POST.name();
    //登入請求地址
    private static final String LOGIN_URL = "/login";
    //登出請求地址
    private static final String LOGOUT_URL = "/logout";

    @Autowired
    private LoginResultHandler loginResultHandler;
    @Autowired
    private SecurityContextRepository securityContextRepository;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                //禁用UsernamePasswordAuthenticationFilter、DefaultLoginPageGeneratingFilter、DefaultLogoutPageGeneratingFilter
                .formLogin(FormLoginConfigurer::disable)
                //禁用BasicAuthenticationFilter
                .httpBasic(HttpBasicConfigurer::disable)
                //禁用CsrfFilter
                .csrf(CsrfConfigurer::disable)
                //禁用SessionManagementFilter
                .sessionManagement(SessionManagementConfigurer::disable)
                //異常處理配置
                .exceptionHandling(exceptionHandlingCustomizer -> exceptionHandlingCustomizer
                        //授權失敗處理器(登入賬號訪問未授權的資源時)
                        .accessDeniedHandler(loginResultHandler)
                        //登入失敗處理器(匿賬號訪問需要未授權的資源時)
                        .authenticationEntryPoint(loginResultHandler))
                //http請求授權
                .authorizeHttpRequests(authorizeHttpRequestsCustomizer -> authorizeHttpRequestsCustomizer
                        //不允許訪問
                        .antMatchers("/test/deny")
                        .denyAll()
                        //允許匿名訪問
                        .antMatchers("/test/anonymous")
                        .hasRole("ANONYMOUS")
                        //允許訪問
                        .antMatchers("/test/permit")
                        .permitAll()
                        //測試使用:擁有ADMIN角色
                        .antMatchers("/test/admin")
                        //擁有ROLE_ADMIN許可權,配置的角色不能以ROLE_作為字首
                        .hasRole("ADMIN")
                        //測試使用:擁有OPERATE許可權
                        .antMatchers("/test/operate")
                        //擁有OPERATE許可權
                        .hasAuthority("OPERATE")
                        //其他的任何請求
                        .anyRequest()
                        //需要認證,且不能是匿名
                        .authenticated())
                //安全上下文配置
                .securityContext(securityContextCustomizer -> securityContextCustomizer
                        //設定自定義securityContext倉庫
                        .securityContextRepository(securityContextRepository)
                        //顯示儲存SecurityContext,官方推薦
                        .requireExplicitSave(true))
                //登出配置
                .logout(logoutCustomizer -> logoutCustomizer
                        //登出地址
                        .logoutUrl(LOGOUT_URL)
                        //登出成功處理器
                        .logoutSuccessHandler(loginResultHandler)
                )
                //註冊自定義登入過濾器的配置器:自動註冊自定義登入過濾器;
                //需要重寫FilterOrderRegistration的構造方法FilterOrderRegistration(){},在構造方法中新增自定義過濾器的序號,否則註冊不成功
                .apply(new RestfulLoginConfigurer<>(new RestfulUsernamePasswordAuthenticationFilter(LOGIN_ARG_USERNAME, LOGIN_ARG_PASSWORD, LOGIN_URL, LOGIN_HTTP_METHOD), LOGIN_URL, LOGIN_HTTP_METHOD))
                //設定登入地址:未設定時系統預設生成登入頁面,登入地址/login
                .loginPage(LOGIN_URL)
                //設定登入成功之後的處理器
                .successHandler(loginResultHandler)
                //設定登入失敗之後的處理器
                .failureHandler(loginResultHandler);

        //建立過濾器鏈物件
        return httpSecurity.build();
    }

}

三、測試介面

1、測試類

package com.yu.demo.web;

import com.yu.demo.entity.ApiResp;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/hello")
    public ApiResp hello() {
        return ApiResp.success("hello");
    }

    /**
     * 匿名允許訪問介面地址
     */
    @GetMapping("/anonymous")
    public ApiResp anonymous() {
        return ApiResp.success("anonymous");
    }

    /**
     * 禁止訪問介面地址
     */
    @GetMapping("/deny")
    public ApiResp deny() {
        return ApiResp.success("deny");
    }

    /**
     * 允許訪問介面地址
     */
    @GetMapping("/permit")
    public ApiResp permit() {
        return ApiResp.success("permit");
    }

    /**
     * 擁有ADMIN角色或ROLE_ADMIN許可權允許訪問介面地址
     */
    @GetMapping("/admin")
    public ApiResp admin() {
        return ApiResp.success("admin");
    }

    /**
     * 擁有OPERATE許可權的允許訪問介面地址
     */
    @GetMapping("/operate")
    public ApiResp operate() {
        return ApiResp.success("operate");
    }

}

2、測試

  1. 登入獲取token

  1. admin介面測試

  1. 其他介面不在一一測試,有疑問或問題評論或私聊

四、總結

  1. 授權是拿使用者的許可權和可以訪問介面的許可權進行匹配,匹配成功時授權成功,匹配失敗時授權失敗
  2. 使用者的許可權物件是SimpleGrantedAuthority,字串屬性role
  3. 介面的role許可權會透過ROLE_{role}轉化為SimpleGrantedAuthority及其字串屬性role
  4. 介面的authority許可權會直接轉化為SimpleGrantedAuthority及其字串屬性role
  5. 擁有ROLE_ANONYMOUS許可權或者ANONYMOUS角色可以訪問匿名介面
  6. 後續會講使用HTTP請求授權+自定義AuthorizationManager方式實現基於RBAC許可權模型,歡迎持續關注
  7. 原始碼下載

相關文章