SpringSecurity系列學習(一):初識SpringSecurity

系列導航

SpringSecurity系列

SpringSecurityOauth2系列

SpringSecurity

Spring Security是spring採用AOP思想,基於servlet過濾器實現的安全框架。它提供了完善的認證機制和方法級的

授權功能。是一款非常優秀的許可權管理框架。

學習SpringSecurity,一般都是從前後端不分離架構開始學習,然後學習前後端分離的JWT + SpringSecurity架構,之後再學習SpringSecurity + Oauth2微服務架構。

現在大部分項目都是前後端分離的,為什麼還需要去看前後端不分離架構下SpringSecurity的一些東西呢?其實這部分的學習只是為了打一個基礎,SpringSecurity的發展也是從前後端不分離開始的,不論是後來的前後端分離架構還是微服務架構,SpringSecurity的主要邏輯都是大同小異的。

當然這部分的學習我們先不進行編碼,主要是去看概念和源碼,因為在做項目的時候,主要還是採用的前後端分離的JWT + SpringSecurity架構或者SpringSecurity + Oauth2微服務架構,編碼我們從第二章開始,這一章我們先看看SpringSecurity中的一些基礎的東西。

認證和授權

說到SpringSecurity就要說到它的核心功能:認證和授權

認證:我是誰的問題,也就是我們通常說的登陸

授權:身份驗證,我能幹什麼。

認證和授權在SpringSecurity中是怎麼樣的流程呢?

這裡我們寫一個簡單的demo,來看一下在SpringSecurity中認證和授權的流程

認證Demo

新建一個springboot工程,引入依賴

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

引入依賴之後,SpringSecurity就已經有默認的配置了,這個時候寫一個簡單的控制器訪問,會被SpringSecurity保護攔截。

/**
 * @author 硝酸銅
 * @date 2021/6/2
 */
@RestController
@RequestMapping(value = "/api")
public class UserResource {

    @GetMapping(value = "/greeting")
    public String greeting(){

        return "Hello World";
    }
}

啟動項目,訪問//localhost:8080/greeting,會被SpringSecurity攔截,重定向到//localhost:8080/login進行登錄,這個頁面是SpringSecurity默認的登陸頁面

默認的用戶名是:user,密碼會在控制台輸出出來:

登錄之後,正常進行業務:

如果我們不使用網頁去調用介面,而是使用postman這類工具去調用介面該怎麼進行認證呢?

默認情況下,SpringSecurity會接受請求頭中的Authorization的值去進行認證,以Basic 開頭,後接帳號密碼,比如在請求介面的時候,添加請求頭Authorization:Basic user a76dbd63-65d2-4cff-aebc-cc5dc4a6973d

這樣就不會被重定向到登陸頁面,而是直接通過認證。

授權demo

SpringSecurity默認配置下,所有介面只要認證通過即可訪問,如果我們需要對一個介面進行限制,必須有哪一種許可權才能訪問,則需要進行安全配置

