利用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 model, @RequestParam Map 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 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
原来我的问题是这个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