Spring-Security-可以同时对接多个用户表

46次阅读

共计 9235 个字符,预计需要花费 24 分钟才能阅读完成。

@[toc]
这个问题也是来自小伙伴的发问:

其实这个问题有好几位小伙伴问过我,然而这个需要比拟冷门,我始终没写文章。

其实只有看懂了松哥后面的文章,这个需要是能够做进去的。因为一个外围点就是 ProviderManager,搞懂了这个,其余的就很容易了。

明天松哥花一点工夫,来和大家剖析一下这个问题的外围,同时通过一个小小案例来演示一下如何同时连贯多个数据源。

1. 原理

1.1 Authentication

玩过 Spring Security 的小伙伴都晓得,在 Spring Security 中有一个十分重要的对象叫做 Authentication,咱们能够在任何中央注入 Authentication 进而获取到以后登录用户信息,Authentication 自身是一个接口,它实际上对 java.security.Principal 做的进一步封装,咱们来看下 Authentication 的定义:

public interface Authentication extends Principal, Serializable {Collection<? extends GrantedAuthority> getAuthorities();
    Object getCredentials();
    Object getDetails();
    Object getPrincipal();
    boolean isAuthenticated();
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

能够看到,这里接口中的办法也没几个,我来大略解释下:

  1. getAuthorities 办法用来获取用户的权限。
  2. getCredentials 办法用来获取用户凭证,一般来说就是明码。
  3. getDetails 办法用来获取用户携带的详细信息,可能是以后申请之类的货色。
  4. getPrincipal 办法用来获取以后用户,可能是一个用户名,也可能是一个用户对象。
  5. isAuthenticated 以后用户是否认证胜利。

Authentication 作为一个接口,它定义了用户,或者说 Principal 的一些根本行为,它有很多实现类:

在这些实现类中,咱们最罕用的就是 UsernamePasswordAuthenticationToken 了,而每一个 Authentication 都有适宜它的 AuthenticationProvider 去解决校验。例如解决 UsernamePasswordAuthenticationToken 的 AuthenticationProvider 是 DaoAuthenticationProvider。

1.2 AuthenticationManager

在 Spring Security 中,用来解决身份认证的类是 AuthenticationManager,咱们也称之为认证管理器。

AuthenticationManager 中标准了 Spring Security 的过滤器要如何执行身份认证,并在身份认证胜利后返回一个通过认证的 Authentication 对象。AuthenticationManager 是一个接口,咱们能够自定义它的实现,然而通常咱们应用更多的是零碎提供的 ProviderManager。

1.3 ProviderManager

ProviderManager 是的最罕用的 AuthenticationManager 实现类。

ProviderManager 治理了一个 AuthenticationProvider 列表,每个 AuthenticationProvider 都是一个认证器,不同的 AuthenticationProvider 用来解决不同的 Authentication 对象的认证。一次残缺的身份认证流程可能会通过多个 AuthenticationProvider。

ProviderManager 相当于代理了多个 AuthenticationProvider,他们的关系如下图:

1.4 AuthenticationProvider

AuthenticationProvider 定义了 Spring Security 中的验证逻辑,咱们来看下 AuthenticationProvider 的定义:

public interface AuthenticationProvider {Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
    boolean supports(Class<?> authentication);
}

能够看到,AuthenticationProvider 中就两个办法:

