Base64指南:用于URL的编码

58

问题:有更好的方法吗?

VB.Net

Function GuidToBase64(ByVal guid As Guid) As String
    Return Convert.ToBase64String(guid.ToByteArray).Replace("/", "-").Replace("+", "_").Replace("=", "")
End Function

Function Base64ToGuid(ByVal base64 As String) As Guid
    Dim guid As Guid
    base64 = base64.Replace("-", "/").Replace("_", "+") & "=="

    Try
        guid = New Guid(Convert.FromBase64String(base64))
    Catch ex As Exception
        Throw New Exception("Bad Base64 conversion to GUID", ex)
    End Try

    Return guid
End Function

C#

public string GuidToBase64(Guid guid)
{
    return Convert.ToBase64String(guid.ToByteArray()).Replace("/", "-").Replace("+", "_").Replace("=", "");
}

public Guid Base64ToGuid(string base64)
{
   Guid guid = default(Guid);
   base64 = base64.Replace("-", "/").Replace("_", "+") + "==";

   try {
       guid = new Guid(Convert.FromBase64String(base64));
   }
   catch (Exception ex) {
       throw new Exception("Bad Base64 conversion to GUID", ex);
   }

   return guid;
}

3
@Charlie:使用.ToString()默认格式时,十六进制编码比Base64编码后的字符串要大。当然,没有人会想直接传输原始(非可打印字节)数据。 - Hemant
3
@Charlie 如何比较 "37945704-cf86-4b2e-a4b5-0db0204902c8" 和 "BFeUN4bPLkuktQ2wIEkCyA" 的大小? - Fredou
2
我会考虑不使用.replace进行URL编码,或者提供一个单独的方法来处理。这将允许关注点分离,API用户可以选择他们想要真正的base64编码还是URL友好的base64编码,具体取决于他们想要实现什么目标。我理解你的目标是用于URL,但除了URL编码步骤之外的所有内容都有可能被那些想要更短的base64编码但不在URL中使用它的人使用。 - AaronLS
1
@Rick,抱歉回复晚了。你说的对,将编码转换为base64会得到不同的结果,但两者都会得到相同的Guid。 - Fredou
4
我建议更改替换方式,即将.Replace("+", "_")和相反的操作以及Replace("/", "-")和相反的操作改为.Replace("+", "-")和相反的操作以及Replace("/", "_")和相反的操作。这将使编码符合RFC 4648的base64url规范(请参见https://tools.ietf.org/html/rfc4648#section-5)。 - Kasper van den Berg
显示剩余7条评论
6个回答

33

你可能想看一下这个网站:http://prettycode.org/2009/11/12/short-guid/

它看起来非常接近你所要做的事情。

public class ShortGuid
{
    private readonly Guid guid;
    private readonly string value;

    /// <summary>Create a 22-character case-sensitive short GUID.</summary>
    public ShortGuid(Guid guid)
    {
        if (guid == null)
        {
            throw new ArgumentNullException("guid");
        }

        this.guid = guid;
        this.value = Convert.ToBase64String(guid.ToByteArray())
            .Substring(0, 22)
            .Replace("/", "_")
            .Replace("+", "-");
    }

    /// <summary>Get the short GUID as a string.</summary>
    public override string ToString()
    {
        return this.value;
    }

    /// <summary>Get the Guid object from which the short GUID was created.</summary>
    public Guid ToGuid()
    {
        return this.guid;
    }

    /// <summary>Get a short GUID as a Guid object.</summary>
    /// <exception cref="System.ArgumentNullException"></exception>
    /// <exception cref="System.FormatException"></exception>
    public static ShortGuid Parse(string shortGuid)
    {
        if (shortGuid == null)
        {
            throw new ArgumentNullException("shortGuid");
        }
        else if (shortGuid.Length != 22)
        {
            throw new FormatException("Input string was not in a correct format.");
        }

        return new ShortGuid(new Guid(Convert.FromBase64String
            (shortGuid.Replace("_", "/").Replace("-", "+") + "==")));
    }

    public static implicit operator String(ShortGuid guid)
    {
        return guid.ToString();
    }

    public static implicit operator Guid(ShortGuid shortGuid)
    {
        return shortGuid.guid;
    }
}

我知道这个答案已经很老了,但是你的代码实际上可行吗? 通过Guid.ToByteArray()生成的字符串结果和字节到十六进制转换将不同于调用Guid.ToString()。根据.Net文档链接:_返回的字节数组中字节的顺序与Guid值的字符串表示形式不同_。 - Mykola Klymyuk

22
使用此技术为 URL 或文件名格式化 GUID 的一个问题是,两个不同的 GUID 可以产生仅在大小写上有所不同的两个值,例如:

使用此技術為URL或檔名格式化GUID的一個問題是,兩個不同的GUID可能會產生僅在大小寫上有所不同的兩個值,例如:

    var b1 = GuidToBase64(new Guid("c9d045f3-e21c-46d0-971d-b92ebc2ab83c"));
    var b2 = GuidToBase64(new Guid("c9d045f3-e21c-46d0-971d-b92ebc2ab8a4"));
    Console.WriteLine(b1);  // 80XQyRzi0EaXHbkuvCq4PA
    Console.WriteLine(b2);  // 80XQyRzi0EaXHbkuvCq4pA

由于URL有时被解释为不区分大小写,而在Windows文件路径和文件名中是不区分大小写的,这可能会导致冲突。


6
对于URL链接来说这是不正确的。根据RFC 3986,只有协议和主机名应该被视为不区分大小写。查询参数、片段标识符和路径应该被视为区分大小写。当然,是否遵守这一规定取决于你的代码/服务器实现。 - Roger Spurrell
3
@RogerSpurrell - 关于URL的问题,你提出了一个不错的观点。但是我更加关注应用程序对URL的特定处理,例如 http://.../user/{id} 中的 {id} 可能是一个类似随机GUID的ID,以避免OWASP暴力预测资源位置漏洞,并且该ID可能会在不区分大小写的数据库中进行查找。 - Joe
这应该是一条注释。它是不正确的,而且有点牵强,因为大小写不敏感的数据库(这意味着使用base32)。 - Vajk Hermecz
1
@VajkHermecz 我不同意。如果您认为答案不正确和/或无用,您完全可以投反对票和/或发表评论,但这并不是NAA,因为它似乎是诚实地尝试回答问题。 - EJoshuaS - Stand with Ukraine

20

我理解你在结尾处使用 == 的原因是因为对于 GUID(16字节),编码后的字符串将始终以 == 结尾。因此,在每次转换中可以节省2个字符。

除了@Skurmedal已经提到的点(应该在输入无效字符串的情况下抛出异常),我认为你发布的代码已经足够好了。


一开始没想到这个,但是当你考虑一下它的聪明之处,它节省了不少空间 :) - Skurmedel
什么才是最好的方法,处理异常还是无论如何都要查询数据库?因为我需要检查结果中是否至少有一行数据,所以最终会增加更多的代码吗? - Fredou
重点只在于您想要放置检查的位置。我的经验是低级库例程应尽可能透明。当然,您是最好的判断者,知道错误检查代码应该放在哪里,因为了解您的产品以及这个库/代码所处的位置。这只是一个需要考虑的观点。 - Hemant
如果你正在处理一个异常,至少你知道出了什么问题。现在可能无关紧要,但将来可能会有影响。我不太了解你的程序 :)我认为查询数据库以获取理论上不存在的内容是最不理想的解决方案。 - Skurmedel
我认为我同意你们关于抛出异常的想法,这更有意义。 - Fredou
你应该将´base64.Replace("-", "/")...´放在´try´块内,以避免未捕获的´NullReferenceException+`,如果´base64 == null´。 - Felix Alcala

3
在 .NET Core 中,您可以使用 Spans 来获得更好的性能和无需内存分配。
using System.Buffers.Text;
using System.Runtime.InteropServices;

namespace Extensions;

public static class GuidExtensions
{
    private const char Dash = '-';
    private const char EqualsChar = '=';
    private const byte ForwardSlashByte = (byte)Slash;
    private const char Plus = '+';
    private const byte PlusByte = (byte)Plus;
    private const char Slash = '/';
    private const char Underscore = '_';
    private const int Base64LengthWithoutEquals = 22;

    public static string EncodeBase64String(this Guid guid)
    {
        Span<byte> guidBytes = stackalloc byte[16];
        Span<byte> encodedBytes = stackalloc byte[24];

        MemoryMarshal.TryWrite(guidBytes, ref guid);
        Base64.EncodeToUtf8(guidBytes, encodedBytes, out _, out _);

        Span<char> chars = stackalloc char[Base64LengthWithoutEquals];

        // Replace any characters which are not URL safe.
        // And skip the final two bytes as these will be '==' padding we don't need.
        for (int i = 0; i < Base64LengthWithoutEquals; i++)
        {
            chars[i] = encodedBytes[i] switch
            {
                ForwardSlashByte => Dash,
                PlusByte => Underscore,
                _ => (char)encodedBytes[i],
            };
        }

        return new(chars);
    }

    public static Guid DecodeBase64String(this ReadOnlySpan<char> id)
    {
        Span<char> base64Chars = stackalloc char[24];

        for (var i = 0; i < Base64LengthWithoutEquals; i++)
        {
            base64Chars[i] = id[i] switch
            {
                Dash => Slash,
                Underscore => Plus,
                _ => id[i],
            };
        }

        base64Chars[22] = EqualsChar;
        base64Chars[23] = EqualsChar;

        Span<byte> idBytes = stackalloc byte[16];
        Convert.TryFromBase64Chars(base64Chars, idBytes, out _);

        return new(idBytes);
    }
}

using AutoFixture.Xunit2;
using FluentAssertions;
using Extensions;
using Xunit;

namespace ExtensionTests;

public class GuidExtensionsTests
{
    private const int Base64LengthWithoutEquals = 22;
    private const string EmptyBase64 = "AAAAAAAAAAAAAAAAAAAAAA";

    [Theory]
    [AutoData]
    public void EncodeBase64String_DecodeBase64String_Should_ReturnInitialGuid(Guid guid)
    {
        string actualBase64 = guid.EncodeBase64String();
        actualBase64.Should().NotBe(string.Empty)
            .And.HaveLength(Base64LengthWithoutEquals);

        Guid actualGuid = ((ReadOnlySpan<char>)actualBase64).DecodeBase64String();
        actualGuid.Should().Be(guid);
    }

    [Theory]
    [InlineData(EmptyBase64)]
    public void EncodeBase64String_Should_ReturnEmptyBase64_When_GuidIsEmpty(string expected)
    {
        string actualBase64 = Guid.Empty.EncodeBase64String();
        actualBase64.Should().Be(expected);
    }

    [Theory]
    [InlineData(EmptyBase64)]
    public void DecodeBase64String_Should_ReturnEmptyGuid_When_StringIsEmptyBase64(string base64)
    {
        Guid actual = ((ReadOnlySpan<char>)base64).DecodeBase64String();
        actual.Should().Be(Guid.Empty);
    }
}

更多信息请阅读关于使用高性能技术将GUID转换为Base64编码的文章,以及一个非常好的视频解释

3
如果您的方法无法将传递给它的Base64转换为GUID,那么您应该抛出异常吗?传递给该方法的数据显然是错误的。

我认为我同意你们的观点,抛出异常更有意义。 - Fredou

0

我正在使用一种方法对我的URL(Guid)进行编码和缩短:https://dotnetfiddle.net/iQ7nGv

public static void Main()
{
    Guid gg = Guid.NewGuid();
    string ss = Encode(gg);
    Console.WriteLine(gg);
    Console.WriteLine(ss);
    Console.WriteLine(Decode(ss));
}

public static string Encode(Guid guid)
{
    string encoded = Convert.ToBase64String(guid.ToByteArray());
    encoded = encoded.Replace("/", "_").Replace("+", "-");
    return encoded.Substring(0, 22);
}

public static Guid Decode(string value)
{
    value = value.Replace("_", "/").Replace("-", "+");
    byte[] buffer = Convert.FromBase64String(value + "==");
    return new Guid(buffer);
}

目前你的回答不够清晰。请编辑并添加更多细节,以帮助其他人理解它如何回答所提出的问题。你可以在帮助中心找到有关如何撰写好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接