Bootstrap

OpenID Connect(OIDC)认证--keycloak与springboot项目的整合

OpenID Connect认证–keycloak与springboot项目的整合

文章目录

    • OpenID Connect认证--keycloak与springboot项目的整合
      • 前言
      • 什么是 Keycloak?
      • 项目配置
        • 1. 创建 Spring Boot 项目
        • 2. 设置 Keycloak 服务器
        • 3. 在 Keycloak 中创建 Realm 和 Client
        • 4. 配置 Spring Boot 应用
        • 5. 配置 Spring Security
        • 6. 前端请求调用来具体整合

前言

在现代应用程序中,安全性是至关重要的。为了简化认证和授权流程,许多开发人员选择使用 Keycloak 作为统一的身份和访问管理解决方案。Spring Boot 是一个流行的 Java 框架,通过与 Keycloak 整合,可以轻松实现 OAuth2 和 **OpenID Connect (OIDC)**的身份认证和授权功能。本篇博客将详细介绍如何在 Spring Boot 项目中集成 Keycloak,帮助你快速搭建一个安全的 Web 应用。

什么是 Keycloak?

Keycloak 是一个开源的身份和访问管理工具,支持标准协议如 OAuth2 和 OpenID Connect。它允许开发人员轻松地将身份验证、授权和单点登录(SSO)功能集成到应用程序中。通过 Keycloak,可以管理用户、角色、权限,并将这些管理功能集中化。

关于keycloak的配置我就不在这里赘述,具体请参考博文:什么是Keycloak?怎么样使用Keycloak实现登录和权限验证?

项目配置

我将通过一个 Spring Boot 项目来演示如何集成 Keycloak。

1. 创建 Spring Boot 项目

创建一个springboot项目,并引入下面的依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.keycloak</groupId>
        <artifactId>keycloak-spring-boot-starter</artifactId>
        <version>22.0.1</version> <!-- 根据需要选择合适的版本 -->
    </dependency>
</dependencies>

如果你使用的gradle,也大差不差:

dependencies {
  implementation(libs.spring.boot.starter.security)
  implementation(libs.spring.boot.starter.oauth2.client)
  implementation(libs.spring.boot.starter.oauth2.resource.server)
  implementation(libs.keycloak.spring.boot.starter)
}

主要就是上述的几个依赖,我使用的gradle来构建项目。

2. 设置 Keycloak 服务器

在整合之前,你需要设置一个 Keycloak 服务器。你可以通过 Docker 快速启动一个 Keycloak 实例:

docker run -d --name keycloak -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin quay.io/keycloak/keycloak:latest start-dev

根据上述的docker容器配置,可以自己设置用户名和密码(均为admin),或者设置成你自己喜欢的方式,具体请参考博文:什么是Keycloak?怎么样使用Keycloak实现登录和权限验证?,或是查看官方文档:keycloak操作手册

由于生产需要我是将keycloak部署在公网上的,大概就是:https://keycloak.xxx.cn/,后面的链接我都用这个来替代。

3. 在 Keycloak 中创建 Realm 和 Client

登录到 Keycloak 管理控制台后:

  1. 创建 Realm: 在左侧菜单中选择 “Add realm”,输入一个名称,例如 myrealm,然后点击 “Create”。

  2. 创建 Client
    在创建好的 Realm 中,导航到 “Clients” 部分,点击 “Create” 创建一个新客户端。
    • Client ID: 输入一个客户端 ID,例如 spring-boot-client
    • Client Protocol: 选择 openid-connect
    • Root URL: 输入 Spring Boot 应用的根 URL,例如 http://localhost:8081
    • Save: 点击 “Save” 保存配置。

在客户端的配置页面,确保 “Access Type” 设置为 confidential 并保存,然后生成并复制客户端的 Secret

上述的信息在后续在配置springboot后端的时候会用到。这部分在这篇博文上也有提到:什么是Keycloak?怎么样使用Keycloak实现登录和权限验证?

