编码和解码字符串
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 |
a | Input 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,否则会插入换行符,这是请求头损坏的常见原因。