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客户端的完整代码。
