又是一个失眠的夜晚,凌晨5点!哎,起来工作吧;

先上2张图:

================= 先说Base64吧,可不是百度,都是自己感悟,逐字写的 ==================

1.首先要理解【可视字符】的概念?

可视字符就是你能看到的字符,我还真不好表达,你要思考一个问题,就是如何把一个字节转换成可视字符,你首先想到的肯定是 ASCII表啊,

你要知道ASCII表里有很多不可视字符,不可视字符,就是控制字符,这些控制字符不可视!那怎么办呢,目前只有2种做法,若有第三种做法

的话可以留言告诉我,我估计没有第三种做法;

做法1:

就是把这个字节,从中间劈开,然后用2个16进制字符来表示,这里要明白什么是16进制,就是 0 - 15,用 0- 9 + A - F(10 - 15)来表示;

一个字节是8bit位,劈开后就是左边4bit + 右边4bit,4bit能表示 多少种情况呢,就是2的4次方,2的4次方是多少呢就是16,所以4bit可以用16进制来表示;

举例:

11111111 --劈开--> 1111 1111 --转换16进制字符表示--> FF;

00000000 --劈开--> 0000 0000 --转换16进制字符表示--> 00;

可见一个字节,可以用2个16进制的字符来表示,而这 0 - 9 + A - F 在ASCII码表里都是【可视字符】,这样就把一个字节变成可视字符了;

 

做法2: 

定义一个转换规则,这个规则如何定义呢,若你是定义这个规则的人,你会如何定义,可以先思考一下?我来说下你接下来的思路做法,这个思路做法估计是大多数程序员的思路;

1. 既然是把一个字节转换成可视字符,那就先找出全球人都能理解的【基本可视】字符,为什么要加个基本呢?$也是可视字符,在美国估计8岁以上的小朋友都知道这个字符代表

   什么意思,因为这个就是美元,但是在中国8岁 - 15岁的小朋友 估计有很大一批都不知道这个符号是啥意思,那么就说明$不能算作全球人都能理解的基本字符;

  经过分析最终得出,全球人认识基本字符有:

  a - z:26个小写字母,西方人肯定都认识,若不认识他们就没法说话,中国的小朋友若不认识就没法读拼音,没法发音,就没法学习语文!

  A - Z:26个大写字母,同理;

  0 - 9:10个阿拉伯数字,这个西方 和 东方 一致啊,真怀疑 人类本身就是外星人,人类就是来侵略地球的外星坏人,还天天寻找外星人;

  好了,有了以上 就是 26 + 26 + 10 = 62个 【全球基本可视字符】,62可不是2的N次方,会导致规则无法定义,那怎么办呢;

  2的6次方 = 64,再加2个字符凑够64个不就好办了,加哪2个字符呢,最终设计这个规则的人 决定用 + / 这2个字符,因为他觉得这2个

  字符很好,认识 0 - 9 的人 都认识 + / ,因为没有  + / 就无法计算啊,真是操蛋 为什么不用 - * 号呢,开玩笑,可能 有更深层的原因

  比如 这个 * 号,在西方 就是乘法符号,在中国 乘法符号 一个叉 x ,呵呵;

  既然他们决定 使用 + / 这2个符号了,那么就凑够了64个基本字符了;就是说终于找出全球人 都认识的 基本字符,共计64个;

 

2. 完成了思路1后,你接下来会思考,一个字节 如何转换成 这64个基本字符的问题;

   64  = 2的6次方,就是说 需要6bit为一组;这样就能表示,就是说bit的总数 需要是6的倍数;

   而1个字节是8bit,那么导致bit的总数一定是8的倍数;

   啊!!!,计算机存储或网络bit的总数一定是8的倍数,而我们需要6的倍数,8 * ? = 6 * ?;

   1个字节 是 8 bit;若6bit为一组,那还剩2bit 怎么办;

   2个字节是 16 bit,若6bit为一组,那还剩4bit 怎么办;

   3个字节是 24bit,正好4组,24 / 6 = 4;

   你会发现,只要【字节】的总数 是 3的倍数,就会【bit的总数】正好是 6的倍数;正好符合我们的要求;

   即3种情况,%是取余的意思:

   A:字节总数 % 3 = 0,正好 

   B:字节总数 % 3 = 1,余数为1个字节,需要考虑 剩余的 2 bit问题,需要填补4个bit;

   C:字节总数 % 3 = 2,余数为2个字节,需要考虑 剩余的 4bit问题,需要填补2个bit;

