起因
项目需要对两个用户表进行配置,于是上网看到了这样的代码
@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();
}
}
可以见到,该作者声明了两个provider对象并分别设置了他们的userService最后把他们加入ProviderManager。但是当我使用同样的代码时,问题出现了
使用过程
// 说明:新版的好像不支持原作者那样单个单个provider初始化了,参数必须是一个colletion
@Override
@Bean
protected AuthenticationManager authenticationManager() {
MyProvider studentDao = new MyProvider();
studentDao.setHideUserNotFoundExceptions(false);
studentDao.setUserDetailsService(studentService);
studentDao.setPasswordEncoder(new MyPasswordEncoder());
MyProvider teacherDao = new MyProvider();
teacherDao.setHideUserNotFoundExceptions(false);
teacherDao.setUserDetailsService(teacherService);
teacherDao.setPasswordEncoder(new MyPasswordEncoder());
List<AuthenticationProvider> daos = new LinkedList<>();
daos.add(studentDao);
daos.add(teacherDao);
ProviderManager manager = new ProviderManager(daos);
return manager;
}
但是此时,当我进行密码验证的时候,发现SpringSecurity永远只能和studentDao中的数据进行比对,而无法访问到teacherDao,此时深入源码,查看ProviderManager类:
ProviderManager.java
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
...以下从略
}
可以看到这行代码for (AuthenticationProvider provider : getProviders())
,Spring Security的确是遍历了ProviderManager
,但是问题出在result = provider.authenticate(authentication);
中,继续深入观察provider的authenticate代码, 最终会到它的顶层抽象类:
AbstractUserDetailsAuthenticationProvider.java
// 不需要看太认真
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
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;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
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);
}
可以看到,这个方法主要进行一些密码比对的工作,关键的是最后一句,return createSuccessAuthentication(principalToReturn, authentication, user);
,再看createSuccessAuthentication()
方法:
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
可以看到,这个方法是无论如何都会返回一个对象而不是null的,此时就会回到ProviderManager类的authenticate
方法,这个方法里的try代码块中
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
当result不为null时就从循环break了,所以永远不可能找到第二个表,我不知道这是SpringSecurity本身的问题还是什么,最终的解决方案是创建一个UserDtailService类轮流查询两个表:
@Service
public class UserService implements UserDetailsService {
@Autowired
StudentClientFeign studentClientFeign;
@Autowired
TeacherClientFeign teacherClientFeign;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Student student = studentClientFeign.findStudentByName(username);
Teacher teacher = teacherClientFeign.findTeacherByName(username);
if(student != null) {
return student;
}
else if(teacher != null) {
return teacher;
}
else {
return new User();
}
}
}
这样就只用配置一个UserService,不存在多service问题了。
但这样就得保证学生和老师的名字不同了,或者后期可能换一种登陆方式,比如用学号/工号登陆之类的。
有时候还是不要太依赖框架,用简单的框架代码自己实现代码逻辑比较复杂的部分比较好。