/**
 * @author 硝酸銅
 * @date 2021/6/2
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests(req -> req.mvcMatchers("/api/greeting").hasAnyRole("ADMIN"));
    }
}

具體為什麼這麼寫我們先不討論,這裡的意思就是訪問/api/greeing這個路徑需要有ADMIN這個角色,重新啟動項目,訪問該路徑:

403禁止訪問,未授權,沒有該許可權

我們現在給用戶授權:

	@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin(Customizer.withDefaults())
                ///api/greeting 路徑需要檢查認證資訊
                .authorizeRequests(req -> req.mvcMatchers("/api/greeting").authenticated());
    }

這裡的意思是,我們不再檢查許可權,只檢查該認證資訊,重新啟動,訪問該路徑:

這就是在SpringSecurity中的認證和授權的過程,其中的具體邏輯和源碼,我們在後面進行詳細學習,現在小夥伴們先了解個大概

安全配置

一開始我們引入SS的時候,會生成默認的配置,比如默認的表單登錄頁面,HTTP BASIC認證等等,其本質就是WebSecurityConfigurerAdapter這個基類帶來的配置

public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> {
    ...
    protected void configure(HttpSecurity http) throws Exception {
        this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
        http.authorizeRequests((requests) -> {
            // 所有的介面都需要通過認證
            ((AuthorizedUrl)requests.anyRequest()).authenticated();
        });
        // 默認的表單登陸頁面
        http.formLogin();
        // 使用HTTP BASIC認證,也就是請求頭中的Authorization:Basic username passowrd
        http.httpBasic();
    }
    ...
}

這個默認的方法分為三個部分:

  1. 配置認證請求
  2. 配置表單
  3. 配置HttpBasic

這三個部分可以通過and()來連接,and()返回一個HttpSecurity,形成鏈式寫法。

如果用函數式寫法(推薦),直接就能使用鏈式寫法。

如果我們需要自定義安全配置,則需要繼承WebSecurityConfigurerAdapter這個基類,重寫configure方法。

import org.springframework.security.config.Customizer;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;

/**
 * `@EnableWebSecurity` 註解 deug參數為true時,開啟調試模式,會有更多的debug輸出,不要用在生產環境
 * @author 硝酸銅
 * @date 2021/6/2
 */
@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //取消CSRF保護
                .csrf(AbstractHttpConfigurer::disable)
                //默認的HTTP Basic Auth認證
                .httpBasic(Customizer.withDefaults())
                //默認的表單登錄
                //.formLogin(Customizer.withDefaults())
                //關閉表單登錄
                .formLogin(AbstractHttpConfigurer::disable)
                //對 /api 路徑下的所有介面進行驗證,需要許可權`ROLE_USER`
                .authorizeRequests(req -> req.antMatchers("/api/**").hasAnyRole("USER"));
    }
  
  	@Override
    public void configure(WebSecurity web) {
        web
                .ignoring()
                .antMatchers("/error",
                        "/resources/**",
                        "/static/**",
                        "/public/**",
                        "/h2-console/**",
                        "/swagger-ui.html",
                        "/swagger-ui/**",
                        "/v3/api-docs/**",
                        "/v2/api-docs/**",
                        "/doc.html",
                        "/swagger-resources/**")
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }
}

重寫configure(HttpSecurity http)讓我們可以配置認證和授權,也就是說走到這個方法的時候,是經過了過濾器鏈的。

啟動過濾器鏈是很昂貴的,佔用了系統很多資源,有時候我們經過一個路徑(比如訪問靜態資源:圖片,影片等),不需要進行認證和授權,也就不需要啟動過濾器鏈,為了節約系統資源,可以通過重寫configure(WebSecurity web)方法來禁用過濾器鏈

一些前後端不分離的安全配置概念(了解即可)

CSRF攻擊

CSRF攻擊對於無狀態應用(前後端分離,使用token,天然免疫)來說是無效的,只有Session類應用需要去預防

當進行登錄的時候,如果沒有禁用CSRF配置,那麼每個POST請求必須攜帶一個CSRF Token,否則不予授權

為什麼會有這樣一個配置呢,這首先要從CSRF攻擊說起

這種攻擊的前提條件是:用戶已經登錄正常站點

很多網站的登錄狀態都是一個有時間周期的Session,這種攻擊就是利用這一點。

當一個受害用戶已經正常的登錄過一個站點,並且這個登錄的Session還在有效期內時,一個惡意用戶發起一個鏈接給受害用戶,比如發起一個銀行賬戶變更通知的鏈接,然後受害用戶登錄點擊進去,那個惡意頁面也和正常的銀行頁面長得非常像。

這個惡意頁面要求受害用戶輸入他的銀行賬戶,密碼,姓名等敏感資訊。等受害用戶輸入之後,這個惡意頁面就將這些資訊發送給網銀,由於受害用戶已經登錄過網銀,並且其Session還沒有過期,這些惡意頁面發送的數據就等於是在受害用戶許可之下發送的,受害用戶的網銀就被輕鬆攻破了。

