我正在尝试将当前使用基本身份验证的 Web 应用程序转换为使用 oauth2。在准备这个项目时,我将 Spring 和 Spring Security 从 v3 升级到 v5,以便我们可以利用 Spring Security 5.4.1 中的 OAuth2 功能。我们的 Web 应用程序仍然通过 xml 配置 spring 和 spring security,并且不使用 spring boot。
我现在已经配置好,以便 oauth2 登录过程正常工作,或者至少看起来可以正常工作。但是,我在配置它时遇到困难,因此当用户注销时,会话也会在授权服务器中结束,并向用户显示来自授权服务器的登录页面。目前,当我注销时,我可以在日志中看到会话已失效并且 JSESSIONID cookie 已删除,但我没有看到它向授权服务器的“end-session-endpoint”发送请求以结束会话授权服务器。然后我看到,当 Spring Security 想要再次登录用户时,授权服务器响应说用户的会话仍然处于活动状态并跳过登录页面。
配置 oauth2 注销的正确方法似乎是将“logout”元素的“success-handler-ref”属性设置为“OidcClientInitiatedLogoutSuccessHandler”bean 的引用。但是,OidcClientInitiatedLogoutSuccessHandler 构造函数需要引用 ClientRegistrationRepository bean,并且配置 ClientRegistrationRepository“bean”的“client-registrations”元素似乎没有可以传递给 OidcClientInitiatedLogoutSuccessHandler 构造函数的“id”或“name”属性。
我确实尝试创建自己的 ClientRegistrationRepository bean 来替换“client-registrations”元素的使用,希望它允许我使用“id”属性配置该类。然而,当我这样做时,它破坏了登录过程,我不知道为什么。
这是当 oauth2 登录正常工作而注销不正常时我的安全 xml 文件的外观。抱歉,如果我包含了太多 xml 文件。我觉得越多越好。
<security:http pattern="/resources/**" security="none"/>
<security:http pattern="/mobile/startup" security="none"/>
<security:http pattern="/mobile/debuginfo" security="none"/>
<security:http auto-config="true" use-expressions="true" disable-url-rewriting="true">
<headers disabled="true"/>
<csrf disabled="true"/>
<expression-handler ref="webExpressionHandler"/>
<intercept-url pattern="/identity/login" access="permitAll"/>
<intercept-url pattern="/mobilelogin" access="permitAll"/>
...
<intercept-url pattern="/**" access="isAuthenticated()"/>
<access-denied-handler error-page="/error/access_denied"/>
<oauth2-client>
<authorization-code-grant/>
</oauth2-client>
<oauth2-login oidc-user-service-ref="hwOidcUserService"/>
<logout logout-url="/logout"
invalidate-session="true"
delete-cookies="JSESSIONID"
logout-success-url="/"/>
</security:http>
<security:client-registrations>
<client-registration registration-id="idsrv"
client-id="homeofficecodeui"
provider-id="idsrv"
client-name="Home Office and Mobile"
client-secret="<SECRET>"
authorization-grant-type="authorization_code"
client-authentication-method="basic"
redirect-uri="<CLIENT_BASE_URL>/login/oauth2/code/idsrv"
scope="openid,offline_access,profile,email,homeofficeapi"/>
<provider provider-id="idsrv"
issuer-uri="<AUTH_SERVER_BASE_URL>"
authorization-uri="<AUTH_SERVER_BASE_URL>/connect/authorize?sec_host=<CLIENT_BASE_URL>"/>
</security:client-registrations>
<beans:bean id="hwOidcUserService"
class="com.example.service.HWOidcUserService"/>
<beans:bean id="webExpressionHandler" class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler">
<beans:property name="permissionEvaluator" ref="permissionEvaluator"/>
</beans:bean>
<beans:bean id="requestContextFilter" class="org.springframework.web.filter.RequestContextFilter"/>
<beans:bean id="customAuthenticationEntryPoint"
class="com.example.security.CustomAuthenticationEntryPoint" />
<beans:bean id="authenticationFailureHandler" class="com.example.security.CustomAuthenticationFailureHandler">
<beans:property name="exceptionMappings">
<beans:props>
<beans:prop key="org.springframework.security.authentication.BadCredentialsException">/login?error=BAD_CREDENTIALS</beans:prop>
<beans:prop key="org.springframework.security.authentication.CredentialsExpiredException">/login?error=CREDENTIALS_EXPIRED</beans:prop>
<beans:prop key="org.springframework.security.authentication.LockedException">/login?error=ACCOUNT_LOCKED</beans:prop>
<beans:prop key="com.example.exception.AccountManuallyLockedException">/login?error=ACCOUNT_LOCKED_MANUALLY</beans:prop>
<beans:prop key="org.springframework.security.authentication.DisabledException">/login?error=AGENCY_DISABLED</beans:prop>
<beans:prop key="org.springframework.security.authentication.InternalAuthenticationServiceException">/login?error=UNKNOWN_ERROR</beans:prop>
</beans:props>
</beans:property>
</beans:bean>
<beans:bean id="passwordValidator" class="com.example.security.PasswordValidator">
</beans:bean>
<authentication-manager>
<authentication-provider ref="alternateCustomAuthenticationProvider"/>
<authentication-provider ref="alternate2CustomAuthenticationProvider"/>
<authentication-provider ref="customAuthenticationProvider"/>
</authentication-manager>
<global-method-security pre-post-annotations="enabled">
<expression-handler ref="expressionHandler"/>
</global-method-security>
<beans:bean id="expressionHandler" class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
<beans:property name="permissionEvaluator" ref="permissionEvaluator"/>
</beans:bean>
<beans:bean id="permissionEvaluator" class="com.example.security.CustomPermissionEvaluator">
</beans:bean>
这是我认为我需要进行的更改,但我无法弄清楚 clientRegistrationRepository 的引用是什么:
<security:http ...
...
<logout logout-url="/logout"
invalidate-session="true"
delete-cookies="JSESSIONID"
success-handler-ref="logoutSuccessHandler"/>
</security:http>
<beans:bean id="logoutSuccessHandler"
class="org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler">
<beans:constructor-arg ref="clientRegistrationRepository"/> <-- Error. Can't figure out what the ref to the clientRegistrationRepository is.
<beans:property name="postLogoutRedirectUri" value="/"/>
</beans:bean>
我已经浏览了 Spring Security Reference,但不幸的是,有关 OidcClientInitiatedLogoutSuccessHandler 的部分不包含 xml 示例。
有谁知道我需要进行哪些更改才能使 oauth2 注销过程正常工作?
黑客解决方案更新
我想出了一个可能属于“黑客”类别的解决方案。我在过滤器链中添加了一个自定义注销过滤器,该过滤器调用授权服务器“end-session-endpoint”以在授权服务器中注销用户。它使用 OidcClientInitiatedLogoutSuccessHandler 类,并通过 ApplicationContext 获取 ClientRegistrationRepository 的句柄。
我将此新过滤器放在 LogoutFilter 之前,因为 LogoutFilter 取消了 SecurityContext 中的身份验证,并且 OidcClientInitiatedLogoutSuccessHandler 需要身份验证才能正常工作。
这是新的自定义注销过滤器:
package com.example.web.login;
import javax.annotation.PostConstruct;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;
public class OAuth2LogoutFilter extends GenericFilterBean implements ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(OAuth2LogoutFilter.class);
private ApplicationContext applicationContext;
private ClientRegistrationRepository clientRegistrationRepository;
private OidcClientInitiatedLogoutSuccessHandler logoutSuccessHandler;
private RequestMatcher logoutRequestMatcher;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@PostConstruct
public void init() {
if (logger.isDebugEnabled())
logger.debug("in init()");
Assert.notNull(applicationContext, "applicationContext can not be null");
clientRegistrationRepository = applicationContext.getBean(InMemoryClientRegistrationRepository.class);
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository can not be null");
logoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
logoutSuccessHandler.setPostLogoutRedirectUri("<CLIENT_BASE_URL>/logoutAfterOAuth");
logoutRequestMatcher = new AntPathRequestMatcher("/logout");
if (logger.isDebugEnabled())
logger.debug("end init()");
}
@Override
public void doFilter(final ServletRequest request,
final ServletResponse response,
final FilterChain chain) throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain chain) throws IOException, ServletException {
if (logger.isDebugEnabled())
logger.debug("in doFilter()" +
"\n\trequest: " + request +
"\n\tresponse: " + response +
"\n\tchain: " + chain);
if (requiresLogout(request)) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (this.logger.isDebugEnabled())
this.logger.debug("Logging out auth: " + auth);
logoutSuccessHandler.onLogoutSuccess(request, response, auth);
return;
}
chain.doFilter(request, response);
if (logger.isDebugEnabled())
logger.debug("end doFilter()");
}
protected boolean requiresLogout(final HttpServletRequest request) {
if (this.logoutRequestMatcher.matches(request))
return true;
if (this.logger.isTraceEnabled())
this.logger.trace("Did not match request to " + logoutRequestMatcher);
return false;
}
}
这是对安全 xml 文件的更改:
<security:http ...
...
<custom-filter ref="oauth2LogoutFilter" before="LOGOUT_FILTER"/>
...
<logout logout-url="/logoutAfterOAuth"
invalidate-session="true"
delete-cookies="JSESSIONID"
logout-success-url="/"/>
</security:http>
<beans:bean id="oauth2LogoutFilter"
class="com.example.web.login.OAuth2LogoutFilter"/>
我仍然希望有人能告诉我正确的非黑客解决方案。
在
OidcClientInitiatedLogoutSuccessHandler
bean 定义的 XML 中,我使用了:
autowire=“constructor”
这样我就可以让 spring 注入
ClientRegistrationRepository
,因为没有 ID 引用。
您可以分享您完整的安全 XML 吗?实际上,我正在努力使用 XML 进行 oauth2-login。
我没有被重定向到 oauth2 服务器登录页面。
正如您提到的,您已重定向到身份验证服务器的登录页面,请分享您完整的 XML。会有帮助吗??