4. 配置 Spring Boot 应用

application.ymlapplication.properties 中,添加 Keycloak 配置信息:

spring:
  application:
    name: customer_management
  security:
    oauth2:
      client:
        registration:
          keycloak:
            client-id: web_app
            authorization-grant-type: implicit
            scope:
              - openid
        provider:
          keycloak:
            issuer-uri: https://keycloak.scysn.cn/realms/test
            user-name-attribute: preferred_username
      resource server:
        jwt:
          issuer-uri: https://keycloak.scysn.cn/realms/test
server:
  port: 8081
application:
  security:
    oauth2:
      audience: account,api://default
  oauth2:
    # authorization-url: http://localhost:9080/realms/ysn/protocol/openid-connect/auth
    authorization-url: https://keycloak.scysn.cn/realms/test/protocol/openid-connect/auth

在上述的配置文件中,主要需要配置的就是下面这几个参数:

spring.security.oauth2.client.registration.keycloak.client-id=login-app
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.scope=openid
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/realms/SpringBootKeycloak
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/SpringBootKeycloak
5. 配置 Spring Security

在 Spring Boot 项目中,通过创建一个 SecurityConfig 类来配置安全性:

在这个配置类中主要是配置以下这个bean对象:

@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
       .requestMatchers(new AntPathRequestMatcher("/customers*", HttpMethod.OPTIONS.name()))
       .permitAll()
       .requestMatchers(new AntPathRequestMatcher("/customers*"))
       .hasRole("user")
       .requestMatchers(new AntPathRequestMatcher("/"))
       .permitAll()
       .anyRequest()
       .authenticated());
    http.oauth2ResourceServer((oauth2) -> oauth2
       .jwt(Customizer.withDefaults()));
    http.oauth2Login(Customizer.withDefaults())
       .logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/"));
    return http.build();
}

在上面的代码中,oauth2Login *()方法将**OAuth2LoginAuthenticationFilter*添加到过滤器链中。该过滤器拦截请求并应用 OAuth 2 身份验证所需的逻辑oauth2ResourceServer方法验证 JWT 令牌与 Keycloak 服务器的绑定。它还强制执行前面讨论的约束。

Keycloak 返回一个包含所有相关信息的令牌。为了使 Spring Security 能够根据用户分配的角色做出决策,我们必须解析令牌并提取相关详细信息。但是 Spring Security 通常在每个角色名称中添加*“ROLES_”前缀,而 Keycloak 则发送纯角色名称。为了解决这个问题,我们创建了一个辅助方法,它为从 Keycloak 检索到的每个角色添加“ROLE_”前缀。*

@Bean
Collection generateAuthoritiesFromClaim(Collection roles) {
    return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(
           Collectors.toList());
}

现在我们可以继续解析令牌了。首先,我们必须检查令牌是否是 OidcUserAuthority 或 OAuth2UserAuthority 的实例。由于 Keycloak 令牌可以是任何一种类型,因此我们需要实现解析逻辑。下面的代码检查令牌的类型并决定解析机制。

boolean isOidc = authority instanceof OidcUserAuthority;
    if (isOidc) { 
       /// Parsing code here
    }

默认情况下,令牌是 OidcUserAuthority 的一个实例。

如果令牌是 OidcUserAuthority 实例,则可以将其配置为包含两个组或领域访问下的角色。因此,我们必须检查两者以提取角色,如下面的代码所示,

if (userInfo.hasClaim(REALM_ACCESS_CLAIM)) {
    var realmAccess = userInfo.getClaimAsMap(REALM_ACCESS_CLAIM);
    var roles = (Collection) realmAccess.get(ROLES_CLAIM);
    mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
} else if (userInfo.hasClaim(GROUPS)) {
    Collection roles = (Collection) userInfo.getClaim(GROUPS);
    mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}

但是,如果令牌是 OAuth2UserAuthority 实例,我们需要按如下方式解析它:

