问题描述

利用Spring cloud oauth2实现Oauth 2权限控制时,调用/oauth/authorize获取授权码,抛出了User must be authenticated with Spring Security before authorization can be completed异常?

请求接口:

控制台异常信息为: 

 

 接口部分源码为:

@RequestMapping(value = "/oauth/authorize")
	public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
			SessionStatus sessionStatus, Principal principal) {

		// Pull out the authorization request first, using the OAuth2RequestFactory. All further logic should
		// query off of the authorization request instead of referring back to the parameters map. The contents of the
		// parameters map will be stored without change in the AuthorizationRequest object once it is created.
		AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);

		Set<String> responseTypes = authorizationRequest.getResponseTypes();

		if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
			throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
		}

		if (authorizationRequest.getClientId() == null) {
			throw new InvalidClientException("A client id must be provided");
		}

		try {

			if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
				throw new InsufficientAuthenticationException(
						"User must be authenticated with Spring Security before authorization can be completed.");
			}
        ...

		}
		catch (RuntimeException e) {
			sessionStatus.setComplete();
			throw e;
		}

	}

 实际debug我们可以看到,授权信息为空,导致抛出了InsufficientAuthenticationException异常。

深究原因

1.在网上查了资料,一种说法是在security的http配置中,需要把/oauth/authorize配置为允许permitAll

 protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin().loginPage("/login").and()
                .logout().addLogoutHandler(sysLogoutHandler).logoutSuccessHandler(sysLogoutSuccessHandler)
                .and()
                // 由于使用的是JWT,我们这里不需要csrf
                .csrf().disable().cors().and()
                .authorizeRequests()
                .antMatchers("/login", "/oauth/authorize").permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();

    }

我在配置完以后,再次请求接口还是报同样的错误,说明,我遇到这个错误不是由于配置引起的。

经过了很久的尝试,我看到了控制台报错的信息。

 GlobalExceptionTranslator部分代码如下:

@RestControllerAdvice
@Slf4j
public class GlobalExceptionTranslator {
    @ExceptionHandler(InsufficientAuthenticationException.class)
    public BaseResponse<Object> handleError(InsufficientAuthenticationException e) {
        log.error("Permission Denied", e);
        return BaseResponse
                .builder()
                .code(ResultCode.INTERNAL_SERVER_ERROR.getCode())
                .msg(e.getMessage())
                .data(null)
                .build();
    }
}

原来我的问题是这个InsufficientAuthenticationException被我捕捉了,然后直接返回我定义好的BaseResponse。

解决办法

解决办法就很简单了,直接删除掉对InsufficientAuthenticationException异常的捕捉。

原理分析

我们在配置了oauth/authorize为permitAll,那么在访问oauth/authorize接口时,会允许直接访问,oauth/authorize接口中会判断用户的授权信息,授权信息不通过,抛出异常,代码进入到ExceptionTranslationFilter中。

 

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
 
		try {
			chain.doFilter(request, response);
 
			logger.debug("Chain processed normally");
		}
		catch (IOException ex) {
			throw ex;
		}
		catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
			RuntimeException ase = (AuthenticationException) throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);
 
			if (ase == null) {
				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
						AccessDeniedException.class, causeChain);
			}
 
			if (ase != null) {
                 // 捕捉到异常后进入此方法中
				handleSpringSecurityException(request, response, chain, ase);
			}
			...
		}
	}
 
 
 
private void handleSpringSecurityException(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, RuntimeException exception)
			throws IOException, ServletException {
		if (exception instanceof AuthenticationException) {
			logger.debug(
					"Authentication exception occurred; redirecting to authentication entry point",
					exception);
          
			sendStartAuthentication(request, response, chain,
					(AuthenticationException) exception);
		}
        ...
}
 
 
protected void sendStartAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain,
			AuthenticationException reason) throws ServletException, IOException {
        
		SecurityContextHolder.getContext().setAuthentication(null);
                // 将saveRequest存到session中,方便身份验证成功后调用
		requestCache.saveRequest(request, response);
		logger.debug("Calling Authentication entry point.");
                // 请求重定向到登录界面,登录界面是security的配置loginPage
		authenticationEntryPoint.commence(request, response, reason);
}
 

requestCache的常用的实现类是HttpSessionRequestCache,一般是访问url时系统判断用户未获得授权,ExceptionTranslationFilter会存储savedRequest到session中,名为“SPRING_SECURITY_SAVED_REQUEST”。

SavedRequest里面包含原先访问的url地址、cookie、header、parameter等信息,一旦Authentication认证成功,successHandler.onAuthenticationSuccess(SavedRequestAwareAuthenticationSuccessHandler)会从session中抽取savedRequest,继续访问原先的url。

public class HttpSessionRequestCache implements RequestCache {
	static final String SAVED_REQUEST = "SPRING_SECURITY_SAVED_REQUEST";  
  /**
	 * HttpSessionRequestCache Stores the current request, provided the configuration properties allow it.
	 */
	public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
		if (requestMatcher.matches(request)) {
			DefaultSavedRequest savedRequest = new DefaultSavedRequest(request,
					portResolver);
 
			if (createSessionAllowed || request.getSession(false) != null) {
				// Store the HTTP request itself. Used by
				// AbstractAuthenticationProcessingFilter
				// for redirection after successful authentication (SEC-29)
				request.getSession().setAttribute(this.sessionAttrName, savedRequest);
				logger.debug("DefaultSavedRequest added to Session: " + savedRequest);
			}
		}
		else {
			logger.debug("Request not saved as configured RequestMatcher did not match");
		}
	}
}
public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException {

		String redirectUrl = null;

		if (useForward) {

			if (forceHttps && "http".equals(request.getScheme())) {
				// First redirect the current request to HTTPS.
				// When that request is received, the forward to the login page will be
				// used.
				redirectUrl = buildHttpsRedirectUrlForRequest(request);
			}

			if (redirectUrl == null) {
				String loginForm = determineUrlToUseForThisRequest(request, response,
						authException);

				if (logger.isDebugEnabled()) {
					logger.debug("Server side forward to: " + loginForm);
				}

				RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);

				dispatcher.forward(request, response);

				return;
			}
		}
		else {
			// redirect to login page. Use https if forceHttps true

			redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

		}

		redirectStrategy.sendRedirect(request, response, redirectUrl);
	}

重定向到登录界面

登录成功后,跳转到百度页面,并得到code授权码

 

用此授权码获取access_token

写在最后

 从遇到问题到解决问题,其实花的时间不长,但是要搞懂来龙去脉还是花了一点时间的,本文也是自己的一个小总结,我认为也是自己能完全写出来的东西才是自己真正掌握的。如果有什么不对的地方,请大家共同探讨。

本文所涉及的源码地址:https://github.com/airhonor/authority-management-sys-2.0

 

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