Advertisement

Securing your Microservices with OAuth2 and Keycloak

阅读量:

作者:禅与计算机程序设计艺术

1.简介

在互联网应用日益普及的背景下,越来越多的企业开始采用微服务架构进行应用开发。微服务架构具有良好的弹性、可扩展性和灵活性,能够轻松应对复杂业务场景,同时也降低了系统集成和部署的难度。然而,微服务架构带来的另一个挑战是安全问题。由于微服务通常运行在独立的容器环境中,因此它们之间缺乏有效的安全认证机制。为了解决这一问题,许多企业都在探索基于OAuth2和OpenID Connect协议的微服务安全解决方案。

OAuth2协议及其OIDC实现是当前最流行的授权方案之一。OAuth2协议作为一个开放且灵活的授权机制,赋予第三方应用访问用户信息的权利。OpenID Connect协议基于OAuth2框架构建,为客户端提供了便捷的身份验证功能。OAuth2协议不仅增强了API的安全性,还支持多种客户端类型,如Web应用、移动应用和桌面应用,实现基于凭证的访问管理,无需泄露敏感信息。而Keycloak是一款开源的Identity and Access Management(IAM)服务器,支持OAuth2和OpenID Connect协议,集成了多样的身份认证和权限管理功能。本文将深入探讨如何通过Keycloak实现微服务的的身份认证与安全访问管理。

2.基本概念术语说明

2.1 OAuth2协议

OAuth2作为一个开放标准,定义了客户端如何授权获取资源所有者权限,以及资源所有者如何通过令牌实现访问权限的授权获取。OAuth2允许客户端应用程序请求指定用户被委派的特定权限,无需泄露用户账号和密码。OAuth2基于四个关键角色参与者:客户端、资源所有者、授权服务提供者和令牌验证者。

  • Resource Owner(RS): 受保护资源的访问主体,通常代表第三方应用或网站,负责向授权服务器提出授权请求。
  • Client(CA): 客户端程序或网站,作为授权请求方,负责申请访问受保护资源的权限。
  • Authorization Server(AS): OAuth2标准中的核心组件,负责颁发授权令牌给客户端,并验证资源所有权方是否已授予相应权限。
  • Resource Server(RS): 存储和管理受保护资源的服务器,负责处理客户端的授权请求,并返回相应的响应数据或资源内容。

OAuth2协议工作流程如下图所示:

Resource Owner向Authorization Server提出授权请求,详细说明需要访问的资源类型以及授权的范围和时间限制。若Resource Owner获得授权,Authorization Server将向Client应用发放一个授权码token。Client应用向资源服务器发送请求,携带授权码token,以获取受保护的资源。资源服务器验证token的有效性后,确认Client应用拥有访问受保护资源的权限,并返回相关资源数据。

2.2 OpenID Connect协议

OpenID Connect方案遵循OAuth2协议,作为一种身份认证协议,在身份验证层面提供了功能模块,实现了用户个人信息的管理,包括名字、邮箱、头像等。该方案通过提供身份验证相关的功能模块,实现了用户个人信息的管理,包括名字、邮箱、头像等。该方案采用了两种身份认证方式,分别为Implicit Flow和Hybrid Flow。

在Implicit Flow模式下,客户端应用被设计为直接向Authorization Server提交请求,从而无需经过用户界面的交互,直接返回授权码token。

在Hybrid Flow模式下,Client应用首先向Authorization Server发出认证请求。当用户已登录时,Authorization Server将返回授权码token。否则,Client应用将跳转至认证服务器进行登录操作,成功登录后将返回授权码token。

2.3 Keycloak架构

