当前位置:

首页 > 编程开发 > Azure AD 令牌刷新机制全解析

Azure AD 令牌刷新机制全解析

本文详细阐述了在JavaWeb应用中集成Microsoft账户登录后,如何处理AzureAD访问令牌过期的问题。当现有访问令牌失效时,不能直接使用刷新令牌进行API调用。文章提供了一种通过直接调用AzureADOAuth2.0令牌端点,利用刷新令牌获取新访问令牌的解决方案,并指导如何将新令牌集成到GraphServiceClient中,同时强调了相关的安全和实现注意事项。

Azure AD 访问令牌刷新机制详解

本文详细阐述了在Java Web应用中集成Microsoft账户登录后,如何处理Azure AD访问令牌过期的问题。当现有访问令牌失效时,不能直接使用刷新令牌进行API调用。文章提供了一种通过直接调用Azure AD OAuth2.0令牌端点,利用刷新令牌获取新访问令牌的解决方案,并指导如何将新令牌集成到GraphServiceClient中,同时强调了相关的安全和实现注意事项。

访问令牌的生命周期与挑战

在开发与Microsoft Graph或其他Azure AD保护资源交互的Web应用程序时,用户通常会通过OAuth 2.0流程进行认证,获取到访问令牌(Access Token)和刷新令牌(Refresh Token)。访问令牌用于授权对受保护资源的访问,但它们具有有限的生命周期(通常为1小时)。当访问令牌过期后,应用程序需要一种机制来获取新的访问令牌,以维持用户会话并继续访问资源,而无需用户重新登录。

例如,在使用Azure SDK for Java或Microsoft Graph SDK时,我们可能会通过TokenCredential来提供访问令牌:

final TokenCredential tokenCredential = request -> {
    // account.getTokenExpiry() 和 account.getAccessToken() 应该动态更新
    final OffsetDateTime offset = OffsetDateTime.ofInstant(account.getTokenExpiry().toInstant(), ZoneId.systemDefault());
    final AccessToken token = new AccessToken(account.getAccessToken(), offset);
    return Mono.create(sink -> sink.success(token));
};

final TokenCredentialAuthProvider tokenCredentialAuthProvider = 
  new TokenCredentialAuthProvider(tokenCredential);

this.graphServiceClient =
    GraphServiceClient
        .builder()
        .authenticationProvider(tokenCredentialAuthProvider)
        .buildClient();

上述代码片段展示了如何使用一个TokenCredential来为GraphServiceClient提供访问令牌。然而,当account.getAccessToken()中的令牌过期时,TokenCredentialAuthProvider本身并不会自动触发令牌刷新。此时,直接使用已过期的访问令牌进行API调用将导致认证失败。因此,我们需要主动地使用刷新令牌来获取新的访问令牌。

解决方案:通过刷新令牌获取新访问令牌

OAuth 2.0协议提供了“刷新令牌(refresh_token)”授权类型,允许客户端在访问令牌过期后,使用刷新令牌向授权服务器请求新的访问令牌。这个过程通常在后台进行,对用户透明。

刷新令牌的API调用

为了获取新的访问令牌,我们需要向Azure AD的OAuth 2.0令牌端点发起一个POST请求。这个端点是:https://login.microsoftonline.com/common/oauth2/v2.0/token。

请求需要包含以下参数:

  • client_id: 您的应用程序注册时获得的客户端ID。
  • grant_type: 必须设置为refresh_token。
  • redirect_uri: 您的应用程序注册时配置的重定向URI。
  • scope: 您希望请求的权限范围。通常与原始授权请求中的范围相同或更小。
  • refresh_token: 用户登录时获取到的刷新令牌。
  • client_secret: 您的应用程序注册时生成的客户端密钥。

以下是一个使用Spring RestTemplate在Java中执行此操作的示例:

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.ZoneId;

public class AzureAdTokenRefresher {

    private final RestTemplate restTemplate;
    private final ObjectMapper objectMapper;

