近期遇到一个问题,在spring oauth2.0默认的密码登录校验中,只能访问单个数据库表,但针对不同的表用户,需要访问不同的表,所以需要传一个新参去判断用户访问不同的表来校验账号密码
本文主要是是讨论oauth2.0支持多表用户登录
我使用的Spring Boot为2.2.5.RELEAS,SpringCloud为Hoxton.SR2
对于多个表用户,需要传递不同的参数来区分是访问哪个表,所以在请求参数中增加了loginType来区分不同的用户
在ResourceServerConfig中配置中配置自定义的配置类
创建 MultipleLoginAuthenticationSecurityConfig,自义定配置 extends SecurityConfigurerAdapter
复写config配置
@Component
public class MultipleLoginAuthenticationSecurityConfig extends SecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler myAuthenticationFailureHandler;
@Resource
private MyUserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void configure(HttpSecurity http) {
MultipleLoginAuthenticationFilter multipleLoginAuthenticationFilter = new MultipleLoginAuthenticationFilter();
multipleLoginAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
multipleLoginAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
multipleLoginAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
MultipleLoginAuthenticationProvider multipleLoginAuthenticationProvider = new MultipleLoginAuthenticationProvider();
multipleLoginAuthenticationProvider.setUserDetailsService(userDetailsService);
multipleLoginAuthenticationProvider.setPasswordEncoder(passwordEncoder);
http.authenticationProvider(multipleLoginAuthenticationProvider)
.addFilterBefore(multipleLoginAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
UsernamePasswordAuthenticationFilter是spring security提供的表单登录Filter
创建一个新的过滤器
public class MultipleLoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private String loginParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_MULTIPLE;
private boolean postOnly = true;
public MultipleLoginAuthenticationFilter() {
super();
}
/**
* 复写新的过滤器方法,并构造新的authenticationToken
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 获取登录传递的账号密码
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
MultipleLoginAuthenticationToken authRequest = new MultipleLoginAuthenticationToken(username,password);
this.setDetails(request, authRequest);
// 用户寻找自定义的校验Provider
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* @param request 获取请求的参数
* @param authRequest
*/
protected void setDetails(HttpServletRequest request, MultipleLoginAuthenticationToken authRequest) {
Map map = new LinkedHashMap<>();
Enumeration paramNames = request.getParameterNames();
while (paramNames.hasMoreElements()) {
String paramName = (String) paramNames.nextElement();
String[] paramValues = request.getParameterValues(paramName);
if (paramValues.length == 1) {
String paramValue = paramValues[0];
if (paramValue.length() != 0) {
map.put(paramName, paramValue);
}
}
}
authRequest.setDetails(map);
}
public void setLoginParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.loginParameter = usernameParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getLoginParameter() {
return loginParameter;
}
}
所以我们需要新创建一个新的provider来替代自带的的密码校验方式
@Slf4j
public class MultipleLoginAuthenticationProvider implements AuthenticationProvider {
@Getter
@Setter
private MyUserDetailsService userDetailsService;
@Getter
private PasswordEncoder passwordEncoder;
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
@Getter
@Setter
private volatile String userNotFoundEncodedPassword;
public MultipleLoginAuthenticationProvider() {
passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Override
@SuppressWarnings("unchecked")
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
MultipleLoginAuthenticationToken authenticationToken = (MultipleLoginAuthenticationToken) authentication;
Map details = (Map) authenticationToken.getDetails();
// 获取自定义的loginType参数
String loginType = details.get("loginType");
// 使用自定义的loginType在userDetailServceice中查询不同的数据库
UserDetails user = this.getUserDetailsService().loadUserByLoginType(authentication.getName(), loginType);
if (user == null) {
throw new InternalAuthenticationServiceException(
"用户信息为空");
}
if (!"APP".equals(loginType)) {
// app登录不校验密码
// 获取当前输入的密码
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, user.getPassword())) {
log.error("用户名或密码错误,用户名:" + authentication.getName());
throw new BadCredentialsException("用户名或密码错误");
}
}
if (details.containsKey("password")) {
details.put("password", null);
}
MultipleLoginAuthenticationToken authenticationResult = new MultipleLoginAuthenticationToken(user, user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class> authentication) {
return MultipleLoginAuthenticationToken.class.isAssignableFrom(authentication);
}
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
this.userNotFoundEncodedPassword = null;
}
}
重新一个authenticationToken,复制 UsernamePasswordAuthenticationToken
public class MultipleLoginAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
public MultipleLoginAuthenticationToken(String username,String password) {
super(null);
this.principal = username;
this.credentials = password;
setAuthenticated(false);
}
public MultipleLoginAuthenticationToken(Object principal,
Collection extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
public Object getCredentials() {
return credentials;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
以上四个文件就基本上将现有的springsecurity默认配置覆盖了
总结:
当配置了自定义配置以后,过滤器先获取了登录的传参信息,并通过下图获取自配置的provider去校验账号密码,获取认证管理器,
ProviderManager.authenticate 负责寻找认证的提供者(provider),当并在provider中校验密码
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = 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 {
// 使用自定义的提供者用来账号密码校验(MultipleLoginAuthenticationProvider.authenticate)
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}