Android通过Graph读取SharePoint文件遇403问题及相关咨询
Q1:Android端访问SharePoint文件是否需要特殊权限?
你遇到的403错误核心在于浏览器与Android应用的请求上下文不统一:
- 浏览器能直接访问,说明文件已配置匿名访问权限,或你的设备处于企业内网域环境,浏览器自动通过NTLM/Kerberos完成了身份验证。
- 但Android应用的HTTP请求是独立上下文,默认不会携带浏览器的身份凭证,哪怕是同一设备也不行。
- 若文件是企业内部资源(即便浏览器能匿名访问),Android应用仍需通过MSAL获取合法访问令牌才能绕过403。所需权限包括:
Files.Read或Files.Read.All(优先选择最小权限,根据文件访问范围决定)- 若访问SharePoint站点文件,还需
Sites.Read.All或对应站点级权限
Q2:示例认证JSON文件及Android清单配置
MSAL配置JSON(auth_config.json,放置在res/raw目录)
{ "client_id": "你的Azure AD应用客户端ID", "authorization_user_agent": "DEFAULT", "redirect_uri": "msal你的客户端ID://auth", "authorities": [ { "type": "AAD", "audience": { "type": "AzureADMyOrg", "tenant_id": "你的Azure租户ID" } } ], "logging": { "level": "INFO", "pii_enabled": false }, "authorization_mode": "AUTHORIZATION_CODE", "multiple_clouds_supported": false, "broker_redirect_uri_registered": true }
AndroidManifest.xml 关键配置片段
<!-- 网络权限 --> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- MSAL 必需的浏览器跳转Activity --> <activity android:name="com.microsoft.identity.client.BrowserTabActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- 替换为你的redirect_uri中的scheme部分 --> <data android:scheme="msal你的客户端ID" /> </intent-filter> </activity>
Q3:Android Kotlin读取SharePoint文件的示例
步骤1:初始化MSAL客户端并处理登录
import com.microsoft.identity.client.* import android.os.Bundle import androidx.appcompat.app.AppCompatActivity class FileDownloadActivity : AppCompatActivity() { private lateinit var singleAccountApp: ISingleAccountPublicClientApplication override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_file_download) // 初始化MSAL单账户客户端 PublicClientApplication.createSingleAccountPublicClientApplication( this, R.raw.auth_config, object : IPublicClientApplication.ISingleAccountApplicationCreatedListener { override fun onCreated(application: ISingleAccountPublicClientApplication) { singleAccountApp = application checkExistingAccount() } override fun onError(exception: MsalException) { // 处理初始化错误,比如配置文件读取失败 } } ) } // 检查是否已有登录账户 private fun checkExistingAccount() { singleAccountApp.getCurrentAccountAsync(object : ISingleAccountPublicClientApplication.CurrentAccountCallback { override fun onAccountLoaded(activeAccount: IAccount?) { if (activeAccount == null) { signIn() // 无账户则触发登录 } else { downloadSharePointFile(activeAccount) // 有账户直接下载 } } override fun onAccountChanged(priorAccount: IAccount?, currentAccount: IAccount?) {} override fun onError(exception: MsalException) {} }) } // 触发登录流程 private fun signIn() { val scopes = arrayOf("Files.Read", "Sites.Read.All") singleAccountApp.signIn( this, null, scopes, object : AuthenticationCallback { override fun onSuccess(result: IAuthenticationResult) { downloadSharePointFile(result.account) } override fun onError(exception: MsalException) {} override fun onCancel() {} } ) } }
步骤2:携带令牌下载文件
import okhttp3.Call import okhttp3.Callback import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import java.io.IOException import java.io.OutputStream private fun downloadSharePointFile(account: IAccount) { // 静默获取访问令牌 singleAccountApp.acquireTokenSilentAsync( arrayOf("Files.Read", "Sites.Read.All"), account.authority, object : SilentAuthenticationCallback { override fun onSuccess(result: IAuthenticationResult) { val accessToken = result.accessToken downloadFileWithToken("你的SharePoint文件链接", accessToken) } override fun onError(exception: MsalException) { if (exception is MsalUiRequiredException) { signIn() // 需要交互登录,重新触发登录流程 } } } ) } // 发起带令牌的下载请求 private fun downloadFileWithToken(fileUrl: String, accessToken: String) { val client = OkHttpClient() val request = Request.Builder() .url(fileUrl) .addHeader("Authorization", "Bearer $accessToken") .build() client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { runOnUiThread { // 主线程更新UI,提示下载失败 } } override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { response.body?.let { body -> // 将文件保存到应用私有目录 val outputStream: OutputStream = openFileOutput("downloaded_file.docx", MODE_PRIVATE) outputStream.write(body.bytes()) outputStream.close() runOnUiThread { // 主线程提示下载成功 } } } else { runOnUiThread { // 处理403等错误,提示权限问题 } } } }) }
注意事项
- 确保Azure AD应用已在Azure门户中配置好API权限,并完成管理员同意(若权限需要)。
- 如果文件确实是完全公开的匿名资源,也可以尝试移除
Authorization头,直接发起普通HTTP请求下载,但企业场景下这种情况极少。
内容的提问来源于stack exchange,提问作者Sakshi Sharma
相关产品推荐
相关产品推荐