    public AzureAdTokenRefresher() {
        this.restTemplate = new RestTemplate();
        this.objectMapper = new ObjectMapper();
    }

    /**
     * 使用刷新令牌获取新的访问令牌。
     * @param refreshToken 用户的刷新令牌
     * @return 包含新访问令牌、刷新令牌(如果返回)和过期时间等信息的JSON字符串
     * @throws IOException 如果JSON解析失败
     */
    public TokenResponse refreshAccessToken(String refreshToken) throws IOException {
        String url = "https://login.microsoftonline.com/common/oauth2/v2.0/token";

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        // TODO: 这些值应从配置文件或环境变量中安全读取
        MultiValueMap map= new LinkedMultiValueMap<>();
        map.add("client_id", "your_client_id"); // 替换为您的应用客户端ID
        map.add("grant_type", "refresh_token");
        map.add("redirect_uri", "http://localhost:5000/login/oauth2/code/microsoft"); // 替换为您的重定向URI
        map.add("scope", "openid profile offline_access User.Read Mail.Read"); // 替换为您的权限范围
        map.add("refresh_token", refreshToken);
        map.add("client_secret", "your_client_secret"); // 替换为您的应用客户端密钥

        HttpEntity> request = new HttpEntity<>(map, headers);

        ResponseEntity response = restTemplate.postForEntity(url, request, String.class);

        if (response.getStatusCode().is2xxSuccessful()) {
            String responseBody = response.getBody();
            JsonNode rootNode = objectMapper.readTree(responseBody);

            String newAccessToken = rootNode.get("access_token").asText();
            String newRefreshToken = rootNode.has("refresh_token") ? rootNode.get("refresh_token").asText() : refreshToken;
            long expiresInSeconds = rootNode.get("expires_in").asLong();

            // 计算新的过期时间
            OffsetDateTime expiryTime = OffsetDateTime.now(ZoneId.systemDefault()).plusSeconds(expiresInSeconds);

            return new TokenResponse(newAccessToken, newRefreshToken, expiryTime);
        } else {
            // 处理错误响应
            throw new RuntimeException("Failed to refresh token: " + response.getStatusCode() + " - " + response.getBody());
        }
    }

    // 辅助类用于封装令牌响应
    public static class TokenResponse {
        private final String accessToken;
        private final String refreshToken;
        private final OffsetDateTime expiryTime;

        public TokenResponse(String accessToken, String refreshToken, OffsetDateTime expiryTime) {
            this.accessToken = accessToken;
            this.refreshToken = refreshToken;
            this.expiryTime = expiryTime;
        }

        public String getAccessToken() { return accessToken; }
        public String getRefreshToken() { return refreshToken; }
        public OffsetDateTime getExpiryTime() { return expiryTime; }
    }
}

此方法将返回一个JSON字符串,其中包含新的access_token、expires_in(访问令牌的有效期,以秒为单位),以及可能更新的refresh_token(某些授权服务器会在每次刷新时颁发新的刷新令牌,旧的刷新令牌会失效)。

将新令牌集成到GraphServiceClient

获取到新的访问令牌后,需要更新应用程序中存储的令牌信息,并确保GraphServiceClient使用这个新令牌。由于原始的TokenCredential是通过Lambda表达式动态获取account对象的令牌,我们只需要更新account对象中存储的访问令牌和其过期时间即可。

假设您的account对象是一个自定义的数据结构,用于存储用户的认证信息:

// 假设您的 Account 类有以下方法
public class UserAccount {
    private String accessToken;
    private String refreshToken;
    private OffsetDateTime tokenExpiry;

    // ... 构造函数,getter和setter ...

    public void updateTokens(String newAccessToken, String newRefreshToken, OffsetDateTime newExpiry) {
        this.accessToken = newAccessToken;
        this.refreshToken = newRefreshToken;
        this.tokenExpiry = newExpiry;
    }
}

当您需要刷新令牌时:

// 假设 currentAccount 是当前用户的 UserAccount 实例
UserAccount currentAccount = // ... 从存储中加载 ...

