通过OAuth / OIDC将弹簧靴连接到KeyCloak
#java #springboot #keycloak #oauth

KeyCloak是用于集中身份和访问管理的开源应用程序。提供了许多身份验证方法,可以根据自己的喜好进行自定义。我们如何将我们的春季靴应用程序与百里叶前端连接到KeyCloak?

this separate article在当前版本21.1.1中的KeyCloak设置。此后,我们的KeyCloak服务器已经在port 8085上运行,包含一个领域和客户端,并准备连接到我们的Spring Boot应用程序。 oauth/oidc自然是我们的首选 - 用户被重定向到外部登录页面,并在成功身份验证后返回我们的Web应用程序。

以前,有一个单独的弹簧启动适配器用于KeyCloak,但这是弃用的。取而代之的是,我们可以直接使用Spring Security 的Oauth板工具。使用库spring-boot-starter-oauth2-client,我们可以将应用程序作为KeyCloak的OAuth / OIDC客户端设置。这种方法也可以应用于Okta或Onelogin等其他身份经理。

准备我们的应用程序

我们在Bootify Builder中快速创建一个带有百叶窗的简单春季启动应用程序。 Open your project一键单击并选择首选前端。此外,我们使用两个字符串字段externalIdemail创建一个实体User,在那里我们将存储登录的用户 - 稍后将详细介绍。我们的初始项目现在可以直接下载和执行。


为我们的简单春季启动应用程序创建用户表

现在我们可以开始为KeyCloak增添添加。除了依赖关系org.springframework.boot:spring-boot-starter-oauth2-client(通过BOM自动提供版本),我们的应用程序还需要许多设置,我们将其添加到我们的application.yml / application.properties

spring:
  security:
    oauth2:
      client:
        provider:
          keycloak-bootify:
            issuer-uri: http://localhost:8085/realms/bootify
        registration:
          keycloak-bootify:
            client-id: testapp
            client-secret: ${KEYCLOAK_CLIENT_SECRET:<<YOUR_CLIENT_SECRET>>}
            client-name: Testapp
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8080/login/oauth2/code/testapp
            scope: openid,profile,email,offline_access
我们的Oauth提供商的注册

provider后面的区域,我们将keycloak-bootify添加到我们的应用程序中。通过OIDC issuer-uri春季启动将检索OAuth Integration 的所有必需信息。您可以在浏览器中打开它以查看提供的内容。

registration后面,我们向提供商添加了一个客户。在那里,我们使用客户端ID以及配置KeyCloak服务器时收到的秘密。如果我们使用带有DevServer的前端,则为重定向URL指定端口8081。准备此后,我们已经可以为弹簧安全性定义中央配置。

@Configuration
public class OAuthSecurityConfig {

    private OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler() {
        final OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler = 
                new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);
        oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}?logoutsuccess=true");
        return oidcLogoutSuccessHandler;
    }

    @Bean
    public SecurityFilterChain configure(final HttpSecurity http) throws Exception {
        return http.cors(withDefaults())
                .csrf((csrf) -> csrfwithDefaults())
                .authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers("/", "/css/**", "/js/**", "/images/**", "/webjars/**").permitAll()
                        .anyRequest().hasAuthority(UserRoles.ROLE_USER))
                .oauth2Login(withDefaults())
                .logout((logout) -> logout
                        .logoutSuccessHandler(oidcLogoutSuccessHandler())
                        .deleteCookies("JSESSIONID"))
                .build();
    }

}

- 我们的OAuth/OIDC

的中央设置

自Spring Boot 3.0以来,必须以HttpSecurity类配置为SecurityFilterChain。在启用CORS保护后,我们定义了一些例外(例如http://localhost:8080"/"),期望其他所有内容的角色"ROLE_USER" 。身份验证是使用OAuth 2登录完成的。此外,我们已经定义了自己的注销处理程序,该处理程序在成功的注销后将我们重定向回主页。

角色映射

与KeyCloak的连接现在应该有效,但是在登录后仍无法访问保护区。根据我们的KeyCloak设置,角色已经作为ID令牌的一部分提供了,但尚未读出。因此,我们必须使用角色映射
扩展我们的配置

public class OAuthSecurityConfig {

    // ...

    /**
     * Custom mapper to use OIDC claims as Spring Security roles.
     */
    @Bean
    public GrantedAuthoritiesMapper userAuthoritiesMapper() {
        return (authorities) -> { 
            final Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
            authorities.forEach((authority) -> { 
                if (authority instanceof OidcUserAuthority oidcAuth) {
                    mappedAuthorities.addAll(mapAuthorities(oidcAuth.getIdToken().getClaims()));
                } else if (authority instanceof OAuth2UserAuthority oauth2Auth) {
                    mappedAuthorities.addAll(mapAuthorities(oauth2Auth.getAttributes()));
                }
            });
            return mappedAuthorities;
        };
    }

    /**
     * Read claims from attribute realm_access.roles as SimpleGrantedAuthority.
     */
    private List<SimpleGrantedAuthority> mapAuthorities(final Map<String, Object> attributes) {
        final Map<String, Object> realmAccess = ((Map<String, Object>)attributes.getOrDefault("realm_access", Collections.emptyMap()));
        final Collection<String> roles = ((Collection<String>)realmAccess.getOrDefault("roles", Collections.emptyList()));
        return roles.stream()
                .map((role) -> new SimpleGrantedAuthority(role))
                .toList();
    }

}

提供GrantedAuthoritiesMapper

这通过我们的配置提供了GrantedAuthoritiesMapper bean,该配置将自动通过春季安全拾取。这读了realm_access.roles字段的角色,并将其转换为SimpleGrantedAuthority。如果我们立即启动应用程序并转到保护区,则应自动重定向到KeyCloak。注册/登录后,我们将其发送回我们的应用程序,我们已成功身份验证并具有所需的角色

OAuth用户与数据库的同步

身份验证的用户主要连接其他业务逻辑,例如存储地址或个人数据。因此,每次登录后,将用户与数据库同步是有意义的。为此,我们将UserSynchronizationService添加到我们的Spring Boot应用程序中。

@Service
public class UserSynchronizationService {

    // ...

    private void syncWithDatabase(final OidcUserInfo userInfo) {
        User user = userRepository.findByExternalId(userInfo.getSubject());
        if (user == null) {
            log.info("adding new user after successful login: {}", userInfo.getSubject());
            user = new User();
            user.setExternalId(userInfo.getSubject());
        } else {
            log.info("updating existing user after successful login: {}", userInfo.getSubject());
        }
        user.setEmail(userInfo.getEmail());
        userRepository.save(user);
    }

    @EventListener(AuthenticationSuccessEvent.class)
    public void onAuthenticationSuccessEvent(final AuthenticationSuccessEvent event) {
        final OidcUser oidcUser = ((OidcUser)event.getAuthentication().getPrincipal());
        syncWithDatabase(oidcUser.getUserInfo());
    }

}

创建或更新数据库中的用户

此服务对AuthenticationSuccessEvent响应,该AuthenticationSuccessEvent会在用户通过OAuth登录后自动触发。即使其他用户数据已更改,每个用户都会由"subject"字段唯一识别。然后,新的或现有的User对象被当前的电子邮件填充并持续。

在Bootify的免费计划中,可以使用自己的数据库模式和CRUD功能初始化当前版本3.1.0中的Spring Boot应用程序。在专业计划中,还可以配置弹簧安全性 - 在这里包括KeyCloak作为选项。这将提供此处描述的设置,该设置已定制为所选设置

» See Features and Pricing