问题描述
我正在尝试将当前使用基本身份验证的 Web 应用程序转换为使用 oauth2。为了准备这个项目,我将 Spring 和 Spring Security 从 v3 升级到 v5,这样我们就可以利用 Spring Security 5.4.1 中的 OAuth2 功能。我们的 web 应用程序仍然通过 xml 配置 spring 和 spring 安全,它不使用 spring 启动。
我现在已经对其进行了配置,以便 oauth2 登录过程可以正常工作,或者至少看起来可以正常工作。但是,我在配置它时遇到了困难,因此当用户注销时,会话也会在授权服务器中结束,并且授权服务器的登录页面会显示给用户。目前,当我注销时,我可以在日志中看到会话无效并且 JSESSIONID cookie 被删除,但我没有看到它向授权服务器的“结束会话端点”发送请求以结束会话授权服务器。然后我看到当spring security想要再次登录用户时,授权服务器响应说用户的会话仍然处于活动状态并跳过登录页面。
似乎为 oauth2 配置注销的正确方法是将 'logout' 元素的 'success-handler-ref' 属性设置为对 'OidcclientinitiatedlogoutSuccessHandler' bean 的引用。但是, OidcclientinitiatedlogoutSuccessHandler 构造函数需要对 ClientRegistrationRepository bean 的引用,并且配置 ClientRegistrationRepository “bean”的“client-registrations”元素似乎没有我可以传递给 OidcclientinitiatedlogoutSuccessHandler 构造函数的“id”或“name”属性。
我确实尝试创建我自己的 ClientRegistrationRepository bean 来替换“client-registrations”元素的使用,希望它允许我使用“id”属性配置该类。但是,当我这样做时,它破坏了登录过程,我不知道为什么。
这是我的安全 xml 文件在 oauth2 登录正常工作而注销不正常时的外观。抱歉,如果我包含了太多的 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,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"/>
我仍然希望有人能告诉我正确的非hacky解决方案。
解决方法
暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!
如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。
小编邮箱:dio#foxmail.com (将#修改为@)