Spring Security自定義登錄原理及實(shí)現(xiàn)詳解
1. 前言
前面的關(guān)于 Spring Security 相關(guān)的文章只是一個(gè)預(yù)熱。為了接下來更好的實(shí)戰(zhàn),如果你錯過了請從 Spring Security 實(shí)戰(zhàn)系列 開始。安全訪問的第一步就是認(rèn)證(Authentication),認(rèn)證的第一步就是登錄。今天我們要通過對 Spring Security 的自定義,來設(shè)計(jì)一個(gè)可擴(kuò)展,可伸縮的 form 登錄功能。
2. form 登錄的流程
下面是 form 登錄的基本流程:

只要是 form 登錄基本都能轉(zhuǎn)化為上面的流程。接下來我們看看 Spring Security 是如何處理的。
3. Spring Security 中的登錄
昨天 Spring Security 實(shí)戰(zhàn)干貨:自定義配置類入口WebSecurityConfigurerAdapter 中已經(jīng)講到了我們通常的自定義訪問控制主要是通過 HttpSecurity 來構(gòu)建的。默認(rèn)它提供了三種登錄方式:
- formLogin() 普通表單登錄
- oauth2Login() 基于 OAuth2.0 認(rèn)證/授權(quán)協(xié)議
- openidLogin() 基于 OpenID 身份認(rèn)證規(guī)范
以上三種方式統(tǒng)統(tǒng)是 AbstractAuthenticationFilterConfigurer 實(shí)現(xiàn)的,
4. HttpSecurity 中的 form 表單登錄
啟用表單登錄通過兩種方式一種是通過 HttpSecurity 的 apply(C configurer) 方法自己構(gòu)造一個(gè) AbstractAuthenticationFilterConfigurer 的實(shí)現(xiàn),這種是比較高級的玩法。 另一種是我們常見的使用 HttpSecurity 的 formLogin() 方法來自定義 FormLoginConfigurer 。我們先搞一下比較常規(guī)的第二種。
4.1 FormLoginConfigurer
該類是 form 表單登錄的配置類。它提供了一些我們常用的配置方法:
- loginPage(String loginPage) : 登錄 頁面而并不是接口,對于前后分離模式需要我們進(jìn)行改造 默認(rèn)為 /login。
- loginProcessingUrl(String loginProcessingUrl) 實(shí)際表單向后臺提交用戶信息的 Action,再由過濾器UsernamePasswordAuthenticationFilter 攔截處理,該 Action 其實(shí)不會處理任何邏輯。
- usernameParameter(String usernameParameter) 用來自定義用戶參數(shù)名,默認(rèn) username 。
- passwordParameter(String passwordParameter) 用來自定義用戶密碼名,默認(rèn) password
- failureUrl(String authenticationFailureUrl) 登錄失敗后會重定向到此路徑, 一般前后分離不會使用它。
- failureForwardUrl(String forwardUrl) 登錄失敗會轉(zhuǎn)發(fā)到此, 一般前后分離用到它。 可定義一個(gè) Controller (控制器)來處理返回值,但是要注意 RequestMethod。
- defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse) 默認(rèn)登陸成功后跳轉(zhuǎn)到此 ,如果 alwaysUse 為 true 只要進(jìn)行認(rèn)證流程而且成功,會一直跳轉(zhuǎn)到此。一般推薦默認(rèn)值 false
- successForwardUrl(String forwardUrl) 效果等同于上面 defaultSuccessUrl 的 alwaysUse 為 true 但是要注意 RequestMethod。
- successHandler(AuthenticationSuccessHandler successHandler) 自定義認(rèn)證成功處理器,可替代上面所有的 success 方式
- failureHandler(AuthenticationFailureHandler authenticationFailureHandler) 自定義失敗成功處理器,可替代上面所有的 success 方式
- permitAll(boolean permitAll) form 表單登錄是否放開
知道了這些我們就能來搞個(gè)定制化的登錄了。
5. Spring Security 聚合登錄 實(shí)戰(zhàn)
接下來是我們最激動人心的實(shí)戰(zhàn)登錄操作。 有疑問的可認(rèn)真閱讀 Spring 實(shí)戰(zhàn) 的一系列預(yù)熱文章。
5.1 簡單需求
我們的接口訪問都要通過認(rèn)證,登陸錯誤后返回錯誤信息(json),成功后前臺可以獲取到對應(yīng)數(shù)據(jù)庫用戶信息(json)(實(shí)戰(zhàn)中記得脫敏)。
我們定義處理成功失敗的控制器:
@RestController
@RequestMapping("/login")
public class LoginController {
@Resource
private SysUserService sysUserService;
/**
* 登錄失敗返回 401 以及提示信息.
*
* @return the rest
*/
@PostMapping("/failure")
public Rest loginFailure() {
return RestBody.failure(HttpStatus.UNAUTHORIZED.value(), "登錄失敗了,老哥");
}
/**
* 登錄成功后拿到個(gè)人信息.
*
* @return the rest
*/
@PostMapping("/success")
public Rest loginSuccess() {
// 登錄成功后用戶的認(rèn)證信息 UserDetails會存在 安全上下文寄存器 SecurityContextHolder 中
User principal = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = principal.getUsername();
SysUser sysUser = sysUserService.queryByUsername(username);
// 脫敏
sysUser.setEncodePassword("[PROTECT]");
return RestBody.okData(sysUser,"登錄成功");
}
}
然后 我們自定義配置覆寫 void configure(HttpSecurity http) 方法進(jìn)行如下配置(這里需要禁用crsf):
@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors()
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/process")
.successForwardUrl("/login/success").
failureForwardUrl("/login/failure");
}
}
}
使用 Postman 或者其它工具進(jìn)行 Post 方式的表單提交 http://localhost:8080/process?username=Felordcn&password=12345 會返回用戶信息:
{
"httpStatus": 200,
"data": {
"userId": 1,
"username": "Felordcn",
"encodePassword": "[PROTECT]",
"age": 18
},
"msg": "登錄成功",
"identifier": ""
}
把密碼修改為其它值再次請求認(rèn)證失敗后 :
{
"httpStatus": 401,
"data": null,
"msg": "登錄失敗了,老哥",
"identifier": "-9999"
}
6. 多種登錄方式的簡單實(shí)現(xiàn)
就這么完了么?現(xiàn)在登錄的花樣繁多。常規(guī)的就有短信、郵箱、掃碼 ,第三方是以后我要講的不在今天范圍之內(nèi)。 如何應(yīng)對想法多的產(chǎn)品經(jīng)理? 我們來搞一個(gè)可擴(kuò)展各種姿勢的登錄方式。我們在上面 2. form 登錄的流程 中的 用戶 和 判定 之間增加一個(gè)適配器來適配即可。 我們知道這個(gè)所謂的 判定就是 UsernamePasswordAuthenticationFilter 。
我們只需要保證 uri 為上面配置的/process 并且能夠通過 getParameter(String name) 獲取用戶名和密碼即可 。
我突然覺得可以模仿 DelegatingPasswordEncoder 的搞法, 維護(hù)一個(gè)注冊表執(zhí)行不同的處理策略。當(dāng)然我們要實(shí)現(xiàn)一個(gè) GenericFilterBean 在 UsernamePasswordAuthenticationFilter 之前執(zhí)行。同時(shí)制定登錄的策略。
6.1 登錄方式定義
定義登錄方式枚舉 ``。
public enum LoginTypeEnum {
/**
* 原始登錄方式.
*/
FORM,
/**
* Json 提交.
*/
JSON,
/**
* 驗(yàn)證碼.
*/
CAPTCHA
}
6.2 定義前置處理器接口
public interface LoginPostProcessor {
/**
* 獲取 登錄類型
*
* @return the type
*/
LoginTypeEnum getLoginTypeEnum();
/**
* 獲取用戶名
*
* @param request the request
* @return the string
*/
String obtainUsername(ServletRequest request);
/**
* 獲取密碼
*
* @param request the request
* @return the string
*/
String obtainPassword(ServletRequest request);
}
6.3 實(shí)現(xiàn)登錄前置處理過濾器
該過濾器維護(hù)了 LoginPostProcessor 映射表。 通過前端來判定登錄方式進(jìn)行策略上的預(yù)處理,最終還是會交給
package cn.felord.spring.security.filter;
import cn.felord.spring.security.enumation.LoginTypeEnum;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY;
import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY;
/**
* 預(yù)登錄控制器
*
* @author Felordcn
* @since 16 :21 2019/10/17
*/
public class PreLoginFilter extends GenericFilterBean {
private static final String LOGIN_TYPE_KEY = "login_type";
private RequestMatcher requiresAuthenticationRequestMatcher;
private Map<LoginTypeEnum, LoginPostProcessor> processors = new HashMap<>();
public PreLoginFilter(String loginProcessingUrl, Collection<LoginPostProcessor> loginPostProcessors) {
Assert.notNull(loginProcessingUrl, "loginProcessingUrl must not be null");
requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(loginProcessingUrl, "POST");
LoginPostProcessor loginPostProcessor = defaultLoginPostProcessor();
processors.put(loginPostProcessor.getLoginTypeEnum(), loginPostProcessor);
if (!CollectionUtils.isEmpty(loginPostProcessors)) {
loginPostProcessors.forEach(element -> processors.put(element.getLoginTypeEnum(), element));
}
}
private LoginTypeEnum getTypeFromReq(ServletRequest request) {
String parameter = request.getParameter(LOGIN_TYPE_KEY);
int i = Integer.parseInt(parameter);
LoginTypeEnum[] values = LoginTypeEnum.values();
return values[i];
}
/**
* 默認(rèn)還是Form .
*
* @return the login post processor
*/
private LoginPostProcessor defaultLoginPostProcessor() {
return new LoginPostProcessor() {
@Override
public LoginTypeEnum getLoginTypeEnum() {
return LoginTypeEnum.FORM;
}
@Override
public String obtainUsername(ServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_USERNAME_KEY);
}
@Override
public String obtainPassword(ServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_PASSWORD_KEY);
}
};
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ParameterRequestWrapper parameterRequestWrapper = new ParameterRequestWrapper((HttpServletRequest) request);
if (requiresAuthenticationRequestMatcher.matches((HttpServletRequest) request)) {
LoginTypeEnum typeFromReq = getTypeFromReq(request);
LoginPostProcessor loginPostProcessor = processors.get(typeFromReq);
String username = loginPostProcessor.obtainUsername(request);
String password = loginPostProcessor.obtainPassword(request);
parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_USERNAME_KEY, username);
parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_PASSWORD_KEY, password);
}
chain.doFilter(parameterRequestWrapper, response);
}
}
6.4 驗(yàn)證
通過 POST 表單提交方式 http://localhost:8080/process?username=Felordcn&password=12345&login_type=0 可以請求成功?;蛘咭韵铝蟹绞揭部梢蕴峤怀晒Γ?/p>

