在仿造用表单登录认证流程时,得知道这个流程中经过了哪些类,并且哪些类是重要的?不记得的童鞋再去回顾下捋一遍登录Spring Security登录流程(源码篇)这篇文章,详细的介绍了表单登录认证的整个流程。如下图:
image.png

1、IpAuthenticationToken

AuthenticationToken仿造UsernamePasswordAuthenticationToken,同样继承自AbstractAuthenticationToken,然后添加一个ip属性,让getPrincipal方法返回ip,然后仿造UsernamePasswordAuthenticationToken增加两个构造方法,一个是未认证的token,一个是认证成功的token,这两个构造方法在认证过程中会被用到。

  1. public class IpAuthenticationToken extends AbstractAuthenticationToken {
  2. private final String ip;
  3. public IpAuthenticationToken(String ip) {
  4. super(null);
  5. this.ip = ip;
  6. this.setAuthenticated(false);
  7. }
  8. public IpAuthenticationToken(String ip, Collection<? extends GrantedAuthority> authorities) {
  9. super(authorities);
  10. this.ip = ip;
  11. super.setAuthenticated(true);
  12. }
  13. @Override
  14. public Object getCredentials() {
  15. return null;
  16. }
  17. @Override
  18. public Object getPrincipal() {
  19. return ip;
  20. }
  21. }

2、IpAuthenticationFilter

该过滤器仿造UsernamePasswordAuthenticationFilter过滤器,也继承自AbstractAuthenticationProcessingFilter,不过需要拦截的请求地址变成/api/auth/ip-login,然后也要像UsernamePasswordAuthenticationFilter过滤器一样重写父类的attemptAuthentication方法,new一个IpAuthenticationToken对象交给ProviderManager中的Providers集合去认证,我们知道,一个Provider只能认证一个AuthenticationToken对象,那么这个时候问题来了,能够认证IpAuthenticationToken对象还并不存在,所以,我们还得仿造DaoAuthenticationProvider去写一个IpAuthenticationProvider类。

  1. @Slf4j
  2. public class IpAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
  3. private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER =
  4. new AntPathRequestMatcher(SecurityConstant.IP_LOGIN_PROCESSING_URL, "POST");
  5. private boolean postOnly = true;
  6. public IpAuthenticationFilter() {
  7. super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
  8. }
  9. @Override
  10. public Authentication attemptAuthentication(
  11. HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
  12. if (this.postOnly && !request.getMethod().equals("POST")) {
  13. throw new AuthenticationServiceException(
  14. "Authentication method not supported: " + request.getMethod());
  15. } else {
  16. String ip = request.getRemoteHost();
  17. log.info(String.format("当前登录用户ip地址为%s", ip));
  18. IpAuthenticationToken authRequest = new IpAuthenticationToken(ip);
  19. return this.getAuthenticationManager().authenticate(authRequest);
  20. }
  21. }
  22. }

3、IpAuthenticationProvider

该认证类方法仿造DaoAuthenticationProvider类,同样实现AuthenticationProvider接口,实现接口中的authenticatesupports方法,让该认证类只对IpAuthenticationToken生效,然后在认证过程中,DaoAuthenticationProvider会去调用UserDetailsService接口去数据库或内存中拿取用户,然后判断加密后密码是否匹配等等校验,而IpAuthenticationProvider的认证逻辑有点不同,只需要判断当前登录用户的ip地址是否在白名单中,这个白名单当前简单点,直接在内存中写死,如果以后有需要,可以类似UserDetailsService一样从数据库中读取,如果当前用户的ip地址是在白名单中,那么返回一个认证成功的IpAuthenticataionToken,否则返回null。

  1. @Component
  2. public class IpAuthenticationProvider implements AuthenticationProvider, MessageSourceAware {
  3. protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
  4. private static final Map<String, SimpleGrantedAuthority> ipAuthorityMap =
  5. new ConcurrentHashMap<>();
  6. static {
  7. ipAuthorityMap.put("127.0.0.1", new SimpleGrantedAuthority("ADMIN"));
  8. }
  9. @Override
  10. public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  11. Assert.isInstanceOf(
  12. IpAuthenticationToken.class,
  13. authentication,
  14. () ->
  15. this.messages.getMessage(
  16. "IpAuthenticationProvider.onlySupports",
  17. "Only IpAuthenticationToken is supported"));
  18. String ip = (String) authentication.getPrincipal();
  19. if (ipAuthorityMap.containsKey(ip)) {
  20. return new IpAuthenticationToken(ip, Collections.singletonList(ipAuthorityMap.get(ip)));
  21. }
  22. return null;
  23. }
  24. @Override
  25. public boolean supports(Class<?> authentication) {
  26. return IpAuthenticationToken.class.isAssignableFrom(authentication);
  27. }
  28. @Override
  29. public void setMessageSource(MessageSource messageSource) {
  30. this.messages = new MessageSourceAccessor(messageSource);
  31. }
  32. }