var oauth2UserAuthority = (OAuth2UserAuthority) authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
if (userAttributes.containsKey(REALM_ACCESS_CLAIM)) {
    Map<String, Object> realmAccess = (Map<String, Object>) userAttributes.get(REALM_ACCESS_CLAIM);
    Collection roles = (Collection) realmAccess.get(ROLES_CLAIM);
    mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}

下面给出了完整的代码供您参考

@Configuration
@EnableWebSecurity
class SecurityConfig {

    private static final String GROUPS = "groups";
    private static final String REALM_ACCESS_CLAIM = "realm_access";
    private static final String ROLES_CLAIM = "roles";

    private final KeycloakLogoutHandler keycloakLogoutHandler;

    SecurityConfig(KeycloakLogoutHandler keycloakLogoutHandler) {
        this.keycloakLogoutHandler = keycloakLogoutHandler;
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Bean
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(sessionRegistry());
    }

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }
    
    @Bean
    public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth
            .requestMatchers(new AntPathRequestMatcher("/customers*"))
            .hasRole("user")
            .requestMatchers(new AntPathRequestMatcher("/"))
            .permitAll()
            .anyRequest()
            .authenticated());
        http.oauth2ResourceServer((oauth2) -> oauth2
            .jwt(Customizer.withDefaults()));
        http.oauth2Login(Customizer.withDefaults())
            .logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/"));
        return http.build();
    }
    
    @Bean
    public GrantedAuthoritiesMapper userAuthoritiesMapperForKeycloak() {
        return authorities -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
            var authority = authorities.iterator().next();
            boolean isOidc = authority instanceof OidcUserAuthority;

            if (isOidc) {
                var oidcUserAuthority = (OidcUserAuthority) authority;
                var userInfo = oidcUserAuthority.getUserInfo();

                // Tokens can be configured to return roles under
                // Groups or REALM ACCESS hence have to check both
                if (userInfo.hasClaim(REALM_ACCESS_CLAIM)) {
                    var realmAccess = userInfo.getClaimAsMap(REALM_ACCESS_CLAIM);
                    var roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
                    mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
                } else if (userInfo.hasClaim(GROUPS)) {
                    Collection<String> roles = (Collection<String>) userInfo.getClaim(
                        GROUPS);
                    mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
                }
            } else {
                var oauth2UserAuthority = (OAuth2UserAuthority) authority;
                Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();

                if (userAttributes.containsKey(REALM_ACCESS_CLAIM)) {
                    Map<String, Object> realmAccess = (Map<String, Object>) userAttributes.get(
                        REALM_ACCESS_CLAIM);
                    Collection<String> roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
                    mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
                }
            }
            return mappedAuthorities;
        };
    }

    Collection<GrantedAuthority> generateAuthoritiesFromClaim(Collection<String> roles) {
        return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(
            Collectors.toList());
    }
}

最后,我们需要处理 Keycloak 的注销。为此,我们添加了KeycloakLogoutHandler类:

@Component
public class KeycloakLogoutHandler implements LogoutHandler {

    private static final Logger logger = LoggerFactory.getLogger(KeycloakLogoutHandler.class);
    private final RestTemplate restTemplate;

    public KeycloakLogoutHandler(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, 
      Authentication auth) {
        logoutFromKeycloak((OidcUser) auth.getPrincipal());
    }

    private void logoutFromKeycloak(OidcUser user) {
        String endSessionEndpoint = user.getIssuer() + "/protocol/openid-connect/logout";
        UriComponentsBuilder builder = UriComponentsBuilder
          .fromUriString(endSessionEndpoint)
          .queryParam("id_token_hint", user.getIdToken().getTokenValue());

        ResponseEntity<String> logoutResponse = restTemplate.getForEntity(
        builder.toUriString(), String.class);
        if (logoutResponse.getStatusCode().is2xxSuccessful()) {
            logger.info("Successfulley logged out from Keycloak");
        } else {
            logger.error("Could not propagate logout to Keycloak");
        }
    }
}

