You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于API实现Keycloak的Auth Code Flow with PKCE(无重定向URL)适配Flutter移动端

Keycloak Auth Code Flow with PKCE 纯API实现方案(适配Flutter移动端)

前置配置(Keycloak客户端)

  • 创建Public类型的客户端(移动端属于Public Client,无法安全保存客户端密钥)
  • 在客户端设置的Advanced标签页,开启Proof Key for Code Exchange (PKCE),并将Code Challenge Method设为S256
  • 在Valid Redirect URIs中添加urn:ietf:wg:oauth:2.0:oob(OAuth2内置的无重定向URI,适配纯API场景)
  • 确保客户端的Standard Flow Enabled选项处于开启状态

步骤1:生成PKCE挑战码

PKCE需要两个核心参数:code_verifier(原始随机串)和code_challenge(哈希后的编码串),Flutter中可通过以下代码实现:

import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';

// 生成随机的code_verifier(长度32-128位)
String generateCodeVerifier() {
  final random = Random.secure();
  final bytes = List<int>.generate(64, (_) => random.nextInt(256));
  return base64Url.encode(bytes).replaceAll('=', '');
}

// 基于code_verifier生成code_challenge(SHA-256哈希后base64url编码)
String generateCodeChallenge(String codeVerifier) {
  final bytes = utf8.encode(codeVerifier);
  final digest = sha256.convert(bytes);
  return base64Url.encode(digest.bytes).replaceAll('=', '');
}

步骤2:获取授权码(Authorization Code)

通过POST请求直接调用Keycloak的授权端点,传递用户名密码跳过UI登录:

import 'package:http/http.dart' as http;

Future<String> getAuthorizationCode({
  required String keycloakUrl,
  required String realm,
  required String clientId,
  required String username,
  required String password,
  required String codeChallenge,
}) async {
  final url = Uri.parse('$keycloakUrl/realms/$realm/protocol/openid-connect/auth');
  
  final response = await http.post(
    url,
    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: {
      'client_id': clientId,
      'response_type': 'code',
      'scope': 'openid email profile', // 根据业务需求调整
      'code_challenge': codeChallenge,
      'code_challenge_method': 'S256',
      'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob',
      'username': username,
      'password': password,
    },
  );

  // 解析响应:oob模式下授权码直接在响应体中返回
  if (response.statusCode == 200) {
    final codeMatch = RegExp(r'code=([^&]+)').firstMatch(response.body);
    return codeMatch?.group(1) ?? '';
  }
  throw Exception('获取授权码失败: ${response.body}');
}

步骤3:用授权码换取Access Token和Refresh Token

拿到授权码后,调用Token端点换取最终令牌:

Future<Map<String, dynamic>> exchangeCodeForTokens({
  required String keycloakUrl,
  required String realm,
  required String clientId,
  required String authorizationCode,
  required String codeVerifier,
}) async {
  final url = Uri.parse('$keycloakUrl/realms/$realm/protocol/openid-connect/token');
  
  final response = await http.post(
    url,
    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: {
      'client_id': clientId,
      'grant_type': 'authorization_code',
      'code': authorizationCode,
      'code_verifier': codeVerifier,
      'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob',
    },
  );

  if (response.statusCode == 200) {
    return jsonDecode(response.body);
  }
  throw Exception('令牌兑换失败: ${response.body}');
}

完整流程调用示例

void main() async {
  // 配置参数
  const keycloakUrl = 'https://your-keycloak-domain.com';
  const realm = 'your-realm-name';
  const clientId = 'your-mobile-client-id';
  const username = 'user@example.com';
  const password = 'user-password';

  // 步骤1:生成PKCE参数
  final codeVerifier = generateCodeVerifier();
  final codeChallenge = generateCodeChallenge(codeVerifier);

  // 步骤2:获取授权码
  final authCode = await getAuthorizationCode(
    keycloakUrl: keycloakUrl,
    realm: realm,
    clientId: clientId,
    username: username,
    password: password,
    codeChallenge: codeChallenge,
  );

  // 步骤3:换取令牌
  final tokens = await exchangeCodeForTokens(
    keycloakUrl: keycloakUrl,
    realm: realm,
    clientId: clientId,
    authorizationCode: authCode,
    codeVerifier: codeVerifier,
  );

  // 使用令牌(示例)
  print('Access Token: ${tokens['access_token']}');
  print('Refresh Token: ${tokens['refresh_token']}');
}

注意事项

  • code_verifier必须是32-128位的随机字符串,仅由字母、数字、-、.、_、~组成
  • 生产环境禁止硬编码用户名密码,需通过flutter_secure_storage等安全存储方案保存用户凭证
  • 令牌过期后,可使用refresh_token调用Token端点刷新,无需重复执行完整流程
  • 确保Keycloak服务器的CORS配置允许Flutter应用的域名发起请求

内容的提问来源于stack exchange,提问作者AlwaysDeveloper

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 09:40:25