  • authenticate 办法用来做验证,就是验证用户身份。
  • supports 则用来判断以后的 AuthenticationProvider 是否反对对应的 Authentication。

在一次残缺的认证中,可能蕴含多个 AuthenticationProvider,而这多个 AuthenticationProvider 则由 ProviderManager 进行对立治理,具体能够参考松哥之前的文章:松哥手把手带你捋一遍 Spring Security 登录流程。

这里咱们来重点看一下 DaoAuthenticationProvider,因为这是咱们最罕用的一个,当咱们应用用户名 / 明码登录的时候,用的就是它,DaoAuthenticationProvider 的父类是 AbstractUserDetailsAuthenticationProvider,咱们就先从它的父类看起:

public abstract class AbstractUserDetailsAuthenticationProvider implements
        AuthenticationProvider, InitializingBean, MessageSourceAware {public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;
            try {
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException notFound) {logger.debug("User'" + username + "'not found");

                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                }
                else {throw notFound;}
            }
        }

        try {preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException exception) {if (cacheWasUsed) {
                cacheWasUsed = false;
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            else {throw exception;}
        }

        postAuthenticationChecks.check(user);

        if (!cacheWasUsed) {this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;

        if (forcePrincipalAsString) {principalToReturn = user.getUsername();
        }

        return createSuccessAuthentication(principalToReturn, authentication, user);
    }
    public boolean supports(Class<?> authentication) {
        return (UsernamePasswordAuthenticationToken.class
                .isAssignableFrom(authentication));
    }
}

AbstractUserDetailsAuthenticationProvider 的代码还是挺长的,这里咱们重点关注两个办法:authenticate 和 supports。

authenticate 办法就是用来做认证的办法,咱们来简略看下办法流程:

  1. 首先从 Authentication 提取出登录用户名。
  2. 而后通过拿着 username 去调用 retrieveUser 办法去获取以后用户对象,这一步会调用咱们本人在登录时候的写的 loadUserByUsername 办法,所以这里返回的 user 其实就是你的登录对象,能够参考微人事的 org/javaboy/vhr/service/HrService.java#L34,也能够参考本系列之前的文章:Spring Security+Spring Data Jpa 强强联手,平安治理只有更简略!。
  3. 接下来调用 preAuthenticationChecks.check 办法去测验 user 中的各个账户状态属性是否失常,例如账户是否被禁用、账户是否被锁定、账户是否过期等等。
  4. additionalAuthenticationChecks 办法则是做明码比对的,好多小伙伴好奇 Spring Security 的明码加密之后,是如何进行比拟的,看这里就懂了,因为比拟的逻辑很简略,我这里就不贴代码进去了。然而留神,additionalAuthenticationChecks 办法是一个形象办法,具体的实现是在 AbstractUserDetailsAuthenticationProvider 的子类中实现的,也就是 DaoAuthenticationProvider。这个其实很好了解,因为 AbstractUserDetailsAuthenticationProvider 作为一个较通用的父类,解决一些通用的行为,咱们在登录的时候,有的登录形式并不需要明码,所以 additionalAuthenticationChecks 办法个别交给它的子类去实现,在 DaoAuthenticationProvider 类中,additionalAuthenticationChecks 办法就是做明码比对的,在其余的 AuthenticationProvider 中,additionalAuthenticationChecks 办法的作用就不肯定了。
  5. 最初在 postAuthenticationChecks.check 办法中查看明码是否过期。
  6. 接下来有一个 forcePrincipalAsString 属性,这个是是否强制将 Authentication 中的 principal 属性设置为字符串,这个属性咱们一开始在 UsernamePasswordAuthenticationFilter 类中其实就是设置为字符串的(即 username),然而默认状况下,当用户登录胜利之后,这个属性的值就变成以后用户这个对象了。之所以会这样,就是因为 forcePrincipalAsString 默认为 false,不过这块其实不必改,就用 false,这样在前期获取以后用户信息的时候反而不便很多。
  7. 最初,通过 createSuccessAuthentication 办法构建一个新的 UsernamePasswordAuthenticationToken。

supports 办法就比较简单了,次要用来判断以后的 Authentication 是否是 UsernamePasswordAuthenticationToken。

