使用 Keycloak 和 Spring Oauth2 保护 REST API

简介: Keycloak是开源身份和访问管理服务器,它是OAuth2和OpenID Connect (OIDC) 协议投诉。本文将解释如何使用Spring OAuth2库通过 Keycloak 保护Spring Boot REST API 。

Keycloak 文档提出了 3 种保护基于 Spring 的 REST APIS 的方法。
1,使用 Keycloak Spring Boot 适配器
2,使用 keycloak Spring 安全适配器
3,使用 OpenID Connect (OIDC)+ OAuth2
让我们看看如何使用 Keycloak OIDC 支持和 Spring OAuth2 库来保护 REST API。本文末尾解释了使用 Spring OAuth2 而不是 Keycloak Adapter 的好处。

让我们探索如何使用 Spring OAuth2 库设置 Keycloak 并与之交互。

第1步:开始使用 Keycloak
请参阅 Keycloak 入门文档以运行和设置 keycloak 管理员用户。
运行 Keycloak 后,使用http://localhost:8080/auth访问 keycloak 管理控制台
设置keycloak 用户名=admin,密码=admin。

第2步:创建开发领域
image.png

第3步:创建客户端(微服务)
Client ID : employee-service
Client Protocol : openid-connect

image.png

第4步:配置客户端
如果 Keycloak 在端口 8080 上运行,请确保您的微服务在另一个端口上运行。在示例中,微服务配置为在 8085 上运行。

Valid Redirect URIs : http://localhost:8085
Service Accounts Enabled : On
Authorization Enabled : On

注意:访问类型confidential支持使用client credentials grant以及authorization code grant来获取访问令牌。如果一个微服务需要调用另一个微服务,调用者将是“confidential”,而被调用者将是“bearer-only”

image.png

第5步:创建客户端角色

在客户端下创建一个角色。在这种情况下,角色 USER 是在员工服务下创建的。
image.png

第6步:创建一个映射器(Mapper)(在访问令牌中获取user_name)
Keycloak 访问令牌是JWT。它是一个JSON,该JSON中的每个字段都称为声明(claim)。默认情况下,登录用户名在访问令牌中名为“preferred_username”的声明中返回。Spring Security OAuth2 资源服务器需要名为“user_name”的声明中的用户名。因此,我们必须创建下面的映射器来将登录的用户名映射到名为 user_name 的新声明。
image.png

第7步:创建用户
image.png

第8步:将客户端角色映射到用户
为了提供对客户端(微服务)的访问,需要将相应的角色分配/映射到用户。
image.png

第9步:从 OpenID 配置端点获取配置
下面是 OpenID Connection 配置 URL,以获取有关所有安全端点的详细信息,
获取地址:http://localhost:8080/auth/realms/dev/.well-known/openid-configuration
要从响应中复制的重要 URL:
发行人(issuer):http://localhost:8080/auth/realms/dev
授权端点(authorization_endpoint): ${issuer}/protocol/openid-connect/auth
token端点(token_endpoint): ${issuer}/protocol/openid-connect/token

自我检查端点(token_introspection_endpoint): ${issuer}/protocol/openid-connect/token/introspect
用户信息端点(userinfo_endpoint): ${issuer}/protocol/openid-connect/userinfo
响应还包含支持的授权类型和范围
grant_types_supported: ["client_credentials", ...]
scopes_supported: ["openid", ...]

使用 Postman 获取访问令牌(用于测试)
选择授权类型为 OAuth 2.0,单击“获取新访问令牌”并输入以下详细信息。
image.png

确保在请求令牌时选择客户端身份验证作为“在正文中发送客户端凭据”。
回调 URL 是 Keycloak 中配置的重定向 URL。
客户端密码对您来说可能不同,从 keycloak 中的客户端配置复制一个。
您也可以使用https://jwt.io检查收到的令牌内容

第10步:创建一个 Spring Boot 应用程序
Spring Boot

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.0.1.RELEASE</version>
  <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.security.oauth.boot</groupId>
  <artifactId>spring-security-oauth2-autoconfigure</artifactId>
  <version>2.0.1.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

第11步:配置 application.properties

一般安全属性

