ES6+ 现在就用系列(五):模板字面量 (Template Literals)

模板字面量

字符串替换

这个和C#6 里面的字符串插值类似。原来ES5里字符串要连接,一般就是用+

特性

  1. 用反引号(`)标识, 它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

  2. 如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

  3. 如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。

  4. 大括号内部可以放入任意的JavaScript表达式,可以进行运算,以及引用对象属性。

  5. 模板字符串之中还能调用函数。

示例代码:

简单字符串替换

1
2
3
4
var name = "Brendan";
console.log(`Yo, ${name}!`);

// => "Yo, Brendan!"

表达式

1
2
3
4
5
6
7
8
var a = 10;
var b = 10;
console.log(`JavaScript first appeared ${a+b} years ago. Crazy!`);

//=> JavaScript first appeared 20 years ago. Crazy!

console.log(`The number of JS MVC frameworks is ${2 * (a + b)} and not ${10 * (a + b)}.`);
//=> The number of JS frameworks is 40 and not 200.

函数

1
2
3
function fn() { return "I am a result. Rarr"; }
console.log(`foo ${fn()} bar`);
//=> foo I am a result. Rarr bar.

$() 可以使用任何表达式和方法调用

1
2
3
4
5
6
7
8
9
10
var user = {name: 'Caitlin Potter'};
console.log(`Thanks for getting this into V8, ${user.name.toUpperCase()}.`);

// => "Thanks for getting this into V8, CAITLIN POTTER";

// And another example
var thing = 'drugs';
console.log(`Say no to ${thing}. Although if you're talking to ${thing} you may already be on ${thing}.`);

// => Say no to drugs. Although if you're talking to drugs you may already be on drugs.

示例代码:

ES5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'use strict';

var customer = { name: "Foo" };
var card = { amount: 7, product: "Bar", unitprice: 42 };

var message = "Hello " + customer.name + ",\n" +
"want to buy " + card.amount + " " + card.product + " for\n" +
"a total of " + (card.amount * card.unitprice) + " bucks?";

console.log(message);

输出:

Hello Foo,
want to buy 7 Bar for
a total of 294 bucks?

ES6:

1
2
3
4
5
6
7
8
9
10
11
var customer = { name: "Foo" }
var card = { amount: 7, product: "Bar", unitprice: 42 }
message = `Hello ${customer.name},
want to buy ${card.amount} ${card.product} for
a total of ${card.amount * card.unitprice} bucks?`

输出:

Hello Foo,
want to buy 7 Bar for
a total of 294 bucks?

Tagged Templates (标签模板?不知道如何翻译)

比如

1
fn`Hello ${you}! You're looking ${adjective} today!`

实际上等于

fn(["Hello ", "! You're looking ", " today!"], you, adjective);

fn可以是任何函数名,也就是把字符串分解传到到方法的第一个参数里,第一个参数必须是数组,数组的每一项,就是被$()分开的没一串字符, 每一个$()里面的值将传给函数的剩余参数。等于下面函数定义,strings是一个数组,values是Rest参数。

1
fn(strings, ...values)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = 5;
var b = 10;

function tag(strings, ...values) {
console.log(strings[0]); // "Hello "
console.log(strings[1]); // " world "
console.log(values[0]); // 15
console.log(values[1]); // 50

return "Bazinga!";
}

tag`Hello ${ a + b } world ${ a * b }`;
// "Bazinga!"

有了 tagged template 我们可以让代码看起来更简洁,比如我们可以把下面的调用

1
get([ "http://example.com/foo?bar=", "&quux=", "" ],bar + baz, quux);

用新的写法

1
get`http://example.com/foo?bar=${bar + baz}&quux=${quux}`

String.raw

存取 raw template string, 就是如果遇见\将增加一个,然后原样输出。

1
2
3
4
5
6
let interpreted = 'raw\nstring';
let esaped = 'raw\\nstring';
let raw = String.raw`raw\nstring`;
console.log(interpreted); // raw
// string
console.log(raw === esaped); // true

ES6+ 现在就用系列(四):箭头函数 =>

箭头函数 =>

ES6 允许使用 => 来定义函数, 他是函数的缩写,这个熟悉C#的人应该了解,这其实就是C#里的lamda表达式

他不只是语法糖 (Syntax sugar), 箭头函数自动绑定 定义此函数作用域的this(Arrow functions automatically bind “this” from the containing scope.)

** 箭头函数没有自己的this,所以内部的this就是外层代码块的this。**

定义格式

1
(<arguments>) => <return statement>

当只有一个参数时,括号可省略,下面两种写法是等价的.

1
2
(x) => x * x
x => x * x

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
'use strict';
// 数组
const items = [1, 2, 3, 4];

// lamda 表达式
let byTwo = items.map(i => i * 2);

// 可以使用block
let byFour = items.map(i => {
return i * 2;
});

// 绑定this
function Person() {
this.company = "deshui.wang";
this.Names = ["Jack", "Alex", "Eric"];
this.print = () => {
return this.Names.map((n) => {
return n + " is from " + "company "+ this.company;
});
};
}

console.log(new Person().print());

// 输出:
[
'Jack is from company deshui.wang',
'Alex is from company deshui.wang',
'Eric is from company deshui.wang'
]

注意事项

  1. 箭头函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象, 原因是箭头函数没有自己的 this.

  2. 不可以当作构造函数,不可以使用 new 命令。

  3. 不可以使用 arguments 对象,该对象在函数体内不存在。可以用 Rest 参数代替。

  4. 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。

  5. arguments、super、new.target 在在箭头函数之中是不存在的,他们指向外层函数的对应变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
        function hello() {
    setTimeout( () => {
    console.log("args:", arguments);
    },100);
    }

    hello( 1, 2, 3, 4 );

    // 输出 1, 2, 3, 4
  6. 箭头函数没有自己的 this,所以不能用call()、apply()、bind()这些方法去改变 this 的指向。

ES6+ 现在就用系列(三):const 命令

本文以及以后讨论的代码,都必须是在严格模式下,因为非严格模式下,有一些写法也符合,所以我们建议代码始终使用严格模式

定义

在之前的ES版本里是没有常量的概念的,常量,就是一旦申明,值就不能改变的。

1
2
3
4
5
'use strict';
const PI = 3.1415;
console.log(PI) // 3.1415

PI = 3; // TypeError: Assignment to constant variable.

特性

  • 一旦申明,必须初始化

  • 作用域只在声明所在的块级,和let相同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
        'use strict';
    const apiBase = "https://deshui.wang/api/v1/";
    const clientId = "123456";

    //block scoped
    if (true) {

    const apiBase = "https://cnblogs.com/api/";

    console.log(apiBase + clientId);
    // https://cnblogs.com/api/123456
    }

    console.log(apiBase+clientId);
    // https://deshui.wang/api/v1/123456

    apiBase = "https://google.com/api";
    //Identifier 'apiBase' has already been declared
  • const 申明的变量,在一个作用域内也不能与let和var申明的重名

  • 如果 const 申明的是个复合类型的变量,那么变量名不指向数据,而是指向数据所在的地址。const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变。

    1
    2
    3
    4
    5
    6
    7
    8
        'use strict';        
    const a = [1, 2];
    a.push(3);
    console.log(a); // 1,2,3
    a.length = 0;
    console.log(a); // []

    a = [4]; // TypeError: Assignment to constant variable.

全局变量

全局对象是最上层层的对象,在浏览器里指的是window对象,在Node.js指的是global对象。

1
2
3
4
5
// 'use strict';
var a="hello";
console.log(global.a);

// 输出: undefined

var 命令和 function 命令声明的全局变量,依旧是全局对象的属性;let命令、const命令、class命令声明的全局变量,不属于全局对象的属性。

上面的代码在Node.js下是不行的,但是浏览器却可以,不管是不是严格模式。

1
2
3
4
5
// 'use strict';
var a="hello";
console.log(window.a);

// 输出: hello

但是,如果使用let, 那么属性将不绑定到window (Chrome developer tools 需要使用以下方法才能打开严格模式)

1
2
3
4
5
6
7
8
(function(){
'use strict'
let a="hello";
console.log(window.a);

})()

// 输出undefined

ES6+ 现在就用系列(二):let 命令

ES6新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。也就是有了块级作用域。

为什么需要块级作用域?

避免 var 变量提升带来的副作用

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var saleCount = 20;

function f(){
console.log(saleCount);
if(saleCount<100)
{
// according some rule, change it to 100
var saleCount=60;
console.log(saleCount);
}
}

f()

输出: // undefined

因为 “var saleCount=60;” 作用域是整个函数,而JavaScript里var定义的变量存在变量提升,也就是console.log(saleCount), 这个saleCount是 “var saleCount=60;” 这一句定义的,当调用的时候,saleCount的值是undefined. 实际上等于下面代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var saleCount = 20;

function f(){

var saleCount;
console.log(saleCount);
if(saleCount<100)
{
// according some rule, change it to 100
saleCount=60;
console.log(saleCount);
}

}

f() // undefined

避免循环变量变为全局变量

1
2
3
4
5
6
7
8
9
10
示例:

for (var i = 0; i < 10; i++){
// do something
}

console.log(i);
输出: 10

很明显,我们不希望i,这个变量变为全局变量。

let 示例代码

1
2
3
4
5
6
7
8
9
10
'use strict'
{
var b=1;
let a=2;
}

console.log(a);
console.log(b);

# 输出: ReferenceError: a is not defined

上一节我们给出了如下的示例:

1
2
3
4
5
6
7
8
9
10
11
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[1]();
a[2]();
a[3]();

输出: 10,10,10

我们看到,输出的结果不是我们想要的,因为i是用var定义的,那么他在全局范围内都是生效的,也就是我们循环结束以后,i的值就是10,那么不管调用数组的那个元素,console.log(i) 输出的都是10, 那么let因为有了块级作用域,就可以避免这个问题。

1
2
3
4
5
6
7
8
9
10
11
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[1]();
a[2]();
a[3]();

输出 1, 2,3

另外,函数本身的作用域也在定义他的块的作用域内。

1
2
3
4
5
6
7
function hello(){console.log("Hello, Jack")};

{
function hello(){console.log("Hello, Tom")};
}

hello();

上面的代码在ES6里面输出了”Hello, Jack”, 而在ES5里输出了”Hello, Tom”.

注意事项

不能先使用,后定义

1
2
3
4
5
6
7
8
9
console.log(x);
console.log(y);

var x = 1;
let y = 2;

# 输出
undefined
ReferenceError: y is not defined

上面的代码由于x是var定义的,一开始x的变量是存在的,只是值是undefined, 但是由于y 是let定义的,就不存在变量提升。

暂时性死区

如果一个变量是使用let定义的,那么这个变量就属于声明时所在的代码块,也就是变量不再受外部影响,下面的a 由于在块里定义了,所以 会报错,因为在那个块里是先使用后定义,如果去掉“let a”, 那么a就是外部的变量,这个时候就不会出错。

1
2
3
4
5
6
7
8
var a = "hello";

{
a = 'world';
let a;
}

// ReferenceError

不能重复申明

也就是不能重复申明同一个变量,即使一个是let申明,一个是用var申明也不行。 下面的代码都会报错。

1
2
3
4
function () {
let a = 10;
var a = 1;
}
1
2
3
4
function () {
let b = 10;
let b = 1;
}

总结

由于let 避免了很多问题,所以建议在ES6的代码里总是使用let 来替代var.

ES6+ 现在就用系列(一):为什么使用ES6+

ES6+

现在主流的浏览器都是支持到ES5, 为了表述方便,我在此发明一个名词”ES6+” 就是ES5以后的版本,包括ES6, ES7. 为什么说现在就用,虽然主流的浏览器只支持到ES5, 但是现在有很多的转换器,可以把一些ES6和ES7的代码转换为ES5的代码。这就意味着我们现在就可以使用这些新特性,然后使用转码器让代码可以运行在主流的浏览器上。

为什么立即开始使用ES6, ES7的新特性?

JavaScript语言的一些糟糕的实现

先不说JavaScript语言本身设计是否有问题,现有JavaScript语言的实现里有很多非常糟糕或者诡异的实现,就是你以为代码的结果是这样,但是他偏偏是那样,这给我们程序带了很多的意向不到的Bug和烦恼,如果你要是JavaScript大牛,你需要了解他内部的实现的Bug, 而且要知道哪些诡异的写法输出了什么诡异的结果,我个人对了解这种东西实在提不起太大的兴趣,因为我只想用“语言”来实现我的项目让人很好的使用我开发的软件,但是由于历史这样或那样的原因,导致JavaScript语言成为浏览器的霸主,我们不得不忍受这些糟糕的问题。下面我来展示一些让你觉得诡异的问题 (如果你不不觉得诡异,恭喜你,你已经是JavaScript的“高手”)

示例1:

1
2
3
4
5
(function() {
return NaN === NaN;
})();

输出: false

示例2:

1
2
3
4
5
(function() {
return (0.1 + 0.2 === 0.3);
})();

输出: false

示例3:

1
2
3
[5, 12, 9, 2, 18, 1, 25].sort();

输出: [1, 12, 18, 2, 25, 5, 9]

示例4:

1
2
3
4
5
6
7
8
9
10
11
var a = "1"
var b = 2
var c = a + b

输出:c = "12"

var a = "1"
var b = 2
var c = +a + b

输出:c = 3

示例5:

1
2
3
4
5
(function() {
return ['10','10','10','10'].map(parseInt);
})();

输出: [10, NaN, 2, 3]

示例6:

1
2
3
4
5
(function() {
return 9999999999999999;
})();

输出: 10000000000000000

示例7:

1
2
3
4
5
6
7
8
9
10
11
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[1]();
a[2]();
a[3]();

输出: 10,10,10

我是觉得如果按正常人的理解,代码不能得到想要的结果,那就算是语言本身的问题。如果一个程序执行的和人期望的不一样,或者还需要一些Hack的方法,那么是很糟糕的。

ES5 一些语言特性的缺失

由于上面的很多问题,所以ES 需要不断的改进, 当然新的版本肯定不可能一下子解决之前所有的问题。

已有JavaScript的问题这一块就不细说了,因为能来看这篇文章的人,应该对下面我列的几个突出的问题都有感受。

  • 没有块级作用域,这个导致上面示例7的问题
  • 全局变量的污染
  • 类的写法比较怪异
  • 没有模块管理
  • 异步调用写法容易产生 “回调地狱”

为什么可以立即使用?

因为现在很多转换器已经可以把ES6所有的特性以及ES7的部分特性转换为ES5,Babel就是一个非常好的转换器,所以我这里建议凡是能被Babel转换的新特性都可以立即在项目里适用。

ES6和ES7的一些新特性,可以大大提高项目的健壮性,同时让代码更易读,同时也可以避免很多ES5之前的很多诡异的东西。Gulp里可以很好的使用babel, 如果你对Gulp不熟悉,可以参考我博客里的Gulp系列。

这里简单说一Gulp和babel如何结合使用

1
2
3
4
5
6
7
8
9
$ npm install -g gulp-babel

var gulp=require('gulp'), babel=require('gulp-babel');

gulp.task('build',function(){
return gulp.src('src/app.js')
.pipe(babel())
.pipe(gulp.dest('build'))
})


后面的系列,我将以此介绍ES6, ES7的一些可以现在就用的主要特性。

程序员之网络安全系列(六):动态密码

前文回顾

我们使用了数字证书,确保了对方的公钥身份,也就是互联网中确定了要访问的网站就是你要访问的网站。

但是我们如何确定要访问这个网站的用户就是要访问的用户呢? 对银行来说需要确保“敏捷的水”登录银行时,必须是”敏捷的水” 而不是别人,不然别人就把钱转走了。

虽然我们从通信,数据加密等方式确保用户密码不背攻击者破解,但是如果攻击者使用键盘记录器等工具知道了用户密码,那么就可以冒充用户了。

比如银行的U盾,因为我对这块业务不了解,我猜银行为每个用户发放了一个公钥?( 知道的同学,可以帮忙解释一下银行的U盾都做了什么? )

我们用数字证书确定了银行的身份,那么银行如何确定我们的身份呢?

两步验证

那么什么是两步认证呢?两步认证就是在每次登陆时候填一个手机短信收取的验证码或者手机应用生成的验证码。当然接收验证码的手机号或者应用是需要绑定的,这样只有拿到这部手机并且知道你帐号密码的人才能登陆帐号。

为什么需要它?

对有些人来说,盗取密码比您想象的更简单

以下任意一种常见操作都可能让您面临密码被盗的风险:

  • 在多个网站上使用同一密码
  • 从互联网上下载软件
  • 点击电子邮件中的链接

想像一下您无法访问自己的帐户及其中的内容,当别有用心的人盗取您的密码后,他们能让您无法访问自己的帐户,还可以执行以下操作:

  • 翻看(甚至删除)您所有的电子邮件、联系人、照片等
  • 冒充您给您的联系人发送垃圾邮件或有害的电子邮件
  • 使用您的帐户重置您其他帐户(银行帐户、购物帐户等)的密码

两步验证可以将别有用心的人阻挡在外,即使他们知道您的密码也无可奈何。

如何工作?

现在大部分比较危险的操作都需要绑定手机号,因为手机号是你用的唯一的。接收到验证码后,我们再输入系统做第二次的验证。

但是由于我们这个验证码也有可能丢失,那么我们只需要让他在一定时间有效就可以了,这就是OTP.

动态口令 (One Time Password)

动态密码: 一个OTP(One Time Password) 是一个密码仅用于一次登录会话或者交易,使用过后,这个密码就无效了。

静态密码的问题:

  • 容易被破解
  • 容易被猜测
  • 容易被盗劫
  • 针对不同的网站,用户需要记忆大量的密码。

使用动态口令主要有2个方面价值:

  • 防止由于盗号而产生的财产损失。
  • 采用动态口令的单位无需忍受定期修改各种应用系统登录密码的烦恼。

有两种方法,生成动态密码:

Event-based OTP (EOTP)

基于事件同步的令牌,其原理是通过某一特定的事件次序及相同的种子值作为输入,在DES算法中运算出一致的密码,其运算机理决定了其整个工作流程同时钟无关,不受时钟的影响,令牌中不存在时间脉冲晶振。但由于其算法的一致性,其口令是预先可知的,通过令牌,你可以预先知道今后的多个密码,故当令牌遗失且没有使用PIN码对令牌进行保护时,存在非法登陆的风险,故使用事件同步的令牌,对PIN码的保护是十分必要的。同样,基于事件同步的令牌同样存在失去同步的风险,例如用户多次无目的的生成口令等,对于令牌的失步,事件同步的服务器使用增大偏移量的方式进行再同步,其服务器端会自动向后推算一定次数的密码,来同步令牌和服务器,当失步情况已经非常严重,大范围超出正常范围时,通过连续输入两次令牌计算出的密码,服务器将在较大的范围内进行令牌同步,一般情况下,令牌同步所需的次数不会超过3次。但在极端情况下,不排除失去同步的可能性,例如电力耗尽,在更换电池时操作失误等。此时,令牌仍可通过手工输入由管理员生成的一组序列值来实现远程同步,而无需寄回服务器端重新同步。

Time based OTP (TOTP)

基于令牌和服务器的时间同步,通过运算来生成一致的动态口令,基于时间同步的令牌,一般更新率为60S,每60S产生一个新口令,但由于其同步的基础是国际标准时间,则要求其服务器能够十分精确的保持正确的时钟,同时对其令牌的晶振频率有严格的要求,从而降低系统失去同步的几率,从另一方面,基于时间同步的令牌在每次进行认证时,服务器端将会检测令牌的时钟偏移量,相应不断的微调自己的时间记录,从而保证了令牌和服务器的同步,确保日常的使用,但由于令牌的工作环境不同,在磁场,高温,高压,震荡,入水等情况下易发生时钟脉冲的不确定偏移和损坏,故对于时间同步的设备进行较好的保护是十分必要的。对于失去时间同步的令牌,目前可以通过增大偏移量的技术(前后10分钟)来进行远程同步,确保其能够继续使用,降低对应用的影响,但对于超出默认时间(共20分钟)的同步令牌,将无法继续使用或进行远程同步,必须返厂或送回服务器端另行处理。同样,对于基于时间同步的服务器,应较好地保护其系统时钟,不要随意更改,以免发生同步问题,从而影响全部基于此服务器进行认证的令牌。

** 以上两种方式在生成密码的过程都不需要与服务器通信,所以极大的保证了密码的安全。**

算法的实现

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

程序员之网络安全系列(五):数字证书以及12306的证书问题

前文回顾

假如,明明和丽丽相互不认识,明明想给丽丽写一封情书,让隔壁老王送去

  1. 如何保证隔壁老王不能看到情书内容?(保密性)
  2. 如何保证隔壁老王不修改情书的内容?(完整性)
  3. 如何保证隔壁老王不冒充明明?(身份认证)
  4. 如何保证明明不能否认情书是自己写的?(来源的不可否认)

我们使用了非对称密钥算法,我们让“隔壁王叔叔”传递了秘钥。

中间人攻击

上面几步还是不够的,比如王叔叔在交换秘钥的过程中做了手脚呢?

如何做手脚?看下图:

  1. 王叔叔自己生成一个公私钥,和明明以及丽丽交换。
  2. 王叔叔冒充丽丽把自己的公钥发给明明。
  3. 明明用王叔叔的公钥对信件加密。
  4. 王叔叔用自己的私钥解密就可以看到明明给丽丽的邮件。
  5. 王叔叔冒充明明把自己的公钥发给丽丽。
  6. 丽丽用王叔叔的公钥对信件加密。
  7. 王叔叔用自己的私钥解密就可以看到丽丽给明明内容。

那么明明如何知道王叔叔给的公钥就是丽丽的公钥呢?那么就引入了数字证书

数字证书

概念介绍

那么王叔叔要让明明相信他给的公钥就是丽丽的公钥,那么他可以开一个证明,比如找权威机构“敏捷的水”给开个介绍信,介绍信上给加个公章。那么这里的介绍性就是数字证书, 公章就是数字签名, 那么”敏捷的水”就是颁发证书的机构CA(Certificate Authority),也就是证书授权中心

CA CA 是“Certificate Authority”的缩写,也叫“证书授权中心”。
它是负责管理和签发证书的第三方机构,就好比例子里面的“敏捷的水”。一般来说,CA 必须是大家都信任的、认可的。因此它必须具有足够的权威性。只有明明和丽丽都信任的人才能来证明,对吧?

CA证书: CA 证书,就是CA颁发的证书。

证书之间的信任关系: 用一个证书来证明另一个证书是真实可信。

证书信任链: 证书之间的信任关系,是可以嵌套的。比如,A 信任 A1,A1 信任 A2,A2 信任 A3……,这就是证书的信任链。只要你信任链上的第一个证书,那后续的证书,都是可以信任的。

根证书(Root Certificate): 下图,处于最顶上的树根位置的那个证书,就是“根证书”。除了根证书,其它证书都要依靠上一级的证书来证明自己。那谁来证明“根证书”?根证书自己证明自己,这时候我们用户就需要自己选择是否相信某个根证书。

根证书是整个证书体系安全的根本。如果某个证书体系中,根证书不再可信了,那么所有被根证书所信任的其它证书,也就不再可信了。  

证书内容

我们看一下亚马逊的证书,当我们点击浏览器左边绿色的锁时,我们可以看到如下的证书。

我们挑重点的解释一下

  • Issuer (证书的发布机构)
    指出是什么机构发布的这个证书,也就是指明这个证书是哪个公司创建的(只是创建证书,不是指证书的使用者)。对于上面的这个证书来说,就是指”Symantec Corporation”这个机构。

  • Not valid before, Not valid after (证书的有效期)

  • Public key (公钥)

这个我们在前面介绍公钥密码体制时介绍过,公钥是用来对消息进行加密的。
  • Subject (主题)

这个证书是发布给谁的,或者说证书的所有者,一般是某个人或者某个公司名称、机构的名称、公司网站的网址等。

  • Signature algorithm (签名所使用的算法)

就是指的这个数字证书的数字签名所使用的加密算法,这样就可以使用证书发布机构的证书里面的公钥,根据这个算法对指纹进行解密,指纹的加密结果就是数字签名。

  • Thumbprint, Thumbprint algorithm (指纹以及指纹算法)

这个是用来保证证书的完整性的,也就是说确保证书没有被修改过,其原理就是在发布证书时,发布者根据指纹算法(一个hash算法)计算整个证书的hash值(指纹)并和证书放在一起,使用者在打开证书时,自己也根据指纹算法计算一下证书的hash值(指纹),如果两者一致,就说明证书没有被修改过,因为证书的内容被修改后,根据证书的内容计算的出的hash值(指纹)是会变化的。 注意,这个指纹会使用CA这个证书机构的私钥用签名算法(Signature algorithm)加密后和证书放在一起,只有用CA的公钥才能解开这个签名。

证书是如何保证身份认证的

申请证书:

  1. Amazon.com 向Symantec 公司(CA) 申请证书。
  2. Symantec(CA) 生成一对公钥A和私钥B。
  3. Symantec(CA) 有自己的公钥C和私钥D。
  4. Symantec(CA) 把Issuer,公钥A,Subject(一般是网站的域名),Valid from,Valid to等信息以明文的形式写到证书里面,然后用一个指纹算法(SHA1或者MD5
    )计算出这些数字证书内容的一个指纹(摘要),并把指纹和指纹算法用自己的私钥D进行加密,然后和证书的内容一起发给Amazon.com。
  5. Symantec(CA) 把私钥B给Amazon.com.

如何使用证书

  1. 用户访问amazon.com 这个网站
  2. amazon.com 把证书发给用户
  3. 浏览器读取证书。
  4. 浏览器发现证书机构是Symantec,然后会在操作系统中受信任的发布机构的证书中去找Symantec的证书,如果找不到,那说明证书的发布机构是个假的,或者不是被权威机构认证的,证书可能有问题,程序会给出一个错误信息。
  5. 如果在系统中找到了Symantec的证书,那么应用程序就会从证书中取出Symantec的公钥C,然后对amzon.com公司的证书里面的指纹和指纹算法用这个公钥C进行解密,然后使用这个指纹算法计算amazon.com证书的指纹,将这个计算的指纹与放在证书中的指纹对比,如果一致,说明amazon.com的证书肯定没有被修改过并且证书是Symantec发布的,证书中的公钥肯定是amazon.com的公钥A, 然后我们就可以用这个公钥A和amazon.com进行通信,因为只有amazon.com 有私钥B, 所以只有amazon.com才能解开信息。

注意 权威机构的证书都是内置在操作系统里的。

由此可见,一个证书受不受信任,那就要看你要不要添加到操作系统里,权威的认证机构的证书都内置在操作系统里的。

另外,我们自己也可以制作自签名的证书,但是需要别人认可你,这个在企业内部或者开发阶段是可以,我们可以自己制作一个证书添加到操作系统里。

那么,问题来了,当你访问https://www.12306.cn 时,你就会得到下面的结果

这是为什么呢?

我相信你看完本文应该清楚,那是因为12306 自己给自己发了个证书,而这个证书默认是没有被操作系统信任,

但是当我把根证书添加到操作系统后,依然是https://www.12306.cn 不行,然后我发现是证书对应的域名不对,证书对应的域名是https://kyfw.12306.cn 访问这个域名后,虽然证书验证通过,但是浏览器的锁还是没有变绿

为什么呢? 看提示,是因为12306使用了一个比较弱的机密算法(This site uses a weak security configuration (SHA-1 signatures), so your connection may not be private.),我猜他们是为了性能??

但是,就算浏览器地址栏的锁不能变绿,我们还得订票不是吗?

我觉得12306可能需要一个操作系统内置的认证机构来发一个证书,不然普通的用户根本不知道怎么安装证书,如果不使用https,那么安全性如何得到保证呢?

最后

回到开头的例子,明明和丽丽可以找一个权威机构来发一个证书,而且自己都内置了这个权威机构的证书。当王叔叔把丽丽的证书给明明时,明明就可以知道证书是不丽丽给的,因此就可以确定证书里的公钥是否是丽丽的,如果可以确定是丽丽的,那么就可以确保加密的内容只有丽丽可以解开,因为只有丽丽有对应的私钥。

程序员之网络安全系列(四):数据加密之非对称秘钥

前文回顾

假如,明明和丽丽相互不认识,明明想给丽丽写一封情书,让隔壁老王送去

  1. 如何保证隔壁老王不能看到情书内容?(保密性)
  2. 如何保证隔壁老王不修改情书的内容?(完整性)
  3. 如何保证隔壁老王不冒充明明?(身份认证)
  4. 如何保证明明不能否认情书是自己写的?(来源的不可否认)

但是上面的问题是明明和丽丽必须提前知道秘钥,但是如果双方提前不知道秘钥,那么明明就需要“隔壁的王叔叔” 把秘钥告诉丽丽,这个显然是风险太大了,因为”隔壁王叔叔“有了秘钥和密文,那么就等于有了明文。

非对称秘钥

DH(Diffie-Hellman)算法

1976年,美国学者Dime和Henman为解决信息公开传送和密钥管理问题,提出一种新的密钥交换协议,允许在不安全的媒体上的通讯双方交换信息,安全地达成一致的密钥,这就是“公开密钥系统”。相对于“对称加密算法”这种方法也叫做“非对称加密算法”。

与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。

算法原理及示例

  1. 假如明明和丽丽希望交换一个密钥。

  2. 明明取一个素数p =97和97的一个原根a=5,让隔壁的王叔叔告诉丽丽。

  3. 明明和丽丽分别选择秘密密钥XA=36和XB=58,并计算各自的公开密钥,然后让隔壁的王叔叔帮忙交换公开秘钥。

     YA=a^XA mod p=5^36 mod 97=50
    
     YB=a^XB mod p=5^58 mod 97=44
  4. 明明和丽丽交换了公开密钥之后,计算共享密钥如下:

     明明:K=(YB) ^XA mod p=44^36 mod 97=75
    
     丽丽:K=(YA) ^XB mod p=50^58 mod 97=75  

由于只有明明知道XA, 而只有丽丽知道XB, 那么“隔壁的王叔叔” 是不可能通过 P, A, YA, YB来得到最终密码K的。

DiffieˉHellman不是加密算法,它只是生成可用作对称密钥的秘密数值。

非对称加密特点

与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。公开密钥与私有密钥是一对,如果 用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。

那么如果甲(收信方)想收到只有自己才能解读的加密信息,那么需要把自己的公钥告诉乙(发送发), 乙通过甲的公钥加密,把加密后的密文告诉甲,由于只有甲有私钥,那么也就只有甲才能加密。
由此可见,非对称加密只需要保存一对公钥和私钥,大大方便了秘钥管理。但是由于要做更多的计算,非对称加密只适合一些小数据量加密,一般情况都是用非对称加密算法来交换秘钥,随后通过对称加密算法来加密数据。

常用非对称加密算法

RSA、Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)。

