如何在 Flutter 中使用 http 进行摘要认证?

Posted

技术标签:

【中文标题】如何在 Flutter 中使用 http 进行摘要认证?【英文标题】:How to make Digest Authentication with http in Flutter? 【发布时间】:2019-12-22 08:31:59 【问题描述】:

我正在尝试使用摘要式身份验证发出 API 请求。我找到了上述问题FLUTTER How to implement Digest Authentification 的答案,但不是很清楚。摘要的文档非常少。

以下是我的代码

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

try 
  HttpClient authenticatingClient = HttpClient();

  authenticatingClient.authenticate = (uri, scheme, realm) 

    authenticatingClient.addCredentials(
        uri,
        realm,
        HttpClientDigestCredentials(
            DIGEST_AUTH_USERNAME, DIGEST_AUTH_PASSWORD));

    return Future.value(true);
  ;

  http.Client client = io_client.IOClient(authenticatingClient);

  final response = await client.post(LOGIN_URL, body: 
    "username": userName,
    "password": password,
    "user_group": 2
  ).timeout(const Duration(seconds: 20));
  if (response.statusCode == 200) 

    debugPrint(response.body);
    CurvesLoginModel curvesLoginModel = standardSerializers.deserializeWith(
        CurvesLoginModel.serializer, json.decode(response.body));
    return curvesLoginModel;
   else 
    return null;
  
 on TimeoutException catch (_) 

  return null;
 on SocketException catch (_) 

  return null;




但是addCredentials 中的realm 是什么。

这也是在http 中为Flutter 实现Digest Authentication 的方法吗? 一旦我到达终点,我就会收到以下错误Unhandled Exception: type 'int' is not a subtype of type 'String' in type cast

【问题讨论】:

【参考方案1】:

Realm 是 Web 服务器提供的任意字符串,可帮助您决定使用哪个用户名,以防您有多个用户名。这有点类似于域。在一个域中,您的用户名可能是 fbloggs,而在另一个 fredb 中。通过告诉您您知道要提供哪个领域/域。

您的转换问题是由于在正文中使用值 2 引起的。那一定是Map<String, String>,但您提供了一个整数。将其替换为2.toString()

【讨论】:

【参考方案2】:

如果有人想知道如何用http做digest auth,那么如下

import 'dart:async';
import 'dart:convert';
import 'dart:math' as math;
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart' as crypto;
import 'package:http/http.dart' as http;

class DigestAuthClient extends http.BaseClient 
  DigestAuthClient(String username, String password, inner)
      : _auth = DigestAuth(username, password),
        // ignore: prefer_if_null_operators
        _inner = inner == null ? http.Client() : inner;

  final http.Client _inner;
  final DigestAuth _auth;

  void _setAuthString(http.BaseRequest request) 
    request.headers['Authorization'] =
        _auth.getAuthString(request.method, request.url);
  

  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) async 
    final response = await _inner.send(request);

    if (response.statusCode == 401) 
      final newRequest = copyRequest(request);
      final String authInfo = response.headers['www-authenticate'];
      _auth.initFromAuthorizationHeader(authInfo);

      _setAuthString(newRequest);

      return _inner.send(newRequest);
    

    // we should reach this point only with errors other than 401
    return response;
  


Map<String, String> splitAuthenticateHeader(String header) 
  if (header == null || !header.startsWith('Digest ')) 
    return null;
  
  String token = header.substring(7); // remove 'Digest '

  var ret = <String, String>;

  final components = token.split(',').map((token) => token.trim());
  for (final component in components) 
    final kv = component.split('=');
    ret[kv[0]] = kv.getRange(1, kv.length).join('=').replaceAll('"', '');
  
  return ret;


String md5Hash(String data) 
  var content = const Utf8Encoder().convert(data);
  var md5 = crypto.md5;
  var digest = md5.convert(content).toString();
  return digest;


// from http_retry
/// Returns a copy of [original].
http.Request _copyNormalRequest(http.Request original) 
  var request = http.Request(original.method, original.url)
    ..followRedirects = original.followRedirects
    ..persistentConnection = original.persistentConnection
    ..body = original.body;
  request.headers.addAll(original.headers);
  request.maxRedirects = original.maxRedirects;

  return request;


http.BaseRequest copyRequest(http.BaseRequest original) 
  if (original is http.Request) 
    return _copyNormalRequest(original);
   else 
    throw UnimplementedError(
        'cannot handle yet requests of type $original.runtimeType');
  


// Digest auth