# 可以设置为 false 以在本地开发期间禁用安全
rest.security.enabled=true
rest.security.api-matcher=/api/** 
rest.security.cors.allowed-origins=* 
rest.security.cors.allowed-headers=* 
rest.security.cors.allowed-methods=GET,POST,PUT,PATCH,DELETE,OPTIONS 
rest.security.cors.max-age=3600

使用 OAuth2 资源服务器保护 REST 端点的属性

rest.security.issuer-uri=http://localhost:8080/auth/realms/dev
security.oauth2.resource.id=employee-service
security.oauth2.resource.token-info-uri=${rest.security.issuer-uri}/protocol/openid-connect/token/introspect
security.oauth2.resource.user-info-uri=${rest.security.issuer-uri}/protocol/openid-connect/userinfo
security.oauth2.resource.jwt.key-value=-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhWOcKAVAwt+5FF/eE2hLaMVD5zQBBr+RLdc7HFUrlvU9Pm548rnD+zRTfOhnl5b6qMjtpLTRe3fG+8chjPwQriRyFKCzg7eYNxuR/2sK4okJbfQSZFs16TFhXtoQW5tWnzK6PqcB2Bpmy3x7QN78Hi04CjNrPz2BX8U+5BYMavYJANpp4XzPE8fZxlROmSSyNeyJdW30rJ/hsWZJ5nnxSZ685eT4IIUHM4g+sQQTZxnCUnazNXng5B5yZz/sh+9GOXDGT286fWdGbhGKU8oujjSJLOHYewFZX5Jw8aMrKKspL/6glRLSiV8FlEHbeRWxFffjZs/D+e9A56XuRJSQ9QIDAQAB\n-----END PUBLIC KEY-----

image.png
注意1: security.oauth2.resource.jwt.key-value 属性值可以从领域级别的公钥复制。这非常重要,这个属性是使用JwtAccessTokenCustomizer的,我们稍后会看到。
注意2:属性值会根据您的配置而有所不同,应注意使用正确的值。
调用另一个微服务的属性(服务帐户)

#如果这个微服务需要调用另一个
#secured micro-service
 security.oauth2.client.client-id=employee-service
security.oauth2.client.client-secret=68977d81-c59b-49aa-aada-58da9a43a850
security.oauth2.client.user-authorization-uri=${rest.security.issuer-uri}/protocol/openid-connect/auth
security.oauth2.client.access-token-uri=${rest.security.issuer-uri}/protocol/openid-connect/token
security.oauth2.client.scope=openid 
security.oauth2.client.grant-type=client_credentials

注意:用于进行安全服务帐户调用的OAuth2RestTemplate需要上述属性。

第12步:JWT 访问令牌定制器

为了让 Spring OAuth2 解析和设置 SecurityConextHolder,它需要来自 token 的角色或权限。此外,为了确定用户有权访问的客户端/应用程序/微服务列表,它需要来自令牌的客户端 ID 列表。这是唯一需要一些特殊处理的设置。

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.resource.JwtAccessTokenConverterConfigurer;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

public class JwtAccessTokenCustomizer extends DefaultAccessTokenConverter 
                                      implements JwtAccessTokenConverterConfigurer {

  private static final Logger LOG = LoggerFactory.getLogger(JwtAccessTokenCustomizer.class);

  private static final String CLIENT_NAME_ELEMENT_IN_JWT = "resource_access";

  private static final String ROLE_ELEMENT_IN_JWT = "roles";

  private ObjectMapper mapper;

  public JwtAccessTokenCustomizer(ObjectMapper mapper) {
    this.mapper = mapper;
    LOG.info("Initialized {}", JwtAccessTokenCustomizer.class.getSimpleName());
  }

  @Override
  public void configure(JwtAccessTokenConverter converter) {
    converter.setAccessTokenConverter(this);
    LOG.info("Configured {}", JwtAccessTokenConverter.class.getSimpleName());
  }

  /**
   * Spring oauth2 expects roles under authorities element in tokenMap, 
   * but keycloak provides it under resource_access. Hence extractAuthentication
   * method is overriden to extract roles from resource_access.
   *
   * @return OAuth2Authentication with authorities for given application
   */
  @Override
  public OAuth2Authentication extractAuthentication(Map<String, ?> tokenMap) {
    LOG.debug("Begin extractAuthentication: tokenMap = {}", tokenMap);
    JsonNode token = mapper.convertValue(tokenMap, JsonNode.class);
    Set<String> audienceList = extractClients(token); // extracting client names
    List<GrantedAuthority> authorities = extractRoles(token); // extracting client roles

    OAuth2Authentication authentication = super.extractAuthentication(tokenMap);
    OAuth2Request oAuth2Request = authentication.getOAuth2Request();

    OAuth2Request request =
        new OAuth2Request(oAuth2Request.getRequestParameters(), 
            oAuth2Request.getClientId(), 
            authorities, true, 
            oAuth2Request.getScope(),
            audienceList, null, null, null);

    Authentication usernamePasswordAuthentication = 
            new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), 
            "N/A", authorities);
            
    LOG.debug("End extractAuthentication");
    return new OAuth2Authentication(request, usernamePasswordAuthentication);
  }

  private List<GrantedAuthority> extractRoles(JsonNode jwt) {
    LOG.debug("Begin extractRoles: jwt = {}", jwt);
    Set<String> rolesWithPrefix = new HashSet<>();

    jwt.path(CLIENT_NAME_ELEMENT_IN_JWT)
        .elements()
        .forEachRemaining(e -> e.path(ROLE_ELEMENT_IN_JWT)
            .elements()
            .forEachRemaining(r -> rolesWithPrefix.add("ROLE_" + r.asText())));

    final List<GrantedAuthority> authorityList = 
           AuthorityUtils.createAuthorityList(rolesWithPrefix.toArray(new String[0]));
           
    LOG.debug("End extractRoles: roles = {}", authorityList);
    return authorityList;
  }

  private Set<String> extractClients(JsonNode jwt) {
    LOG.debug("Begin extractClients: jwt = {}", jwt);
    if (jwt.has(CLIENT_NAME_ELEMENT_IN_JWT)) {
      JsonNode resourceAccessJsonNode = jwt.path(CLIENT_NAME_ELEMENT_IN_JWT);
      final Set<String> clientNames = new HashSet<>();
      resourceAccessJsonNode.fieldNames()
          .forEachRemaining(clientNames::add);

      LOG.debug("End extractClients: clients = {}", clientNames);
      return clientNames;

    } else {
      throw new IllegalArgumentException("Expected element " + 
                CLIENT_NAME_ELEMENT_IN_JWT + " not found in token");
    }
  }
}

