Spring Security自定义登录原理及实现详解
1.前言
前面的关于SpringSecurity相关的文章只是一个预热。为了接下来更好的实战,如果你错过了请从SpringSecurity实战系列开始。安全访问的第一步就是认证(Authentication),认证的第一步就是登录。今天我们要通过对SpringSecurity的自定义,来设计一个可扩展,可伸缩的form登录功能。
2.form登录的流程
下面是form登录的基本流程:
只要是form登录基本都能转化为上面的流程。接下来我们看看SpringSecurity是如何处理的。
3.SpringSecurity中的登录
昨天SpringSecurity实战干货:自定义配置类入口WebSecurityConfigurerAdapter中已经讲到了我们通常的自定义访问控制主要是通过HttpSecurity来构建的。默认它提供了三种登录方式:
- formLogin()普通表单登录
- oauth2Login()基于OAuth2.0认证/授权协议
- openidLogin()基于OpenID身份认证规范
以上三种方式统统是AbstractAuthenticationFilterConfigurer实现的,
4.HttpSecurity中的form表单登录
启用表单登录通过两种方式一种是通过HttpSecurity的apply(Cconfigurer)方法自己构造一个AbstractAuthenticationFilterConfigurer的实现,这种是比较高级的玩法。另一种是我们常见的使用HttpSecurity的formLogin()方法来自定义FormLoginConfigurer。我们先搞一下比较常规的第二种。
4.1FormLoginConfigurer
该类是form表单登录的配置类。它提供了一些我们常用的配置方法:
- loginPage(StringloginPage):登录页面而并不是接口,对于前后分离模式需要我们进行改造默认为/login。
- loginProcessingUrl(StringloginProcessingUrl)实际表单向后台提交用户信息的Action,再由过滤器UsernamePasswordAuthenticationFilter拦截处理,该Action其实不会处理任何逻辑。
- usernameParameter(StringusernameParameter)用来自定义用户参数名,默认username。
- passwordParameter(StringpasswordParameter)用来自定义用户密码名,默认password
- failureUrl(StringauthenticationFailureUrl)登录失败后会重定向到此路径,一般前后分离不会使用它。
- failureForwardUrl(StringforwardUrl)登录失败会转发到此,一般前后分离用到它。可定义一个Controller(控制器)来处理返回值,但是要注意RequestMethod。
- defaultSuccessUrl(StringdefaultSuccessUrl,booleanalwaysUse)默认登陆成功后跳转到此,如果alwaysUse为true只要进行认证流程而且成功,会一直跳转到此。一般推荐默认值false
- successForwardUrl(StringforwardUrl)效果等同于上面defaultSuccessUrl的alwaysUse为true但是要注意RequestMethod。
- successHandler(AuthenticationSuccessHandlersuccessHandler)自定义认证成功处理器,可替代上面所有的success方式
- failureHandler(AuthenticationFailureHandlerauthenticationFailureHandler)自定义失败成功处理器,可替代上面所有的success方式
- permitAll(booleanpermitAll)form表单登录是否放开
知道了这些我们就能来搞个定制化的登录了。
5.SpringSecurity聚合登录实战
接下来是我们最激动人心的实战登录操作。有疑问的可认真阅读Spring实战的一系列预热文章。
5.1简单需求
我们的接口访问都要通过认证,登陆错误后返回错误信息(json),成功后前台可以获取到对应数据库用户信息(json)(实战中记得脱敏)。
我们定义处理成功失败的控制器:
@RestController @RequestMapping("/login") publicclassLoginController{ @Resource privateSysUserServicesysUserService; /** *登录失败返回401以及提示信息. * *@returntherest */ @PostMapping("/failure") publicRestloginFailure(){ returnRestBody.failure(HttpStatus.UNAUTHORIZED.value(),"登录失败了,老哥"); } /** *登录成功后拿到个人信息. * *@returntherest */ @PostMapping("/success") publicRestloginSuccess(){ //登录成功后用户的认证信息UserDetails会存在安全上下文寄存器SecurityContextHolder中 Userprincipal=(User)SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Stringusername=principal.getUsername(); SysUsersysUser=sysUserService.queryByUsername(username); //脱敏 sysUser.setEncodePassword("[PROTECT]"); returnRestBody.okData(sysUser,"登录成功"); } }
然后我们自定义配置覆写voidconfigure(HttpSecurityhttp)方法进行如下配置(这里需要禁用crsf):
@Configuration @ConditionalOnClass(WebSecurityConfigurerAdapter.class) @ConditionalOnWebApplication(type=ConditionalOnWebApplication.Type.SERVLET) publicclassCustomSpringBootWebSecurityConfiguration{ @Configuration @Order(SecurityProperties.BASIC_AUTH_ORDER) staticclassDefaultConfigurerAdapterextendsWebSecurityConfigurerAdapter{ @Override protectedvoidconfigure(AuthenticationManagerBuilderauth)throwsException{ super.configure(auth); } @Override publicvoidconfigure(WebSecurityweb)throwsException{ super.configure(web); } @Override protectedvoidconfigure(HttpSecurityhttp)throwsException{ 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.多种登录方式的简单实现
就这么完了么?现在登录的花样繁多。常规的就有短信、邮箱、扫码,第三方是以后我要讲的不在今天范围之内。如何应对想法多的产品经理?我们来搞一个可扩展各种姿势的登录方式。我们在上面2.form登录的流程中的用户和判定之间增加一个适配器来适配即可。我们知道这个所谓的判定就是UsernamePasswordAuthenticationFilter。
我们只需要保证uri为上面配置的/process并且能够通过getParameter(Stringname)获取用户名和密码即可。
我突然觉得可以模仿DelegatingPasswordEncoder的搞法,维护一个注册表执行不同的处理策略。当然我们要实现一个GenericFilterBean在UsernamePasswordAuthenticationFilter之前执行。同时制定登录的策略。
6.1登录方式定义
定义登录方式枚举``。
publicenumLoginTypeEnum{ /** *原始登录方式. */ FORM, /** *Json提交. */ JSON, /** *验证码. */ CAPTCHA }
6.2定义前置处理器接口
publicinterfaceLoginPostProcessor{ /** *获取登录类型 * *@returnthetype */ LoginTypeEnumgetLoginTypeEnum(); /** *获取用户名 * *@paramrequesttherequest *@returnthestring */ StringobtainUsername(ServletRequestrequest); /** *获取密码 * *@paramrequesttherequest *@returnthestring */ StringobtainPassword(ServletRequestrequest); }
6.3实现登录前置处理过滤器
该过滤器维护了LoginPostProcessor映射表。通过前端来判定登录方式进行策略上的预处理,最终还是会交给
packagecn.felord.spring.security.filter; importcn.felord.spring.security.enumation.LoginTypeEnum; importorg.springframework.security.web.util.matcher.AntPathRequestMatcher; importorg.springframework.security.web.util.matcher.RequestMatcher; importorg.springframework.util.Assert; importorg.springframework.util.CollectionUtils; importorg.springframework.web.filter.GenericFilterBean; importjavax.servlet.FilterChain; importjavax.servlet.ServletException; importjavax.servlet.ServletRequest; importjavax.servlet.ServletResponse; importjavax.servlet.http.HttpServletRequest; importjava.io.IOException; importjava.util.Collection; importjava.util.HashMap; importjava.util.Map; importstaticorg.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY; importstaticorg.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY; /** *预登录控制器 * *@authorFelordcn *@since16:212019/10/17 */ publicclassPreLoginFilterextendsGenericFilterBean{ privatestaticfinalStringLOGIN_TYPE_KEY="login_type"; privateRequestMatcherrequiresAuthenticationRequestMatcher; privateMapprocessors=newHashMap<>(); publicPreLoginFilter(StringloginProcessingUrl,Collection loginPostProcessors){ Assert.notNull(loginProcessingUrl,"loginProcessingUrlmustnotbenull"); requiresAuthenticationRequestMatcher=newAntPathRequestMatcher(loginProcessingUrl,"POST"); LoginPostProcessorloginPostProcessor=defaultLoginPostProcessor(); processors.put(loginPostProcessor.getLoginTypeEnum(),loginPostProcessor); if(!CollectionUtils.isEmpty(loginPostProcessors)){ loginPostProcessors.forEach(element->processors.put(element.getLoginTypeEnum(),element)); } } privateLoginTypeEnumgetTypeFromReq(ServletRequestrequest){ Stringparameter=request.getParameter(LOGIN_TYPE_KEY); inti=Integer.parseInt(parameter); LoginTypeEnum[]values=LoginTypeEnum.values(); returnvalues[i]; } /** *默认还是Form. * *@returntheloginpostprocessor */ privateLoginPostProcessordefaultLoginPostProcessor(){ returnnewLoginPostProcessor(){ @Override publicLoginTypeEnumgetLoginTypeEnum(){ returnLoginTypeEnum.FORM; } @Override publicStringobtainUsername(ServletRequestrequest){ returnrequest.getParameter(SPRING_SECURITY_FORM_USERNAME_KEY); } @Override publicStringobtainPassword(ServletRequestrequest){ returnrequest.getParameter(SPRING_SECURITY_FORM_PASSWORD_KEY); } }; } @Override publicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)throwsIOException,ServletException{ ParameterRequestWrapperparameterRequestWrapper=newParameterRequestWrapper((HttpServletRequest)request); if(requiresAuthenticationRequestMatcher.matches((HttpServletRequest)request)){ LoginTypeEnumtypeFromReq=getTypeFromReq(request); LoginPostProcessorloginPostProcessor=processors.get(typeFromReq); Stringusername=loginPostProcessor.obtainUsername(request); Stringpassword=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可以请求成功。或者以下列方式也可以提交成功:
更多的登录方式只需要实现接口LoginPostProcessor注入PreLoginFilter
7.总结
今天我们通过各种技术的运用实现了从简单登录到可动态扩展的多种方式并存的实战运用。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。