针对B、C 分别需要填补4bit、2bit,来组合成6bit;既然你填补了 ,那么就要有个标记来区分;

即3种情况:

1. 没填补;尾部不追加;

2. 填补了4bit,尾部追加2个=来标记;

3. 填补了2bit,尾部追加1个 = 来标记;

一个 = 表示填补了2bit;2个 = 就表示填补了4bit;

为什么要用 = 这个符号呢,是因为这个 符号 也是 全球人 都认识 都统一的符号,+ / 总得有 = 号来得出最终的结果吧;

 

3.有了以上2步思路,那么你还需要设计一个 0 - 63的码表,来指示一组6bit 所表示的基本可视字符,就是上图的Base64码表;

 

然后就设计完成了,把一个字节 变成 基本可视字符的规则,以上这个思路的最终规则就是Base64现在的规则;

 

========================================================

1. 什么是Base64?

 Base64是把二进制或字节转成【可视】字符(binary-to-text)的一种编码;

Base64 中的 Base是关键,就是全球人 都认可的 最基本的字符,Base就是根基的意思;

 

2.为什么要Base64?

Aes、Rsa 对称与非对称加密场景,加密后出来的都是字节数组,需要把字节数组转换为可视字符;

举例:加密后得出3个字节,你需要把3个字节转成能看到的字符,若直接根据AscII码表来转换,有的控制字符,不可视;

 

3. MD5为什么不Base64输出?

没有必要,MD5是单向算法,他就是算出来很大的整数值,值足够大能表示很多东西的唯一,

算出来最终是16个字节,固定16个字节,那么就是说这个值能表示的最大值为:2的128次方 够大吧,地球上的每一粒沙子能表示吗;

既然固定16个字节,你若非要用Base64来表示也行,但是通常都是直接 16进制 固定32个16进制字符来表示更直观;

=======================================

用例子来说明 Base64

public class TestMain {
    public static void main(String[] args) throws UnsupportedEncodingException {
        String a = Base64.getEncoder().encodeToString("a".getBytes("UTF-8"));
        String ab = Base64.getEncoder().encodeToString("ab".getBytes("UTF-8"));
        String abc = Base64.getEncoder().encodeToString("abc".getBytes("UTF-8"));

        System.out.println(a);
        System.out.println(ab);
        System.out.println(abc);
    }
}

 

我们来分析 以上 这个例子:

首先看下ascii表,字母 --- 二进制(4位排列)---- 二进制(6位一组排列):

a ---> 0110 0001 ---> 011000 01(按6位一组排列,缺少4位)

ab ---> 0110 0001 0110 0010 ---> 011000 010110 0010(按6位一组排列,缺少2位)

abc ---> 0110 0001 0110 0010 0110 0011 ---> 011000 010110 001001 100011(按6位一组排列,正好,因为 4*6 = 3* 8 = 24)

 

有了以上二进制,下面再继续说,说明一点 a 若按6位一组排列是不够的,缺少低4位,严重注意一点 a 的 011000  01(这个01是要看成高位,为什么要看成高位,因为看成高位才能表达更多字符,这点切记,想象后面是补的0),这个时候 他们规定了一个规则,就是你需要把最后的二进制01看成6位一组的高位

然后就可以计算出对应的值:

a ---- 011000  01 --- 24(Y)  16(Q,切记这个01是高位)  ==

ab ---  011000 010110 0010  ---- 24(Y)  22(W)  8(I,切记0010是高位,后面你可以想象成还有2个0)   =

abc --- 011000 010110 001001 100011  ---  24(Y)  22(W)  9(J)  35(j) 正好,不用补=

 

========================================= 接下来说下UrlEncode ==========================================

url特殊字符:

      十六进制
1 + URL 中+号表示空格 %2B
2 空格 URL中的空格可以用+号或者编码 %20
3 / 分隔目录和子目录 %2F
4 ? 分隔实际的 URL 和参数 %3F
5 % 指定特殊字符 %25
6 # 表示书签 %23
7 & URL 中指定的参数间的分隔符 %26
8 = URL 中指定参数的值 %3D