防止受到CSRF攻擊的方式

第一種:CSRF Token

由伺服器生成,並設置到瀏覽器Cookie當中,前端每次都會從cookie中將這個token讀取出來,服務端要求每個請求都需要帶上這個token。提交到服務端之後,服務端會比較CSRF Token,看他是不是和服務端保存在Session中的token一致。這個token每個請求都是不一樣的

第二種:在響應當中設置Cookie的SameSite屬性

private AuthenticationSuccessHandler jsonLoginSuccessHandler(){
       return (req,res,auth) ->{
           //..
           Collection<String > headers = res.getHeaders(HttpHeaders.SET_COOKIE);
           res.addHeader(HttpHeaders.SET_COOKIE,String.format("%s; %s",header,"SameSite=Strict"));
       };
    }

即在響應當中的Cookie當中設置SameSite屬性

但是這個對於瀏覽器兼容性來說不友好,ie不支援。

所以現在主流還是CSRF Token方法

設置CSRF

http.csrf(csrf -> {
            //保存策略,可以保存在在session(HttpSessionCsrfTokenRepository)或者cookie(CookieCsrfTokenRepository)中
            csrf.csrfTokenRepository()
                    //忽略哪些路徑
                    .ignoringRequestMatchers()
                    //哪些需要保護
                    .requireCsrfProtectionMatcher();
        })

Remember me 功能

基於Session的功能:Session過期後,用戶不需要登錄就能直接訪問

SpringSecurity提供開箱即用的配置rememberMe

原理:使用Cookie存儲用戶名,過期時間,以及一個Hash,Hash:md5(用戶名+過期時間+密碼+key)

當用戶訪問的時候,會判斷Session有沒有過期,如果過期了,就直接導到登錄頁。

如果沒有過期,服務端就根據用戶名,從資料庫裡面查到的用戶名,密碼,過期時間,key,進行md5加密,然後與客戶端提交的md5進行對比,如果一致,則認證成功。

注意:md5加密中有密碼,也就是說如果用戶修改了密碼,則需要重新登錄。

http.rememberMe(rememberMe -> {
            //存儲策略,
            rememberMe.tokenRepository()
                    //設置Cookie名稱
                    .rememberMeCookieName()
                    //有效期設置,單位s
                    .tokenValiditySeconds()
                    //設置用戶查詢服務,實現UserDetailsService介面的類,提供根據用戶名查詢用戶的方法
                    .userDetailsService()
                    //是否用安全的Cookie
                    .useSecureCookie();
        })

退出

前後端不分離的退出設置

http
                .logout(logout -> {
                    //退出登錄的url
                    logout.logoutUrl()
                            //退出登錄成功,重定向的url
                            .logoutSuccessUrl()
                            //設置LogoutHandler,自定義退出登錄邏輯
                            .addLogoutHandler()
                            //刪除Cookies
                            .deleteCookies()
                            //取消Session
                            .invalidateHttpSession()
                            //清理認證
                            .clearAuthentication();
                })

前後端分離的登陸和退出採用增加過濾器或者介面的方式,不需要使用這個配置

Spring Security過濾器鏈

過濾器

其實任何的Spring Web程式,在本質上都是一個Servlet程式

Spring Security Filter在HTTP請求到達你的Controller之前,過濾每一個傳入的HTTP請求

  1. 首先,過濾器需要從請求中提取一個用戶名/密碼。它可以通過一個基本的HTTP頭,或者表單欄位,或者cookie等等。
  2. 然後,過濾器需要對用戶名/密碼組合進行驗證比如資料庫。
  3. 在驗證成功後,過濾器需要檢查用戶是否被授權訪問請求的URI。
  4. 如果請求通過了所有這些檢查,那麼過濾器就可以讓請求通過你的DispatcherServlet後重定向到@Controllers或者@RestController

