詳解SpringSecurity如何實(shí)現(xiàn)前后端分離
Spring Security存在的問題
前后端分離模式是指由前端控制頁面路由,后端接口也不再返回html數(shù)據(jù),而是直接返回業(yè)務(wù)數(shù)據(jù),數(shù)據(jù)一般是JSON格式。
Spring Security默認(rèn)支持的表單認(rèn)證方式,會存在兩個(gè)問題:
- 表單的HTTP Content-Type是application/x-www-form-urlencoded,不是JSON格式。
- Spring Security會在用戶未登錄或登錄成功時(shí)會發(fā)起頁面重定向,重定向到登錄頁或登錄成功頁面。
要支持前后端分離的模式,我們要對這些問題進(jìn)行改造。
改造Spring Security的認(rèn)證方式
1. 登錄請求改成JSON方式
Spring Security默認(rèn)提供賬號密碼認(rèn)證方式,具體實(shí)現(xiàn)是在UsernamePasswordAuthenticationFilter類中。因?yàn)槭潜韱翁峤?,所以Filter中用request.getParameter(this.usernameParameter) 來獲取用戶賬號和密碼信息。當(dāng)我們將請求類型改成application/json后,getParameter方法就獲取不到信息。
要解決這個(gè)問題,就要新建一個(gè)Filter來替換UsernamePasswordAuthenticationFilter ,然后重新實(shí)現(xiàn)獲取用戶的方法。
1.1 新建JSON版Filter - JsonUsernamePasswordAuthenticationFilter
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import org.springframework.data.util.Pair;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
public class JsonUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
Pair<String, String> usernameAndPassword = obtainUsernameAndPassword(request);
String username = usernameAndPassword.getFirst();
username = (username != null) ? username.trim() : "";
String password = usernameAndPassword.getSecond();
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
@SneakyThrows
private Pair<String, String> obtainUsernameAndPassword(HttpServletRequest request) {
JSONObject map = JSON.parseObject(request.getInputStream(), JSONObject.class);
return Pair.of(map.getString(getUsernameParameter()), map.getString(getPasswordParameter()));
}
}
1.2 新建Configurer來注冊Filter - JsonUsernamePasswordLoginConfigurer
注冊Filter有兩種方式,一給是直接調(diào)用httpSecurity的addFilterAt(Filter filter, Class<? extends Filter> atFilter) ,另一個(gè)是通過AbstractHttpConfigurer 來注冊。因?yàn)槲覀兝^承了原來的賬密認(rèn)證方式,考慮到兼容原有邏輯,我們選擇Spring Security默認(rèn)的Configurer注冊方式來注冊Filter。AbstractHttpConfigurer 在初始化 UsernamePasswordAuthenticationFilter 的時(shí)候,會額外設(shè)置一些信息。
新建一個(gè)JsonUsernamePasswordLoginConfigurer直接繼承AbstractAuthenticationFilterConfigurer。
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
public final class JsonUsernamePasswordLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractAuthenticationFilterConfigurer<H, JsonUsernamePasswordLoginConfigurer<H>, JsonUsernamePasswordAuthenticationFilter> {
public JsonUsernamePasswordLoginConfigurer() {
super(new JsonUsernamePasswordAuthenticationFilter(), null);
}
// 去掉登錄處理接口的權(quán)限校驗(yàn)
@Override
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return new AntPathRequestMatcher(loginProcessingUrl, "POST");
}
}
1.3 將自定義Configurer注冊到HttpSecurity上
這一步比較簡單,我們先關(guān)閉原來的表單認(rèn)證,然后注冊我們自己的Configurer,實(shí)現(xiàn)JSON版認(rèn)證方式。
http
.formLogin().disable()
.apply(new JsonUsernamePasswordLoginConfigurer<>())
經(jīng)過這三步,Spring Security就能識別JSON格式的用戶信息了。
2. 關(guān)閉頁面重定向
有幾個(gè)場景會觸發(fā)Spring Security的重定向:
- 當(dāng)前用戶未登錄,重定向到登錄頁面
- 登錄驗(yàn)證成功,重定向到默認(rèn)頁面
- 退出登錄成功,重定向到默認(rèn)頁面
我們要對這幾個(gè)場景分別處理,給前端返回JSON格式的描述信息,而不是發(fā)起重定向。
2.1 當(dāng)前用戶未登錄
用戶發(fā)起未登錄的請求會被AuthorizationFilter攔截,并拋出AccessDeniedException異常。異常被AuthenticationEntryPoint處理,默認(rèn)會觸發(fā)重定向到登錄頁。Spring Security開放了配置,允許我們自定義AuthenticationEntryPoint。那么我們就通過自定義AuthenticationEntryPoint來取消重定向行為,將接口改為返回JSON信息。
http.exceptionHandling(it -> it.authenticationEntryPoint((request, response, authException) -> {
String msg = "{\\"msg\\": \\"用戶未登錄\\"}";
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
PrintWriter writer = response.getWriter();
writer.write(msg);
writer.flush();
writer.close();
}))
2.2 登錄成功/失敗
登錄成功或失敗后的行為由AuthenticationSuccessHandler 和AuthenticationFailureHandler 來控制。原來是在**formLogin(it->it.successHandler(null))**里配置它們,由于上面我們自定義了JsonUsernamePasswordLoginConfigurer ,所以要在我們自己的Configurer 上配置AuthenticationSuccessHandler 和AuthenticationFailureHandler 。
http
.formLogin().disable()
.apply((SecurityConfigurerAdapter) new JsonUsernamePasswordLoginConfigurer<>()
.successHandler((request, response, authentication) -> {
String msg = "{\\"msg\\": \\"登錄成功\\"}";
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
PrintWriter writer = response.getWriter();
writer.write(msg);
writer.flush();
writer.close();
})
.failureHandler((request, response, exception) -> {
String msg = "{\\"msg\\": \\"用戶名密碼錯(cuò)誤\\"}";
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
PrintWriter writer = response.getWriter();
writer.write(msg);
writer.flush();
writer.close();
}));
2.3 退出登錄
退出登錄是在LogoutConfigurer配置,退出成功后,會觸發(fā)LogoutSuccessHandler操作,我們也重寫它的處理邏輯。
http.logout(it -> it
.logoutSuccessHandler((request, response, authentication) -> {
String msg = "{\\"msg\\": \\"退出成功\\"}";
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
PrintWriter writer = response.getWriter();
writer.write(msg);
writer.flush();
writer.close();
}))
3. 最后處理CSRF校驗(yàn)
前后端分離后,如果頁面是放在CDN上,那么前段直至發(fā)起登錄請求之前,都沒機(jī)會從后端拿到CSRF Token。所以,登錄請求會被Spring Security的CsrfFilter攔截。
要避免這種情況,一種方式是發(fā)起登錄請求前,先調(diào)用接口獲取CSRF Token;另一種方式是先關(guān)閉登錄接口的CSRF校驗(yàn)。方式二配置如下:
http.csrf(it -> it.ignoringRequestMatchers("/login", "POST"))
總結(jié)
至此,前后端分離的基本工作就完成了。在實(shí)踐的過程中必然還有其他問題,歡迎大家一起交流探討。
以上就是詳解SpringSecurity如何實(shí)現(xiàn)前后端分離的詳細(xì)內(nèi)容,更多關(guān)于SpringSecurity前后端分離的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringMvc3+extjs4實(shí)現(xiàn)上傳與下載功能
這篇文章主要為大家詳細(xì)介紹了SpringMvc3+extjs4實(shí)現(xiàn)上傳與下載功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06
InterProcessMutex實(shí)現(xiàn)zookeeper分布式鎖原理
本文主要介紹了InterProcessMutex實(shí)現(xiàn)zookeeper分布式鎖原理,文中根據(jù)實(shí)例編碼詳細(xì)介紹的十分詳盡,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
java.lang.NumberFormatException異常解決方案詳解
這篇文章主要介紹了java.lang.NumberFormatException異常解決方案詳解,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08
java?啟動參數(shù)?springboot?idea詳解
這篇文章主要介紹了java?啟動參數(shù)?springboot?idea的相關(guān)知識,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-09-09

