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";
}
}

啟動專案,訪問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預設配置下,所有介面只要認證通過即可訪問,如果我們需要對一個介面進行限制,必須有哪一種許可權才能訪問,則需要進行安全配置

/**
* @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 http://localhost:8080/api/greeting to session
2021-09-18 14:10:54.545 DEBUG 8004 --- [nio-8080-exec-1] DefaultRedirectStrategy : Redirecting to http://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 http://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 http://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

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