KeycloakLogoutHandler类实现LogoutHandler类并向Keycloak发送注销请求。

上述的配置内容大量来源于:Keycloak 与 Spring Boot 结合使用的快速指南这篇文章。

以下这个类是根据真实的生产环境来具体整合的:

package cn.scysn.shared.authentication.infrastructure.primary;

import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.*;

import java.time.Duration;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import jakarta.annotation.Resource;
import org.keycloak.adapters.springsecurity.authentication.KeycloakLogoutHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import cn.scysn.shared.authentication.domain.Role;
import cn.scysn.shared.generation.domain.ExcludeFromGeneratedCodeCoverage;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
class SecurityConfiguration {

private static final int TIMEOUT = 2000;

private final ApplicationSecurityProperties applicationSecurityProperties;
private final CorsFilter corsFilter;
private final HandlerMappingIntrospector introspector;


@Value("${spring.security.oauth2.client.provider.keycloak.issuer-uri}")
private String issuerUri;

private static final String GROUPS = "groups";
private static final String REALM_ACCESS_CLAIM = "realm_access";
private static final String ROLES_CLAIM = "roles";

public SecurityConfiguration(
 CorsFilter corsFilter,
 ApplicationSecurityProperties applicationSecurityProperties,
 HandlerMappingIntrospector introspector
) {
 this.corsFilter = corsFilter;
 this.applicationSecurityProperties = applicationSecurityProperties;
 this.introspector = introspector;
}

@Bean
public SessionRegistry sessionRegistry() {
 return new SessionRegistryImpl();
}

@Bean
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
 return new RegisterSessionAuthenticationStrategy(sessionRegistry());
}

@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
 return new HttpSessionEventPublisher();

}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
 // @formatter:off
 return http
   .csrf(csrf -> csrf.disable())
   .addFilterBefore(corsFilter, CsrfFilter.class)
   .headers(headers -> headers
     .contentSecurityPolicy(csp -> csp.policyDirectives(applicationSecurityProperties.getContentSecurityPolicy()))
     .frameOptions(FrameOptionsConfig::sameOrigin)
     .referrerPolicy(referrer -> referrer.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
     .permissionsPolicy(permissions ->
       permissions.policy("camera=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=()"))
   )
   .authorizeHttpRequests(authz -> authz
     .requestMatchers(antMatcher(HttpMethod.OPTIONS, "/**")).permitAll()
     .requestMatchers(antMatcher("/app/**")).permitAll()
     .requestMatchers(antMatcher("/i18n/**")).permitAll()
     .requestMatchers(antMatcher("/content/**")).permitAll()
     .requestMatchers(antMatcher("/swagger-ui/**")).permitAll()
     .requestMatchers(antMatcher("/swagger-ui.html")).permitAll()
     .requestMatchers(antMatcher("/v3/api-docs/**")).permitAll()
     .requestMatchers(antMatcher("/test/**")).permitAll()
     .requestMatchers(new MvcRequestMatcher(introspector, "/api/authenticate")).permitAll()
     .requestMatchers(new MvcRequestMatcher(introspector, "/api/auth-info")).permitAll()
     .requestMatchers(new MvcRequestMatcher(introspector, "/api/admin/**")).hasAuthority(Role.ADMIN.key())
     .requestMatchers(new MvcRequestMatcher(introspector, "/api/**")).authenticated()
     .requestMatchers(new MvcRequestMatcher(introspector, "/management/health")).permitAll()
     .requestMatchers(new MvcRequestMatcher(introspector, "/management/health/**")).permitAll()
     .requestMatchers(new MvcRequestMatcher(introspector, "/management/info")).permitAll()
     .requestMatchers(new MvcRequestMatcher(introspector, "/management/prometheus")).permitAll()
     .requestMatchers(new MvcRequestMatcher(introspector, "/management/**")).hasAuthority(Role.ADMIN.key())
     .anyRequest().hasAnyAuthority(Role.USER.key())
   )
   .oauth2Login(withDefaults())
   .logout(logout -> logout.logoutSuccessUrl("/"))
   .oauth2ResourceServer(oauth2 -> oauth2
     .jwt(jwt -> jwt.jwtAuthenticationConverter(authenticationConverter()))
   )
   .oauth2Client(withDefaults())
   .build();
 // @formatter:on
}

