Spring Security 自定义短信登录认证的实现
自定义登录filter
上篇文章我们说到,对于用户的登录,security通过定义一个filter拦截login路径来实现的,所以我们要实现自定义登录,需要自己定义一个filter,继承AbstractAuthenticationProcessingFilter,从request中提取到手机号和验证码,然后提交给AuthenticationManager:
publicclassSmsAuthenticationFilterextendsAbstractAuthenticationProcessingFilter{ publicstaticfinalStringSPRING_SECURITY_FORM_PHONE_KEY="phone"; publicstaticfinalStringSPRING_SECURITY_FORM_VERIFY_CODE_KEY="verifyCode"; privatestaticfinalAntPathRequestMatcherDEFAULT_ANT_PATH_REQUEST_MATCHER=newAntPathRequestMatcher("/smsLogin", "POST"); protectedSmsAuthenticationFilter(){ super(DEFAULT_ANT_PATH_REQUEST_MATCHER); } @Override publicAuthenticationattemptAuthentication(HttpServletRequestrequest,HttpServletResponseresponse)throwsAuthenticationException,IOException,ServletException{ Stringphone=request.getParameter(SPRING_SECURITY_FORM_PHONE_KEY); StringverifyCode=request.getParameter(SPRING_SECURITY_FORM_VERIFY_CODE_KEY); if(StringUtils.isBlank(phone)){ phone=""; } if(StringUtils.isBlank(verifyCode)){ verifyCode=""; } SmsAuthenticationTokenauthenticationToken=newSmsAuthenticationToken(phone,verifyCode); setDetails(request,authenticationToken); returngetAuthenticationManager().authenticate(authenticationToken); } protectedvoidsetDetails(HttpServletRequestrequest,SmsAuthenticationTokenauthRequest){ authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } }
其中SmsAuthenticationToken参照UsernamePasswordAuthenticationToken来实现:
publicclassSmsAuthenticationTokenextendsAbstractAuthenticationToken{ privatefinalObjectprincipal; privateObjectcredentials; publicSmsAuthenticationToken(Objectprincipal,Objectcredentials){ super(null); this.principal=principal; this.credentials=credentials; //初始化完成,但是还未认证 setAuthenticated(false); } publicSmsAuthenticationToken(Collectionauthorities,Objectprincipal,Objectcredentials){ super(authorities); this.principal=principal; this.credentials=credentials; setAuthenticated(true); } @Override publicObjectgetCredentials(){ returncredentials; } @Override publicObjectgetPrincipal(){ returnprincipal; } }
自定义provider实现身份认证
我们知道AuthenticationManager最终会委托给Provider来实现身份验证,所以我们要判断验证码是否正确,需要自定义Provider:
@Slf4j @Component publicclassSmsAuthenticationProviderimplementsAuthenticationProvider{ @Autowired privateUserDetailsServiceuserDetailsService; @Override publicAuthenticationauthenticate(Authenticationauthentication){ Assert.isInstanceOf(SmsAuthenticationToken.class,authentication, ()->"SmsAuthenticationProvider.onlySupportsOnlySmsAuthenticationTokenissupported"); SmsAuthenticationTokenauthenticationToken=(SmsAuthenticationToken)authentication; Stringphone=(String)authenticationToken.getPrincipal(); StringverifyCode=(String)authenticationToken.getCredentials(); UserDetailsuserDetails=userDetailsService.loadUserByUsername(phone); if(userDetails==null){ thrownewInternalAuthenticationServiceException("cannotgetuserinfo"); } //验证码是否正确 if(!StringUtils.equals(CacheUtil.getValue(phone),verifyCode)){ thrownewAuthenticationCredentialsNotFoundException("验证码错误"); } returnnewSmsAuthenticationToken(userDetails.getAuthorities(),userDetails,verifyCode); } @Override publicbooleansupports(Class>authentication){ returnauthentication.isAssignableFrom(SmsAuthenticationToken.class); } }
上面的CacheUtil是封装的guavacache的实现,模拟发送验证码存储到内存中,在这个地方取出来做对比,如果对比失败就抛异常,对比成功就返回一个新的token,这个token中是包含了用户具有的权限的。
@Slf4j publicclassCacheUtil{ privatestaticfinalLoadingCacheCACHE=CacheBuilder.newBuilder() //基于容量回收:总数量100个 .maximumSize(100) //定时回收:没有写访问1分钟后失效清理 .expireAfterWrite(1,TimeUnit.MINUTES) //当在缓存中未找到所需的缓存项时,会执行CacheLoader的load方法加载缓存 .build(newCacheLoader (){ @Override publicStringload(Stringkey)throwsException{ log.debug("没有找到缓存:{}",key); return""; } }); publicstaticvoidputValue(Stringkey,Stringvalue){ CACHE.put(key,value); } publicstaticStringgetValue(Stringkey){ try{ returnCACHE.get(key); }catch(ExecutionExceptione){ e.printStackTrace(); } return""; } }
身份认证结果回调
filter将手机号和验证码交给provider做验证,经过provider的校验,结果无非就两种,一种验证成功,一种验证失败,对于这两种不同的结果,我们需要实现两个handler,在获取到结果之后做回调。因为我们这儿只是简单的做url跳转,所以只需要继承SimpleUrlAuthenticationSuccessHandler:
对于success的:
@Component publicclassSmsAuthSuccessHandlerextendsSimpleUrlAuthenticationSuccessHandler{ publicSmsAuthSuccessHandler(){ super("/index"); } }
对于failure的:
@Component publicclassSmsAuthFailureHandlerextendsSimpleUrlAuthenticationFailureHandler{ publicSmsAuthFailureHandler(){ super("/failure"); } }
上面整个登录流程的组件就完成了,接下来需要将它们整合起来。
整合登录组件
具体怎么整合,我们可以参考表单登录中,UsernamePasswordAuthenticationFilter是怎么整合进去的,回到配置类,还记得我们是怎么配置Security的吗:
@Configuration publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{ @Override protectedvoidconfigure(HttpSecurityhttp)throwsException{ http.formLogin() .loginPage("/login")//登录页面 .successForwardUrl("/index")//登录成功后的页面 .failureForwardUrl("/failure")//登录失败后的页面 .and() //设置URL的授权 .authorizeRequests() //这里需要将登录页面放行 .antMatchers("/login") .permitAll() //除了上面,其他所有请求必须被认证 .anyRequest() .authenticated() .and() //关闭csrf .csrf().disable(); } }
分析表单登录实现
看第一句,调用了http.formLogin(),在HttpSecurity的formLogin方法定义如下:
publicFormLoginConfigurerformLogin()throwsException{ returngetOrApply(newFormLoginConfigurer<>()); } private >CgetOrApply(Cconfigurer) throwsException{ //注意这个configure为SecurityConfigurerAdapter CexistingConfig=(C)getConfigurer(configurer.getClass()); if(existingConfig!=null){ returnexistingConfig; } returnapply(configurer); }
apply方法为AbstractConfiguredSecurityBuilder中的方法,我们目前先不关注它的实现,后面会仔细展开讲。现在只需要知道通过这个方法就能将configurer加入到security配置中。
这个地方添加了一个FormLoginConfigurer类,对于这个类官方给的解释为:
Addsformbasedauthentication.Allattributeshavereasonabledefaultsmakingallparametersareoptional.Ifno{@link#loginPage(String)}isspecified,adefaultloginpagewillbegeneratedbytheframework.
翻译过来就是:
添加基于表单的身份验证。所有属性都有合理的默认值,从而使所有参数都是可选的。如果未指定loginPage,则框架将生成一个默认的登录页面。
看一下它的构造方法:
publicFormLoginConfigurer(){ super(newUsernamePasswordAuthenticationFilter(),null); usernameParameter("username"); passwordParameter("password"); }
发现UsernamePasswordAuthenticationFilter被传递给了父类,我们去它的父类AbstractAuthenticationFilterConfigurer看一下:
publicabstractclassAbstractAuthenticationFilterConfigurer,TextendsAbstractAuthenticationFilterConfigurer,FextendsAbstractAuthenticationProcessingFilter> extendsAbstractHttpConfigurer { protectedAbstractAuthenticationFilterConfigurer(FauthenticationFilter,StringdefaultLoginProcessingUrl){ this(); //这个filter就是UsernamePasswordAuthenticationFilter this.authFilter=authenticationFilter; if(defaultLoginProcessingUrl!=null){ loginProcessingUrl(defaultLoginProcessingUrl); } } @Override publicvoidconfigure(Bhttp)throwsException{ PortMapperportMapper=http.getSharedObject(PortMapper.class); if(portMapper!=null){ this.authenticationEntryPoint.setPortMapper(portMapper); } RequestCacherequestCache=http.getSharedObject(RequestCache.class); if(requestCache!=null){ this.defaultSuccessHandler.setRequestCache(requestCache); } //通过getSharedObject获取共享对象。这里获取到AuthenticationManager this.authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); //设置成功和失败的回调 this.authFilter.setAuthenticationSuccessHandler(this.successHandler); this.authFilter.setAuthenticationFailureHandler(this.failureHandler); if(this.authenticationDetailsSource!=null){ this.authFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource); } SessionAuthenticationStrategysessionAuthenticationStrategy=http .getSharedObject(SessionAuthenticationStrategy.class); if(sessionAuthenticationStrategy!=null){ this.authFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy); } RememberMeServicesrememberMeServices=http.getSharedObject(RememberMeServices.class); if(rememberMeServices!=null){ this.authFilter.setRememberMeServices(rememberMeServices); } Ffilter=postProcess(this.authFilter); //添加filter http.addFilter(filter); } }
可以看到这个地方主要做了三件事:
- 将AuthenticationManager设置到filter中
- 添加成功/失败的回调
- 将过滤器添加到过滤器链中
仿照表单登录,实现配置类
仿照上面的三个步骤,我们可以自己实现一个配置类,查看AbstractAuthenticationFilterConfigurer的类继承关系:
它最上面的顶级父类为SecurityConfigurerAdapter,我们就继承它来实现我们基本的配置就行了(也可以继承AbstractHttpConfigurer,没有歧视的意思),并且实现上面的三步:
@Component publicclassSmsAuthenticationSecurityConfigextendsSecurityConfigurerAdapter{ @Autowired privateSmsAuthSuccessHandlersmsAuthSuccessHandler; @Autowired privateSmsAuthFailureHandlersmsAuthFailureHandler; @Autowired privateSmsAuthenticationProvidersmsAuthenticationProvider; @Override publicvoidconfigure(HttpSecuritybuilder)throwsException{ SmsAuthenticationFiltersmsAuthenticationFilter=newSmsAuthenticationFilter(); smsAuthenticationFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class)); smsAuthenticationFilter.setAuthenticationSuccessHandler(smsAuthSuccessHandler); smsAuthenticationFilter.setAuthenticationFailureHandler(smsAuthFailureHandler); builder.authenticationProvider(smsAuthenticationProvider); builder.addFilterAfter(smsAuthenticationFilter,UsernamePasswordAuthenticationFilter.class); } }
和上面有一点不同,我们自定义的filter需要指定一下顺序,通过addFilterAfter方法将我们的filter添加到过滤器链中,并且将自定义的provider也一并配置了进来。
添加配置到security中
这样我们的所有组件就已经组合到一起了,修改一下配置类:
@Autowired privateSmsAuthenticationSecurityConfigsmsAuthenticationSecurityConfig; @Override protectedvoidconfigure(HttpSecurityhttp)throwsException{ http.formLogin() .loginPage("/login") .and() .apply(smsAuthenticationSecurityConfig) .and() //设置URL的授权 .authorizeRequests() //这里需要将登录页面放行 .antMatchers("/login","/verifyCode","/smsLogin","/failure") .permitAll() //anyRequest()所有请求authenticated()必须被认证 .anyRequest() .authenticated() .and() //关闭csrf .csrf().disable(); }
再修改一下登录页面的登录接口和字段名:
login