第13步:OAuth2 资源服务器配置器

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
@EnableWebSecurity
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
@ConditionalOnProperty(prefix = "rest.security", value = "enabled", havingValue = "true")
@Import({SecurityProperties.class})
public class SecurityConfigurer extends ResourceServerConfigurerAdapter {

  @Autowired
  private ResourceServerProperties resourceServerProperties;

  @Autowired
  private SecurityProperties securityProperties;

  @Override
  public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    resources.resourceId(resourceServerProperties.getResourceId());
  }


  @Override
  public void configure(final HttpSecurity http) throws Exception {

    http.cors()
        .configurationSource(corsConfigurationSource())
        .and()
        .headers()
        .frameOptions()
        .disable()
        .and()
        .csrf()
        .disable()
        .authorizeRequests()
        .antMatchers(securityProperties.getApiMatcher())
        .authenticated();

  }

  @Bean
  public CorsConfigurationSource corsConfigurationSource() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    if (null != securityProperties.getCorsConfiguration()) {
      source.registerCorsConfiguration("/**", securityProperties.getCorsConfiguration());
    }
    return source;
  }

  @Bean
  public JwtAccessTokenCustomizer jwtAccessTokenCustomizer(ObjectMapper mapper) {
    return new JwtAccessTokenCustomizer(mapper);
  }

  @Bean
  public OAuth2RestTemplate oauth2RestTemplate(OAuth2ProtectedResourceDetails details) {
    OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(details);

    //Prepare by getting access token once
    oAuth2RestTemplate.getAccessToken();
    return oAuth2RestTemplate;
  }
}

注意:如果该微服务需要调用其他微服务,则需要 OAuth2RestTemplate。

第14步:保护 REST 端点

PreAuthorize注释用于保护具有适当角色的 REST 端点。参考下面的例子。

import org.arun.springoauth.config.SecurityContextUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/employees")
public class EmployeeRestController {

  @GetMapping(path = "/username")
  @PreAuthorize("hasAnyAuthority('ROLE_USER')")
  public ResponseEntity<String> getAuthorizedUserName() {
    return ResponseEntity.ok(SecurityContextUtils.getUserName());
  }

  @GetMapping(path = "/roles")
  @PreAuthorize("hasAnyAuthority('ROLE_USER')")
  public ResponseEntity<Set<String>> getAuthorizedUserRoles() {
    return ResponseEntity.ok(SecurityContextUtils.getUserRoles());
  }
}

第15步:如果不需要,请禁用基本身份验证
为了禁用默认安全性,可以排除SecurityAutoConfiguration和UserDetailsS​​erviceAutoConfiguration。

@SpringBootApplication(exclude = {SecurityAutoConfiguration.class,
                         UserDetailsServiceAutoConfiguration.class})
