@[toc]
这个问题也是来自小伙伴的提问:
其实这个问题有好几位小伙伴问过我,但是这个需求比较冷门,我一直没写文章。
其实只要看懂了松哥前面的文章,这个需求是可以做出来的。因为一个核心点就是 ProviderManager,搞懂了这个,其他的就很容易了。
今天松哥花一点时间,来和大家分析一下这个问题的核心,同时通过一个小小案例来演示一下如何同时连接多个数据源。
玩过 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; }
可以看到,这里接口中的方法也没几个,我来大概解释下:
Authentication 作为一个接口,它定义了用户,或者说 Principal 的一些基本行为,它有很多实现类:
在这些实现类中,我们最常用的就是 UsernamePasswordAuthenticationToken 了,而每一个 Authentication 都有适合它的 AuthenticationProvider 去处理校验。例如处理 UsernamePasswordAuthenticationToken 的 AuthenticationProvider 是 DaoAuthenticationProvider。
在 Spring Security 中,用来处理身份认证的类是 AuthenticationManager,我们也称之为认证管理器。
AuthenticationManager 中规范了 Spring Security 的过滤器要如何执行身份认证,并在身份认证成功后返回一个经过认证的 Authentication 对象。AuthenticationManager 是一个接口,我们可以自定义它的实现,但是通常我们使用更多的是系统提供的 ProviderManager。
ProviderManager 是的最常用的 AuthenticationManager 实现类。
ProviderManager 管理了一个 AuthenticationProvider 列表,每个 AuthenticationProvider 都是一个认证器,不同的 AuthenticationProvider 用来处理不同的 Authentication 对象的认证。一次完整的身份认证流程可能会经过多个 AuthenticationProvider。
ProviderManager 相当于代理了多个 AuthenticationProvider,他们的关系如下图:
AuthenticationProvider 定义了 Spring Security 中的验证逻辑,我们来看下 AuthenticationProvider 的定义:
public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class<?> authentication); }
可以看到,AuthenticationProvider 中就两个方法:
在一次完整的认证中,可能包含多个 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 方法就是用来做认证的方法,我们来简单看下方法流程:
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 方法进行认证。
好了,大致的认证流程说完之后,相信大家已经明白了我们要从哪里下手了。
要想接入多个数据源,我们只需要提供多个自定义的 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(); } }
根据第一小节中的原理,在用户身份认证时,两个 DaoAuthenticationProvider 会被依次执行,这样我们配置的两个数据源就生效了。
配置完成后,启动项目。
在 postman 中进行测试,我们可以使用 javaboy 登录,登录成功后的用户具备 admin 角色,所以可以访问 http://localhost:8080/admin,也可以使用 sang 登录,登录后的用户具备 user 角色,可以访问 http://localhost:8080/hello。
好啦,本文和小伙伴们分享了一下 Spring Security 中如何同时接入多个数据源的问题,感兴趣的小伙伴可以尝试一下哦~
本文案例下载地址:https://github.com/lenve/spring-security-samples
如果小伙伴们觉得有收获,记得点个在看鼓励下松哥哦~