上传文件
  1. base64hub
  2. 在代码中使用 Base64
  3. Kotlin
代码

Kotlin 中的 Base64

Kotlin 自带跨平台的 kotlin.io.encoding.Base64 对象,从 Kotlin 2.2 起成为稳定 API。在 JVM 上也可以使用 java.util.Base64。下面的输出均来自 Java 21 上的 Kotlin 2.4.21。

编码和解码字符串

Base64.encode 接受 ByteArray,返回 String。encodeToByteArray() 和 decodeToString() 在两个方向上处理 UTF-8:

import kotlin.io.encoding.Base64

val enc = Base64.encode("héllo wörld".encodeToByteArray())
println(enc)                                // aMOpbGxvIHfDtnJsZA==
println(Base64.decode(enc).decodeToString()) // héllo wörld

Base64 本身就是标准实例(Base64.Default)。此外还有 Base64.UrlSafe 和 Base64.Mime。

在早于 2.2 的 Kotlin 中,这个 API 需要 @OptIn(ExperimentalEncodingApi::class)。如果你只能用旧版本,java.util.Base64 的用法与 Java 中的 Base64 完全相同:

java.util.Base64.getEncoder().encodeToString("hi".toByteArray())  // aGk=

典型用途是 HTTP 请求头和 Data URI:

val token = Base64.encode("user:pass".encodeToByteArray())
println("Authorization: Basic $token")       // Authorization: Basic dXNlcjpwYXNz

val png = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47)
println("data:image/png;base64," + Base64.encode(png))  // data:image/png;base64,iVBORw==

URL 安全的 Base64 与填充选项

Base64.UrlSafe 把 +/ 换成 -_。填充由 withPadding 控制,它会返回一个新的配置实例:

val d = "<<???>>".encodeToByteArray()

println(Base64.UrlSafe.encode(d))                  // PDw_Pz8-Pg==

val urlNoPad = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT)
println(urlNoPad.encode(d))                        // PDw_Pz8-Pg
println(urlNoPad.decode("PDw_Pz8-Pg").decodeToString())  // <<???>>

四个选项:

选项编码解码
PRESENT(默认)写出 =要求有 =
ABSENT不写 =拒绝 =
PRESENT_OPTIONAL写出 =两种都接受
ABSENT_OPTIONAL不写 =两种都接受

默认实例在这一点上很严格:

Base64.UrlSafe.decode("PDw_Pz8-Pg")
// IllegalArgumentException: The padding option is set to PRESENT,
// but the input is not properly padded

对于 JWT 的各部分和其他令牌,通常应选 ABSENT_OPTIONAL 或 PRESENT_OPTIONAL,因为不同的生成方对填充的处理并不一致。背景知识见 Base64 与 Base64URL 的区别 和 填充详解。

例如读取 JWT 的载荷:

val jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.sig"
val url = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL)
println(url.decode(jwt.split(".")[1]).decodeToString())   // {"sub":"1234"}

MIME 换行

Base64.Mime 每行写 76 个字符,用 \r\n 分隔,解码时会忽略行分隔符:

Base64.Mime.encode("x".repeat(60).encodeToByteArray())
// "eHh4eHh4...eHh4" (76 chars) + "\r\n" + "eHh4"

Base64.Mime.decode("aGVs\r\nbG8K").decodeToString()  // "hello\n"

想对比的话,把同样的输入粘贴到 MIME Base64 编码器 中即可。

编码和解码文件

小文件可以直接放进内存:

import java.io.File

println(Base64.encode(File("hello.txt").readBytes()))   // aGVsbG8K
File("copy.txt").writeBytes(Base64.decode("aGVsbG8K"))

流式处理大文件

在 JVM 上,encodingWith 和 decodingWith 用来包装流。它们在 Kotlin 2.4 中仍是实验性的,因此需要 opt-in,并单独导入:

import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.io.encoding.decodingWith
import kotlin.io.encoding.encodingWith
import java.io.File

