0

0

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

霞舞

霞舞

发布时间:2025-09-18 14:28:02

|

1011人浏览过

|

来源于php中文网

原创

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中执行此操作的示例:

AI发型设计
AI发型设计

虚拟发型试穿工具和发型模拟器

下载
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保护的资源。

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

831

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

737

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

733

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

397

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

398

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

446

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

430

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16925

2023.08.03

Java 项目构建与依赖管理(Maven / Gradle)
Java 项目构建与依赖管理(Maven / Gradle)

本专题系统讲解 Java 项目构建与依赖管理的完整体系,重点覆盖 Maven 与 Gradle 的核心概念、项目生命周期、依赖冲突解决、多模块项目管理、构建加速与版本发布规范。通过真实项目结构示例,帮助学习者掌握 从零搭建、维护到发布 Java 工程的标准化流程,提升在实际团队开发中的工程能力与协作效率。

10

2026.01.12

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.4万人学习

C# 教程
C# 教程

共94课时 | 6.5万人学习

Java 教程
Java 教程

共578课时 | 45.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号