ОК, вот как мне удалось выполнить эту задачу.
- У меня есть аутентифицированный пользователь (на самом деле сервис) с ролью ROLE_OAUTH и открытым сеансом аутентификации, а также некоторая важная информация о контексте в сеансе, который был встроен в запрос OAuth;
- Теперь при попытке доступа к защищенному ресурсу, требующему другой роли, скажем ROLE_USER, Spring дает мне AccessDeniedException и отправляет403 запрещенный ответ (см. AccessDeniedHandlerImpl ), любезно предлагая переопределить поведение по умолчанию, если это необходимо, в пользовательском AccessDeniedHandler.Вот пример кода:
public class OAuthAwareAccessDeniedHandler implements AccessDeniedHandler {
private static final Log LOG = LogFactory.getLog(OAuthAwareAccessDeniedHandler.class);
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (oauthSecurityUtils.isUserWithOnlyOAuthRole(auth)) {
LOG.debug("Prohibited to authorize OAuth user trying to access protected resource.., redirected to /login");
// Remember the request pathway
RequestCache requestCache = new HttpSessionRequestCache();
requestCache.saveRequest(request, response);
response.sendRedirect(request.getContextPath() + "/login");
return;
}
LOG.debug("Ordinary redirection to /accessDenied URL..");
response.sendRedirect(request.getContextPath() + "/accessDenied");
}
}
Теперь нам нужно добавить этот новый обработчик в конфигурацию:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// all the config
.and()
.exceptionHandling().accessDeniedHandler(oauthAwareAccessDeniedHandler());
}
После этого шага значение по умолчанию
UsernamePasswordAuthenticationFilter обработает ввод путем создания другого объекта аутентификации с введенными учетными данными, а поведение по умолчанию просто теряет существующую информацию, связанную с предыдущим объектом аутентификации OAuth.Поэтому нам нужно переопределить это поведение по умолчанию, расширив этот класс, например, добавив этот фильтр перед стандартным UsernamePasswordAuthenticationFilter.
public class OAuthAwareUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final Log LOG = LogFactory.getLog(LTIAwareUsernamePasswordAuthenticationFilter.class);
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
Authentication previousAuth = SecurityContextHolder.getContext().getAuthentication();
// Check for OAuth authentication in place
if (oauthSecurityUtils.isUserWithOnlyOAuthRole(previousAuth)) {
LOG.debug("OAuth authentication exists, try to authenticate with UsernamePasswordAuthenticationFilter in the usual way");
SecurityContextHolder.clearContext();
Authentication authentication = null;
try {// Attempt to authenticate with standard UsernamePasswordAuthenticationFilter
authentication = super.attemptAuthentication(request, response);
} catch (AuthenticationException e) {
// If fails by throwing an exception, catch it in unsuccessfulAuthentication() method
LOG.debug("Failed to upgrade authentication with UsernamePasswordAuthenticationFilter");
SecurityContextHolder.getContext().setAuthentication(previousAuth);
throw e;
}
LOG.debug("Obtained a valid authentication with UsernamePasswordAuthenticationFilter");
Principal newPrincipal = authentication.getPrincipal();
// Here extract all needed information about roles and domain-specific info
Principal rememberedPrincipal = previousAuth.getPrincipal();
// Then enrich this remembered principal with the new information and return it
LOG.debug("Created an updated authentication for user");
return newAuth;
}
LOG.debug("No OAuth authentication exists, try to authenticate with UsernamePasswordAuthenticationFilter in the usual way");
return super.attemptAuthentication(request, response);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
Authentication previousAuth = SecurityContextHolder.getContext().getAuthentication();
if (oauthSecurityUtils.isUserWithOnlyOAuthRole(previousAuth)) {
LOG.debug("unsuccessfulAuthentication upgrade for OAuth user, previous authentication :: "+ previousAuth);
super.unsuccessfulAuthentication(request, response, failed);
LOG.debug("fallback to previous authentication");
SecurityContextHolder.getContext().setAuthentication(previousAuth);
} else {
LOG.debug("unsuccessfulAuthentication for a non-OAuth user with UsernamePasswordAuthenticationFilter");
super.unsuccessfulAuthentication(request, response, failed);
}
}
}
Единственное, что нужноОсталось добавить этот фильтр перед Имя пользователяPasswordAuthenticationFilter и применить его только к указанной конечной точке:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.addFilterBefore(oauthAwareUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// here come ant rules
.and()
.formLogin()
.and()
.exceptionHandling().accessDeniedHandler(oauthAwareAccessDeniedHandler());
}
Вот и все.Этот пример проверен на работоспособность.Может быть, некоторые побочные эффекты будут найдены позже, не уверен.Также я уверен, что это может быть сделано более изощренным способом, но сейчас я пойду с этим кодом.