@OptIn(ExperimentalEncodingApi::class)
fun encodeFile(src: File, dst: File) {
    dst.outputStream().encodingWith(Base64).use { out ->
        src.inputStream().use { it.copyTo(out) }
    }
}

@OptIn(ExperimentalEncodingApi::class)
fun decodeFile(src: File, dst: File) {
    src.inputStream().decodingWith(Base64).use { inp ->
        dst.outputStream().use { inp.copyTo(it) }
    }
}

关闭编码流时才会写出最后一组和填充,use 块会替你完成关闭。一个 300 KB 测试文件的流式输出与 Base64.encode(readBytes()) 完全相同,往返后也得到相同的字节。如果你更想走非实验性的路线,java.util.Base64.getEncoder().wrap(outputStream) 也能完成同样的工作。

校验与异常信息

Kotlin 的解码器比 Java 的更严格。它会拒绝不为零的填充位,而 Java 和大多数其他库都接受。每种失败都是 IllegalArgumentException:

传给 Base64.decode 的输入错误信息
PDw_Pz8-Pg==Invalid symbol '_'(137) at index 3
"aGVs\nbG8K"Invalid symbol '\n'(12) at index 4
aGk=!!Symbol '!'(41) at index 3 is prohibited after the pad character
aGk==Symbol '='(75) at index 3 is prohibited after the pad character
aGl=The pad bits must be zeros
aInput should have at least 2 symbols for Base64 decoding, startIndex: 0, endIndex: 1

括号中的数字是字符编码的八进制值(137 是 _,12 是换行符)。校验辅助函数只需一行:

fun isBase64(s: String) = runCatching { Base64.decode(s) }.isSuccess

想看某个字符串具体在哪里出错,可以用 Base64 校验工具;Base64 错误指南 列出了常见原因。

常见陷阱

  • 用默认实例解码不带填充的输入:JWT 的各段和很多令牌都没有 =。请配置 PRESENT_OPTIONAL 或 ABSENT_OPTIONAL,而不是手动给字符串补填充。
  • Android 的 DEFAULT 标志:android.util.Base64.encodeToString(bytes, Base64.DEFAULT) 每 76 个字符以及末尾都会插入换行符。放在 HTTP 请求头里,请求就会出错。请用 NO_WRAP,令牌则用 NO_WRAP or URL_SAFE or NO_PADDING。
  • String.toByteArray() 的字符集:在 Kotlin 中它默认就是 UTF-8,不像旧版 JDK 上 Java 的 getBytes(),所以 "héllo".toByteArray() 是安全的。
  • 把二进制数据解码成字符串:对图片字节调用 decodeToString() 会把无效序列替换成 U+FFFD,从而损坏数据。请保留 ByteArray。

版本说明

  • Kotlin 1.8.20:以实验性 API 的形式加入 kotlin.io.encoding.Base64。
  • Kotlin 2.0.20:withPadding 和 PaddingOption。
  • Kotlin 2.2:核心 API 进入稳定状态;JVM 上的流辅助函数仍为实验性。

常见问题

还需要写 @OptIn(ExperimentalEncodingApi::class) 吗?

在 Kotlin 2.2 及更高版本中,Base64.encode 和 Base64.decode 不再需要。但流辅助函数 encodingWith 和 decodingWith 在 Kotlin 2.4 中仍然需要显式选择加入(opt-in)。

在 Kotlin 中如何解码不带填充的 Base64?

使用配置过的实例:对不带填充的输入用 Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT),或者用 PRESENT_OPTIONAL 同时接受两种情况。默认实例要求必须有填充。

应该用 android.util.Base64 还是 kotlin.io.encoding.Base64?

新代码中,Kotlin API 属于标准库的一部分,在所有 Android 版本上都能用。android.util.Base64 除非传入 Base64.NO_WRAP,否则会插入换行符,这是请求头损坏的常见原因。

继续阅读