Spring Security自定義登錄原理及實(shí)現(xiàn)詳解
1. 前言
前面的關(guān)于 Spring Security 相關(guān)的文章只是一個(gè)預(yù)熱。為了接下來(lái)更好的實(shí)戰(zhàn),如果你錯(cuò)過(guò)了請(qǐng)從 Spring Security 實(shí)戰(zhàn)系列 開(kāi)始。安全訪問(wèn)的第一步就是認(rèn)證(Authentication),認(rèn)證的第一步就是登錄。今天我們要通過(guò)對(duì) Spring Security 的自定義,來(lái)設(shè)計(jì)一個(gè)可擴(kuò)展,可伸縮的 form 登錄功能。
2. form 登錄的流程
下面是 form 登錄的基本流程:
只要是 form 登錄基本都能轉(zhuǎn)化為上面的流程。接下來(lái)我們看看 Spring Security 是如何處理的。
3. Spring Security 中的登錄
昨天 Spring Security 實(shí)戰(zhàn)干貨:自定義配置類入口WebSecurityConfigurerAdapter 中已經(jīng)講到了我們通常的自定義訪問(wèn)控制主要是通過(guò) HttpSecurity 來(lái)構(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 表單登錄
啟用表單登錄通過(guò)兩種方式一種是通過(guò) HttpSecurity 的 apply(C configurer) 方法自己構(gòu)造一個(gè) AbstractAuthenticationFilterConfigurer 的實(shí)現(xiàn),這種是比較高級(jí)的玩法。 另一種是我們常見(jiàn)的使用 HttpSecurity 的 formLogin() 方法來(lái)自定義 FormLoginConfigurer 。我們先搞一下比較常規(guī)的第二種。
4.1 FormLoginConfigurer
該類是 form 表單登錄的配置類。它提供了一些我們常用的配置方法:
loginPage(String loginPage) : 登錄 頁(yè)面而并不是接口,對(duì)于前后分離模式需要我們進(jìn)行改造 默認(rèn)為 /login。 loginProcessingUrl(String loginProcessingUrl) 實(shí)際表單向后臺(tái)提交用戶信息的 Action,再由過(guò)濾器UsernamePasswordAuthenticationFilter 攔截處理,該 Action 其實(shí)不會(huì)處理任何邏輯。 usernameParameter(String usernameParameter) 用來(lái)自定義用戶參數(shù)名,默認(rèn) username 。 passwordParameter(String passwordParameter) 用來(lái)自定義用戶密碼名,默認(rèn) password failureUrl(String authenticationFailureUrl) 登錄失敗后會(huì)重定向到此路徑, 一般前后分離不會(huì)使用它。 failureForwardUrl(String forwardUrl) 登錄失敗會(huì)轉(zhuǎn)發(fā)到此, 一般前后分離用到它。 可定義一個(gè) Controller (控制器)來(lái)處理返回值,但是要注意 RequestMethod。 defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse) 默認(rèn)登陸成功后跳轉(zhuǎn)到此 ,如果 alwaysUse 為 true 只要進(jìn)行認(rèn)證流程而且成功,會(huì)一直跳轉(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 表單登錄是否放開(kāi)知道了這些我們就能來(lái)搞個(gè)定制化的登錄了。
5. Spring Security 聚合登錄 實(shí)戰(zhàn)
接下來(lái)是我們最激動(dòng)人心的實(shí)戰(zhàn)登錄操作。 有疑問(wèn)的可認(rèn)真閱讀 Spring 實(shí)戰(zhàn) 的一系列預(yù)熱文章。
5.1 簡(jiǎn)單需求
我們的接口訪問(wèn)都要通過(guò)認(rèn)證,登陸錯(cuò)誤后返回錯(cuò)誤信息(json),成功后前臺(tái)可以獲取到對(duì)應(yīng)數(shù)據(jù)庫(kù)用戶信息(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會(huì)存在 安全上下文寄存器 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 會(huì)返回用戶信息:
{ 'httpStatus': 200, 'data': { 'userId': 1, 'username': 'Felordcn', 'encodePassword': '[PROTECT]', 'age': 18 }, 'msg': '登錄成功', 'identifier': '' }
把密碼修改為其它值再次請(qǐng)求認(rèn)證失敗后 :
{ 'httpStatus': 401, 'data': null, 'msg': '登錄失敗了,老哥', 'identifier': '-9999' }
6. 多種登錄方式的簡(jiǎn)單實(shí)現(xiàn)
就這么完了么?現(xiàn)在登錄的花樣繁多。常規(guī)的就有短信、郵箱、掃碼 ,第三方是以后我要講的不在今天范圍之內(nèi)。 如何應(yīng)對(duì)想法多的產(chǎn)品經(jīng)理? 我們來(lái)搞一個(gè)可擴(kuò)展各種姿勢(shì)的登錄方式。我們?cè)谏厦?2. form 登錄的流程 中的 用戶 和 判定 之間增加一個(gè)適配器來(lái)適配即可。 我們知道這個(gè)所謂的 判定就是 UsernamePasswordAuthenticationFilter 。
我們只需要保證 uri 為上面配置的/process 并且能夠通過(guò) getParameter(String name) 獲取用戶名和密碼即可 。
我突然覺(jué)得可以模仿 DelegatingPasswordEncoder 的搞法, 維護(hù)一個(gè)注冊(cè)表執(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)登錄前置處理過(guò)濾器
該過(guò)濾器維護(hù)了 LoginPostProcessor 映射表。 通過(guò)前端來(lái)判定登錄方式進(jìn)行策略上的預(yù)處理,最終還是會(huì)交給
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)證
通過(guò) POST 表單提交方式 http://localhost:8080/process?username=Felordcn&password=12345&login_type=0 可以請(qǐng)求成功。或者以下列方式也可以提交成功:
更多的登錄方式 只需要實(shí)現(xiàn)接口 LoginPostProcessor 注入 PreLoginFilter
7. 總結(jié)
今天我們通過(guò)各種技術(shù)的運(yùn)用實(shí)現(xiàn)了從簡(jiǎn)單登錄到可動(dòng)態(tài)擴(kuò)展的多種方式并存的實(shí)戰(zhàn)運(yùn)用。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持好吧啦網(wǎng)。
相關(guān)文章:
1. python excel和yaml文件的讀取封裝2. moment轉(zhuǎn)化時(shí)間戳出現(xiàn)Invalid Date的問(wèn)題及解決3. python爬蟲(chóng)實(shí)戰(zhàn)之制作屬于自己的一個(gè)IP代理模塊4. Android中的緩存5. idea重置默認(rèn)配置的方法步驟6. Android Studio插件7. Python內(nèi)存映射文件讀寫方式8. php實(shí)現(xiàn)當(dāng)前用戶在線人數(shù)9. .net6 在中標(biāo)麒麟下的安裝和部署過(guò)程10. Java CountDownLatch應(yīng)用場(chǎng)景代碼實(shí)例