// 检查令牌是否即将过期或已过期
if (currentAccount.getTokenExpiry().isBefore(OffsetDateTime.now(ZoneId.systemDefault()).plusMinutes(5))) { // 提前5分钟刷新
    try {
        AzureAdTokenRefresher refresher = new AzureAdTokenRefresher();
        AzureAdTokenRefresher.TokenResponse tokenResponse = refresher.refreshAccessToken(currentAccount.getRefreshToken());

        // 更新 account 对象中的令牌信息
        currentAccount.updateTokens(
            tokenResponse.getAccessToken(),
            tokenResponse.getRefreshToken(), // 使用新的刷新令牌,如果返回了的话
            tokenResponse.getExpiryTime()
        );
        // ... 将更新后的 currentAccount 保存回存储 ...

        // GraphServiceClient 的 TokenCredential 会在下次请求时自动获取更新后的令牌
        // 因为它的实现是每次请求时从 account 对象中获取最新令牌。
        // 如果 GraphServiceClient 需要重新构建,则在此处重新构建。
        // 对于上述 lambda 表达式实现的 TokenCredential,通常不需要重新构建 GraphServiceClient。

    } catch (IOException | RuntimeException e) {
        // 处理令牌刷新失败的情况,可能需要用户重新登录
        System.err.println("Failed to refresh access token: " + e.getMessage());
        // 标记用户需要重新认证
    }
}

注意事项

  1. 安全性: client_id和client_secret是敏感信息,绝不应硬编码在代码中或直接暴露给客户端。它们应该从安全配置(如环境变量、Azure Key Vault、配置文件)中读取。client_secret应严格保密。
  2. 刷新令牌的存储: 刷新令牌具有较长的生命周期,并且可以用于获取新的访问令牌。因此,它们必须像密码一样安全地存储(例如,加密存储在数据库中,或使用安全的会话管理)。
  3. 错误处理: 令牌刷新请求可能会失败,例如网络问题、刷新令牌过期或被吊销等。您的应用程序应妥善处理这些错误,并可能需要提示用户重新登录。
  4. 刷新令牌的生命周期: 虽然刷新令牌的生命周期通常比访问令牌长得多,但它们也可能过期或被吊销(例如,用户更改密码、管理员撤销权限)。因此,应用程序需要准备好在刷新令牌失效时引导用户重新进行完整的认证流程。
  5. 并发刷新: 如果应用程序是多线程或分布式部署的,并且多个请求可能同时尝试刷新同一个用户的令牌,需要实现适当的同步机制(如分布式锁)来避免不必要的重复刷新或竞态条件。
  6. redirect_uri和scope: 在刷新令牌请求中使用的redirect_uri和scope应与最初获取刷新令牌时使用的值保持一致。
  7. 响应解析: 令牌端点返回的JSON响应可能包含除了access_token和expires_in之外的其他字段,如token_type、scope和新的refresh_token。务必解析并利用这些信息。特别是,如果返回了新的refresh_token,您应该用它替换旧的刷新令牌。

总结

在与Azure AD集成的Java Web应用程序中,实现访问令牌刷新是维护用户会话和提供无缝用户体验的关键。虽然Graph SDK或Azure SDK的TokenCredential机制本身不直接处理刷新,但通过直接调用Azure AD的OAuth 2.0令牌端点,我们可以利用刷新令牌获取新的访问令牌。正确地实现这一机制,并遵循安全最佳实践,可以确保应用程序能够稳定、安全地访问Microsoft Graph和其他Azure AD保护的资源。

本文内容来源于互联网,如有侵权请联系删除。
作者最新文章
编程开发
相关文章 更多
C++动态数组初始化怎么写?常用语句与代码示例
C++动态数组初始化怎么写?常用语句与代码示例

深入解析C++中动态数组的初始化机制,涵盖new操作符的不同用法、基本类型与类对象的初始化差异,以及为何在现代C++开发中应优先使用std::vector。