我们有时候需要在GET的参数中传递参数,例如是byte[]类型的内容,一般来说是转成Base64的编码格式,但是带来一个问题是:base64编码后,里面可能会出现一些字符是url里面冲突的,

例如=+/这些,虽然可以URLEncode进行编码,但这还是会给程序带来一些复杂度或者不确定性。

关于URL编码的详细规范,可以查看RFC 3986, 这里只对我们会涉及的信息做简要说明。

RFC 3986中有如下两个重要规定:

  • Url中只允许包含英文字母(a-zA-Z)、数字(0-9)、- _ . ~4个特殊字符以及所有保留字符。
  • 保留字符包括:! * ' ( ) ; : @ & = + $ , / ? # [ ]

另外,还有很多字符,当它们直接放在Url中的时候,可能会引起解析程序的歧义。

这些字符被视为不安全字符,原因有很多。

  • 空格:Url在传输的过程,或者用户在排版的过程,或者文本处理程序在处理url的过程,都有可能引入无关紧要的空格,或者将那些有意义的空格给去掉;
  • 引号以及<>:引号和尖括号通常用于在普通文本中起到分隔Url的作用;
  • #:通常用于表示书签或者锚点;
  • %:百分号本身用作对不安全字符进行编码时使用的特殊字符,因此本身需要编码;
  • { } | \ ^ [ ] ` ~:某一些网关或者传输代理会篡改这些字符。

这里估计99%的人都会迷糊,呵呵,我将近10多年的经验,才知道答案!!!

首先要知道3个标准:

1. 针对浏览器地址栏URL的标准,在1994年订立的RFC1738中。(显然已过时,可忽略)
对字符串中除了-_.三个字符之外的所有非字母数字字符都替换成百分号(%)后跟两位十六进制数。
十六进制数中字母必须为大写。

http://tools.ietf.org/html/rfc1738

2. 依然是针对浏览器地址栏URL的标准,在2005年定义的RFC3986中,将针对- _.~(可见又扩充了一个波浪线字符)四个字符之外的所有非字母数字字符进行百分号编码。

http://tools.ietf.org/html/rfc3986

3.这个不是针对浏览器地址栏的,而是针对Post请求的form传递数据的,在W3C找到HTML标准的说明
http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4
在这里清楚的看到编码方式是根据ContextType的不同而区别对待的,在form的ContextType是[x-www-form-urlencoded]的时候会对form中的键/值对进行编码,空格被转义成+,

其他字符按照[RFC1738]标准处理成%HH的形式,基本问题就出在这里;

 

 

出现歧义的地方,我来说下:

1. 你要区分 get请求 与 application/x-www-form-urlencoded,我来举个例子:

 

 

知道了以上两种的区别后,针对以上2种,有两套标准;

这2套标准有个细微的差异,那就是空格的问题;

get 请求的话,要求空格 变成 %20;

post_form的话,要求空格变成 + ;

现实中你会发现,别人都帮你搞定了,就是说 get请求的话,你把空格弄成+也没有问题,post_form弄成%20;也没有问题;出现问题的时候再去解决,你有个依据就行;

那么接下来谈一下Java里的 UrlEncoder

 

 

System.out.println(URLEncoder.encode("hello word", "utf-8")); // hello+word

可以看到Java的 URLEncoder是 遵循 post_form规范的,真的吗?

 

总结:http协议中 有post和get提交:

1.如果是get提交 或者是路径的话 如: http://www.bai du.com?wo=he he&ni=abc 就应该遵循RFC1738、RFC2396;就变成:http://www.bai%20du.com?wo=he%20he&ni=abc;

2.如果是post提交:因为post提交的参数依然是被弄成键值对的方式传递的类似GET的QueryString方式,即需要提交的参数应该是: wo=he he&ni=abc;但是由于 html是一种常用语言,它里面有post提价方式,他也有自己的规范,他规定post生成的键值对参数中 参数的值如果有空格应该编码成+号(注意不是%20)见上面链接打开后的下图:

 

 Java官方的URLEncoder.encode 实际上是为了post请求的content-type为x-www-form-urlencoded来设计的。所以没有什么bug可言。

结论:

1.资源路径中含有空格时应该转码为%20,

举例:http://www.baidu.com/he he/index.jsp -----> http://www.baidu.com/he%20he/index.jsp

2.get请求的QueryString里含有空格的话应该转码为%20;

举例:http://www.abc.com?wo=he he ------> http://www.abc.com?wo=he%20he

3.post请求时,content-type = application/x-www-form-urlencoded (一般默认都是这个)时,空格应该转码为+;

举例:向http://www.abc.com/发post请求,参数的值有空格,最终的参数键值对是 wo=he+he; 

 

 2017.11.05补充。。。。。。。。。。。。。。

在Delphi中;

get请求的时候可以用,TNetEncoding.URL.EncodeQuery,

post请求的时候可以用,TNetEncoding.URL.EncodeForm

 

2019-11-03 补充。。。。。。。。。。。。

procedure TfrmAesForm.Button3Click(Sender: TObject);
begin
  {
    空格的问题,根据国际URL标准,GET请求是包含在路径里的,所以与Post请求的标准不同

    GET请求参数包含在URL路径里,他们有一个标准 RFC1738,RFC3986; 此标准要求空格转为%20

    POST请求参数不包含在URL路径里,他们的参数传输有一个自己的标准  application/x-www-form-urlencoded MIME format; 此标准要求空格转为+



  }

  Memo1.Lines.Add(TNetEncoding.URL.EncodeQuery(' '));//%20 GET请求的参数用这个URLEncode

  Memo1.Lines.Add(TNetEncoding.URL.EncodeForm(' '));//+ POST请求的参数用这个URLEncode

  Memo1.Lines.Add(TNetEncoding.URL.Encode(' '));//+ get提交的时候不要用这个,这个会搞成+
end;

 

 

=======================================================================================================================

好了,再来说下这个Base64URL;

我们知道 Base64会出来3个特殊字符,+、/、=,很遗憾这3个字符,都是URL特殊意义的字符;尤其是+号,跨越2个标准,非常容易产生意义,从而

导致Token校验失败;/、=这2个字符基本没有什么异议,get和post_form都会被转换成%XX形式;

System.out.println(URLEncoder.encode("/", "utf-8")); //%2F
System.out.println(URLEncoder.encode("=", "utf-8")); //%%3D

 

这样来回转换标准,各种转换会导致 错误的概率上升;

尤其是Token这样重要的字符串,有的时候需要get请求,有的时候需要http头部,有的时候需要post_form,有的时候需要post_json;

get 需要 urlencode() 采用get标准

post_form 需要 urlencode 采用HTML 标准

http头部,直接base64就可;

post_json,也是二进制的,也是直接 base64即可;

为了避免转换,减少出错的概率,token最好设计成不需要urlencode,绕开这2个标准;

于是就有了接下来的Base64Url,看定义:

 

这样做后,开发中 与 token有关的坑就会消失,就是说 无论是 get 还是 post 还是其它请求,token 都不需要 关心 + 号的问题和 urlEncode UrlDecode的问题

 

这里你可能有个疑问,那就是编码时,把末尾的=标记去除了,那解码时怎么接,没有标记符号了怎么解,答案是:

1Byte 示例:Base64("a") ,需要补4bit 需要2个等号  YQ==

2Byte 示例:Base64("ab")  ,需要补2bit 需要1个等号  YWI=

3Byte 示例:Base64("abc")  ,需要补4bit 需要2个等号 = YWJj

你会发现一个规律,就是Base64出来的结果在字符串一定 /4 整除,所以当尾部的=号被消掉后,依然是可以解码的,Base64Url解码方式:

              1)把"-"替换成"+"

              2)把"_"替换成"/"

              3)(计算BASE64URL编码长度)%4

                         a)结果为0,不做处理

                         b)结果为2,字符串添加"=="

                         c)结果为3,字符串添加"="

 

Java里的方式为:

 

    public static void main(String[] args) throws UnsupportedEncodingException {
        System.out.println("原始Base64");
        System.out.println(Base64.getEncoder().encodeToString("a".getBytes(StandardCharsets.UTF_8)));
        System.out.println(Base64.getEncoder().encodeToString("阿".getBytes(StandardCharsets.UTF_8)));
        System.out.println(Base64.getEncoder().encodeToString("号".getBytes(StandardCharsets.UTF_8)));
        System.out.println("===========");
        System.out.println("Base64Url,但是没有忽略=字符:");
        System.out.println(Base64.getUrlEncoder().encodeToString("a".getBytes(StandardCharsets.UTF_8)));
        System.out.println(Base64.getUrlEncoder().encodeToString("阿".getBytes(StandardCharsets.UTF_8)));
        System.out.println(Base64.getUrlEncoder().encodeToString("号".getBytes(StandardCharsets.UTF_8)));
        System.out.println("===========");
        System.out.println("最终需要的,忽略=字符的:");
        System.out.println(Base64.getUrlEncoder().withoutPadding().encodeToString("a".getBytes(StandardCharsets.UTF_8)));
        System.out.println(Base64.getUrlEncoder().withoutPadding().encodeToString("阿".getBytes(StandardCharsets.UTF_8)));
        System.out.println(Base64.getUrlEncoder().withoutPadding().encodeToString("号".getBytes(StandardCharsets.UTF_8)));
        System.out.println("===========");
        System.out.println("解码方法,统一就这一个:");
        System.out.println(new String(Base64.getUrlDecoder().decode("YQ"), StandardCharsets.UTF_8));
        System.out.println(new String(Base64.getUrlDecoder().decode("6Zi_"), StandardCharsets.UTF_8));
        System.out.println(new String(Base64.getUrlDecoder().decode("5Y-3"), StandardCharsets.UTF_8));
    }

Java官方的Base64Url为:

编码:Base64.getUrlEncoder().withoutPadding().encodeToString("xxx".getBytes(StandardCharsets.UTF_8));
解码:new String(Base64.getUrlDecoder().decode("xxx"), StandardCharsets.UTF_8);

Delphi 官方的Base64Url为:

 

procedure TForm4.btn1Click(Sender: TObject);
begin
  var strs := mmo1.Lines.Text.Split([sLineBreak]);
  mmo2.Lines.Clear;
  for var str in strs do
  begin
    mmo2.Lines.Add(TBase64URLEncoding.Base64URL.Encode(str));
  end;
end;

procedure TForm4.btn2Click(Sender: TObject);
begin
  var strs := mmo2.Lines.Text.Split([sLineBreak]);
  mmo3.Lines.Clear;
  for var str in strs do
  begin
    mmo3.Lines.Add(TBase64URLEncoding.Base64URL.Decode(str));
  end;
end;

TBase64URLEncoding.Base64URL.Encode(str)

TBase64URLEncoding.Base64URL.Decode(str)

-------------

最后不同一下 Base64Url(xxx) != UrlEncode(Base64(xxx));这完全是两码事,是不同的规则定义。

public static void main(String[] args) throws UnsupportedEncodingException {
        System.out.println(Base64.getUrlEncoder().withoutPadding().encodeToString("a".getBytes(StandardCharsets.UTF_8)));
        System.out.println(Base64.getUrlEncoder().withoutPadding().encodeToString("阿".getBytes(StandardCharsets.UTF_8)));
        System.out.println(Base64.getUrlEncoder().withoutPadding().encodeToString("号".getBytes(StandardCharsets.UTF_8)));
        System.out.println("===========");
        System.out.println(URLEncoder.encode(Base64.getEncoder().encodeToString("a".getBytes(StandardCharsets.UTF_8)), "UTF-8"));
        System.out.println(URLEncoder.encode(Base64.getEncoder().encodeToString("阿".getBytes(StandardCharsets.UTF_8)), "UTF-8"));
        System.out.println(URLEncoder.encode(Base64.getEncoder().encodeToString("号".getBytes(StandardCharsets.UTF_8)), "UTF-8"));
        
    }

/* 输出:
YQ
6Zi_
5Y-3
===========
YQ%3D%3D
6Zi%2F
5Y%2B3

*/

 

posted on 2017-03-03 15:17  del88  阅读(7654)  评论(0编辑  收藏  举报