是否使用Spring Boot(安全)和Keyloak启用角色身份验证?

cclgggtu  于 2022-10-23  发布在  Spring
关注(0)|答案(6)|浏览(207)

我在试着做一件简单的事情。
要向单个终结点发出请求并(从客户端)发送承载令牌,我希望验证该令牌,并根据在我的终结点上的密钥遮盖接受/拒绝请求中分配的角色。
我看了很多教程,甚至是书,但大多数我就是看不懂。
按照此步骤设置我的密钥遮盖信息(领域、角色、用户)https://medium.com/@bcarunmail/securing-rest-api-using-keycloak-and-spring-oauth2-6ddf3a1efcc2
所以,
我基本上使用一个客户端设置了我的密匙斗篷,该客户端是一个具有特定角色“User”的用户,并如下配置:

@Configuration
@KeycloakConfiguration
//@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class SecurityConf extends KeycloakWebSecurityConfigurerAdapter
{
    /**
     * Registers the KeycloakAuthenticationProvider with the authentication manager.
     */
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(keycloakAuthenticationProvider());
    }

    /**
     * Defines the session authentication strategy.
     */
    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Bean
    public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }

    @Bean
    public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(
            KeycloakAuthenticationProcessingFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(
            KeycloakPreAuthActionsFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        super.configure(http);
        http
                .authorizeRequests()
                .antMatchers("/user/*").hasRole("admin")
                .antMatchers("/admin*").hasRole("user")

    }
}

我不明白为什么在许多教程中我会看到这一点(作为最后一条规则):

.anyRequest().permitAll();

基本上,当我设置我没有安全性时,我可以在没有持有者令牌的情况下调用端点。
但当我把这作为最后一条规则时

.anyRequest().denyAll();

我总是得403分。
调试中,我发现了这个:
请求是处理身份验证

f.KeycloakAuthenticationProcessingFilter : Attempting Keycloak authentication
o.k.a.BearerTokenRequestAuthenticator    : Found [1] values in authorization header, selecting the first value for Bearer.
o.k.a.BearerTokenRequestAuthenticator    : Verifying access_token
o.k.a.BearerTokenRequestAuthenticator    : successful authorized
a.s.a.SpringSecurityRequestAuthenticator : Completing bearer authentication. Bearer roles: [] 
o.k.adapters.RequestAuthenticator        : User 'testuser' invoking 'http://localhost:9090/api/user/123' on client 'users'
o.k.adapters.RequestAuthenticator        : Bearer AUTHENTICATED
f.KeycloakAuthenticationProcessingFilter : Auth outcome: AUTHENTICATED
o.s.s.authentication.ProviderManager     : Authentication attempt using org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider
o.s.s.core.session.SessionRegistryImpl   : Registering session 5B871A0E2AF55B70DC8E3B7436D79333, for principal testuser
f.KeycloakAuthenticationProcessingFilter : Authentication success using bearer token/basic authentication. Updating SecurityContextHolder to contain: org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken@355f68d6: Principal: testuser; Credentials: [PROTECTED]; Authenticated: true; Details: org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount@5d7a32a9; Not granted any authorities
[nio-9090-exec-3] o.s.security.web.FilterChainProxy        : /api/user/123 at position 8 of 15 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
nio-9090-exec-3] o.s.s.w.s.DefaultSavedRequest            : pathInfo: both null (property equals)
[nio-9090-exec-3] o.s.s.w.s.DefaultSavedRequest            : queryString: both null (property equals)

看起来我没有得到无名小卒的角色。
我的依赖项:

<dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-starter</artifactId>
            <version>6.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-security-adapter</artifactId>
            <version>6.0.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

我的问题?
我请求访问令牌,发送:

client_id -> my client from keycloak
username -> my user from keycloak
password -> my password from keycloak
grant_type -> password
client_secret -> from keycloak

我得到一个令牌,然后我用来请求我的应用程序endint。我的请求始终有效,无论我使用的是哪个端点(角色为USER或角色为ADMIN的)。
在我的房子里,我有这样的东西:

keycloak:
  auth-server-url: http://localhost:8080/auth/
  resource: users-api
  credentials:
    secret : my-secret
  use-resource-role-mappings : true
  realm: my-realm
  realmKey:  my-key
  public-client: true
  principal-attribute: preferred_username
  bearer-only: true

你知道如何实际启用这种情况下的角色吗?
我必须配置客户端才能使用JWT吗?有什么好主意吗?
我还在我的端点上添加了注解

@Secured("admin")
@PreAuthorize("hasAnyAuthority('admin')")

但他们似乎什么都不做。
--编辑--
在修复URL以匹配资源之后,我仍然得到403。