使用最广泛的是RSA算法,Elgamal是另一种常用的非对称加密算法。

最后

我们对数据的完整性使用Hash进行了保证,用DH算法交换了秘钥,使用RSA算法对数进行了加密,那么如果王叔叔在交换秘钥的过程中做了手脚呢?

如何做手脚?看下图:

  1. 王叔叔自己生成一个公私钥,和明明以及丽丽交换。
  2. 王叔叔冒充丽丽把自己的公钥发给明明。
  3. 明明用王叔叔的公钥对信件加密。
  4. 王叔叔用自己的私钥解密就可以看到明明给丽丽的邮件。
  5. 王叔叔冒充明明把自己的公钥发给丽丽。
  6. 丽丽用王叔叔的公钥对信件加密。
  7. 王叔叔用自己的私钥解密就可以看到丽丽给明明内容。

至此,邮件内容又赤裸裸地被王叔叔看到了,怎么办呢?我们下文继续解释。

程序员之网络安全系列(三):数据加密之对称加密算法

前文回顾

假如,明明和丽丽相互不认识,明明想给丽丽写一封情书,让隔壁老王送去

  1. 如何保证隔壁老王不能看到情书内容?(保密性)
  2. 如何保证隔壁老王不修改情书的内容?(完整性)
  3. 如何保证隔壁老王不冒充明明?(身份认证)
  4. 如何保证明明不能否认情书是自己写的?(来源的不可否认)