public class Startup {

  public static void main(String[] args) {
    SpringApplication.run(Startup.class, args);
  }
}

在 Keycloak 适配器上使用 Spring OAuth2 的好处:
1,大多数情况下,Keycloak Server 升级也需要升级 Keycloak 适配器。如果在 100 多个微服务中使用 Keycloak 适配器,那么所有这些微服务都需要升级以使用更新版本的 Keycloak 适配器。这需要对所有微服务进行回归测试,而且非常耗时。这可以通过使用在协议级别与 Keycloak 集成的 Spring OAuth2 来避免。由于这些协议不会经常更改,并且 Keycloak Server 升级将继续支持相应的协议版本,因此 Keycloak Server 升级时,微服务中不需要更改。
2,有时,Spring Boot 版本升级需要升级 Keycloak 和 Keycloak Spring Boot 适配器或 Keycloak Spring Security 适配器。如果我们使用 Spring OAuth2,则可以避免这种情况。
3,如果组织决定从 Keycloak 迁移到另一个 OAuth2 OpenID Authx Provider,则所有使用 Keycloak Spring Boot 适配器和 Keyclock Spring Security 的微服务都需要重构。使用 Spring OAuth2 + Spring Security 可以显着简化迁移。
4,Spring OAuth2 中可用的OAuth2RestTemplate类负责使用 OAuth2Context 根据需要刷新和缓存访问令牌。当需要在微服务之间安全交互时,这是一个很大的好处。

目录
相关文章
|
24天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
64 2
|
2月前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
52 4
|
2月前
|
IDE Java API
基于Spring Boot REST API设计指南
【10月更文挑战第4天】 在现代的软件开发中,RESTful API已经成为了构建网络应用的标准之一。它通过HTTP协议提供了与资源交互的方式,使得不同的应用程序能够进行数据交互。Spring Boot作为一个功能强大的框架,它简化了配置和开发流程,成为了构建RESTful API的理想选择。本文将详细介绍如何在Spring Boot中设计和实现高质量的RESTful API,并提供一些最佳实践。
52 1
|
2月前
|
缓存 Java API
基于Spring Boot REST API设计指南
【10月更文挑战第11天】 在构建现代Web应用程序时,RESTful API已成为一种标准,使得不同的应用程序能够通过HTTP协议进行通信,实现资源的创建、读取、更新和删除等操作。Spring Boot作为一个功能强大的框架,能够轻松创建RESTful API。本文将详细介绍如何在Spring Boot中设计和实现高质量的RESTful API。
131 61
|
14天前
|
Java 测试技术 API
详解Swagger:Spring Boot中的API文档生成与测试工具
详解Swagger:Spring Boot中的API文档生成与测试工具
29 4
|
1月前
|
缓存 API 网络架构
掌握现代API开发:GraphQL vs REST
【10月更文挑战第24天】本文深入探讨了现代API开发中两种主流技术——GraphQL和REST的设计理念、技术特点及实际开发中的对比分析。GraphQL通过声明式数据请求和强类型系统提供更高的灵活性和性能,而REST则以其无状态特性和成熟的生态系统见长。文章还讨论了两者在客户端-服务器交互、安全性和工具支持方面的优劣,帮助开发者根据项目需求做出明智选择。
|
18天前
|
人工智能 Java API
Spring AI Fluent API:与AI模型通信的流畅体验
【11月更文挑战第24天】随着人工智能(AI)技术的飞速发展,越来越多的应用场景开始融入AI技术以提升用户体验和系统效率。在Java开发中,与AI模型通信成为了一个重要而常见的需求。为了满足这一需求,Spring AI引入了ChatClient,一个提供流畅API(Fluent API)的客户端,用于与各种AI模型进行通信。本文将深入探讨ChatClient的底层原理、业务场景、概念、功能点,并通过Java代码示例展示如何使用Fluent API与AI模型进行通信。
32 0
|
2月前
|
安全 Java API
基于Spring Boot REST API设计指南
【10月更文挑战第10天】 在现代Web应用开发中,RESTful API扮演着至关重要的角色。Spring Boot作为一个高效、便捷的Java开发框架,为构建RESTful API提供了强大的支持。本文将分享基于Spring Boot的REST API设计指南,涵盖从项目初始化到API文档配置的全过程。
53 0
|
2月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
197 2
|
17天前
|
缓存 IDE Java
SpringBoot入门(7)- 配置热部署devtools工具
SpringBoot入门(7)- 配置热部署devtools工具
35 1
SpringBoot入门(7)- 配置热部署devtools工具