String _formatNonceCount(int nc) 
  return nc.toRadixString(16).padLeft(8, '0');


String _computeHA1(String realm, String algorithm, String username,
    String password, String nonce, String cnonce) 
  String ha1;

  if (algorithm == null || algorithm == 'MD5') 
    final token1 = "$username:$realm:$password";
    ha1 = md5Hash(token1);
   else if (algorithm == 'MD5-sess') 
    final token1 = "$username:$realm:$password";
    final md51 = md5Hash(token1);
    final token2 = "$md51:$nonce:$cnonce";
    ha1 = md5Hash(token2);
  

  return ha1;


Map<String, String> computeResponse(
    String method,
    String path,
    String body,
    String algorithm,
    String qop,
    String opaque,
    String realm,
    String cnonce,
    String nonce,
    int nc,
    String username,
    String password) 
  var ret = <String, String>;

  // ignore: non_constant_identifier_names
  String HA1 = _computeHA1(realm, algorithm, username, password, nonce, cnonce);

  // ignore: non_constant_identifier_names
  String HA2;

  if (qop == 'auth-int') 
    final bodyHash = md5Hash(body);
    final token2 = "$method:$path:$bodyHash";
    HA2 = md5Hash(token2);
   else 
    // qop in [null, auth]
    final token2 = "$method:$path";
    HA2 = md5Hash(token2);
  

  final nonceCount = _formatNonceCount(nc);
  ret['username'] = username;
  ret['realm'] = realm;
  ret['nonce'] = nonce;
  ret['uri'] = path;
  ret['qop'] = qop;
  ret['nc'] = nonceCount;
  ret['cnonce'] = cnonce;
  if (opaque != null) 
    ret['opaque'] = opaque;
  
  ret['algorithm'] = algorithm;

  if (qop == null) 
    final token3 = "$HA1:$nonce:$HA2";
    ret['response'] = md5Hash(token3);
   else if (qop == 'auth' || qop == 'auth-int') 
    final token3 = "$HA1:$nonce:$nonceCount:$cnonce:$qop:$HA2";
    ret['response'] = md5Hash(token3);
  

  return ret;


class DigestAuth 
  DigestAuth(this.username, this.password);

  String username;
  String password;

  // must get from first response
  String _algorithm;
  String _qop;
  String _realm;
  String _nonce;
  String _opaque;

  int _nc = 0; // request counter
  String _cnonce; // client-generated; should change for each request

  String _computeNonce() 
    math.Random rnd = math.Random();

    List<int> values = List<int>.generate(16, (i) => rnd.nextInt(256));

    return hex.encode(values);
  

  String getAuthString(String method, Uri url) 
    _cnonce = _computeNonce();
    _nc += 1;
    // if url has query parameters, append query to path
    var path = url.hasQuery ? "$url.path?$url.query" : url.path;

    // after the first request we have the nonce, so we can provide credentials
    var authValues = computeResponse(method, path, '', _algorithm, _qop,
        _opaque, _realm, _cnonce, _nonce, _nc, username, password);
    final authValuesString = authValues.entries
        .where((e) => e.value != null)
        .map((e) => [e.key, '="', e.value, '"'].join(''))
        .toList()
        .join(', ');
    final authString = 'Digest $authValuesString';
    return authString;
  

  void initFromAuthorizationHeader(String authInfo) 
    Map<String, String> values = splitAuthenticateHeader(authInfo);
    _algorithm = values['algorithm'];
    _qop = values['qop'];
    _realm = values['realm'];
    _nonce = values['nonce'];
    _opaque = values['opaque'];
  

  bool isReady() 
    return _nonce != null;
  

然后在调用你的 api 时

final response =
          await DigestAuthClient(DIGEST_AUTH_USERNAME, DIGEST_AUTH_PASSWORD)
              .post(LOGIN_URL, body: 
        "USERNAME": userName,
        "PASSWORD": password,
        "USER_GROUP": "2"
      ).timeout(const Duration(seconds: 20));

所有功劳归于以下图书馆https://pub.dev/packages/http_auth

【讨论】:

以上是关于如何在 Flutter 中使用 http 进行摘要认证?的主要内容,如果未能解决你的问题,请参考以下文章

FLUTTER 如何实现 Digest 认证

如何使用提琴手的http摘要身份验证?

如何在 Flutter/Dart 中使用 url 编码的标头和正文发出 HTTP POST 请求

如何使用 Flutter Web 进行深度链接?

如何在 Flutter 上禁用 SSL 固定?

如何在 Flutter 中使用 JSON 正文发出 http DELETE 请求?