using namespace 使用中遇到的问题怎么解决
using namespace 使用中遇到的问题怎么解决

命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

c语言函数递归 实操经验总结:这些技巧很实用
c语言函数递归 实操经验总结:这些技巧很实用

理解递归的基本原理在C语言中,递归是一种函数调用自身的编程技术。要掌握它,首先需要理解其核心思想:将一个复杂的大问题,分解为一个或几个与原问题相似但规模更小的子问题,直到子问题足够简单,可以直接求解。这个过程通常包含两个关键部分:递归出口和递归体。递归出口定义了问题何时不再继续分解,即最简单、可直接

c语言函数递归 怎么选?常见方案对比分析
c语言函数递归 怎么选?常见方案对比分析

递归函数的基本概念与适用场景在C语言编程中,递归是一种函数调用自身的编程技巧。它并非适用于所有问题,但在处理某些具有自相似结构的问题时,能提供极其清晰和优雅的解决方案。递归的核心思想是将一个大规模问题分解为一个或多个同类型但规模更小的子问题,直到子问题简单到可以直接求解。典型的适用场景包括树形结构的

Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解
Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解

理解内存管理的基石在Objective-C的编程世界中,内存管理是开发者必须掌握的核心技能之一。它直接关系到应用的性能、稳定性与资源利用效率。与一些采用自动垃圾回收机制的语言不同,Objective-C在很长一段时间里,依赖一套基于引用计数的、需要开发者部分介入的管理规则。这套规则的核心思想是明确的

如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏
如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏

理解 dealloc 的角色与时机在 iOS 应用开发中,内存管理是保障应用性能与稳定性的基石。dealloc 方法是 Objective-C 中对象生命周期结束时的关键回调,它标志着对象即将被系统回收内存。正确理解其触发时机至关重要:当一个对象的引用计数降为零时,运行时系统会自动调用该对象的 de

深入理解 Objective-C 中的 dealloc 方法:内存管理核心机制
深入理解 Objective-C 中的 dealloc 方法:内存管理核心机制

内存管理的基石在Objective-C的世界里,内存管理是开发者必须掌握的核心技能之一。作为一门在手动引用计数(MRC)时代诞生的语言,Objective-C要求程序员对对象的生命周期有清晰的认识。dealloc方法正是这一生命周期中至关重要的终点站。它是一个实例方法,当对象的引用计数降为零时,系统

理解 native2ascii:Java 国际化开发中的字符编码工具
理解 native2ascii:Java 国际化开发中的字符编码工具

native2ascii 工具的基本定位在Ja va应用程序的国际化与本地化开发过程中,处理非拉丁字符集是一个常见且关键的环节。Ja va内部使用Unicode字符集来统一表示全球各种语言的文字,但其属性文件(.properties)在历史上要求使用ASCII编码,或者更准确地说,要求非ASCII字

如何使用 native2ascii 转换中文字符为 Unicode 转义序列
如何使用 native2ascii 转换中文字符为 Unicode 转义序列

理解 native2ascii 工具的基本用途在软件开发,特别是涉及国际化处理的场景中,开发者常常需要处理不同编码的文本资源。native2ascii 是 Ja va 开发工具包(JDK)中提供的一个命令行实用程序,其主要功能是将包含本地字符编码(非ASCII字符)的文件,转换为包含 Unicode

Java native2ascii 命令详解:解决属性文件乱码问题
Java native2ascii 命令详解:解决属性文件乱码问题

native2ascii 命令的由来与作用在Ja va开发中,处理国际化资源文件是一个常见需求。资源文件通常以.properties格式存储,用于支持多语言界面。然而,Ja va属性文件默认采用ISO-8859-1字符集编码,这导致了一个直接的问题:当文件中包含非拉丁字符(如中文、日文、韩文等)时,

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

macOS软件
macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

Mac软件 更多
灵活计算器
灵活计算器
macOS/iOS/Android

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师
macOS

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

WINDOWS 更多
Windows 10
Windows 10
Windows

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘
Windows/macOS/iOS/Android

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。