4、IpLoginConfigurer

看完Spring Security 初始化流程梳理(源码篇二)🎁这篇文章的童鞋应该知道,一条过滤器链是怎么产生的?遍历HttpSecurity中的configurers集合,对每一个configurer配置类进行initconfigure方法,这样就将配置好的过滤器添加到HttpSecurity#filters集合中,然后HttpSecurity再执行performBuild方法使用filters集合构建出一条过滤器链。所以在这里我们得仿造FormLoginConfigurer编写一个IpLoginConfigurer,继承自SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>,重写其中的configure方法,在configure方法中需要将IpLoginFilter添加到HttpSecurity#filters集合中。

  1. @Component
  2. @RequiredArgsConstructor
  3. public class IpLoginConfigurer
  4. extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
  5. private final MyAuthenticationSuccessHandler authenticationSuccessHandler;
  6. private final MyAuthenticationFailureHandler authenticationFailureHandler;
  7. private final IpAuthenticationProvider ipAuthenticationProvider;
  8. @Override
  9. public void init(HttpSecurity http) {
  10. http.authenticationProvider(postProcess(new IpAuthenticationProvider()));
  11. }
  12. @Override
  13. public void configure(HttpSecurity http) {
  14. IpAuthenticationFilter ipAuthenticationFilter = new IpAuthenticationFilter();
  15. ipAuthenticationFilter.setAuthenticationManager(
  16. http.getSharedObject(AuthenticationManager.class));
  17. ipAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
  18. ipAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
  19. http.addFilterBefore(ipAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
  20. }
  21. }

一定得记住将ipAuthenticationProvider通过HttpSecurity对象添加到ProviderManager中的providers属性中,否则在后面认证过程中会应找不到ipAuthenticationProvider而认证不了。 :::warning 🤔我将http.authenticationProvider(ipAuthenticationProvider)写在configure方法中居然也可以?这个地方我还没搞懂为什么???正确的添加时机应该是init方法的时候,因为在HttpSecurity中的beforeConfigure中就已经将AuthenticationManager对象构建出来了。所以我没看懂为什么在configure方法中添加ipAuthenticationProvider也可以,但是是添加到了父AuthenticationManager对象中的providers集合中,搞不懂? :::

  1. @Override
  2. protected void beforeConfigure() throws Exception {
  3. if (this.authenticationManager != null) {
  4. setSharedObject(AuthenticationManager.class, this.authenticationManager);
  5. }
  6. else {
  7. setSharedObject(AuthenticationManager.class, getAuthenticationRegistry().build());
  8. }
  9. }

5、小结

对比Spring Security默认的表单登录认证,使用Ip认证登录需要对照增加的类:

  • UsernamePasswordAuthenticationToken -> IpAuthenticationToken
  • UsernamePasswordAuthenticationFilter -> IpAuthenticationFilter
  • DaoAuthenticationProvider -> IpAuthenticationProvider
  • UserDetailService -> ConcurrentHashMap
  • FormLoginConfigurer -> IpLoginConfigurer

后续使用短信登录或者第三方登录的时候也可以按照这个思路进行扩展,加油!🥳