Collection<GrantedAuthority> generateAuthoritiesFromClaim(Collection<String> roles) {
 return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(
   Collectors.toList());
}

private Converter<Jwt, AbstractAuthenticationToken> authenticationConverter() {
 JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
 jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new JwtGrantedAuthorityConverter());

 return jwtAuthenticationConverter;
}

@Bean
public GrantedAuthoritiesMapper userAuthoritiesMapperForKeycloak() {
 return authorities -> {
   Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
   var authority = authorities.iterator().next();
   boolean isOidc = authority instanceof OidcUserAuthority;

   if (isOidc) {
     OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority;
     OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();

     // Tokens can be configured to return roles under
     // Groups or REALM ACCESS hence have to check both
     if (userInfo.hasClaim(REALM_ACCESS_CLAIM)) {
       Map<String, Object> realmAccess = userInfo.getClaimAsMap(REALM_ACCESS_CLAIM);
       Collection<String> roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
       mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
     } else if (userInfo.hasClaim(GROUPS)) {
       Collection<String> roles = userInfo.getClaim(
         GROUPS);
       mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
     }
   } else {
     OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority) authority;
     Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
     if (userAttributes.containsKey(REALM_ACCESS_CLAIM)) {
       Map<String, Object> realmAccess = (Map<String, Object>) userAttributes.get(
         REALM_ACCESS_CLAIM);
       Collection<String> roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
       mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
     }
   }
   return mappedAuthorities;
 };
}

/**
   * Map authorities from "groups" or "roles" claim in ID Token.
   *
   * @return a {@link GrantedAuthoritiesMapper} that maps groups from the IdP to Spring Security Authorities.
   */
  @Bean
  public GrantedAuthoritiesMapper userAuthoritiesMapper() {
    return authorities -> {
      Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

      authorities.forEach(authority -> {
        // Check for OidcUserAuthority because Spring Security 5.2 returns
        // each scope as a GrantedAuthority, which we don't care about.
        if (authority instanceof OidcUserAuthority oidcUserAuthority) {
          mappedAuthorities.addAll(Claims.extractAuthorityFromClaims(oidcUserAuthority.getUserInfo().getClaims()));
        }
      });
      return mappedAuthorities;
    };
  }

  @Bean
  @ExcludeFromGeneratedCodeCoverage(reason = "Only called with a valid client registration repository")
  public JwtDecoder jwtDecoder(ClientRegistrationRepository clientRegistrationRepository, RestTemplateBuilder restTemplateBuilder) {
    NimbusJwtDecoder jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuerUri);

    OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(applicationSecurityProperties.getOauth2().getAudience());
    OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
    OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

    jwtDecoder.setJwtValidator(withAudience);
    jwtDecoder.setClaimSetConverter(
      new CustomClaimConverter(
        clientRegistrationRepository.findByRegistrationId("oidc"),
        restTemplateBuilder.setConnectTimeout(Duration.ofMillis(TIMEOUT)).setReadTimeout(Duration.ofMillis(TIMEOUT)).build()
      )
    );

    return jwtDecoder;
  }
}
6. 前端请求调用来具体整合

关于前端的具体整合请参考文章:

Keycloak 与 Spring Boot 结合使用的快速指南

Keycloak 授权服务

还有这篇keycloak官方的安全应用程序和服务指南

后端整合这篇文章已经讲解清楚,或者直接参考原文档来具体操作也很清晰。

;