如何通过受 Keycloak 保护的 Spring restTemplate 调用 rest 端点
How to call rest endpoint via Spring restTemplate that is protected by Keycloak
先决条件
我有两个 Java Spring 应用程序(App 'A' 和 App 'B')是通过 JHipster(单体应用程序)创建的。这两个应用程序都为 authentication/authorization 使用 keycloak。
这两个应用程序都有一个 angular 前端并支持通过 ouath (spring-security) 登录。这是我的 SecurityConfiguration
申请 A 和 B:
@Configuration
@Import(SecurityProblemSupport.class)
@EnableOAuth2Sso
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final CorsFilter corsFilter;
private final SecurityProblemSupport problemSupport;
public SecurityConfiguration(CorsFilter corsFilter, SecurityProblemSupport problemSupport) {
this.corsFilter = corsFilter;
this.problemSupport = problemSupport;
}
@Bean
public AjaxLogoutSuccessHandler ajaxLogoutSuccessHandler() {
return new AjaxLogoutSuccessHandler();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
return new SecurityEvaluationContextExtension();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/app/**/*.{js,html}")
.antMatchers("/i18n/**")
.antMatchers("/content/**")
.antMatchers("/swagger-ui/index.html")
.antMatchers("/test/**")
.antMatchers("/h2-console/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.addFilterBefore(corsFilter, CsrfFilter.class)
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.logout()
.logoutUrl("/api/logout")
.logoutSuccessHandler(ajaxLogoutSuccessHandler())
.permitAll()
.and()
.headers()
.frameOptions()
.disable()
.and()
.authorizeRequests()
.antMatchers("/api/profile-info").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/v2/api-docs/**").permitAll()
.antMatchers("/swagger-resources/configuration/ui").permitAll()
.antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN);
}
}
在 App B 中我也有一个 ResourceServerConfiguration
。这将检查 header 是否包含 "Authorization" 键。如果为 true,则用户可以通过 JWT(Bearer Authentication)登录。我通过 Postman 对此进行了测试,它工作正常:
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(final HttpSecurity http) throws Exception {
http
.requestMatcher(new RequestHeaderRequestMatcher("Authorization")).authorizeRequests()
.anyRequest().authenticated();
}
}
此外,这两个应用程序都在同一个 keycloak 领域并且具有 access-type "public".
问题:
现在我想通过应用程序 A 的 Spring RestTemplate 调用应用程序 B 的端点。问题是,我没有 access_token 可以放在我的休息中 request/restTemplate。当我查看从前端发送的请求时,我只有一个 JSESSIONID。 header中没有access_token/JWT。
问题
有没有办法让当前用户的 access_token 脱离 JSESSIONID/the HttpSession 或 spring 安全上下文?我是否需要像 Tokenstore 这样的东西来存储来自 keycloak 的每个令牌?
有没有其他人遇到过类似的问题或者知道我该如何解决这个问题?
经过一些研究发现问题出在生成的 jhipster 代码中。
我按照应用程序中的身份验证过程看到,身份验证后直接调用了 /account 端点,从中检索了用户信息。该调用由前端触发。第一次调用此端点时,有一个主体和可用的不记名令牌。在 /account 端点内,调用带有主体对象的 userService。更多精确
getUserFromAuthentication(OAuth2Authentication authentication)
被调用。在此方法中,有一部分将 OAuth2Authentication 替换为新的 UsernamePasswordAuthenticationToken 并将其插入到 SecurityContext 中:
UsernamePasswordAuthenticationToken token = getToken(details, user,
grantedAuthorities);
authentication = new OAuth2Authentication(authentication.getOAuth2Request(), token);
SecurityContextHolder.getContext().setAuthentication(authentication);
所以在那之后,access_token 丢失了。我不太确定为什么它被新的 OAuth2Authentication 取代,但我倾向于扩展这部分并将 access_token 保留在我的 securityContext 中以供进一步调用。
先决条件
我有两个 Java Spring 应用程序(App 'A' 和 App 'B')是通过 JHipster(单体应用程序)创建的。这两个应用程序都为 authentication/authorization 使用 keycloak。
这两个应用程序都有一个 angular 前端并支持通过 ouath (spring-security) 登录。这是我的 SecurityConfiguration
申请 A 和 B:
@Configuration
@Import(SecurityProblemSupport.class)
@EnableOAuth2Sso
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final CorsFilter corsFilter;
private final SecurityProblemSupport problemSupport;
public SecurityConfiguration(CorsFilter corsFilter, SecurityProblemSupport problemSupport) {
this.corsFilter = corsFilter;
this.problemSupport = problemSupport;
}
@Bean
public AjaxLogoutSuccessHandler ajaxLogoutSuccessHandler() {
return new AjaxLogoutSuccessHandler();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
return new SecurityEvaluationContextExtension();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/app/**/*.{js,html}")
.antMatchers("/i18n/**")
.antMatchers("/content/**")
.antMatchers("/swagger-ui/index.html")
.antMatchers("/test/**")
.antMatchers("/h2-console/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.addFilterBefore(corsFilter, CsrfFilter.class)
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.logout()
.logoutUrl("/api/logout")
.logoutSuccessHandler(ajaxLogoutSuccessHandler())
.permitAll()
.and()
.headers()
.frameOptions()
.disable()
.and()
.authorizeRequests()
.antMatchers("/api/profile-info").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/v2/api-docs/**").permitAll()
.antMatchers("/swagger-resources/configuration/ui").permitAll()
.antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN);
}
}
在 App B 中我也有一个 ResourceServerConfiguration
。这将检查 header 是否包含 "Authorization" 键。如果为 true,则用户可以通过 JWT(Bearer Authentication)登录。我通过 Postman 对此进行了测试,它工作正常:
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(final HttpSecurity http) throws Exception {
http
.requestMatcher(new RequestHeaderRequestMatcher("Authorization")).authorizeRequests()
.anyRequest().authenticated();
}
}
此外,这两个应用程序都在同一个 keycloak 领域并且具有 access-type "public".
问题:
现在我想通过应用程序 A 的 Spring RestTemplate 调用应用程序 B 的端点。问题是,我没有 access_token 可以放在我的休息中 request/restTemplate。当我查看从前端发送的请求时,我只有一个 JSESSIONID。 header中没有access_token/JWT。
问题
有没有办法让当前用户的 access_token 脱离 JSESSIONID/the HttpSession 或 spring 安全上下文?我是否需要像 Tokenstore 这样的东西来存储来自 keycloak 的每个令牌?
有没有其他人遇到过类似的问题或者知道我该如何解决这个问题?
经过一些研究发现问题出在生成的 jhipster 代码中。 我按照应用程序中的身份验证过程看到,身份验证后直接调用了 /account 端点,从中检索了用户信息。该调用由前端触发。第一次调用此端点时,有一个主体和可用的不记名令牌。在 /account 端点内,调用带有主体对象的 userService。更多精确
getUserFromAuthentication(OAuth2Authentication authentication)
被调用。在此方法中,有一部分将 OAuth2Authentication 替换为新的 UsernamePasswordAuthenticationToken 并将其插入到 SecurityContext 中:
UsernamePasswordAuthenticationToken token = getToken(details, user,
grantedAuthorities);
authentication = new OAuth2Authentication(authentication.getOAuth2Request(), token);
SecurityContextHolder.getContext().setAuthentication(authentication);
所以在那之后,access_token 丢失了。我不太确定为什么它被新的 OAuth2Authentication 取代,但我倾向于扩展这部分并将 access_token 保留在我的 securityContext 中以供进一步调用。