要使Spring Security生效,從可行性上來說,我們需要有一個Spring Security的Filter能夠被Servlet容器(比如Tomcat、Undertow等)感知到,這個Filter便是DelegatingFilterProxy,該Filter並不受Spring IoC容器的管理,也不是Spring Security引入的,而是Spring Framework中的一個通用的Filter。在Servlet容器眼中,DelegatingFilterProxy只是一個Filter而已,跟其他的Servlet Filter沒什麼卻別。

雖然DelegatingFilterProxy本身不在IoC容器中,它卻能夠訪問到IoC容器中的其他對象(通過WebApplicationContextUtils.getWebApplicationContext可以獲取到IoC容器,進而操作容器中的Bean),這些對象才是真正完成Spring Security邏輯的對象。這些對象中的部分對象本身也實現了javax.servlet.Filter介面,但是他們並不能被Servlet容器感知到,比如UsernamePasswordAuthenticationFilter

過濾器鏈

通過這個過濾器示例,可以了解到通過過濾器完成認證和授權的基本過程。

在SpringSecurity中,這一過程不是通過一個過濾器來完成的,而是一系列的過濾器,也就是一個過濾器鏈,認證有認證的過濾器,授權有授權的過濾器,除此之外還有更多的,不同功能的過濾器

這種過濾器鏈的好處:

  1. 每個過濾器的職責單一
  2. 鏈式處理是一種比較好的方式,由簡單的邏輯構成複雜的邏輯

當一個項目啟動的時候,其Spring Security的日誌輸出:

2021-09-18 14:10:50.935  INFO 8265 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@56da7487, org.springframework.security.web.context.SecurityContextPersistenceFilter@6f94a5a5, org.springframework.security.web.header.HeaderWriterFilter@7ceb4478, org.springframework.security.web.authentication.logout.LogoutFilter@7cbeac65, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@a451491, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@a92be4f, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@10f7c76, org.springframework.security.web.session.SessionManagementFilter@25ad4f71, org.springframework.security.web.access.ExceptionTranslationFilter@77bbadc, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@2b680207]

這就是Spring Security的過濾器鏈

重新訪問/api/greeing這個路徑,我們來看看日誌:

2021-09-18 14:10:54.545 DEBUG 8004 --- [nio-8080-exec-1] FilterSecurityInterceptor                : Failed to authorize filter invocation [GET /api/greeting] with attributes [authenticated]
2021-09-18 14:10:54.545 DEBUG 8004 --- [nio-8080-exec-1] HttpSessionRequestCache                  : Saved request //localhost:8080/api/greeting to session
2021-09-18 14:10:54.545 DEBUG 8004 --- [nio-8080-exec-1] DefaultRedirectStrategy                  : Redirecting to //localhost:8080/login

認證失敗,重定向到了login

登錄之後:

2021-09-18 14:11:03.247 DEBUG 8004 --- [nio-8080-exec-6] DaoAuthenticationProvider                : Authenticated user
... ... 
2021-09-18 14:11:03.247 DEBUG 8004 --- [nio-8080-exec-6] DefaultRedirectStrategy                  : Redirecting to //localhost:8080/api/greeting
... ...
2021-09-18 14:11:03.247 DEBUG 8004 --- [nio-8080-exec-9] FilterSecurityInterceptor                : Authorized filter invocation [GET /api/greeting] with attributes [authenticated]
2021-09-18 14:11:03.247 DEBUG 8004 --- [nio-8080-exec-9] FilterChainProxy                         : Secured GET /api/greeting

常見的內建過濾器鏈

SpringSecurity過濾器很多,並且還可以自己添加過濾器,如何添加過濾器我們之後在分析認證流程源碼的時候會介紹。

不需要將每個SpringSecurity過濾器都搞明白,只需要知道一些常見的過濾器的作用就行了

