Spring Session/Redis 和 Oauth2 不能一起工作
Spring Session/Redis and Oauth2 not working together
Oauth2 和 Redis 不能一起玩。一旦我启用 Spring 会话,在我通过身份验证 (OIDC) 并发送回应用程序后会创建两个会话 ID——一个来自 Redis 的 JSESSIONID,另一个来自 Spring Security Oauth。一旦我禁用 Redis/Spring 会话,一切都运行良好。
我创建了一个非常小的 Maven 应用程序,可以从以下位置下载:
http://folk.uio.no/erlendfg/oidc/oidc.zip
如果我 运行 在本地主机上通过 Jetty 和 Redis 应用程序,我可以在本地重现问题。如 Firefox 的屏幕截图所示,创建了两个会话 cookie:
http://folk.uio.no/erlendfg/oidc/two-sessions.png
我遵循了 Baeldung 的指南,但做了一些小改动以使应用程序与我们的 OIDC 提供商兼容。
https://www.baeldung.com/spring-security-openid-connect
所有这些 类 都可以在 zip 文件中找到(参见上面的 link)。最重要的是:
RedisConfiguration.java
@Configuration
@EnableRedisHttpSession(redisNamespace = "oidc", maxInactiveIntervalInSeconds = 10800)
public class RedisConfiguration {
@Bean
public LettuceConnectionFactory connectionFactory() {
return new LettuceConnectionFactory("localhost", 6379);
}
}
FeideOpenIdConnectConfig.java
@Configuration
@EnableOAuth2Client
public class FeideOpenIdConnectConfig {
@Value("${feide.auth.clientId}")
private String clientId;
@Value("${feide.auth.clientSecret}")
private String clientSecret;
@Value("${feide.auth.accessTokenUri}")
private String accessTokenUri;
@Value("${feide.auth.userAuthorizationUri}")
private String userAuthorizationUri;
@Value("${feide.auth.preEstablishedRedirectUri}")
private String preEstablishedRedirectUri;
@Bean
public OAuth2ProtectedResourceDetails feideOpenId() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setClientId(clientId);
details.setClientSecret(clientSecret);
details.setAccessTokenUri(accessTokenUri);
details.setUserAuthorizationUri(userAuthorizationUri);
details.setScope(Arrays.asList("openid", "email", "userid-feide", "profile", "groups"));
details.setPreEstablishedRedirectUri(preEstablishedRedirectUri);
details.setUseCurrentUri(false);
details.setGrantType("authorization_code");
return details;
}
@Bean
public OAuth2RestTemplate feideOpenIdTemplate(OAuth2ClientContext clientContext) {
return new OAuth2RestTemplate(feideOpenId(), clientContext);
}
}
FeideConnectFilter.java
public class FeideConnectFilter extends OAuth2ClientAuthenticationProcessingFilter {
public FeideConnectFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
}
@Override
public Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
OAuth2AccessToken accessToken;
try {
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
throw new BadCredentialsException("Could not obtain access token", e);
}
try {
String idToken = accessToken.getAdditionalInformation().get("id_token").toString();
Jwt tokenDecoded = JwtHelper.decodeAndVerify(idToken, verifier("https://auth.dataporten.no/openid/jwks"));
@SuppressWarnings("unchecked")
Map<String, String> authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class);
verifyClaims(authInfo, "https://auth.dataporten.no");
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
OpenIdConnectUserDetails user = new OpenIdConnectUserDetails(authInfo, accessToken);
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
} catch (Exception e) {
throw new BadCredentialsException("Could not obtain user details from token", e);
}
}
@Override
protected boolean requiresAuthentication(final HttpServletRequest request, final HttpServletResponse response) {
if (super.requiresAuthentication(request, response)) {
return true;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Already authenticated:
if (authentication != null) {
return false;
}
OAuth2AccessToken accessToken = restTemplate.getAccessToken();
if (accessToken == null) {
return true;
}
return true;
}
@SuppressWarnings("rawtypes")
protected void verifyClaims(final Map claims, final String issuer) {
int exp = (Integer) claims.get("exp");
Date expireDate = new Date(exp * 1000L);
Date now = new Date();
if (expireDate.before(now) || !claims.get("iss").equals(issuer) ||
!claims.get("aud").equals(restTemplate.getResource().getClientId())) {
throw new RuntimeException("Invalid claims");
}
}
protected RsaVerifier verifier(final String jwkSigningUri) throws Exception {
CustomUrlJwkProvider provider = new CustomUrlJwkProvider(new URL(jwkSigningUri));
Jwk jwk = provider.getJwk();
return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
}
protected HttpHeaders getHttpHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + restTemplate.getAccessToken());
return headers;
}
}
SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Inject
private OAuth2RestTemplate restTemplate;
@Bean
public FeideConnectFilter feideConnectFilter() {
FeideConnectFilter filter = new FeideConnectFilter("/oauth/login");
filter.setRestTemplate(restTemplate);
return filter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAfter(new OAuth2ClientContextFilter(), AbstractPreAuthenticatedProcessingFilter.class)
.addFilterAfter(feideConnectFilter(), OAuth2ClientContextFilter.class)
.httpBasic()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/oauth2/login"))
.and()
.authorizeRequests()
.anyRequest().authenticated();
}
}
过滤器(在 WebInitializer.java 中)按以下顺序添加:
private void addFilters(final ServletContext container, final WebApplicationContext applicationContext) {
container.addFilter("springSessionRepositoryFilter", DelegatingFilterProxy.class).addMappingForUrlPatterns(null, false, "/*");
container.addFilter("springSecurityFilterChain", DelegatingFilterProxy.class).addMappingForUrlPatterns(null, false, "/*");
}
OIDC 与 Spring Session 一起工作,如果我使用 Spring Security 自己的 OIDC 实现,它是在版本 5 中引入的。顺便说一句,它实现起来非常容易,几乎没有代码全部。 Spring 安全团队在 Spring 安全性中添加了对 OIDC 和 SAML2.0 的支持,他们做得很好。换句话说,我找到了解决办法。
Oauth2 和 Redis 不能一起玩。一旦我启用 Spring 会话,在我通过身份验证 (OIDC) 并发送回应用程序后会创建两个会话 ID——一个来自 Redis 的 JSESSIONID,另一个来自 Spring Security Oauth。一旦我禁用 Redis/Spring 会话,一切都运行良好。
我创建了一个非常小的 Maven 应用程序,可以从以下位置下载: http://folk.uio.no/erlendfg/oidc/oidc.zip
如果我 运行 在本地主机上通过 Jetty 和 Redis 应用程序,我可以在本地重现问题。如 Firefox 的屏幕截图所示,创建了两个会话 cookie: http://folk.uio.no/erlendfg/oidc/two-sessions.png
我遵循了 Baeldung 的指南,但做了一些小改动以使应用程序与我们的 OIDC 提供商兼容。 https://www.baeldung.com/spring-security-openid-connect
所有这些 类 都可以在 zip 文件中找到(参见上面的 link)。最重要的是:
RedisConfiguration.java
@Configuration
@EnableRedisHttpSession(redisNamespace = "oidc", maxInactiveIntervalInSeconds = 10800)
public class RedisConfiguration {
@Bean
public LettuceConnectionFactory connectionFactory() {
return new LettuceConnectionFactory("localhost", 6379);
}
}
FeideOpenIdConnectConfig.java
@Configuration
@EnableOAuth2Client
public class FeideOpenIdConnectConfig {
@Value("${feide.auth.clientId}")
private String clientId;
@Value("${feide.auth.clientSecret}")
private String clientSecret;
@Value("${feide.auth.accessTokenUri}")
private String accessTokenUri;
@Value("${feide.auth.userAuthorizationUri}")
private String userAuthorizationUri;
@Value("${feide.auth.preEstablishedRedirectUri}")
private String preEstablishedRedirectUri;
@Bean
public OAuth2ProtectedResourceDetails feideOpenId() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setClientId(clientId);
details.setClientSecret(clientSecret);
details.setAccessTokenUri(accessTokenUri);
details.setUserAuthorizationUri(userAuthorizationUri);
details.setScope(Arrays.asList("openid", "email", "userid-feide", "profile", "groups"));
details.setPreEstablishedRedirectUri(preEstablishedRedirectUri);
details.setUseCurrentUri(false);
details.setGrantType("authorization_code");
return details;
}
@Bean
public OAuth2RestTemplate feideOpenIdTemplate(OAuth2ClientContext clientContext) {
return new OAuth2RestTemplate(feideOpenId(), clientContext);
}
}
FeideConnectFilter.java
public class FeideConnectFilter extends OAuth2ClientAuthenticationProcessingFilter {
public FeideConnectFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
}
@Override
public Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
OAuth2AccessToken accessToken;
try {
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
throw new BadCredentialsException("Could not obtain access token", e);
}
try {
String idToken = accessToken.getAdditionalInformation().get("id_token").toString();
Jwt tokenDecoded = JwtHelper.decodeAndVerify(idToken, verifier("https://auth.dataporten.no/openid/jwks"));
@SuppressWarnings("unchecked")
Map<String, String> authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class);
verifyClaims(authInfo, "https://auth.dataporten.no");
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
OpenIdConnectUserDetails user = new OpenIdConnectUserDetails(authInfo, accessToken);
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
} catch (Exception e) {
throw new BadCredentialsException("Could not obtain user details from token", e);
}
}
@Override
protected boolean requiresAuthentication(final HttpServletRequest request, final HttpServletResponse response) {
if (super.requiresAuthentication(request, response)) {
return true;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Already authenticated:
if (authentication != null) {
return false;
}
OAuth2AccessToken accessToken = restTemplate.getAccessToken();
if (accessToken == null) {
return true;
}
return true;
}
@SuppressWarnings("rawtypes")
protected void verifyClaims(final Map claims, final String issuer) {
int exp = (Integer) claims.get("exp");
Date expireDate = new Date(exp * 1000L);
Date now = new Date();
if (expireDate.before(now) || !claims.get("iss").equals(issuer) ||
!claims.get("aud").equals(restTemplate.getResource().getClientId())) {
throw new RuntimeException("Invalid claims");
}
}
protected RsaVerifier verifier(final String jwkSigningUri) throws Exception {
CustomUrlJwkProvider provider = new CustomUrlJwkProvider(new URL(jwkSigningUri));
Jwk jwk = provider.getJwk();
return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
}
protected HttpHeaders getHttpHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + restTemplate.getAccessToken());
return headers;
}
}
SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Inject
private OAuth2RestTemplate restTemplate;
@Bean
public FeideConnectFilter feideConnectFilter() {
FeideConnectFilter filter = new FeideConnectFilter("/oauth/login");
filter.setRestTemplate(restTemplate);
return filter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAfter(new OAuth2ClientContextFilter(), AbstractPreAuthenticatedProcessingFilter.class)
.addFilterAfter(feideConnectFilter(), OAuth2ClientContextFilter.class)
.httpBasic()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/oauth2/login"))
.and()
.authorizeRequests()
.anyRequest().authenticated();
}
}
过滤器(在 WebInitializer.java 中)按以下顺序添加:
private void addFilters(final ServletContext container, final WebApplicationContext applicationContext) {
container.addFilter("springSessionRepositoryFilter", DelegatingFilterProxy.class).addMappingForUrlPatterns(null, false, "/*");
container.addFilter("springSecurityFilterChain", DelegatingFilterProxy.class).addMappingForUrlPatterns(null, false, "/*");
}
OIDC 与 Spring Session 一起工作,如果我使用 Spring Security 自己的 OIDC 实现,它是在版本 5 中引入的。顺便说一句,它实现起来非常容易,几乎没有代码全部。 Spring 安全团队在 Spring 安全性中添加了对 OIDC 和 SAML2.0 的支持,他们做得很好。换句话说,我找到了解决办法。