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工程,引入依賴

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-security</artifactId>
  4. </dependency>

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

  1. /**
  2. * @author 硝酸銅
  3. * @date 2021/6/2
  4. */
  5. @RestController
  6. @RequestMapping(value = "/api")
  7. public class UserResource {
  8. @GetMapping(value = "/greeting")
  9. public String greeting(){
  10. return "Hello World";
  11. }
  12. }

啟動專案,訪問http://localhost:8080/greeting,會被SpringSecurity攔截,重定向到http://localhost:8080/login進行登入,這個頁面是SpringSecurity預設的登陸頁面

預設的使用者名稱是:user,密碼會在控制檯輸出出來:

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

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

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

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

授權demo

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

  1. /**
  2. * @author 硝酸銅
  3. * @date 2021/6/2
  4. */
  5. @EnableWebSecurity
  6. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  7. @Override
  8. protected void configure(HttpSecurity http) throws Exception {
  9. http.authorizeRequests(req -> req.mvcMatchers("/api/greeting").hasAnyRole("ADMIN"));
  10. }
  11. }

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

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

我們現在給使用者授權:

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

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

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

安全配置

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

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

這個預設的方法分為三個部分:

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

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

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

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

  1. import org.springframework.security.config.Customizer;
  2. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  3. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  4. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  5. import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
  6. /**
  7. * `@EnableWebSecurity` 註解 deug引數為true時,開啟除錯模式,會有更多的debug輸出,不要用在生產環境
  8. * @author 硝酸銅
  9. * @date 2021/6/2
  10. */
  11. @EnableWebSecurity(debug = true)
  12. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  13. @Override
  14. protected void configure(HttpSecurity http) throws Exception {
  15. http
  16. //取消CSRF保護
  17. .csrf(AbstractHttpConfigurer::disable)
  18. //預設的HTTP Basic Auth認證
  19. .httpBasic(Customizer.withDefaults())
  20. //預設的表單登入
  21. //.formLogin(Customizer.withDefaults())
  22. //關閉表單登入
  23. .formLogin(AbstractHttpConfigurer::disable)
  24. //對 /api 路徑下的所有介面進行驗證,需要許可權`ROLE_USER`
  25. .authorizeRequests(req -> req.antMatchers("/api/**").hasAnyRole("USER"));
  26. }
  27. @Override
  28. public void configure(WebSecurity web) {
  29. web
  30. .ignoring()
  31. .antMatchers("/error",
  32. "/resources/**",
  33. "/static/**",
  34. "/public/**",
  35. "/h2-console/**",
  36. "/swagger-ui.html",
  37. "/swagger-ui/**",
  38. "/v3/api-docs/**",
  39. "/v2/api-docs/**",
  40. "/doc.html",
  41. "/swagger-resources/**")
  42. .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
  43. }
  44. }

重寫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屬性

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

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

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

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

設定CSRF

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

Remember me 功能

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

SpringSecurity提供開箱即用的配置rememberMe

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

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

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

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

  1. http.rememberMe(rememberMe -> {
  2. //儲存策略,
  3. rememberMe.tokenRepository()
  4. //設定Cookie名稱
  5. .rememberMeCookieName()
  6. //有效期設定,單位s
  7. .tokenValiditySeconds()
  8. //設定使用者查詢服務,實現UserDetailsService介面的類,提供根據使用者名稱查詢使用者的方法
  9. .userDetailsService()
  10. //是否用安全的Cookie
  11. .useSecureCookie();
  12. })

退出

前後端不分離的退出設定

  1. http
  2. .logout(logout -> {
  3. //退出登入的url
  4. logout.logoutUrl()
  5. //退出登入成功,重定向的url
  6. .logoutSuccessUrl()
  7. //設定LogoutHandler,自定義退出登入邏輯
  8. .addLogoutHandler()
  9. //刪除Cookies
  10. .deleteCookies()
  11. //取消Session
  12. .invalidateHttpSession()
  13. //清理認證
  14. .clearAuthentication();
  15. })

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

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的日誌輸出:

  1. 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這個路徑,我們來看看日誌:

  1. 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]
  2. 2021-09-18 14:10:54.545 DEBUG 8004 --- [nio-8080-exec-1] HttpSessionRequestCache : Saved request http://localhost:8080/api/greeting to session
  3. 2021-09-18 14:10:54.545 DEBUG 8004 --- [nio-8080-exec-1] DefaultRedirectStrategy : Redirecting to http://localhost:8080/login

