背景 最近在搭建一个Spring Cloud的项目,在搭建途中,遇到了一些问题,这里记录一下。
基本上我是参照这个项目搭建的,https://gitee.com/log4j/pig  。不过因为只是参考,所以我还做了一些改动,而且我们Spring Cloud的版本也不一样,我是F版的。
问题1 /oauth/token 401问题 在AuthorizationServer搭建完成以后,启动访问/oauth/token接口获取access_token。传入用户名和密码,然后一直得到一个401错误,日志也没有,我最开始还以为是Spring Security把这个接口给拦截了,后来DEBUG了一下源码,发现在BasicAuthenticationFilter.doFilterInternal()里有这么一句判断。
1 2 3 4 5 6 String header = request.getHeader("Authorization" ); if  (header == null  || !header.toLowerCase().startsWith("basic " )) {	chain.doFilter(request, response); 	return ; } 
就是如果你的header里面没有Authorization(BasicAuthenticationFilter.doFilterInternal),或者Authorization不是以basic 开头的,直接就返回401了。虽然我忘记了传这个参数,但是日志里没有任何提示,这个错误真是让我好一顿找才给解决。
这里面是client_id:client_secret的base64编码。到这还没完,因为Spring Cloud F版会有那个PasswordEncoder,所以他在校验secret的时候会和服务器配置的时候会进行加密,如果存储的密钥不是相应的加密方式,他也会报错,这个错误在网上都搜得到了。
Spring Security 4.x -> 5.x 踩坑记录 
DaoAuthenticationProvider.additionalAuthenticationChecks()方法里,就是检查密码的地方。
问题2 Unsupported grant type: password 接着我又开始遇到这个错误,搜了一下说是AuthenticationManager无法注入。
在AuthorizationServerEndpointsConfigurer.getDefaultTokenGranters里面,如果AuthenticationManager类的实例的话,那么就不支持password的授权模式,也就是ResourceOwnerPasswordTokenGranter。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private  List<TokenGranter> getDefaultTokenGranters ()  	ClientDetailsService clientDetails = clientDetailsService(); 	AuthorizationServerTokenServices tokenServices = tokenServices(); 	AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices(); 	OAuth2RequestFactory requestFactory = requestFactory(); 	List<TokenGranter> tokenGranters = new  ArrayList<TokenGranter>(); 	tokenGranters.add(new  AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails, 			requestFactory)); 	tokenGranters.add(new  RefreshTokenGranter(tokenServices, clientDetails, requestFactory)); 	ImplicitTokenGranter implicit = new  ImplicitTokenGranter(tokenServices, clientDetails, requestFactory); 	tokenGranters.add(implicit); 	tokenGranters.add(new  ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory)); 	if  (authenticationManager != null ) { 		tokenGranters.add(new  ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, 				clientDetails, requestFactory)); 	} 	return  tokenGranters; } 
Spring Security 4.x -> 5.x 踩坑记录 的也提到了这个问题,不过我这里遇到了更奇怪的现象,在WebSecurityConfigurerAdapter加上下面的配置后,并没有解决我的问题。
1 2 3 4 5 6 @Bean(name = BeanIds.AUTHENTICATION_MANAGER) @Override public  AuthenticationManager authenticationManagerBean ()  throws  Exception     return  super .authenticationManagerBean(); } 
结果我发现在我的项目中,AuthorizationServerConfig竟然比WebSecurityConfigurer先加载,所以在public void configure(AuthorizationServerEndpointsConfigurer endpoints)注入并设置endpoints.authenticationManager(authenticationManager)的时候,放的是一个null进去。
我还觉得蛮奇怪的,下面的代码也会注入一个null
1 2 @Autowired private  AuthenticationManager authenticationManager;
然后我把这个参数改到构造方法里面去启动,结果就告诉我循环依赖,后来想了想,可能是因为Spring在处理循环依赖的时候,把一些注入类自动处理成null了。
在我解决了循环依赖以后,我就能登录成功了。
问题3 如何定制BadCredentialsException,UserNameNotFound的异常消息 接下来就是输入错误密码的时候得到的错误了,如果用户名或者密码错误了,Spring Security会返回一个
1 2 3 4 { 	"error" : "invalid_grant" , 	"error_ description" : "坏的凭证"  } 
这个错误信息太不友好了,我们一般给客户端返回的消息都是统一标准的格式,比如:
1 2 3 4 { 	"code" :"401" , 	"msg" :"用户名或密码错误"  } 
这样另类的消息格式是绝对不允许的。所以我就想定制化这个消息格式,是相信Spring Security肯定提供了相应的机制来供我们实现这个消息的定制,我先是在网上搜了很久,都没有找到解决方案,只是找到了一些哥们跟我有同样的问题,但是却没有人给出解决方案。
比如这个stackoverflow.com上的这个哥们 。
没办法了,找不到方案就自己想办法吧,我就从抛出异常的地方开始一行一行的debug,接着发现这个异常被TokenEndpoint类里的一个ExceptionHandler给拦截然后输出的。
1 2 3 4 5 6 7 @ExceptionHandler(OAuth2Exception.class) public  ResponseEntity<OAuth2Exception> handleException (OAuth2Exception e)  throws  Exception 	if  (logger.isWarnEnabled()) { 		logger.warn("Handling error: "  + e.getClass().getSimpleName() + ", "  + e.getMessage()); 	} 	return  getExceptionTranslator().translate(e); } 
那么这个getExceptionTranslator到底是个什么东西,最后翻了一下到底是在哪里设置的这个属性,最终发现是AuthorizationServerEndpointsConfigurer的一个字段,是不是很眼熟?
和设置AuthenticationManager的是同一个类,所以定制一个
1 2 3 4 5 6 7 8 9 @Component public  class  CustomWebResponseExceptionTranslator  implements  WebResponseExceptionTranslator <OAuth2Exception >     @Override      public  ResponseEntity translate (Exception e)  throws  Exception          return  new  ResponseEntity<>(RestResp.error(CommonErrorCode.AUTHORIZED_ERROR), HttpStatus.OK);     } } 
然后在AuthorizationServerConfig里加上配置,
1 2 3 4 5 6 7 8 9 @Override public  void  configure (AuthorizationServerEndpointsConfigurer endpoints)           endpoints                          .exceptionTranslator(customWebResponseExceptionTranslator)             .authenticationManager(authenticationManager); } 
最后得到了我们想要的输出结果:
1 2 3 4 {     "code" : "401" ,     "msg" : "用户名或密码错误"  } 
问题4 invalid_token 错误消息定制 如果传入的token是错误的,那么会得到这样格式的一个错误消息:
1 2 3 4 {   "error" : "invalid_token" ,   "error_description" : "Cannot convert access token to JSON"  } 
实际上有可能是这个token在redis里没有等好几种错误
DefaultTokenServices.loadAuthentication(String accessTokenValue)
1 2 3 4 5 6 7 8 9 10 11 12 13 if  (accessToken == null ) {	throw  new  InvalidTokenException("Invalid access token: "  + accessTokenValue); } else  if  (accessToken.isExpired()) {	tokenStore.removeAccessToken(accessToken); 	throw  new  InvalidTokenException("Access token expired: "  + accessTokenValue); } OAuth2Authentication result = tokenStore.readAuthentication(accessToken); if  (result == null ) {	 	throw  new  InvalidTokenException("Invalid access token: "  + accessTokenValue); } 
想要定制化这个错误消息,需要制定一个AuthExceptionEntryPoint.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Log4j2 @Component public  class  AuthExceptionEntryPoint  implements  AuthenticationEntryPoint      @Autowired      private  ObjectMapper objectMapper;     @Override      public  void  commence (HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)  throws  IOException, ServletException          log.info("Token失效,禁止访问 {}" , request.getRequestURI());         response.setCharacterEncoding(StandardCharsets.UTF_8.displayName());         response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);         RestResp result = RestResp.error(CommonErrorCode.UNAUTHORIZED, "Token错误" );         response.setStatus(HttpStatus.SC_OK);         PrintWriter printWriter = response.getWriter();         printWriter.append(objectMapper.writeValueAsString(result));     } } 
然后ResourceServerConfiguration里增加配置
1 2 3 4 5 6 7 @Override     public  void  configure (ResourceServerSecurityConfigurer resources)       resources.expressionHandler(expressionHandler);     resources.authenticationEntryPoint(authExceptionEntryPoint);     resources.accessDeniedHandler(iuMiaoAccessDeniedHandler);     resources.tokenStore(redisTokenStore()); } 
就能得到自定义的错误。
1 2 3 4 {     "code" : "10000401" ,     "msg" : "未授权: Token错误"  }