因为 AbstractUserDetailsAuthenticationProvider 曾经把 authenticate 和 supports 办法实现了,所以在 DaoAuthenticationProvider 中,咱们次要关注 additionalAuthenticationChecks 办法即可:

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {@SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {if (authentication.getCredentials() == null) {
            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
        String presentedPassword = authentication.getCredentials().toString();
        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
    }
}

大家能够看到,additionalAuthenticationChecks 办法次要用来做明码比对的,逻辑也比较简单,就是调用 PasswordEncoder 的 matches 办法做比对,如果明码不对则间接抛出异样即可。

失常状况下,咱们应用用户名 / 明码登录,最终都会走到这一步。

而 AuthenticationProvider 都是通过 ProviderManager#authenticate 办法来调用的。因为咱们的一次认证可能会存在多个 AuthenticationProvider,所以,在 ProviderManager#authenticate 办法中会一一遍历 AuthenticationProvider,并调用他们的 authenticate 办法做认证,咱们来略微瞅一眼 ProviderManager#authenticate 办法:

public Authentication authenticate(Authentication authentication)
        throws AuthenticationException {for (AuthenticationProvider provider : getProviders()) {result = provider.authenticate(authentication);
        if (result != null) {copyDetails(authentication, result);
            break;
        }
    }
    ...
    ...
}

能够看到,在这个办法中,会遍历所有的 AuthenticationProvider,并调用它的 authenticate 办法进行认证。

好了,大抵的认证流程说完之后,置信大家曾经明确了咱们要从哪里下手了。

2. 案例

要想接入多个数据源,咱们只须要提供多个自定义的 AuthenticationProvider,并交给 ProviderManager 进行治理,每一个 AuthenticationProvider 对应不同的数据源即可。

首先咱们创立一个 Spring Boot 我的项目,引入 security 和 web 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

而后创立一个测试 Controller,如下:

@RestController
public class HelloController {@GetMapping("/hello")
    public String hello() {return "hello";}
    @GetMapping("/admin")
    public String admin() {return "admin";}
}

最初配置 SecurityConfig:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    @Primary
    UserDetailsService us1() {return new InMemoryUserDetailsManager(User.builder().username("javaboy").password("{noop}123").roles("admin").build());
    }
    @Bean
    UserDetailsService us2() {return new InMemoryUserDetailsManager(User.builder().username("sang").password("{noop}123").roles("user").build());
    }
    @Override
    @Bean
    protected AuthenticationManager authenticationManager() throws Exception {DaoAuthenticationProvider dao1 = new DaoAuthenticationProvider();
        dao1.setUserDetailsService(us1());

        DaoAuthenticationProvider dao2 = new DaoAuthenticationProvider();
        dao2.setUserDetailsService(us2());

        ProviderManager manager = new ProviderManager(dao1, dao2);
        return manager;
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()
                .antMatchers("/hello").hasRole("user")
                .antMatchers("/admin").hasRole("admin")
                .and()
                .formLogin()
                .loginProcessingUrl("/doLogin")
                .permitAll()
                .and()
                .csrf().disable();
    }
}
  1. 首先提供两个 UserDetailsService 实例,这里为了不便演示,我采纳 InMemoryUserDetailsManager 来构建 UserDetailsService,在理论开发中,大家自行定义 UserDetailsService 即可,能够参考(Spring Security+Spring Data Jpa 强强联手,平安治理只有更简略!)一文。
  2. 接下来自定义 AuthenticationManager,AuthenticationManager 的实例实际上就是 ProviderManager。先结构两个 DaoAuthenticationProvider 实例,每一个传入不同的 UserDetailsService 实例,相当于每一个 DaoAuthenticationProvider 代表了一个 UserDetailsService 实例。
  3. 最初配置 HttpSecurity,这个本系列后面文章讲过很屡次了,这里就不再赘述。

依据第一大节中的原理,在用户身份认证时,两个 DaoAuthenticationProvider 会被顺次执行,这样咱们配置的两个数据源就失效了。

配置实现后,启动我的项目。

在 postman 中进行测试,咱们能够应用 javaboy 登录,登录胜利后的用户具备 admin 角色,所以能够拜访 http://localhost:8080/admin,也能够应用 sang 登录,登录后的用户具备 user 角色,能够拜访 http://localhost:8080/hello。

3. 小结

好啦,本文和小伙伴们分享了一下 Spring Security 中如何同时接入多个数据源的问题,感兴趣的小伙伴能够尝试一下哦~

微信公众号【江南一点雨】后盾回复 multiusers 能够获取本文案例下载地址哦~

如果小伙伴们感觉有播种,记得点个在看激励下松哥哦~

正文完
 0