認證失敗,重定向到了login

登入之後:

  1. 2021-09-18 14:11:03.247 DEBUG 8004 --- [nio-8080-exec-6] DaoAuthenticationProvider : Authenticated user
  2. ... ...
  3. 2021-09-18 14:11:03.247 DEBUG 8004 --- [nio-8080-exec-6] DefaultRedirectStrategy : Redirecting to http://localhost:8080/api/greeting
  4. ... ...
  5. 2021-09-18 14:11:03.247 DEBUG 8004 --- [nio-8080-exec-9] FilterSecurityInterceptor : Authorized filter invocation [GET /api/greeting] with attributes [authenticated]
  6. 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中處理認證這個過程的

  1. private String usernameParameter = "username";
  2. private String passwordParameter = "password";
  3. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
  4. //第一步:判斷請求方法是不是POST,如果不是就返回一個異常
  5. if (this.postOnly && !request.getMethod().equals("POST")) {
  6. throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
  7. } else {
  8. //第二步:從HttpRequest中獲得使用者名稱和密碼
  9. String username = this.obtainUsername(request);
  10. username = username != null ? username : "";
  11. username = username.trim();
  12. String password = this.obtainPassword(request);
  13. password = password != null ? password : "";
  14. //第三步:構造一個UsernamePasswordAuthenticationToken,一個更高層的安全物件,以後再說明,這裡先不深究
  15. UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
  16. //第四步:設定setDetails,設定ip等資訊
  17. this.setDetails(request, authRequest);
  18. //最後:getAuthenticationManager是認證處理的最終的一個機制(後面說明,這裡先不深究),對安全物件進行認證
  19. return this.getAuthenticationManager().authenticate(authRequest);
  20. }
  21. }
  22. @Nullable
  23. protected String obtainPassword(HttpServletRequest request) {
  24. //從HttpRequest獲取引數名為password的引數作為密碼
  25. return request.getParameter(this.passwordParameter);
  26. }
  27. @Nullable
  28. protected String obtainUsername(HttpServletRequest request) {
  29. //從HttpRequest獲取引數名為username的引數作為賬號
  30. return request.getParameter(this.usernameParameter);
  31. }
  32. protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
  33. authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
  34. }

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

  1. public class RestAuthticationFilter extends UsernamePasswordAuthenticationFilter {
  2. /**
  3. * json格式:
  4. *
  5. * {
  6. * “username": "user",
  7. * "password": "12345678"
  8. * }
  9. *
  10. * @param request 請求體
  11. * @param response 返回體
  12. * @return Authentication
  13. * @throws AuthenticationException 認證異常
  14. */
  15. @Override
  16. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
  17. InputStream is = null;
  18. String username = null;
  19. String password = null;
  20. try {
  21. is = request.getInputStream();
  22. JSONObject jsonObject= JSON.parseObject(is, JSONObject.class);
  23. username = jsonObject.getString("username");
  24. password = jsonObject.getString("password");
  25. } catch (IOException e) {
  26. e.printStackTrace();
  27. throw new BadCredentialsException("json格式錯誤,沒有找到使用者名稱或密碼");
  28. }
  29. //認證,同父類
  30. UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
  31. this.setDetails(request, authRequest);
  32. return this.getAuthenticationManager().authenticate(authRequest);
  33. }
  34. /**
  35. * 認證成功邏輯
  36. */
  37. @Override
  38. protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException {
  39. res.setStatus(HttpStatus.OK.value());
  40. res.setContentType(MediaType.APPLICATION_JSON_VALUE);
  41. res.setCharacterEncoding("UTF-8");
  42. res.getWriter().println(JSON.toJSONString(auth));
  43. }
  44. }

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

  1. import com.alibaba.fastjson.JSON;
  2. import com.cupricnitrate.uaa.filter.RestAuthticationFilter;
  3. import lombok.SneakyThrows;
  4. import lombok.extern.slf4j.Slf4j;
  5. import org.springframework.http.HttpStatus;
  6. import org.springframework.http.MediaType;
  7. import org.springframework.security.config.Customizer;
  8. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  9. import org.springframework.security.config.annotation.web.builders.WebSecurity;
  10. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  11. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  12. import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
  13. import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
  14. import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
  15. /**
  16. * `@EnableWebSecurity` 註解 deug引數為true時,開啟除錯模式,會有更多的debug輸出
  17. *
  18. * @author 硝酸銅
  19. * @date 2021/6/2
  20. */
  21. @EnableWebSecurity(debug = true)
  22. @Slf4j
  23. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  24. @Override
  25. protected void configure(HttpSecurity http) throws Exception {
  26. http
  27. //禁用生成預設的登陸頁面
  28. .formLogin(AbstractHttpConfigurer::disable)
  29. //關閉httpBasic,採用自定義過濾器
  30. .httpBasic(AbstractHttpConfigurer::disable)
  31. //前後端分離架構不需要csrf保護,這裡關閉
  32. .csrf(AbstractHttpConfigurer::disable)
  33. //禁用生成預設的登出頁面
  34. .logout(AbstractHttpConfigurer::disable)
  35. .authorizeRequests(req -> req
  36. //可公開訪問路徑
  37. .antMatchers("/authorize/**").permitAll()
  38. //訪問 /admin路徑下的請求 要有ROLE_ADMIN許可權
  39. .antMatchers("/admin/**").hasRole("ADMIN")
  40. //訪問 /api路徑下的請求 要有ROLE_USER
  41. .antMatchers("/api/**").hasRole("USER")
  42. //其他介面只需要認證即可
  43. .anyRequest().authenticated()
  44. )
  45. //前後端分離是無狀態的,不用session了,直接禁用。
  46. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
  47. //在新增我們自定義的過濾器,替代UsernamePasswordAuthenticationFilter
  48. .addFilterAt(restAuthticationFilter(), UsernamePasswordAuthenticationFilter.class);
  49. /*
  50. .csrf(csrf -> csrf.disable())
  51. //預設的HTTP Basic Auth認證
  52. .httpBasic(Customizer.withDefaults())
  53. //自定義表單登入
  54. .formLogin(form -> form.successHandler((req,res,auth)->{
  55. res.setStatus(HttpStatus.OK.value());
  56. res.setContentType(MediaType.APPLICATION_JSON_VALUE);
  57. res.setCharacterEncoding("UTF-8");
  58. res.getWriter().println(JSON.toJSONString(auth));
  59. log.info("認證成功");}))
  60. //對 /api 路徑下的所有介面進行驗證
  61. .authorizeRequests(req -> req.antMatchers("/api/**").hasAnyRole("USER"));*/
  62. }
  63. @SneakyThrows
  64. private RestAuthticationFilter restAuthticationFilter() {
  65. RestAuthticationFilter filter = new RestAuthticationFilter();
  66. //配置AuthenticationManager,是父類的一個方法
  67. filter.setAuthenticationManager(authenticationManager());
  68. //filter的入口
  69. filter.setFilterProcessesUrl("/authorize/login");
  70. return filter;
  71. }
  72. @Override
  73. public void configure(WebSecurity web) throws Exception {
  74. // /public 路徑下的請求,都不會啟動過濾器鏈
  75. web.ignoring().mvcMatchers("/public/**");
  76. }
  77. }

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

  1. ###
  2. POST http://localhost:8080/authorize/login
  3. Content-Type: application/json
  4. {
  5. "username": "user",
  6. "password": "12345678"
  7. }
  8. HTTP/1.1 200
  9. X-Content-Type-Options: nosniff
  10. X-XSS-Protection: 1; mode=block
  11. Cache-Control: no-cache, no-store, max-age=0, must-revalidate
  12. Pragma: no-cache
  13. Expires: 0
  14. X-Frame-Options: DENY
  15. Set-Cookie: JSESSIONID=9BAAD30C4014FD926C940972E1D13D00; Path=/; HttpOnly
  16. Content-Type: application/json;charset=UTF-8
  17. Content-Length: 344
  18. Date: Fri, 04 Jun 2021 10:12:37 GMT
  19. Keep-Alive: timeout=60
  20. Connection: keep-alive
  21. {
  22. "authenticated": true,
  23. "authorities": [
  24. {
  25. "authority": "ROLE_ADMIN"
  26. },
  27. {
  28. "authority": "ROLE_USER"
  29. }
  30. ],
  31. "details": {
  32. "remoteAddress": "127.0.0.1"
  33. },
  34. "name": "user",
  35. "principal": {
  36. "accountNonExpired": true,
  37. "accountNonLocked": true,
  38. "authorities": [
  39. {
  40. "$ref": "$.authorities[0]"
  41. },
  42. {
  43. "$ref": "$.authorities[1]"
  44. }
  45. ],
  46. "credentialsNonExpired": true,
  47. "enabled": true,
  48. "username": "user"
  49. }
  50. }

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

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