org.springframework.security.web.authentication.www.BasicAuthenticationFilter

此過濾器會自動解析HTTP請求中頭部名字為Authentication,且以Basic開頭的頭資訊。

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

認證操作全靠這個過濾器,默認匹配URL為/login且必須為POST請求。之後我們自定義認證流程其實也是通過重寫這個過濾器實現。

org.springframework.security.web.authentication.AnonymousAuthenticationFilter

SecurityContextHolder中認證資訊為空,則會創建一個匿名用戶存入到SecurityContextHolder中。SecurityContextHolder是什麼在下一章解釋

spring security為了兼容未登錄的訪問,也走了一套認證流程,只不過是一個匿名的身份。

org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter

如果沒有在配置文件中指定認證頁面,則由該過濾器生成一個默認認證頁面。

org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter

由此過濾器可以生產一個默認的退出登錄頁面

自定義Filter

如果我們想自定義認證的流程,比如使用前後端分離的架構時,認證的時候不重定向到一個頁面,而是使用Restful風格的介面進行認證,返回json響應。這個時候就需要我們自定義一個Filter了

在自定義這樣一個Filter前,我們需要先搞清楚SpringSecurity在驗證用戶的時候,走的什麼邏輯。

關於認證的具體源碼我們之後再討論,我只現在只需要知道在表單登錄的時候,用處理登錄邏輯的過濾器叫做UsernamePasswordAuthenticationFilter,其在方法attemptAuthentication中處理認證這個過程的

	private String usernameParameter = "username";
    private String passwordParameter = "password";

	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    	//第一步:判斷請求方法是不是POST,如果不是就返回一個異常
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            //第二步:從HttpRequest中獲得用戶名和密碼
            String username = this.obtainUsername(request);
            username = username != null ? username : "";
            username = username.trim();
            String password = this.obtainPassword(request);
            password = password != null ? password : "";
            
            //第三步:構造一個UsernamePasswordAuthenticationToken,一個更高層的安全對象,以後再說明,這裡先不深究
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            
            //第四步:設置setDetails,設置ip等資訊
            this.setDetails(request, authRequest);
            //最後:getAuthenticationManager是認證處理的最終的一個機制(後面說明,這裡先不深究),對安全對象進行認證
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
        //從HttpRequest獲取參數名為password的參數作為密碼
        return request.getParameter(this.passwordParameter);
    }

    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        //從HttpRequest獲取參數名為username的參數作為帳號
        return request.getParameter(this.usernameParameter);
    }

    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

看完這個源碼,可以想到如果我們想要實現前後端分離架構的認證,也可以自定義一個過濾器,走這個認證流程,不過我們HTTP Request中傳遞的json中去讀取用戶名和密碼,登陸成功返回一個json

public class RestAuthticationFilter extends UsernamePasswordAuthenticationFilter {

    /**
     * json格式:
     *
     * {
     *     「username": "user",
     *     "password": "12345678"
     * }
     *
     * @param request 請求體
     * @param response 返回體
     * @return Authentication
     * @throws AuthenticationException 認證異常
     */
     @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

        InputStream is = null;
        String username = null;
        String password = null;
        try {
            is = request.getInputStream();
            JSONObject jsonObject= JSON.parseObject(is, JSONObject.class);
            username = jsonObject.getString("username");
            password = jsonObject.getString("password");
        } catch (IOException e) {
            e.printStackTrace();
            throw new BadCredentialsException("json格式錯誤,沒有找到用戶名或密碼");
        }


