写点什么

深入剖析 Base64 加解密中遇到的坑点

  • 2025-02-11
    福建
  • 本文字数:2612 字

    阅读完需:约 9 分钟

前言


最近开发过程中遇到了关于使用base64加密传输遇到的神奇问题。需求就是用户的id在链接上露出时需要加密处理,于是后端把下发的用户id改成了base64加密处理后下发了,前端只需要把加密后的用户id原样传给后端就行。就是这个看似简单的流程,前端啥也没干只是原样透传,但后端有概率拿到的用户id不对。


问题描述


本地写个后端服务模拟当时的情景:


后端框架:nest


@Get('getUserInfo') getUserInfo(@Req() req) {  const query = req.query  const cookie = req.cookies  console.log('cookie', cookie)  // 优先取参数中的userId,没有则取cookie中的uid  const userId = query.userId || cookie.uid  // base64加密  const token = Buffer.from(userId).toString('base64')  console.log('加密后的token', token)  // 返回base64加密后的token  return {    code: 0,    data: {      userId,      // base64加密      token    }  }}
复制代码


前端请求后:



服务这边能够正常拿到 cookie 并使用 base64 加密,然后把加密后的 token 返回给前端

前端也正常拿到了后端返回的加密后的 token



最后前端只需要在用户分享时把加密的 token 带在链接上,从这个链接进入时再把链接上加密的token带给后端即可,中间不需要做任何处理。


就是这个过程,出现了奇怪的现象,绝大多数用户都是 OK,但是会有一些用户的 token 带给后端时,后端解不出来了。


心想这跟前端好像没啥关系,因为前端压根没处理后端返回的token,后端给我啥,我只是原样给他传了啥。


经排查发现,所有有问题的用户 id 都是加密后的token中包含了+符号


比如这样的:zm+3DQ/gYeMzQ/HM2L76+CA==传到后端时,所有的+都变成了空格,导致后端解出来是错的



URL 是如何进行编码的


这个问题的主要原因还是因为 URL 被编码造成的,由于请求是get请求,所以最终所有的参数都是拼接在链接上的,最开始前端传给后端的token是没有经过编码的,那它为什么自己编码了?并且编码后与预期的还不一致?


由于种种历史原因,RFC 与 W3C 都定义过 URL 的编码标准


RFC 规范


在 RFC3986 中提到:除了 数字 、 字母 、 -_.~ 不会被转义,其他字符都会被以百分号(%)后跟两位十六进制数 %{hex} 的方式进行转义。在这个规则中空格会被转为%20,而+会被转为%2B



W3C 规范


在 W3C 规范中却又说空格可以被编码为+%20



为什么会同时存在两种规范,这不是在挖坑吗?


因为 URL 中不能存在空格,所以在 URL 中的空格会自动替换成+%20


这就是上面出现+变空格的原因,在你不确定正在以哪一个规范进行编解码时,就很容易出现这个问题。它可能是浏览器造成的,也可能是开发语言的规范不同造成的。


比如 Google 搜索:


当我们搜索s+2时,地址栏出现的是s%2B2



当我们搜索s 2时,地址栏出现的却是s+2



这里就是空格被编码为+了,你要是不了解W3C这条规范,是不是觉得匪夷所思了🤔


前端编码规范


在 JS 中对字符串进行编码的方法有三个:escapeencodeURIencodeURIComponent


escape已经被废弃了,不再推荐使用,所以我们这里只需要关注后面两个的区别


encodeURI


该函数只会编码 URI 中完全禁止的字符。该函数的目的是对 URI 进行完整的编码,因此对以下在 URI 中具有特殊含义的 ASCII 标点符号,encodeURI是不会进行转义的(;/?😡&=+$,#)


所以对于encodeURI来说,空格会被编码为%20,但是+并不会编码。因为空格是 URI 中禁止的字符,而+不是



总结来说就是:


encodeURL 除了这些 A-Z a-z 0-9 ; , / ? : @ & = + $ – _ . ! ~ * ‘ ( ) # 不会被编码,其余字符都会被编码


encodeURIComponent


功能与 encodeURI 类似,但是encodeURIComponent编码的范围更广,并且该函数一般用于对 URI 的参数部分进行编码


对于encodeURIComponent来说,空格会被编码为%20+会被编码为%2B



总结来说就是:


encodeURLComponent 除了这些 A-Z a-z 0-9 - _ . ! ~ * ' ( ) 不会被编码,其余字符都会被编码


两者使用场景的差异


  • 当 encode 的内容不作为 URI 参数时,使用encodeURI进行编码

const url = encodeURI('https://www.qidian.com')// 'https://www.qidian.com'
复制代码


  • 当 encode 的内容作为 URI 参数时,使用encodeURIComponent进行编码

const deepLink = `weixin://webview?url=${encodeURIComponent('https://www.baidu.com')}`//  weixin://webview?url=https%3A%2F%2Fwww.baidu.com
复制代码


结论


对于 JS 的编码方法来说,只有encodeURIComponent会对+进行编码,并且编码规范是RFC3986,也就是说使用这个方法空格会被转为%20,而+会被转为%2B。从而也就不会出现+变空格或空格变+的问题。


上述问题是如何产生的?


上面分别介绍了 URL 的编码规范,以及前端编码方法应用的规范。总结下来就是空格不会在前端产生,前端应用的编码规范不会将空格编码成+,也不会把+解码成空格。



并且特意写了个node服务来模拟当时的场景。


结论是:只要传给后端的base64字符串在前端经过了编码就不会有问题。因为上面我们介绍过浏览器的编码规范,确实是会存在+变空格的问题。所以我们需要主动编码,不要把编码的机会留给浏览器。


  1. 前端编码了,后端拿到的也是正常的



  1. 没编码,后端拿到的+变成空格了



所以当时前端未进行编码时,从 CURL 中就能看到+已经变成了空格,但后面前端编码后,curl 看是正常的,后端解码出来却还是有问题的。


我这边怎么都复现不了当时传给后端是编码过的base64字符串,后端拿到的却还是+变成了空格


没办法,只好找后端同学问问他当时是怎么解码的...


经过一番验证后,结论是他那边多解码了一次,他们框架层有一次自动解码


'zm%2B3DQ%2FgYeMzQ%2FHM2L76CA%3D%3D'   ---->  'zm+3DQ/gYeMzQ/HM2L76CA=='
复制代码


实际上这里就已经是正确的了,但后端同学又自己解码了一次,按理来说再次解码应该也不会有问题



但是!!!这是因为javascript遵循的是RFC3986规范,但java好像并不是


java自带的decode方法底层是这样实现的



这里是按W3C的规范,由于URL中不能存在空格,所以URL Encode 会把空格替换成+,然后解码也同样会将+替换成空格。真相了....


解决方案


  • 按理来说我们只需要保证传给后端的+字符按RFC3986规范编码成了%2B就不会有问题,不要把编码的机会留给浏览器,在 JS 中只需调用encodeURIComponent即可

  • 后端接收到带空格的base64字符串时,通过正则将空格替换为+,因为base64中不会出现空格

  • 由于标准Base64编码包含 64 个字符A-Z, a-z,0-9,+,/,=,有一种 URL safe 的base64格式,把其中的+,/换成-,_,也能够解决上面的问题。


文章转载自:前端南玖

原文链接:https://www.cnblogs.com/songyao666/p/18707544

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
深入剖析Base64加解密中遇到的坑点_前端_快乐非自愿限量之名_InfoQ写作社区