上一节,我们使用了Hash算法保证了情书的完整性,也就是确保“隔壁王叔叔”没有修改明明的情书,那么这一节我们来看看如何保证“隔壁王叔叔”不能看到情书的内容,也就是保密性。

数据加密

要想不让别人看到数据,那么我们就们就需要对数据加密。

加密技术 是最常用的安全保密手段,利用技术手段把重要的数据变为乱码(加密)传送,到达目的地后再用相同或不同的手段还原(解密)。
加密包括两个元素:算法和密钥。一个加密算法是将普通的文本(或者可以理解的信息)与一窜数字(密钥)的结合,产生不可理解的密文的步骤,密钥是用来对数据进行编码和解码的一种算法。

举个例子:

假设我们要对LOVE加密,我们可以先定义字母的顺序ABCDEFGHIJKLMNOPQRSTUVWXYZ,然后我们让每个字母向后移动两位,那么LOVE就变为了NQXG

L------>N
O------>Q
V------>X
E------>
LOVE--->NQXG

我想这就是最简单的加密方式。

密钥加密技术的密码体制分为对称密钥体制和非对称密钥体制两种。

对数据加密的技术分为两类,即对称加密(私人密钥加密)和非对称加密(公开密钥加密)。对称加密以数据加密标准(DES,Data Encryption Standard)算法为典型代表,非对称加密通常以RSA(Rivest Shamir Ad1eman)算法为代表。对称加密的加密密钥和解密密钥相同,而非对称加密的加密密钥和解密密钥不同,加密密钥可以公开而解密密钥需要保密。