更多的登錄方式 只需要實(shí)現(xiàn)接口 LoginPostProcessor 注入 PreLoginFilter
7. 總結(jié)
今天我們通過各種技術(shù)的運(yùn)用實(shí)現(xiàn)了從簡單登錄到可動態(tài)擴(kuò)展的多種方式并存的實(shí)戰(zhàn)運(yùn)用。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
java發(fā)送http請求時(shí)如何處理異步回調(diào)結(jié)果
這篇文章主要介紹了java發(fā)送http請求時(shí)如何處理異步回調(diào)結(jié)果問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-06-06
Java如何利用狀態(tài)模式(state pattern)替代if else
這篇文章主要給大家介紹了關(guān)于Java如何利用狀態(tài)模式(state pattern)替代if else的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
springboot跨域過濾器fetch react Response to p
這篇文章主要介紹了springboot跨域過濾器fetch react Response to preflight request doesn‘t pass access control check問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03
hibernate一對多關(guān)聯(lián)映射學(xué)習(xí)小結(jié)
這篇文章主要介紹了hibernate一對多關(guān)聯(lián)映射學(xué)習(xí)小結(jié),需要的朋友可以參考下2017-09-09
Java8?lambda表達(dá)式的10個(gè)實(shí)例講解
這篇文章主要介紹了Java8?lambda表達(dá)式的10個(gè)實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03
Spring Cache和EhCache實(shí)現(xiàn)緩存管理方式
這篇文章主要介紹了Spring Cache和EhCache實(shí)現(xiàn)緩存管理方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06