"realm_access": {
    "roles": [
      "offline_access",
      "admin",
      "uma_authorization"
    ]
  },
  "resource_access": {
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    }
  },

这是不是与我的问题有关?

xxb16uws

xxb16uws1#

在调试堆栈中:我看到您正在调用/api/user/123,并且在您的安全配置中,您正在保护不同的/user/*,请将您的安全性更改为:

.antMatchers("/api/user/*").hasRole("user")
                .antMatchers("/api/admin*").hasRole("admin")

P.S:不需要注册KeycloakAuthenticationProcessingFilterKeycloakPreAuthActionsFilter

tzxcd3kk

tzxcd3kk2#

  • 我知道这是一个老帖子,但我写这篇文章只是为了将来参考,以防其他人有同样的问题。*

如果您查看日志,您会发现Keyshaak成功地验证了访问令牌,但没有任何授予权限。这就是为什么Spring不授权该请求,而您会得到HTTP403禁止

f.KeycloakAuthenticationProcessingFilter : Authentication success using bearer token/basic authentication. Updating SecurityContextHolder to contain: org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken@355f68d6: Principal: testuser; Credentials: [PROTECTED]; Authenticated: true; Details: org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount@5d7a32a9; Not granted any authorities

这是因为Keyloak适配器配置为使用资源(即客户端级)角色Map,而不是领域级角色Map:

Use-resource-Role-mappings:如果设置为True,适配器将在令牌内部查找用户的应用程序级角色Map。如果为False,它将在领域级别查找用户角色Map。这是可选的。默认值为FALSE。

Here是关于适配器配置的链接。
因此,如果您希望通过领域角色获得授权,则属性应如下所示:

keycloak:
  auth-server-url: http://localhost:8080/auth/
  resource: users-api
  credentials:
    secret : my-secret
  use-resource-role-mappings : false
  realm: my-realm
  realmKey:  my-key
  public-client: true
  principal-attribute: preferred_username
  bearer-only: true

**注意:**如果您想同时使用领域级和客户端级角色Map,那么您应该重写KeyloakAuthenticationProvider.authate()方法,通过自己组合来提供所需的角色。

qxgroojn

qxgroojn3#

permitAll:

每当您想要允许任何请求访问特定的资源/URL时,都可以使用permitAll。例如,每个人都应该可以访问登录URL。

DenyAll:

当您想要阻止特定URL的访问时,无论请求来自何处或由谁发出请求(管理员)
您还存在与URL和角色不匹配的问题(您向用户授予具有管理员权限的URL,反之亦然)。(最好使用ROLE_ADMIN或ADMIN或USER角色)从堆栈中我可以看到未授予任何权限,因此请与权威人士重新检查代码

http
         .csrf().disable()
         .authorizeRequests()
         .antMatchers("/api/user/**").hasRole("ADMIN")
         .antMatchers("/api/admin/**").hasRole("USER")
         .anyRequest().authenticated();
sd2nnvve

sd2nnvve4#

1.你会在没有@Configuration的情况下尝试吗?我认为在SecurityConf类上只需要@KeycloakConfiguration注解。
1.你的antMatcher是否尊重区分大小写?

http
    .csrf().disable()
    .authorizeRequests()
    .antMatchers("/api/user/**").hasRole("user")
    .antMatchers("/api/admin/**").hasRole("admin")
    .anyRequest().authenticated();

1.也请尝试此配置,以删除Java定义的Role_*约定:

@Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        // SimpleAuthorityMapper is used to remove the ROLE_* conventions defined by Java so
        // we can use only admin or user instead of ROLE_ADMIN and ROLE_USER
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

1.如果您的所有端点都有相同的逻辑,安全配置应该足够了,您不需要其他注解。但是,如果您有另一个具有admin角色的端点,而该端点不在您的“/api/admin”控制器中,您可以尝试:

@PreAuthorize("hasRole('admin')")
omjgkv6w

omjgkv6w5#

回答晚了,但希望能对其他面临同样问题的人有所帮助。我面临着与您完全相同的问题,对于我来说,在Configuration类中,我必须通过设置授予的权限Map器(@Override方法仅用于调试)来更改默认的keyloakAuthenticationProvider:

@Bean
public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
  KeycloakAuthenticationProvider keycloakAuthenticationProvider = new KeycloakAuthenticationProvider() {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
      System.out.println("===========+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> authenticate ");
      KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication;
      for (String role : token.getAccount().getRoles()) {
        System.out.println("===========+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Role : " + role);
      }

      return super.authenticate(authentication);
    }
  };
  keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
  auth.authenticationProvider(keycloakAuthenticationProvider);
}

}

4dc9hkyq

4dc9hkyq6#

2022更新

用于Spring的密钥斗篷适配器是deprecated。不要用它。请改用spring-boot-starter-oauth2-resource-server

轻松解决

spring-boot-starter-oauth2-resource-server之上有一个非常方便的set of libs,配置可以非常简单:

@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public static class SecurityConfig {
    @Bean
    ExpressionInterceptUrlRegistryPostProcessor expressionInterceptUrlRegistryPostProcessor() {
        return (ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) -> registry
            .antMatchers("/api/user/**").hasAuthority("USER")
            .antMatchers("/api/admin/**").hasAuthority("ADMIN")
            .anyRequest().authenticated();
        }
    }
}
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,resource_access.employee-service.roles,resource_access.other-client.roles
com.c4-soft.springaddons.security.cors[0].path=/api/**
com.c4-soft.springaddons.security.permit-all=/actuator/health/readiness,/actuator/health/liveness,/v3/api-docs/**

纯 Spring 解决方案

要仅对spring-boot-starter-oauth2-resource-server执行相同的操作,需要编写相当多的Java conf:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig {

    public interface Jwt2AuthoritiesConverter extends Converter<Jwt, Collection<? extends GrantedAuthority>> {
    }

    @SuppressWarnings("unchecked")
    @Bean
    public Jwt2AuthoritiesConverter authoritiesConverter() {
        // This is a converter for roles as embedded in the JWT by a Keycloak server
        // Roles are taken from both realm_access.roles & resource_access.{client}.roles
        return jwt -> {
            final var realmAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
            final var realmRoles = (Collection<String>) realmAccess.getOrDefault("roles", List.of());

            final var resourceAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("resource_access", Map.of());
            // We assume here you have "employee-service" (as in the tutorial referenced in the question) and "other-client" clients configured with "client roles" mapper in Keycloak
            final var confidentialClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("employee-service", Map.of());
            final var confidentialClientRoles = (Collection<String>) confidentialClientAccess.getOrDefault("roles", List.of());
            final var publicClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("other-client", Map.of());
            final var publicClientRoles = (Collection<String>) publicClientAccess.getOrDefault("roles", List.of());

            return Stream.concat(realmRoles.stream(), Stream.concat(confidentialClientRoles.stream(), publicClientRoles.stream()))
                    .map(SimpleGrantedAuthority::new).toList();
        };
    }

    public interface Jwt2AuthenticationConverter extends Converter<Jwt, AbstractAuthenticationToken> {
    }

    @Bean
    public Jwt2AuthenticationConverter authenticationConverter(Jwt2AuthoritiesConverter authoritiesConverter) {
        return jwt -> new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt));
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, Jwt2AuthenticationConverter authenticationConverter, ServerProperties serverProperties)
            throws Exception {

        // Enable OAuth2 with custom authorities mapping
        http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(authenticationConverter);

        // Enable anonymous
        http.anonymous();

        // Enable and configure CORS
        http.cors().configurationSource(corsConfigurationSource());

        // State-less session (state in access-token only)
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // Enable CSRF with cookie repo because of state-less session-management
        http.csrf().disable();

        // Return 401 (unauthorized) instead of 403 (redirect to login) when authorization is missing or invalid
        http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
            response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Restricted Content\"");
            response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
        });

        // If SSL enabled, disable http (https only)
        if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
            http.requiresChannel().anyRequest().requiresSecure();
        } else {
            http.requiresChannel().anyRequest().requiresInsecure();
        }

        // Route security: authenticated to all routes but actuator and Swagger-UI
        // @formatter:off
        http.authorizeRequests()
            .antMatchers("/actuator/health/readiness", "/actuator/health/liveness", "/v3/api-docs", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
            .antMatchers("/api/user/**").hasAuthority("USER")
            .antMatchers("/api/admin/**").hasAuthority("ADMIN")
            .anyRequest().authenticated();
        // @formatter:on

        return http.build();
    }

    private CorsConfigurationSource corsConfigurationSource() {
        // Very permissive CORS config...
        final var configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setExposedHeaders(Arrays.asList("*"));

        // Limited to API routes (neither actuator nor Swagger-UI)
        final var source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", configuration);

        return source;
    }
}
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://localhost:8443/realms/master

重要说明

上面的两种配置都没有转换到key shaak角色(大小写不变,没有ROLE_前缀),这是使用hasAuthority(...)而不是hasRole(...)的原因。
此外,仅考虑在以下级别使用定义的角色:

  • “境界”
  • “员工服务”客户端(已在问题中引用的教程中定义
  • “其他-客户端”(只是为了演示可以使用任何其他任意客户端)

相关问题