Spring Security自定義登錄原理及實現(xiàn)詳解
1. 前言
前面的關于 Spring Security 相關的文章只是一個預熱。為了接下來更好的實戰(zhàn),如果你錯過了請從 Spring Security 實戰(zhàn)系列 開始。安全訪問的第一步就是認證(Authentication),認證的第一步就是登錄。今天我們要通過對 Spring Security 的自定義,來設計一個可擴展,可伸縮的 form 登錄功能。
2. form 登錄的流程
下面是 form 登錄的基本流程:
只要是 form 登錄基本都能轉化為上面的流程。接下來我們看看 Spring Security 是如何處理的。
3. Spring Security 中的登錄
昨天 Spring Security 實戰(zhàn)干貨:自定義配置類入口WebSecurityConfigurerAdapter 中已經講到了我們通常的自定義訪問控制主要是通過 HttpSecurity 來構建的。默認它提供了三種登錄方式:
- formLogin() 普通表單登錄
- oauth2Login() 基于 OAuth2.0 認證/授權協(xié)議
- openidLogin() 基于 OpenID 身份認證規(guī)范
以上三種方式統(tǒng)統(tǒng)是 AbstractAuthenticationFilterConfigurer 實現(xiàn)的,
4. HttpSecurity 中的 form 表單登錄
啟用表單登錄通過兩種方式一種是通過 HttpSecurity 的 apply(C configurer) 方法自己構造一個 AbstractAuthenticationFilterConfigurer 的實現(xiàn),這種是比較高級的玩法。 另一種是我們常見的使用 HttpSecurity 的 formLogin() 方法來自定義 FormLoginConfigurer 。我們先搞一下比較常規(guī)的第二種。
4.1 FormLoginConfigurer
該類是 form 表單登錄的配置類。它提供了一些我們常用的配置方法:
- loginPage(String loginPage) : 登錄 頁面而并不是接口,對于前后分離模式需要我們進行改造 默認為 /login。
- loginProcessingUrl(String loginProcessingUrl) 實際表單向后臺提交用戶信息的 Action,再由過濾器UsernamePasswordAuthenticationFilter 攔截處理,該 Action 其實不會處理任何邏輯。
- usernameParameter(String usernameParameter) 用來自定義用戶參數(shù)名,默認 username 。
- passwordParameter(String passwordParameter) 用來自定義用戶密碼名,默認 password
- failureUrl(String authenticationFailureUrl) 登錄失敗后會重定向到此路徑, 一般前后分離不會使用它。
- failureForwardUrl(String forwardUrl) 登錄失敗會轉發(fā)到此, 一般前后分離用到它。 可定義一個 Controller (控制器)來處理返回值,但是要注意 RequestMethod。
- defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse) 默認登陸成功后跳轉到此 ,如果 alwaysUse 為 true 只要進行認證流程而且成功,會一直跳轉到此。一般推薦默認值 false
- successForwardUrl(String forwardUrl) 效果等同于上面 defaultSuccessUrl 的 alwaysUse 為 true 但是要注意 RequestMethod。
- successHandler(AuthenticationSuccessHandler successHandler) 自定義認證成功處理器,可替代上面所有的 success 方式
- failureHandler(AuthenticationFailureHandler authenticationFailureHandler) 自定義失敗成功處理器,可替代上面所有的 success 方式
- permitAll(boolean permitAll) form 表單登錄是否放開
知道了這些我們就能來搞個定制化的登錄了。
5. Spring Security 聚合登錄 實戰(zhàn)
接下來是我們最激動人心的實戰(zhàn)登錄操作。 有疑問的可認真閱讀 Spring 實戰(zhàn) 的一系列預熱文章。
5.1 簡單需求
我們的接口訪問都要通過認證,登陸錯誤后返回錯誤信息(json),成功后前臺可以獲取到對應數(shù)據庫用戶信息(json)(實戰(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(), "登錄失敗了,老哥"); } /** * 登錄成功后拿到個人信息. * * @return the rest */ @PostMapping("/success") public Rest loginSuccess() { // 登錄成功后用戶的認證信息 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) 方法進行如下配置(這里需要禁用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 或者其它工具進行 Post 方式的表單提交 http://localhost:8080/process?username=Felordcn&password=12345 會返回用戶信息:
{ "httpStatus": 200, "data": { "userId": 1, "username": "Felordcn", "encodePassword": "[PROTECT]", "age": 18 }, "msg": "登錄成功", "identifier": "" }
把密碼修改為其它值再次請求認證失敗后 :
{ "httpStatus": 401, "data": null, "msg": "登錄失敗了,老哥", "identifier": "-9999" }
6. 多種登錄方式的簡單實現(xiàn)
就這么完了么?現(xiàn)在登錄的花樣繁多。常規(guī)的就有短信、郵箱、掃碼 ,第三方是以后我要講的不在今天范圍之內。 如何應對想法多的產品經理? 我們來搞一個可擴展各種姿勢的登錄方式。我們在上面 2. form 登錄的流程 中的 用戶 和 判定 之間增加一個適配器來適配即可。 我們知道這個所謂的 判定就是 UsernamePasswordAuthenticationFilter 。
我們只需要保證 uri 為上面配置的/process 并且能夠通過 getParameter(String name) 獲取用戶名和密碼即可 。
我突然覺得可以模仿 DelegatingPasswordEncoder 的搞法, 維護一個注冊表執(zhí)行不同的處理策略。當然我們要實現(xiàn)一個 GenericFilterBean 在 UsernamePasswordAuthenticationFilter 之前執(zhí)行。同時制定登錄的策略。
6.1 登錄方式定義
定義登錄方式枚舉 ``。
public enum LoginTypeEnum { /** * 原始登錄方式. */ FORM, /** * Json 提交. */ JSON, /** * 驗證碼. */ 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 實現(xiàn)登錄前置處理過濾器
該過濾器維護了 LoginPostProcessor 映射表。 通過前端來判定登錄方式進行策略上的預處理,最終還是會交給
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; /** * 預登錄控制器 * * @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]; } /** * 默認還是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 驗證
通過 POST 表單提交方式 http://localhost:8080/process?username=Felordcn&password=12345&login_type=0 可以請求成功?;蛘咭韵铝蟹绞揭部梢蕴峤怀晒Γ?/p>
更多的登錄方式 只需要實現(xiàn)接口 LoginPostProcessor 注入 PreLoginFilter
7. 總結
今天我們通過各種技術的運用實現(xiàn)了從簡單登錄到可動態(tài)擴展的多種方式并存的實戰(zhàn)運用。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
Java如何利用狀態(tài)模式(state pattern)替代if else
這篇文章主要給大家介紹了關于Java如何利用狀態(tài)模式(state pattern)替代if else的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-11-11springboot跨域過濾器fetch react Response to p
這篇文章主要介紹了springboot跨域過濾器fetch react Response to preflight request doesn‘t pass access control check問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03Spring Cache和EhCache實現(xiàn)緩存管理方式
這篇文章主要介紹了Spring Cache和EhCache實現(xiàn)緩存管理方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06