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

前言

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

问题描述

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

后端框架:nest

js 复制代码
@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进行编码
js 复制代码
const url = encodeURI('https://www.qidian.com')
// 'https://www.qidian.com'
  • 当encode的内容作为URI参数时,使用encodeURIComponent进行编码
js 复制代码
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字符串,后端拿到的却还是+变成了空格

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

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

js 复制代码
'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格式,把其中的+,/换成-,_,也能够解决上面的问题。
相关推荐
辻戋4 分钟前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保6 分钟前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun1 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp1 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.2 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl4 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫6 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友6 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理7 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻7 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js