        //認證,同父類
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        this.setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
  /**
     * 認證成功邏輯
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException {
      			res.setStatus(HttpStatus.OK.value());
            res.setContentType(MediaType.APPLICATION_JSON_VALUE);
            res.setCharacterEncoding("UTF-8");
            res.getWriter().println(JSON.toJSONString(auth));
    }
}

過濾器寫完之後,編寫配置文件,前後端分離架構的認證配置

import com.alibaba.fastjson.JSON;
import com.cupricnitrate.uaa.filter.RestAuthticationFilter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * `@EnableWebSecurity` 註解 deug參數為true時,開啟調試模式,會有更多的debug輸出
 *
 * @author 硝酸銅
 * @date 2021/6/2
 */
@EnableWebSecurity(debug = true)
@Slf4j
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //禁用生成默認的登陸頁面
                .formLogin(AbstractHttpConfigurer::disable)
                //關閉httpBasic,採用自定義過濾器
                .httpBasic(AbstractHttpConfigurer::disable)
                //前後端分離架構不需要csrf保護,這裡關閉
                .csrf(AbstractHttpConfigurer::disable)
                //禁用生成默認的註銷頁面
                .logout(AbstractHttpConfigurer::disable)
                .authorizeRequests(req -> req
                        //可公開訪問路徑
                        .antMatchers("/authorize/**").permitAll()
                        //訪問 /admin路徑下的請求 要有ROLE_ADMIN許可權
                        .antMatchers("/admin/**").hasRole("ADMIN")
                        //訪問 /api路徑下的請求 要有ROLE_USER
                        .antMatchers("/api/**").hasRole("USER")
                        //其他介面只需要認證即可
                        .anyRequest().authenticated()
                )
                //前後端分離是無狀態的,不用session了,直接禁用。
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
                //在添加我們自定義的過濾器,替代UsernamePasswordAuthenticationFilter
                .addFilterAt(restAuthticationFilter(), UsernamePasswordAuthenticationFilter.class);

                /*
                .csrf(csrf -> csrf.disable())
                //默認的HTTP Basic Auth認證
                .httpBasic(Customizer.withDefaults())
                //自定義表單登錄
                .formLogin(form -> form.successHandler((req,res,auth)->{
                    res.setStatus(HttpStatus.OK.value());
                    res.setContentType(MediaType.APPLICATION_JSON_VALUE);
                    res.setCharacterEncoding("UTF-8");
                    res.getWriter().println(JSON.toJSONString(auth));
                    log.info("認證成功");}))
                //對 /api 路徑下的所有介面進行驗證
                .authorizeRequests(req -> req.antMatchers("/api/**").hasAnyRole("USER"));*/

    }

    @SneakyThrows
    private RestAuthticationFilter restAuthticationFilter() {
        RestAuthticationFilter filter = new RestAuthticationFilter();
        //配置AuthenticationManager,是父類的一個方法
        filter.setAuthenticationManager(authenticationManager());

        //filter的入口
        filter.setFilterProcessesUrl("/authorize/login");
        return filter;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // /public 路徑下的請求,都不會啟動過濾器鏈
        web.ignoring().mvcMatchers("/public/**");
    }
}

我們使用idea 的Http-client功能調用介面試一下

###
POST //localhost:8080/authorize/login
Content-Type: application/json

{
  "username": "user",
  "password": "12345678"
}


HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Set-Cookie: JSESSIONID=9BAAD30C4014FD926C940972E1D13D00; Path=/; HttpOnly
Content-Type: application/json;charset=UTF-8
Content-Length: 344
Date: Fri, 04 Jun 2021 10:12:37 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "authenticated": true,
  "authorities": [
    {
      "authority": "ROLE_ADMIN"
    },
    {
      "authority": "ROLE_USER"
    }
  ],
  "details": {
    "remoteAddress": "127.0.0.1"
  },
  "name": "user",
  "principal": {
    "accountNonExpired": true,
    "accountNonLocked": true,
    "authorities": [
      {
        "$ref": "$.authorities[0]"
      },
      {
        "$ref": "$.authorities[1]"
      }
    ],
    "credentialsNonExpired": true,
    "enabled": true,
    "username": "user"
  }
}

成功返回,走自定義邏輯,並且返回了json

這才是前後端分離架構的認證邏輯