最近使用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)
進行