外部API跨平台应用微信登录一键打通iOS安卓开发者如何避免数据泄露实测分享接入经验权限配置与常见问题解决方案
做跨平台应用时,登录这件事总让我头疼一阵子,但微信登录这个方案确实省心不少。我自己在折腾完iOS和Android两端接入之后,踩过不少坑,也学到了一些避免数据泄露的实操经验,今天就把它掰开揉碎聊聊。
先把事儿说清楚:微信登录到底是怎么回事
微信登录走的是OAuth 2.0协议,用户授权后你能拿到一个access_token,再用它换openid。这个openid是用户在你们应用里的唯一标识,注意不是unionid,unionid需要额外绑定才能跨应用识别。
流程本身不复杂,但细节决定成败。
iOS端接入:Swift代码实测
先说iOS这边,用的是微信SDK的官方封装。
import WechatOpenSDK
class WeChatLoginManager: ObservableObject {
static let shared = WeChatLoginManager()
private let appId = "your_app_id"
private let secret = "your_app_secret"
private init() {}
/// 发起微信登录授权请求
func sendAuthRequest() {
let authReq = SendAuthReq()
authReq.scope = "snsapi_userinfo"
authReq.state = "wechat_login"
WXApi.send(authReq)
}
/// 处理微信返回的授权结果
func handleOpenURL(_ url: URL) -> Bool {
return WXApi.handleOpen(url, delegate: self)
}
/// 用code换取access_token和openid
func exchangeCodeForToken(_ code: String, completion: @escaping (Result<WeChatTokenInfo, Error>) -> Void) {
let url = "https://api.weixin.qq.com/sns/oauth2/access_token"
var components = URLComponents(string: url)!
components.queryItems = [
URLQueryItem(name: "appid", value: appId),
URLQueryItem(name: "secret", value: secret),
URLQueryItem(name: "code", value: code),
URLQueryItem(name: "grant_type", value: "authorization_code")
]
guard let requestURL = components.url else {
completion(.failure(WeChatError.invalidURL))
return
}
var request = URLRequest(url: requestURL)
request.httpMethod = "GET"
// ⚠️ 关键点:不要在客户端直接暴露secret
// 生产环境应该把code发到自己的后端,由后端去换token
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
completion(.failure(error ?? WeChatError.networkError))
return
}
do {
let decoder = JSONDecoder()
let tokenInfo = try decoder.decode(WeChatTokenInfo.self, from: data)
completion(.success(tokenInfo))
} catch {
completion(.failure(WeChatError.decodeError(error)))
}
}.resume()
}
}
这里有个非常重要的安全实践:在iOS代码里我看到了把secret直接放在客户端的做法,这其实是个危险信号。正确的做法是——客户端只拿code,code是单次有效的,然后code发到你们的后端服务器,由后端用secret去换token。这样secret永远不会暴露在客户端。
// 正确的架构:客户端 → 自己的后端 → 微信服务器
// 客户端拿到code后
func loginWithWeChatCode(_ code: String) {
let url = "https://api.yourserver.com/auth/wechat"
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = ["code": code]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
URLSession.shared.dataTask(with: request) { data, response, error in
// 后端返回你们系统自己的token,而不是微信的token
// 这样微信的secret永远不会离开你的服务器
}.resume()
}
Android端接入:Kotlin代码实测
Android这边用Kotlin,逻辑跟iOS类似,但有些坑不一样。
class WeChatLoginManager(private val applicationContext: Context) {
companion object {
private const val WX_APP_ID = "your_app_id"
private const val REQUEST_CODE_AUTH = 1001
}
private val api: IWXAPI = WXAPIFactory.createWXAPI(applicationContext, WX_APP_ID, true)
// 结果回调接口
interface AuthCallback {
fun onSuccess(tokenInfo: WeChatTokenInfo)
fun onError(error: WeChatAuthError)
}
/**
* 发起授权请求
*/
fun sendAuthRequest(callback: AuthCallback) {
val scope = listOf("snsapi_userinfo", "snsapi_base")
val req = SendAuthReq()
req.scope = scope.joinToString(",")
req.state = "wechat_auth_${System.currentTimeMillis()}"
api.registerApp(WX_APP_ID)
api.sendReq(req)
}
/**
* 从回调URL中提取code
*/
fun handleAuthResponse(intent: Intent?): WeChatTokenInfo? {
if (intent == null) return null
val transaction = intent.getStringExtra(Constants.EXTRA_TRANSACTION)
val code = intent.getStringExtra(Constants.EXTRA_CODE) ?: return null
// transaction用来防重放攻击,验证state是否匹配
if (!transaction?.startsWith("wechat_auth_") == true) {
return null
}
// ⚠️ 重要:这里只拿到code,不要在这里换token
// 把code传给后端服务器
return WeChatTokenInfo(code = code, transaction = transaction)
}
/**
* 用code换取微信token(应该在服务端执行)
* 这里只是展示参数构造,实际调用走后端API
*/
fun buildTokenExchangeUrl(code: String): String {
// 这个URL构造逻辑应该在后端
// 展示给开发者看参数结构
return "https://api.weixin.qq.com/sns/oauth2/access_token?" +
"appid=${WX_APP_ID}" +
"&secret=YOUR_SERVER_SECRET" + // 这个secret只在后端
"&code=$code" +
"&grant_type=authorization_code"
}
}
// 数据结构
data class WeChatTokenInfo(
val code: String,
val transaction: String,
val accessToken: String? = null,
val openId: String? = null,
val expiresIn: Int? = null
)
// 错误类型
sealed class WeChatAuthError : RuntimeException() {
object NetworkError : WeChatAuthError()
object AuthDenied : WeChatAuthError()
object InvalidCode : WeChatAuthError()
data class ServerError(val code: Int, val message: String) : WeChatAuthError()
}
权限配置:这些坑我替你踩过了
微信开放平台的后台配置,看似简单,实际上很多细节容易搞错。
iOS Bundle ID和Android包名签名必须匹配
这是最常见的报错来源。你在微信开放平台配的Bundle ID(iOS)或包名+签名(Android),必须跟实际打包时的一模一样。
// iOS配置检查清单:
✅ App ID在微信开放平台已绑定
✅ Bundle ID完全匹配(区分大小写)
✅ Scheme设置为 wx + AppID(如 wx1234567890)
✅ Info.plist中LSApplicationQueriesSchemes包含weixin
✅ 真机测试,模拟器不行
// Android配置检查清单:
✅ 应用包名与微信开放平台一致
✅ 签名SHA1正确( release包和debug包不同!)
✅ 包名格式:com.example.xxx(不能有大写字母)
✅ 应用签名工具生成的包名和签名要对应
我用过AS里的”应用签名”功能来查看正确的签名,命令如下:
# 查看debug签名(Windows)
keytool -list -v -keystore %USERPROFILE%\.android\debug.keystore -alias androiddebugkey -storepass android -keypass android
# 查看release签名
keytool -list -v -keystore your_release.keystore -alias your_alias
Android Manifest配置容易被漏
<!-- AndroidManifest.xml 必须添加 -->
<activity
android:name="com.tencent.mm.sdk.openapi.WXMediaEntryActivity"
android:exported="true" />
<!-- 微信回调Activity -->
<activity
android:name=". WXCallbackActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="wx你的AppID" />
</intent-filter>
</activity>
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
iOS Info.plist配置
<!-- iOS Info.plist -->
<key>LSApplicationQueriesSchemes</key>
<array>
<string>weixin</string>
<string>wechat</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>wx你的AppID</string>
</array>
</dict>
</array>
数据泄露防护:这部分是最关键的
接入微信登录技术本身不难,但安全方面出了事就是大事。我总结了几个血泪教训。
1. Access Token绝不放客户端
// ❌ 错误做法:把微信token存到UserDefaults
UserDefaults.standard.set(tokenInfo.accessToken, forKey: "wechat_token")
// ✅ 正确做法:用你们自己的系统token,微信token存在服务端
// 客户端只存自己的session
UserDefaults.standard.set(yourSystemToken, forKey: "user_session_token")
微信的access_token有效期很短(7200秒),而且不应该存在客户端。你们系统应该维护自己的token体系,微信token只在服务端持有。
2. Code单次有效,防重放
// 每次登录生成唯一的state
val state = "wechat_${UUID.randomUUID()}_${System.currentTimeMillis()}"
// 存储state到Redis(带TTL,5分钟过期)
redis.set("state:${state}", "pending", 300)
// 后端收到code后验证
fun validateCodeAndState(code: String, state: String): Boolean {
val storedState = redis.get("state:$state")
if (storedState != "pending") return false
// 验证code
val tokenResponse = exchangeCode(code)
// 用完即删
redis.del("state:$state")
return tokenResponse != null
}
3. 敏感数据加密存储
// 用户信息不要明文存
import CryptoKit
func encryptUserData(_ data: String, key: SymmetricKey) -> String? {
let inputData = Data(data.utf8)
let sealedBox = try? AES.GCM.seal(inputData, using: key)
return sealedBox?.combined?.base64String
}
func decryptUserData(_ base64Data: String, key: SymmetricKey) -> String? {
guard let sealedBoxData = Data(base64Literal: base64Data) else { return nil }
let sealedBox = try? AES.GCM.SealedBox(combined: sealedBoxData)
let decrypted = try? AES.GCM.open(sealedBox!, using: key)
return String(data: decrypted, encoding: .utf8)
}
4. HTTPS强制,不要HTTP
// 网络请求必须强制HTTPS
func buildSecureURL(_ path: String) -> URL? {
var components = URLComponents(string: "https://api.yourserver.com\(path)")!
// 禁用HTTP降级
components.url?.scheme = "https"
return components.url
}
在Android侧:
<!-- Android 强制HTTPS -->
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">api.yourserver.com</domain>
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</domain-config>
</network-security-config>
常见问题排查速查表
Q1:返回-1,没有具体错误信息
这通常意味着微信SDK没装或者版本不对。检查:
- 真机上是否安装了微信
- SDK版本是否与微信开放平台要求匹配
- iOS用pod install后Clean Build
- Android检查包名和签名是否一致
Q2:返回-2,用户取消授权
这是正常情况,给用户友好提示”您取消了授权”即可,不要当成错误处理。
Q3:返回-4,应用没有注册
检查AppID是否正确,是否在微信开放平台创建了应用,以及Bundle ID/包名是否匹配。
Q4:拿到的openid在iOS和Android上不一样
这是最坑的一点。如果两个端用的是不同的AppID,openid自然不同。要跨平台识别同一用户,需要在微信开放平台绑定”应用关联”,然后获取unionid。
unionid使用条件:
1. 多个应用绑定到同一个微信开放平台账号
2. 同一openid+同一APPID = 唯一用户
3. 同一unionid(跨APPID)= 同一微信用户
Q5:access_token过期了怎么办
// 用refresh_token换取新的access_token
func refreshToken(_ refreshToken: String) async throws -> WeChatTokenInfo {
let url = "https://api.weixin.qq.com/sns/oauth2/refresh_token"
// ... 类似exchangeCodeForToken的逻辑
// refresh_token有效期30天,过期后需要用户重新授权
}
后端架构建议
最后说一句后端该怎么做。你们应该有一个专门的认证服务:
客户端 → 你们的后端API → 微信OAuth服务器
↓
你们自己的用户系统
微信只负责证明”这是那个微信用户”,你们的系统负责”这个用户是谁、有什么权限”。两层完全分离,这样即使微信那边出问题,你们的服务还是能正常运行。
# 伪代码:后端认证服务
@app.post("/auth/wechat")
async def wechat_login(request: WeChatAuthRequest):
# 1. 验证state防重放
if not verify_state(request.state):
raise HTTPException(400, "Invalid state")
# 2. 用code换微信token(secret在这里,不在客户端)
wx_token = await exchange_code_for_token(request.code)
# 3. 获取用户openid
openid = await get_openid(wx_token.access_token)
# 4. 查找或创建本地用户
user = await find_or_create_user(openid, wx_token)
# 5. 签发你们自己的JWT
jwt_token = generate_jwt(user)
return {"token": jwt_token, "user_id": user.id}
把这些要点记住,微信登录接入基本就不会踩什么大坑了。最重要的是记住:code是钥匙,token是房子,secret是房产证。钥匙可以给客户端,房子可以共享,但房产证永远留在自己家。
