最近使用SpringBoot2.0新版构建项目,新版的SpringBoot相关依赖的jar很多包结构做了变更,相关依赖也有很多不同,本人负责公司的基础服务,相关登录认证,资源认证采用了开源的 Spring-Security-Oauth2
来构建,但是构建过程中会遇到很多坑,所以做此记录。
坑一:
Spring boot 2.0.X引用的security 依赖是 spring security 5.X版本,此版本需要提供一个PasswordEncorder的实例,否则后台汇报错误:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
注解暴露一个PasswordEncorder实例
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
坑二:
采内存配置clientId
和secret
时,请求{{url}}/oauth/token
获取token接口时:
WARN [http-nio-8020-exec-2 ] o.s.s.c.b.BCryptPasswordEncoder:90 - [ ] Encoded password does not look like BCrypt
通过debug断点查看首先进行验证的是配置的clientId
和secret
,认证的过滤器为BasicAuthenticationFilter
阅读源码可知该过滤器主要是对头部header配置的Authorization : Basic XXXX
头部信息进行认证。核心代码为:
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
final boolean debug = this.logger.isDebugEnabled();
//获取头部信息Authorization的basic认证信息
String header = request.getHeader("Authorization");
if (header == null || !header.toLowerCase().startsWith("basic ")) {
chain.doFilter(request, response);
return;
}
try {
//获取头部信息Authorization的basic认证信息(尽心base64解码)
String[] tokens = extractAndDecodeHeader(header, request);
assert tokens.length == 2;
String username = tokens[0];
if (debug) {
this.logger
.debug("Basic Authentication Authorization header found for user '"
+ username + "'");
}
if (authenticationIsRequired(username)) {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, tokens[1]);
authRequest.setDetails(
this.authenticationDetailsSource.buildDetails(request));
//认证头部信息,通过authenticationManager选择合适的provider尽心认证,失败则抛出异常AuthenticationException
Authentication authResult = this.authenticationManager
.authenticate(authRequest);
if (debug) {
this.logger.debug("Authentication success: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);
}
}
//根据抛出的异常信息,做不同处理
catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();
if (debug) {
this.logger.debug("Authentication request for failed: " + failed);
}
this.rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, failed);
if (this.ignoreFailure) {
chain.doFilter(request, response);
}
else {
this.authenticationEntryPoint.commence(request, response, failed);
}
return;
}
chain.doFilter(request, response);
}
Basic认证authenticationManager
使用的是DaoAuthenticationProvider
中父类抽象类AbstractUserDetailsAuthenticationProvider
的authenticate
认证方法,获取basic认证信息,主要认证核心代码为:
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 {
//获取basic认证用户信息即根据clientId获取ClientDetails信息(secret,scope,authorizedGrantTypes.....)
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 {
//检查是否被lock,是否过期等
preAuthenticationChecks.check(user);
//检查clientId和secret是否匹配
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);
}
其中检测clientId
和secret
是否匹配的核心代码为:
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
//此处采用配置的passwordEncoder编码并检查secret是否匹配
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword()))
核心认证secret是否配置的代码。
所以我们在memory模式下配置ClientDetailsServiceConfigurer
时
需要将secret
进行passwordEncoder进行encoder处理。
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient(clientId).secret(passwordEncoder.encode(secret)).authorizedGrantTypes("password", "refresh_token").scopes("read,write")
.accessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)).refreshTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(7));
}
passwordEncoder.encode(secret)
进行