Keycloak是一款开源的的身份管理(IAM)服务器,采用Java语言开发,支持核心协议包括OAuth2和OpenID Connect。该系统由服务器和客户端库两部分组成,其中服务器通常部署于云环境或物理服务器上,负责为用户和资源提供身份认证与权限控制服务。客户端库则为应用开发者提供了相关的接口和方法,简化了Keycloak的集成使用。当Keycloak服务器完成安装后,系统会自动创建一个默认管理员账户和初始密码,用户可通过浏览器访问[http://localhost:8080/auth/admin]页面进行登录,从而开始管理Keycloak服务器。以下是Keycloak服务器的主要组件:[http://localhost:8080/auth/admin,登录后就可以管理Keycloak服务器了。以下是Keycloak服务器的主要模块:

该模块负责完成身份验证任务,并赋予用户访问受保护资源的权限。该系统支持多种认证方案,包括基于用户名密码的认证、通过手机短信验证码进行的身份验证、多因素认证方案以及基于社会化的身份验证方式。

User Federation (用户联合模块):该模块旨在管理异构系统环境中的用户信息,涵盖LDAP、Active Directory、SAML2和OAuth2等标准。Keycloak则支持一个管理界面,便于管理员进行多种联合源配置管理。

该模块支持用户信息的增删改查操作,包括密码修改功能,同时提供用户状态设置和账号锁定功能。

Session Management模块(登录管理功能模块): 该模块负责用户登录、注销、记住我等功能的实现。

Identity Providers模块(Identity Providers module):该模块主要管理外部身份认证服务,包括但不限于Google、Facebook、GitHub、Twitter、OIDC、UMA等。

3.核心算法原理和具体操作步骤以及数学公式讲解

3.1 注册Keycloak服务器

首先,建议下载并安装Keycloak服务器。您可以在官方网站[https://www.keycloak.org/downloads/提供适合您的版本,并根据安装说明进行安装。

安装完毕后,登录时,浏览器会引导您输入[http://localhost:8080/auth/,系统将要求您输入管理员用户名和密码,完成登录后,您将进入Keycloak管理控制台页面,如图所示:

3.2 创建Realm

在生成Realm页面时,您可以通过填写Realm名称、显示名称等信息,然后启动'Create'按钮来完成创建。创建完成后,您将看到以下页面:

单击左侧菜单中的“Clients”选项卡,生成一个客户端,选择类型为openid-connect,并输入客户端ID和客户端Secret,如图所示。

3.3 配置客户端属性

点击已创建的客户端,用户可查看其详细配置信息,涵盖名称字段、描述字段、访问策略配置、身份认证配置、默认角色设置、主题配置、账号后端配置及属性映射设置。

3.3.1 设置客户端访问策略

访问策略分为两种模式:白名单模式和黑名单模式。在白名单模式下,仅限于特定的IP地址、网络段和域名能够访问客户端,而黑名单模式下,不包括这些IP地址、网络段和域名的所有其他网络资源都能够访问客户端。建议采用白名单策略,因为这种模式能够有效防止策略被滥用。

点击“Access Type”旁边的设置按钮,切换到白名单模式:

3.3.2 设置客户端身份认证

身份认证可采用不同的方式,涵盖Header或Cookie进行身份验证、基于JWT的Bearer Token认证、客户端ID和密钥认证,以及其他方式等。建议采用客户端ID和密钥认证,从而,客户端能够获取相应的Token。

点击“Credentials”旁边的设置按钮,切换到客户端ID和密钥认证:

3.3.3 设置默认角色

默认角色即为用户自动分配的角色,无需用户进行显式的授权设置。建议为客户端创建一个角色,并勾选“Composite Roles”,因为这将允许客户端拥有多个角色权限。

点击“Roles”旁边的设置按钮,添加一个默认角色:

3.3.4 设置属性映射

属性映射机制是指第三方系统的用户属性与Keycloak平台用户属性之间的对应关系。建议优先采用默认配置方案,以确保属性映射的高效性和兼容性。

点击“Attributes”旁边的设置按钮,切换到属性映射:

3.4 配置角色和组

单击左侧菜单中的Roles选项卡,支持创建角色、编辑角色以及删除角色等操作。在创建角色时,允许使用父子关系,并赋予相关权限。

单击左侧菜单中的Groups选项卡,可以执行创建、编辑和删除组等基本操作。当创建一个组时,可以为该组添加成员和角色,并设置组的可见性状态。

3.5 生成访问令牌

Clients

点击左侧菜单中的【 Clients

右键点击屏幕右上方的“Credential选项卡”,进入该选项卡后即可查看“Client ID”和“Client Secret”这两个重要信息。其中,“Client ID”对应的是客户端的唯一标识符,“Client Secret”则是客户端的密钥,这些信息必须严格保密,不能泄露给任何外部人员。

通过Postman工具,可以测试Token的获取流程。单击‘Authorization’标签页中的‘Basic Auth’选项,输入对应的‘Client ID’和‘Client Secret’字段,单击生成新的访问令牌的按钮。

获取到Access Token后,就可以调用受保护资源了。

3.6 使用JavaScript访问受保护资源

受保护资源可以被JavaScript、Java、Python、Go语言等多种语言访问。以下是如何通过JavaScript访问资源的示例代码:

复制代码
    // 请求参数设置
    let token = "eyJh..."; // 从服务端获取到的Token值
    let url = "http://localhost:8081/api/resource";
    let headers = {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${token}` // 将Token放在Authorization字段中
    };
    
    // 发起GET请求
    fetch(url, {method: 'GET', headers})
     .then((response) => response.json())
     .then((data) => console.log("Response data:", data))
     .catch((error) => console.error("Error:", error));
    
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

4.具体代码实例和解释说明

4.1 安装Keycloak服务器

假设您已经按照官网文档下载并安装了Keycloak服务器。

4.2 创建Realm

登录至Keycloak管理控制台,单击左侧菜单中的"Realms"选项卡,随后点击"Create Realm"按钮。在弹出的界面中,依次填写Realm名称、显示名称、Timezone和Locale等必要的配置信息,最后点击"Create"按钮以完成创建。创建完成后,您将看到以下页面:

4.3 创建客户端

在左侧菜单中,点击Clients选项卡,随后单击“Create”按钮。接着,依次输入客户端ID、名称、Root URL、Access Type、Valid Redirect URIs等信息,最后,选择“openid-connect”作为客户端类型。

配置客户端访问策略:在"Access Type"选择框下拉列表中,采用白名单策略进行配置,分别添加客户端允许访问的IP地址、网络段或域名。

配置客户端身份认证流程:在"Client authentication"选项卡的下拉列表中,选择客户端ID和密钥认证项,并输入客户端密钥信息。

进行默认角色设置:单击“Role Mappings”标签页,选中复选框以完成默认角色配置。

设置属性映射:点击“Attributes”标签页,保持默认属性映射即可。

创建完成后,将看到如下页面:

4.4 添加角色和组

单击左侧菜单中的“Roles”选项卡,单击“Create”按钮,输入角色名称,接着单击“Save”按钮。创建完成后,您将看到以下页面:

单击左侧菜单中的Groups选项卡,单击“Create”按钮,输入组名称,然后单击“Save”按钮。创建完成后,您将看到以下页面:

4.5 获取访问令牌

访问Keycloak管理控制台界面,单击左侧导航栏中的Clients选项卡,随后定位到之前创建的客户端,找到页面底部的Credentials按钮,点击它,将看到如下页面内容。

点击“View Details”按钮,将会出现如下信息:

  • 客户端ID:即为之前创建的客户端ID。
  • 密钥:该客户端对应的密钥。
  • 生成时间:具体说明该密钥的生成时间。
  • 有效期限:详细描述该密钥的有效期限。

点击“Revoke”按钮,即可吊销该密钥。

假设我们已经获取到有效的密钥。

4.6 测试访问受保护资源

4.6.1 Java Web服务端

4.6.1.1 创建Maven项目

创建一个名为java-web-service的Maven项目。

4.6.1.2 添加依赖

pom.xml文件中添加以下依赖:

复制代码
    <dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-core</artifactId>
    <version>${keycloack.version}</version>
    </dependency>
    
      
      
      
      
    
    代码解读

其中,${keycloack.version}替换为您实际使用的Keycloak版本。

4.6.1.3 配置Realm和主题

在src/main/resources目录中,建立一个名为keycloak.json的文件,并在此处详细说明内容。

复制代码
    {
       "realm": "${your-realm}", 
       "auth-server-url": "http://${host}:${port}/auth/", 
       "ssl-required": "external" 
    }
    
      
      
      
      
    
    代码解读

其中,{your-realm}替换为您实际使用的Realm名称,{host}替换为Keycloak服务器的主机名或IP地址,${port}替换为服务器端的口号。

在src/main/webapp目录下,还需搭建一个名为theme的文件夹。并在此处添加两个HTML文件:kc_header.ftl和kc_footer.ftl。

4.6.1.4 编写Java代码

编写Java代码,连接到Keycloak服务器,获取Token,并调用受保护资源。

首先,加载配置文件:

复制代码
    public static final String KEYCLOAK_CONFIG_FILE = "keycloak.json"; 
    
    @Produces
    @Named("KeycloakConfig")
    public KeycloakDeployment getKeycloakDeployment() throws IOException { 
    return KeycloakDeploymentBuilder.build(KEYCLOAK_CONFIG_FILE); 
    }
    
      
      
      
      
      
      
    
    代码解读

接着,创建Keycloak对象,连接到服务器:

复制代码
    public static final String REALM_NAME = "demo";  
    
    private static final String AUTHENTICATION_METHOD = "bearer-only";  
    private static final int TOKEN_EXPIRE_TIME = 180;  
    
    @Inject
    @Named("KeycloakConfig")
    protected KeycloakDeployment keycloakDeployment;  
    
    @Produces @RequestScoped
    @Named("KeycloakContext")  
    public KeycloakInstance getKeycloak() {  
    Keycloak keycloak = KeycloakPrincipal.instance().getKeycloakSessionFactory().create().getKeycloak();  
    keycloak.init(keycloakDeployment);  
    return keycloak;  
    }  
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

再者,编写Java代码,获取Token,并调用受保护资源。

复制代码
    @Path("/api/resource")  
    public class MyProtectedService {  
    private static final Logger LOGGER = LoggerFactory.getLogger(MyProtectedService.class);  
    
    @Inject
    @Named("KeycloakContext")  
    protected KeycloakInstance keycloak;  
    
    /** * 检测是否有访问受保护资源的权限
     */
    @GET
    public Response test(@QueryParam("username") String username) {
        try {
            JWTCredential credential = new JWTAuthzClient(
                    keycloak).obtainAccessToken(AUTHENTICATION_METHOD,
                            TOKEN_EXPIRE_TIME);
    
            if (!checkPermissions(credential, username)) {
                return Response
                       .status(Response.Status.FORBIDDEN)
                       .entity("{\"message\":\"You don't have permission for access the resource.\"}")
                       .type(MediaType.APPLICATION_JSON)
                       .build();
            }
    
            JSONObject jsonObj = new JSONObject();
            jsonObj.put("result", "success");
    
            return Response
                   .ok(jsonObj.toString(), MediaType.APPLICATION_JSON)
                   .build();
    
        } catch (Exception e) {
            LOGGER.error("test failed.", e);
            return Response
                   .status(Response.Status.INTERNAL_SERVER_ERROR)
                   .entity("{\"message\":\"Internal server error.\", \"exceptionMessage\":\"" + e.getMessage() + "\"}")
                   .type(MediaType.APPLICATION_JSON)
                   .build();
        }
    }
    
    /** * 检测当前用户是否有访问指定用户的权限
     */
    private boolean checkPermissions(JWTCredential jwtCredential,
                                    String targetUsername) throws ParseException,
            KeycloakSecurityContextNotAvailableException {
    
        Set<String> requiredGroups = getRequiredGroupsByTargetUser(jwtCredential,
                                                                     targetUsername);
    
        JWTClaimsSet claimsSet = jwtCredential.getToken().getOtherClaims();
    
        List<String> groups = JsonSerialization.asList(claimsSet.getStringClaim("groups"), String.class);
    
        for (String group : groups) {
            if (requiredGroups.contains(group)) {
                return true;
            }
        }
    
        return false;
    }
    
    /** * 根据目标用户的用户名,获取对应的需要访问的组
     */
    private Set<String> getRequiredGroupsByTargetUser(JWTCredential jwtCredential,
                                                      String targetUsername) throws ParseException,
            KeycloakSecurityContextNotAvailableException {
    
        Set<String> result = new HashSet<>();
    
        JWTClaimsSet claimsSet = jwtCredential.getToken().getOtherClaims();
    
        Object obj = JsonSerialization.readValue(claimsSet.getStringClaim("permissions"), Permission[].class);
        Permission[] permissions = (Permission[]) obj;
    
        for (Permission p : permissions) {
            if (targetUsername.equals(p.getUsername())) {
                result.addAll(Arrays.asList(p.getGroups()));
            }
        }
    
        return result;
    }
    
    }
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

以上是Java Web服务端的完整代码。

4.6.2 JavaScript客户端

4.6.2.1 安装依赖包

使用npm命令安装axios和keycloak-js包:

复制代码
    npm install axios keycloak-js --save
    
    
    代码解读
4.6.2.2 配置配置文件

创建名为config.js的文件,并添加以下内容:

复制代码
    module.exports = {
      realm: "${your-realm}", 
      clientId: "${your-client-id}"
    }
    
      
      
      
    
    代码解读

其中,请将您的Realm名称替换成实际使用的名称,客户端ID替换成您创建的客户端ID。

4.6.2.3 编写JavaScript代码

编写JavaScript代码用于连接到Keycloak服务器,与之建立连接,获取相应的Token,并调用受保护的资源。

首先,加载配置文件:

复制代码
    const config = require('./config');
    
    const keycloakUrl = `${window.location.protocol}//${window.location.hostname}:8080/auth`;
    
    const keycloakRealm = config.realm ||'master';
    const keycloakClientId = config.clientId || '';
    
      
      
      
      
      
    
    代码解读

接着,启动Keycloak客户端,连接到服务器:

复制代码
    var keycloak = Keycloak({
      url: keycloakUrl,
      realm: keycloakRealm,
      clientId: keycloakClientId
    });
    
    keycloak.init({ onLoad: 'check-sso' })
     .success(() => {
          console.info('Authenticated.');
    
          const accessToken = keycloak.token;
    
          axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
    
          // call protected resources here...
      })
     .error(() => {
          console.warn('Failed to authenticate.');
      });
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

最后,调用受保护资源:

复制代码
    axios.get('/api/resource?username=${user-name}')
     .then((response) => {
          console.log(response.data);
      })
     .catch((error) => {
          console.error(`Failed to fetch data: ${error}`);
      });
    
      
      
      
      
      
      
    
    代码解读

以上是JavaScript客户端的完整代码。

全部评论 (0)

还没有任何评论哟~