上篇文章我们说到,对于用户的登录,security通过定义一个filter拦截login路径来实现的,所以我们要实现自定义登录,需要自己定义一个filter,继承AbstractAuthenticationProcessingFilter,从request中提取到手机号和验证码,然后提交给AuthenticationManager:
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String SPRING_SECURITY_FORM_PHONE_KEY = "phone"; public static final String SPRING_SECURITY_FORM_VERIFY_CODE_KEY = "verifyCode"; private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/smsLogin", "POST"); protected SmsAuthenticationFilter() { super(DEFAULT_ANT_PATH_REQUEST_MATCHER); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { String phone = request.getParameter(SPRING_SECURITY_FORM_PHONE_KEY); String verifyCode = request.getParameter(SPRING_SECURITY_FORM_VERIFY_CODE_KEY); if (StringUtils.isBlank(phone)){ phone = ""; } if (StringUtils.isBlank(verifyCode)){ verifyCode = ""; } SmsAuthenticationToken authenticationToken = new SmsAuthenticationToken(phone, verifyCode); setDetails(request,authenticationToken); return getAuthenticationManager().authenticate(authenticationToken); } protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } }
其中SmsAuthenticationToken参照UsernamePasswordAuthenticationToken来实现:
public class SmsAuthenticationToken extends AbstractAuthenticationToken { private final Object principal; private Object credentials; public SmsAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; //初始化完成,但是还未认证 setAuthenticated(false); } public SmsAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object principal, Object credentials) { super(authorities); this.principal = principal; this.credentials = credentials; setAuthenticated(true); } @Override public Object getCredentials() { return credentials; } @Override public Object getPrincipal() { return principal; } }
我们知道AuthenticationManager最终会委托给Provider来实现身份验证,所以我们要判断验证码是否正确,需要自定义Provider:
@Slf4j @Component public class SmsAuthenticationProvider implements AuthenticationProvider { @Autowired private UserDetailsService userDetailsService; @Override public Authentication authenticate(Authentication authentication) { Assert.isInstanceOf(SmsAuthenticationToken.class, authentication, () -> "SmsAuthenticationProvider.onlySupports Only SmsAuthenticationToken is supported"); SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication; String phone = (String) authenticationToken.getPrincipal(); String verifyCode = (String) authenticationToken.getCredentials(); UserDetails userDetails = userDetailsService.loadUserByUsername(phone); if (userDetails == null){ throw new InternalAuthenticationServiceException("cannot get user info"); } //验证码是否正确 if (!StringUtils.equals(CacheUtil.getValue(phone),verifyCode)){ throw new AuthenticationCredentialsNotFoundException("验证码错误"); } return new SmsAuthenticationToken(userDetails.getAuthorities(),userDetails,verifyCode); } @Override public boolean supports(Class<?> authentication) { return authentication.isAssignableFrom(SmsAuthenticationToken.class); } }
上面的CacheUtil是封装的guava cache的实现,模拟发送验证码存储到内存中,在这个地方取出来做对比,如果对比失败就抛异常,对比成功就返回一个新的token,这个token中是包含了用户具有的权限的。
@Slf4j public class CacheUtil { private static final LoadingCacheCACHE = CacheBuilder.newBuilder() //基于容量回收:总数量100个 .maximumSize(100) //定时回收:没有写访问1分钟后失效清理 .expireAfterWrite(1, TimeUnit.MINUTES) //当在缓存中未找到所需的缓存项时,会执行CacheLoader的load方法加载缓存 .build(new CacheLoader () { @Override public String load(String key) throws Exception { log.debug("没有找到缓存: {}",key); return ""; } }); public static void putValue(String key, String value){ CACHE.put(key,value); } public static String getValue(String key){ try { return CACHE.get(key); } catch (ExecutionException e) { e.printStackTrace(); } return ""; } }
filter将手机号和验证码交给provider做验证,经过provider的校验,结果无非就两种,一种验证成功,一种验证失败,对于这两种不同的结果,我们需要实现两个handler,在获取到结果之后做回调。因为我们这儿只是简单的做url跳转,所以只需要继承SimpleUrlAuthenticationSuccessHandler:
对于success的:
@Component public class SmsAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { public SmsAuthSuccessHandler() { super("/index"); } }
对于failure的:
@Component public class SmsAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler { public SmsAuthFailureHandler() { super("/failure"); } }
上面整个登录流程的组件就完成了,接下来需要将它们整合起来。
具体怎么整合,我们可以参考表单登录中,UsernamePasswordAuthenticationFilter是怎么整合进去的,回到配置类,还记得我们是怎么配置Security的吗:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login") //登录页面 .successForwardUrl("/index") //登录成功后的页面 .failureForwardUrl("/failure") //登录失败后的页面 .and() // 设置URL的授权 .authorizeRequests() // 这里需要将登录页面放行 .antMatchers("/login") .permitAll() //除了上面,其他所有请求必须被认证 .anyRequest() .authenticated() .and() // 关闭csrf .csrf().disable(); } }
看第一句,调用了http.formLogin(),在HttpSecurity的formLogin方法定义如下:
public FormLoginConfigurerformLogin() throws Exception { return getOrApply(new FormLoginConfigurer<>()); } private > C getOrApply(C configurer) throws Exception { //注意这个configure为SecurityConfigurerAdapter C existingConfig = (C) getConfigurer(configurer.getClass()); if (existingConfig != null) { return existingConfig; } return apply(configurer); }
apply方法为AbstractConfiguredSecurityBuilder中的方法,我们目前先不关注它的实现,后面会仔细展开讲。现在只需要知道通过这个方法就能将configurer加入到security配置中。
这个地方添加了一个FormLoginConfigurer类,对于这个类官方给的解释为:
Adds form based authentication. All attributes have reasonable defaults making all parameters are optional. If no {@link #loginPage(String)} is specified, a default login page will be generated by the framework.
翻译过来就是:
添加基于表单的身份验证。所有属性都有合理的默认值,从而使所有参数都是可选的。如果未指定loginPage,则框架将生成一个默认的登录页面。
看一下它的构造方法:
public FormLoginConfigurer() { super(new UsernamePasswordAuthenticationFilter(), null); usernameParameter("username"); passwordParameter("password"); }
发现UsernamePasswordAuthenticationFilter被传递给了父类,我们去它的父类AbstractAuthenticationFilterConfigurer看一下:
public abstract class AbstractAuthenticationFilterConfigurer, T extends AbstractAuthenticationFilterConfigurer, F extends AbstractAuthenticationProcessingFilter> extends AbstractHttpConfigurer{ protected AbstractAuthenticationFilterConfigurer(F authenticationFilter, String defaultLoginProcessingUrl) { this(); //这个filter就是UsernamePasswordAuthenticationFilter this.authFilter = authenticationFilter; if (defaultLoginProcessingUrl != null) { loginProcessingUrl(defaultLoginProcessingUrl); } } @Override public void configure(B http) throws Exception { PortMapper portMapper = http.getSharedObject(PortMapper.class); if (portMapper != null) { this.authenticationEntryPoint.setPortMapper(portMapper); } RequestCache requestCache = 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); } SessionAuthenticationStrategy sessionAuthenticationStrategy = http .getSharedObject(SessionAuthenticationStrategy.class); if (sessionAuthenticationStrategy != null) { this.authFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy); } RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class); if (rememberMeServices != null) { this.authFilter.setRememberMeServices(rememberMeServices); } F filter = postProcess(this.authFilter); //添加filter http.addFilter(filter); } }
可以看到这个地方主要做了三件事:
仿照上面的三个步骤,我们可以自己实现一个配置类,查看AbstractAuthenticationFilterConfigurer的类继承关系:
它最上面的顶级父类为SecurityConfigurerAdapter,我们就继承它来实现我们基本的配置就行了(也可以继承AbstractHttpConfigurer,没有歧视的意思),并且实现上面的三步:
@Component public class SmsAuthenticationSecurityConfig extends SecurityConfigurerAdapter{ @Autowired private SmsAuthSuccessHandler smsAuthSuccessHandler; @Autowired private SmsAuthFailureHandler smsAuthFailureHandler; @Autowired private SmsAuthenticationProvider smsAuthenticationProvider; @Override public void configure(HttpSecurity builder) throws Exception { SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter(); smsAuthenticationFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class)); smsAuthenticationFilter.setAuthenticationSuccessHandler(smsAuthSuccessHandler); smsAuthenticationFilter.setAuthenticationFailureHandler(smsAuthFailureHandler); builder.authenticationProvider(smsAuthenticationProvider); builder.addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
和上面有一点不同,我们自定义的filter需要指定一下顺序,通过addFilterAfter方法将我们的filter添加到过滤器链中,并且将自定义的provider也一并配置了进来。
这样我们的所有组件就已经组合到一起了,修改一下配置类:
@Autowired private SmsAuthenticationSecurityConfig smsAuthenticationSecurityConfig; @Override protected void configure(HttpSecurity http) throws Exception { 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
这样通过短信验证码登录的功能就已经实现了。
建议大家可以自己重新实现一个自定义邮箱验证码登录,加深映像。
上面只是简单的使用,接下来我们分析configure是如何工作的。
大家注意自己要打开idea跟着过一遍源码
其实通过上面的配置我们可以发现,在security中的过滤器其实都是通过各种xxxConfigure来进行配置的,我们可以简单的理解为filter就是和配置类绑定在一起的。明白了这个概念,我们继续往下分析。
看上面AbstractAuthenticationFilterConfigurer的类继承关系图,从最上面开始分析,SecurityBuilder和SecurityConfigurer都是接口:
public interface SecurityBuilder{ /** * 构建一个对象并返回 */ O build() throws Exception; } public interface SecurityConfigurer > { /** * 初始化 */ void init(B builder) throws Exception; void configure(B builder) throws Exception; }
上面两个接口的具体实现交给了SecurityConfigurerAdapter,在spring security中很多配置类都是继承自SecurityConfigurerAdapter来实现的。看一下实现类SecurityConfigurerAdapter的源码:
public abstract class SecurityConfigurerAdapter> implements SecurityConfigurer { private B securityBuilder; private CompositeObjectPostProcessor objectPostProcessor = new CompositeObjectPostProcessor(); @Override public void init(B builder) throws Exception { } @Override public void configure(B builder) throws Exception { } /** * 返回SecurityBuilder,这样就可以进行链式调用了 */ public B and() { return getBuilder(); } /** * 获取到SecurityBuilder */ protected final B getBuilder() { Assert.state(this.securityBuilder != null, "securityBuilder cannot be null"); return this.securityBuilder; } /** * 执行对象的后置处理。默认值为委派给ObjectPostProcessor完成 * @return 可使用的已修改对象 */ @SuppressWarnings("unchecked") protected T postProcess(T object) { return (T) this.objectPostProcessor.postProcess(object); } public void addObjectPostProcessor(ObjectPostProcessor<?> objectPostProcessor) { this.objectPostProcessor.addObjectPostProcessor(objectPostProcessor); } public void setBuilder(B builder) { this.securityBuilder = builder; } /** * ObjectPostProcessor的一个实现 */ private static final class CompositeObjectPostProcessor implements ObjectPostProcessor
嗯。。。这两个方法都是空实现,应该是交给后面的子类去自己重写方法。多出来的内容就只是初始化了CompositeObjectPostProcessor,并基于它封装了两个方法。
CompositeObjectPostProcessor是ObjectPostProcessor的一个实现,ObjectPostProcessor实际上是一个后置处理器。
其次addObjectPostProcessor方法实际上就是在list中添加了一个后置处理器并排序。然后在postProcess方法中对这个list遍历,判断ObjectPostProcessor泛型类型和传过来的参数类型是否为父子关系,再次调用postProcess方法。
这个地方可能有点疑惑,为什么要再调用一次postProcess,这不就成递归了吗,我们注意一下CompositeObjectPostProcessor类是private的,也就是只能在SecurityConfigurerAdapter内部使用,这里再次调用postProcess方法应该是其他的ObjectPostProcessor的实现。
可以看一下ObjectPostProcessor总共有两个实现,另外还有一个是AutowireBeanFactoryObjectPostProcessor:
final class AutowireBeanFactoryObjectPostProcessor implements ObjectPostProcessor
这里面主要是通过autowireBeanFactory将对象注入到容器当中,在security中,很多对象都是new出来的,这些new出来的对象和容器没有任何关联,也不方便管理,所以通过AutowireBeanFactoryObjectPostProcessor来完成对象的注入。
也就是说,在SecurityConfigurerAdapter中定义的这两个方法,其实就是将对象放进spring容器当中,方便管理。
SecurityConfigurerAdapter的内容就这么多了,继续往下看AbstractHttpConfigurer:
public abstract class AbstractHttpConfigurer, B extends HttpSecurityBuilder> extends SecurityConfigurerAdapter { @SuppressWarnings("unchecked") public B disable() { getBuilder().removeConfigurer(getClass()); return getBuilder(); } @SuppressWarnings("unchecked") public T withObjectPostProcessor(ObjectPostProcessor<?> objectPostProcessor) { addObjectPostProcessor(objectPostProcessor); return (T) this; } }
代码很少,第二个方法就是调用SecurityConfigurerAdapter的方法,这里主要看第一个disable方法,我们在配置类中就已经使用过了, 在禁用csrf的时候调用了 csrf().disable(),就是通过这个方法,将csrf的配置移除了。
继续看disable方法是调用了AbstractConfiguredSecurityBuilder中的removeConfigurer方法,实际上就是移除LinkedHashMap中的一个元素:
private final LinkedHashMap>, List >> configurers = new LinkedHashMap<>(); public > List removeConfigurers(Class clazz) { List configs = (List ) this.configurers.remove(clazz); if (configs == null) { return new ArrayList<>(); } return new ArrayList<>(configs); }
既然有移除的方法,那肯定就有添加的方法:
private final List> configurersAddedInInitializing = new ArrayList<>(); private final Map , Object> sharedObjects = new HashMap<>(); @SuppressWarnings("unchecked") private > void add(C configurer) { Assert.notNull(configurer, "configurer cannot be null"); Class<? extends SecurityConfigurer > clazz = (Class<? extends SecurityConfigurer >) configurer .getClass(); synchronized (this.configurers) { if (this.buildState.isConfigured()) { throw new IllegalStateException("Cannot apply " + configurer + " to already built object"); } List > configs = null; if (this.allowConfigurersOfSameType) { configs = this.configurers.get(clazz); } configs = (configs != null) ? configs : new ArrayList<>(1); configs.add(configurer); this.configurers.put(clazz, configs); if (this.buildState.isInitializing()) { this.configurersAddedInInitializing.add(configurer); } } }
我们自定义短信登录的时候,在配置类中添加自定义配置: .apply(smsAuthenticationSecurityConfig),这个apply方法实际上就是调用上面的方法,将配置添加了进去。
既然配置都添加到这个容器当中了,那什么时候取出来用呢:
private Collection> getConfigurers() { List > result = new ArrayList<>(); for (List > configs : this.configurers.values()) { result.addAll(configs); } return result; } //执行所有configurer的初始化方法 private void init() throws Exception { Collection > configurers = getConfigurers(); for (SecurityConfigurer configurer : configurers) { configurer.init((B) this); } for (SecurityConfigurer configurer : this.configurersAddedInInitializing) { configurer.init((B) this); } } //获取到所有的configure,遍历执行configure方法 private void configure() throws Exception { //从LinkedHashMap中获取到configurer Collection > configurers = getConfigurers(); for (SecurityConfigurer configurer : configurers) { configurer.configure((B) this); } }
在init和configure方法中,调用了配置类的configure方法,到这里其实整个流程就已经通了。
我们一般自定义登录,都会实现这个configure方法,在这个方法里初始化一个filter,然后加入到过滤器链中。
而这个类的init和configure方法,实际上是在调用SecurityBuilder 的build方法被调用的,具体的代码链路就不说了,大家感兴趣的可以自己去看一下。
最后贴一下AbstractConfiguredSecurityBuilder的所有代码(已精简):
public abstract class AbstractConfiguredSecurityBuilder> extends AbstractSecurityBuilder { private final LinkedHashMap >, List >> configurers = new LinkedHashMap<>(); private final List > configurersAddedInInitializing = new ArrayList<>(); private final Map , Object> sharedObjects = new HashMap<>(); private final boolean allowConfigurersOfSameType; private ObjectPostProcessor
到此这篇关于Spring Security 自定义短信登录认证的实现的文章就介绍到这了,更多相关SpringSecurity 短信登录认证内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!