对称加密

对称加密采用了对称密码编码技术,它的特点是文件加密和解密使用相同的密钥,即加密密钥也可以用作解密密钥。
比如,我们给WORD文档设置密码1234, 那么其他人想要打开文档也必须输入1234才能打开。

常用加密算法:

  • DES(Data Encryption Standard):数据加密标准,速度较快,适用于加密大量数据的场合。

  • 3DES(Triple DES):是基于DES,对一块数据用三个不同的密钥进行三次加密,强度更高。

  • AES(Advanced Encryption Standard):高级加密标准,是下一代的加密算法标准,速度快,安全级别高;

  • RC4,也是为 RSA Data Security, Inc. 开发的密码系统的商标名称。

    传统的DES由于只有56位的密钥,从1997年开始,RSA公司发起了一个称作“向DES挑战”的竞技赛。在首届挑战赛上,罗克·维瑟用了96天时间破解了用DES加密的一段信息。1999年12月22日,RSA公司发起“第三届DES挑战赛(DES Challenge III)”。2000年1月19日,由电子边疆基金会组织研制的25万美元的DES解密机以22.5小时的战绩,成功地破解了 DES加密算法。DES已逐渐完成了它的历史使命。

    高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。经过五年的甄选流程,高级加密标准由美国国家标准与技术研究院(NIST)于2001年11月26日发布于FIPS PUB 197,并在2002年5月26日成为有效的标准。2006年,高级加密标准已然成为对称密钥加密中最流行的算法之一。

对称加密算法的优点:

  • 算法公开
  • 计算量小
  • 加密速度快,加密效率高

对称加密算法的缺点

  • 加解密双方需要使用相同的秘钥
  • 秘钥管理很不方便,如果用户很多,那么秘钥的管理成几何性增长
  • 任何一方秘钥泄露,数据都不安全了

最后

通过本节,我们知道当明明给丽丽情书时,可以用DES或者AES对数据进行加密,即使“隔壁王叔叔”拿到信件也看不懂内容,同时使用上一节的Hash算法保证了情书的内容完整,但是这就需要明明和丽丽提前设置一个秘钥。

代码示例

下面的代码输出如下结果

    I Love You, Li Li
    Encrypeted: 0t9glwGMmwtGs8B4QCotyZkKf091WElCwG659QiVVw0=
    Decrypeted: I Love You, Li Li

.NET 源码:

    using System;
    using System.Security.Cryptography;
    using System.IO;
    using System.Text;

    namespace AES
    {
        class MainClass
        {
            public static void Main (string[] args)
            {
                string password = "Don't believe wang shu shu";
                string orginTextToSent = "I Love You, Li Li";
                Console.WriteLine (orginTextToSent);

                string encryptedText=EncryptText(orginTextToSent, password);

                Console.WriteLine ("Encrypeted: " + encryptedText);

                string DecryptedText = DecryptText (encryptedText, password);
                Console.WriteLine ("Decrypeted: " + DecryptedText);

            }



            public static byte[] AES_Encrypt(byte[] bytesToBeEncrypted, byte[] passwordBytes)
            {
                byte[] encryptedBytes = null;

                // Set your salt here, change it to meet your flavor:
                // The salt bytes must be at least 8 bytes.
                byte[] saltBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };

                using (MemoryStream ms = new MemoryStream())
                {
                    using (RijndaelManaged AES = new RijndaelManaged())
                    {
                        AES.KeySize = 256;
                        AES.BlockSize = 128;

                        var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
                        AES.Key = key.GetBytes(AES.KeySize / 8);
                        AES.IV = key.GetBytes(AES.BlockSize / 8);

                        AES.Mode = CipherMode.CBC;

                        using (var cs = new CryptoStream(ms, AES.CreateEncryptor(), CryptoStreamMode.Write))
                        {
                            cs.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
                            cs.Close();
                        }
                        encryptedBytes = ms.ToArray();
                    }
                }

                return encryptedBytes;
            }

            public static byte[] AES_Decrypt(byte[] bytesToBeDecrypted, byte[] passwordBytes)
            {
                byte[] decryptedBytes = null;

                // Set your salt here, change it to meet your flavor:
                // The salt bytes must be at least 8 bytes.
                byte[] saltBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };

                using (MemoryStream ms = new MemoryStream())
                {
                    using (RijndaelManaged AES = new RijndaelManaged())
                    {
                        AES.KeySize = 256;
                        AES.BlockSize = 128;

                        var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
                        AES.Key = key.GetBytes(AES.KeySize / 8);
                        AES.IV = key.GetBytes(AES.BlockSize / 8);

                        AES.Mode = CipherMode.CBC;

                        using (var cs = new CryptoStream(ms, AES.CreateDecryptor(), CryptoStreamMode.Write))
                        {
                            cs.Write(bytesToBeDecrypted, 0, bytesToBeDecrypted.Length);
                            cs.Close();
                        }
                        decryptedBytes = ms.ToArray();
                    }
                }

                return decryptedBytes;
            }


            public static string EncryptText(string input, string password)
            {
                // Get the bytes of the string
                byte[] bytesToBeEncrypted = Encoding.UTF8.GetBytes(input);
                byte[] passwordBytes = Encoding.UTF8.GetBytes(password);

                // Hash the password with SHA256
                passwordBytes = SHA256.Create().ComputeHash(passwordBytes);

                byte[] bytesEncrypted = AES_Encrypt(bytesToBeEncrypted, passwordBytes);

                string result = Convert.ToBase64String(bytesEncrypted);

                return result;
            }

            public static string DecryptText(string input, string password)
            {
                // Get the bytes of the string
                byte[] bytesToBeDecrypted = Convert.FromBase64String(input);
                byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
                passwordBytes = SHA256.Create().ComputeHash(passwordBytes);

                byte[] bytesDecrypted = AES_Decrypt(bytesToBeDecrypted, passwordBytes);

                string result = Encoding.UTF8.GetString(bytesDecrypted);

                return result;
            }

        }
    }

程序员之网络安全系列(二):如何安全保存用户密码及哈希算法

前言

在很多网站的早期,甚至是现在仍然有一些网站,当你点击忘记密码功能时,你的邮箱会收到一封邮件,然后里面赫然写着你的密码,很多普通用户还会觉得庆幸,总算是找回来了,殊不知,这是多么可怕地一件事,说明了网站是“几乎是”明文存储你的密码,一旦数据用户数据泄露或者被拖库,那么用户密码将赤裸裸的暴露了,想想之前几次互联网密码泄露事件。

那么如何解决呢?

加密

为了不让密码明文存储,我们需要对密码进行加密,这样即使数据库用户密码暴露,也是加密后的。但是如何让加密后的数据难以解密呢?我们现在比较流行的做法就是把密码进行Hash存储。

Hash

哈希算法将任意长度的二进制值映射为较短的固定长度的二进制值,这个小的二进制值称为哈希值。哈希值是一段数据唯一且极其紧凑的数值表示形式. 典型的哈希算法包括 MD2、MD4、MD5 和 SHA-1

Hash算法是给消息生成摘要,那么什么是摘要呢?

举个例子:

比如你给你女朋友写了一封邮件,确保没被人改过,你可以生成这样一份摘要 “第50个字是我,第100个字是爱, 第998个字是你”,那么你女朋友收到这个摘要,检查一下你的邮件就可以了。

Hash算法有两个非常主要的特征:

  • 不能通过摘要来反推出原文
  • 原文的非常细小的改动,都会引起Hash结果的非常大的变化

因此,这个比较适合用来保存用户密码,因为不能反推出用户密码,Hash结果一致就证明原文一致,我们来用Ruby代码试一下上面的第二点 (MD5是一种常用的Hash算法)

2.2.3 :003 > require 'digest/md5.so'
=> true
2.2.3 :004 > puts Digest::MD5.hexdigest('I love you')
e4f58a805a6e1fd0f6bef58c86f9ceb3
=> nil
2.2.3 :005 > puts Digest::MD5.hexdigest('I love you!')
690a8cda8894e37a6fff4d1790d53b33
=> nil
2.2.3 :006 > puts Digest::MD5.hexdigest('I love you !')
b2c63c3ca6019cff3bad64fcfa807361
=> nil
2.2.3 :007 > puts Digest::MD5.hexdigest('I love you')
e4f58a805a6e1fd0f6bef58c86f9ceb3
=> nil
2.2.3 :008 > 

那么我们在使用MD5保存密码时候的验证流程是什么呢?

  • 用户注册时,把用户密码是MD5(password)后保存到数据库。
  • 用户输入用户名和密码
  • 服务器从数据库查找用户名
  • 如果有这个用户,A=MD5(input password), B=Database password
  • 如果A==B, 那么说明用户密码输入正确,如果不相等,用户输入错误。

为什么Hash(MD5)后仍然不够安全?

穷举

但是,如果你认为就只是这样密码就不会被人知道,那么就不对了,这只是比明文更安全,为什么?

因为,大部分人的密码都非常简单,当拿到MD5的密码后,攻击者也可以通过比对的方式,比如你的密码是4218

2.2.3 :008 > puts Digest::MD5.hexdigest('4218')
d278df4919453195d221030324127a0e

那么攻击者可以把1到4218个数字都MD5一下,然后和你密码的MD5对比一下,就知道你原密码是什么了。

曾经我的密码箱密码忘了,我把锁给撬了,后来我才想起可以用穷举法,最多就999次不就打开了?那么问题来了,你的密码箱还安全吗?

彩虹表

除了穷举法外,由于之前的密码泄露,那么攻击者们,手上都有大量的彩虹表,比如”I love you”,生日等等,这个表保存了这些原值以及MD5后的值,那么使用时直接从已有库里就可以查出来对应的密码。

加盐 Salt

那么,由于简单的对密码进行Hash算法不够安全,那么我们就可以对密码加Salt,比如密码是”I love you”, 虽然彩虹表里有这条数据,但是如果加上”安红我爱你”,这样MD5结果就大不一样.

jacks-MacBook-Air:~ jack$ irb
2.2.3 :001 > require 'digest/md5.so'
=> true
2.2.3 :002 > puts Digest::MD5.hexdigest('I love you')
e4f58a805a6e1fd0f6bef58c86f9ceb3
=> nil
2.2.3 :003 > puts Digest::MD5.hexdigest('I love you安红我爱你')
b10d890bf46b1a045eb99af5d43c7b13
=> nil
2.2.3 :004 > puts Digest::MD5.hexdigest('I dont love you')
c82294c9a7b6e4a372ad25ed4d6011c9
=> nil
2.2.3 :005 > puts Digest::MD5.hexdigest('I dont love you安红我爱你')
dce67bcdfdf007445dd4a2c2dc3d29c1
=> nil
2.2.3 :006 >

如此一来,因为攻击者很难猜到“安红我爱你”,那么自然彩虹表里是没有的,当然我建议你在实际项目中不要使用”安红我爱你”,你应该使用一个连你自己都猜不到的较长的字符串。

加盐了,就安全了吗?

实际上,加盐并不能100%保证安全,假如有人泄露了你的Salt呢?实际上通过反编译程序很容易可以拿到这个,由于WEB程序一般放在WEB服务器上,那么就需要保证服务器不被攻击,当然这个是运维人员去操心。

为了让加盐更安全,一般情况下我们可以使用一个“盐+盐”,也就是为每个用户保存一个”Salt”, 然后再使用全局的盐,我们可以对用户的盐使用自己的加密算法。那么代码就如下:

if MD5(userInputPpassword+globalsalt+usersalt)===user.databasePassword) 
{
    login success
}

普通用户如何做?

由于这个是写给程序员,当然是说在前端用户注册时密码应该如何设置,很简单,我们要求用户必须输入强密码!但是,我知道很多用户觉得很烦,这样你就失掉了一个用户,但我们需要做一个适当的折中,比如至少有一个大写字母,小写字母和数字的组合。

最后

我们来看看解决了之前文章下面例子的什么问题。

假如,明明和丽丽相互不认识,明明想给丽丽写一封情书,让隔壁老王送去

  1. 如何保证隔壁老王不能看到情书内容?(保密性)
  2. 如何保证隔壁老王不修改情书的内容?(完整性)
  3. 如何保证隔壁老王不冒充明明?(身份认证)
  4. 如何保证明明不能否认情书是自己写的?(来源的不可否认)

通过了解hash算法,”明明” 就有办法让丽丽知道信的内容没有修改,他可以对邮件进行Hash生成邮件的摘要,然后让”隔壁的李叔叔”把摘要送给丽丽,丽丽拿到邮件的摘要后,把邮件内容也Hash一下,然后把结果和”隔壁的李叔叔”给的摘要对比一下,然后通过比较结果就知道邮件有没有被”隔壁的王叔叔”更改过了。