成人在线亚洲_国产日韩视频一区二区三区_久久久国产精品_99国内精品久久久久久久

您的位置:首頁(yè)技術(shù)文章
文章詳情頁(yè)

Spring Security自定義登錄原理及實(shí)現(xiàn)詳解

瀏覽:2日期:2023-08-15 14:42:46

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 登錄的基本流程:

Spring Security自定義登錄原理及實(shí)現(xiàn)詳解

只要是 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)求成功。或者以下列方式也可以提交成功:

Spring Security自定義登錄原理及實(shí)現(xiàn)詳解

更多的登錄方式 只需要實(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)。

標(biāo)簽: Spring
相關(guān)文章: