git中submodule子模块的添加、使用和删除

背景

项目中经常使用别人维护的模块,在git中使用子模块的功能能够大大提高开发效率。
使用子模块后,不必负责子模块的维护,只需要在必要的时候同步更新子模块即可。
本文主要讲解子模块相关的基础命令,详细使用请参考man page。

子模块的添加

添加子模块非常简单,命令如下:
git submodule add <url> <path>

其中,url为子模块的路径,path为该子模块存储的目录路径。
执行成功后,git status会看到项目中修改了.gitmodules,并增加了一个新文件(为刚刚添加的路径)
git diff –cached查看修改内容可以看到增加了子模块,并且新文件下为子模块的提交hash摘要
git commit提交即完成子模块的添加

子模块的使用

克隆项目后,默认子模块目录下无任何内容。需要在项目根目录执行如下命令完成子模块的下载:
git submodule init
git submodule update

或:
git submodule update –init –recursive
执行后,子模块目录下就有了源码,再执行相应的makefile即可。

子模块的更新

子模块的维护者提交了更新后,使用子模块的项目必须手动更新才能包含最新的提交。
在项目中,进入到子模块目录下,执行 git pull更新,查看git log查看相应提交。
完成后返回到项目目录,可以看到子模块有待提交的更新,使用git add,提交即可。

删除子模块

有时子模块的项目维护地址发生了变化,或者需要替换子模块,就需要删除原有的子模块。
删除子模块较复杂,步骤如下:

rm -rf 子模块目录 删除子模块目录及源码
vi .gitmodules 删除项目目录下.gitmodules文件中子模块相关条目
vi .git/config 删除配置项中子模块相关条目
rm .git/module/* 删除模块下的子模块目录,每个子模块对应一个目录,注意只删除对应的子模块目录即可
执行完成后,再执行添加子模块命令即可,如果仍然报错,执行如下:

git rm –cached 子模块名称

完成删除后,提交到仓库即可。

RSYNC 的核心算法

原文来自:http://coolshell.cn/articles/7425.html 
 

rsync是unix/linux下同步文件的一个高效算法,它能同步更新两处计算机的文件与目录,并适当利用查找文件中的不同块以减少数据传输。rsync中一项与其他大部分类似程序或协定中所未见的重要特性是镜像是只对有变更的部分进行传送。rsync可拷贝/显示目录属性,以及拷贝文件,并可选择性的压缩以及递归拷贝。rsync利用由Andrew Tridgell发明的算法。这里不介绍其使用方法,只介绍其核心算法。我们可以看到,Unix下的东西,一个命令,一个工具都有很多很精妙的东西,怎么学也学不完,这就是Unix的文化啊。

本来不想写这篇文章的,因为原先发现有很多中文blog都说了这个算法,但是看了一下,发现这些中文blog要么翻译国外文章翻译地非常烂,要么就是介绍这个算法介绍得很乱让人看不懂,还有错误,误人不浅,所以让我觉得有必要写篇rsync算法介绍的文章。(当然,我成文比较仓促,可能会有一些错误,请指正)

问题

首先, 我们先来想一下rsync要解决的问题,如果我们要同步的文件只想传不同的部分,我们就需要对两边的文件做diff,但是这两个问题在两台不同的机器上,无法做diff。如果我们做diff,就要把一个文件传到另一台机器上做diff,但这样一来,我们就传了整个文件,这与我们只想传输不同部的初衷相背。

于是我们就要想一个办法,让这两边的文件见不到面,但还能知道它们间有什么不同。这就出现了rsync的算法。

算法

rsync的算法如下:(假设我们同步源文件名为fileSrc,同步目的文件叫fileDst

1)分块Checksum算法。首先,我们会把fileDst的文件平均切分成若干个小块,比如每块512个字节(最后一块会小于这个数),然后对每块计算两个checksum,

  • 一个叫rolling checksum,是弱checksum,32位的checksum,其使用的是Mark Adler发明的adler-32算法,
  • 另一个是强checksum,128位的,以前用md4,现在用md5 hash算法。

为什么要这样?因为若干年前的硬件上跑md4的算法太慢了,所以,我们需要一个快算法来鉴别文件块的不同,但是弱的adler32算法碰撞概率太高了,所以我们还要引入强的checksum算法以保证两文件块是相同的。也就是说,弱的checksum是用来区别不同,而强的是用来确认相同。(checksum的具体公式可以参看这篇文章

2)传输算法。同步目标端会把fileDst的一个checksum列表传给同步源,这个列表里包括了三个东西,rolling checksum(32bits)md5 checksume(128bits)文件块编号

我估计你猜到了同步源机器拿到了这个列表后,会对fileSrc做同样的checksum,然后和fileDst的checksum做对比,这样就知道哪些文件块改变了。

但是,聪明的你一定会有以下两个疑问:

  • 如果我fileSrc这边在文件中间加了一个字符,这样后面的文件块都会位移一个字符,这样就完全和fileDst这边的不一样了,但理论上来说,我应该只需要传一个字符就好了。这个怎么解决?
  • 如果这个checksum列表特别长,而我的两边的相同的文件块可能并不是一样的顺序,那就需要查找,线性的查找起来应该特别慢吧。这个怎么解决?

很好,让我们来看一下同步源端的算法。

3)checksum查找算法。同步源端拿到fileDst的checksum数组后,会把这个数据存到一个hash table中,用rolling checksum做hash,以便获得O(1)时间复杂度的查找性能。这个hash table是16bits的,所以,hash table的尺寸是2的16次方,对rolling checksum的hash会被散列到0 到 2^16 – 1中的某个整数值。(对于hash table,如果你不清楚,建议回去看大学时的数据结构教科书)

顺便说一下,我在网上看到很多文章说,“要对rolling checksum做排序”(比如这篇这篇),这两篇文章都引用并翻译了原作者的这篇文章,但是他们都理解错了,不是排序,就只是把fileDst的checksum数据,按rolling checksum做存到2^16的hash table中,当然会发生碰撞,把碰撞的做成一个链表就好了。这就是原文中所说的第二步——搜索有碰撞的情况。

4)比对算法。这是最关键的算法,细节如下:

4.1)取fileSrc的第一个文件块(我们假设的是512个长度),也就是从fileSrc的第1个字节到第512个字节,取出来后做rolling checksum计算。计算好的值到hash表中查。

4.2)如果查到了,说明发现在fileDst中有潜在相同的文件块,于是就再比较md5的checksum,因为rolling checksume太弱了,可能发生碰撞。于是还要算md5的128bits的checksum,这样一来,我们就有 2^-(32+128) = 2^-160的概率发生碰撞,这太小了可以忽略。如果rolling checksum和md5 checksum都相同,这说明在fileDst中有相同的块,我们需要记下这一块在fileDst下的文件编号

4.3)如果fileSrc的rolling checksum 没有在hash table中找到,那就不用算md5 checksum了。表示这一块中有不同的信息。总之,只要rolling checksum 或 md5 checksum 其中有一个在fileDst的checksum hash表中找不到匹配项,那么就会触发算法对fileSrc的rolling动作。于是,算法会住后step 1个字节,取fileSrc中字节2-513的文件块要做checksum,go to (4.1) – 现在你明白什么叫rolling checksum了吧。

4.4)这样,我们就可以找出fileSrc相邻两次匹配中的那些文本字符,这些就是我们要往同步目标端传的文件内容了。

图示

怎么,你没看懂? 好吧,我送佛送上西,画个示意图给你看看(对图中的东西我就不再解释了)。

这样,最终,在同步源这端,我们的rsync算法可能会得到下面这个样子的一个数据数组,图中,红色块表示在目标端已匹配上,不用传输(注:我专门在其中显示了两块chunk #5,相信你会懂的),而白色的地方就是需要传输的内容(注意:这些白色的块是不定长的),这样,同步源这端把这个数组(白色的就是实际内容,红色的就放一个标号)压缩传到目的端,在目的端的rsync会根据这个表重新生成文件,这样,同步完成。

最后想说一下,对于某些压缩文件使用rsync传输可能会传得更多,因为被压缩后的文件可能会非常的不同。对此,对于gzip和bzip2这样的命令,记得开启 “rsyncalbe” 模式。

二叉树中序遍历打印节点信息

public static void main(String[] args){
BinaryTree tree = create();
MidOrderPrint(tree);
}


static class BinaryTree{
String data;
BinaryTree left;
BinaryTree right;

public BinaryTree(String data) {
this.data = data;
}
}

/**
* ........... a
* ........../.....\
* ........b........c
* ....../....\
* ....d........e
* .....\ ...../
* ......f....g

* dfbgeac
*/
private static BinaryTree create(){
BinaryTree a = new BinaryTree("a");

BinaryTree b = new BinaryTree("b");
BinaryTree c = new BinaryTree("c");
a.left = b;
a.right = c;

BinaryTree d = new BinaryTree("d");
BinaryTree e = new BinaryTree("e");

b.left = d;
b.right = e;

BinaryTree f = new BinaryTree("f");
BinaryTree g = new BinaryTree("g");

d.right = f;

e.left = g;

return a;
}

static void MidOrderPrint(BinaryTree tree){
if(tree == null) return ;

Stack<BinaryTree> stack = new Stack<>();
BinaryTree cur = tree;

while(!stack.isEmpty() || cur != null){
if(cur != null){
stack.push(cur);
cur = cur.left;
}else{
cur = stack.pop();
System.out.print(cur.data);
cur = cur.right;
}
}
}

有100个已排序数组,先需要合并为一个排序数组如何实现?数组长度在10w ~40w间

public static void main(String[] args){
int[][] arrs = new int[][]{
{1, 3, 4},
{1, 2, 5},
{0, 7, 5, 8}
};

int[] result = merge(arrs);

for(int num : result){
System.out.print(num+" ");
}
}


private static int getAllCount(int[][] arrs){
int size = 0;
for(int[] item : arrs){
if(item == null) continue;
size += item.length;
}
return size;
}

private static int[] getArrayIdxsAndSet_0(int size){
int[] idxs = new int[size];
// 初始化为零
for(int i = 0; i < idxs.length; i++){
idxs[i] = 0;
}
return idxs;
}

public static int[] merge(int[][] arrs) {
if(arrs == null) return null;

int allCount = getAllCount(arrs);

int[] result = new int[allCount];
int[] arrayIdxs = getArrayIdxsAndSet_0(arrs.length);

for(int resultIdx = 0; resultIdx < allCount; resultIdx++){

int minValue = 0;
int minArrayIdx = 0;
boolean tempNotSet = true;

for(int i = 0; i < arrs.length; i++){
int currentIdx = arrayIdxs[i];

if(currentIdx < arrs[i].length){
if(tempNotSet){
tempNotSet = false;
minValue = arrs[i][currentIdx];
minArrayIdx = i;
}else if(minValue > arrs[i][currentIdx]) {
minValue = arrs[i][currentIdx];
minArrayIdx = i;
}
}
}

arrayIdxs[minArrayIdx]++;
result[resultIdx] = minValue;
}

return result;
}

Two Sum

实例:
Given nums = [2, 7, 11, 15], target = 9,
Because nums[0] + nums[1] = 2 + 7 = 9,
return [0, 1].

public int[] twoSum(int[] nums, int target) {
Map map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target – nums[i];
if (map.containsKey(complement)) {
return new int[] { map.get(complement), i };
}
map.put(nums[i], i);
}
throw new IllegalArgumentException(“No two sum solution”);
}

字符串全排列

/**
* Created by zzh on 2018/12/30.
*/
public class Test {
public static List<List<Character>> allSort(List<Character> chars){
if(chars.size() == 1) {
List<List<Character>> list = new ArrayList<>();
list.add(chars);
return list;
}

List<List<Character>> list = new ArrayList<>();

List<Character> useList = new ArrayList<>();
List<Character> others = new ArrayList<>(chars);

for (Character ch : chars) {

if (useList.contains(ch)) {
// 如果已经使用过重复元素的一个,就不再使用
continue;
} else {
// 记录使用过的元素
useList.add(ch);
}

// 移除当前的元素,得到剩下的元素
others.remove(ch);

// 拿剩下的数据进行全排列
List<List<Character>> othersAllSort = allSort(others);

// 当前的元素和剩下的元素的全排列,拼接下就可以了
for (List<Character> othersAllSortItem : othersAllSort) {
List<Character> dest = new ArrayList<>(othersAllSortItem);
dest.add(0, ch);

list.add(dest);
}

// 恢复元素列表,便于下次循环复用
others.add(ch);
}

return list;
}

public static void main(String[] args){
List<Character> src = Arrays.asList('a', 'a', 'b');

List<List<Character>> allSortList = allSort(src);

allSortList.forEach(Test::print);
}

private static void print(List<Character> chars){
for (Character ch : chars) {
System.out.print(ch + " ");
}
System.out.println();
}
}

a a b
a b a
b a a

怎么桥接两个无线路由器:TP-link路由器

第一台无线路由器设置

首先我们登入和宽带猫连接的路由器:如图

在无线设置-基本设置中设置“SSID号”、“信道”,设置固定信道号,如图:  

在无线设置-无线安全设置中设置无线信号加密信息

记录该无线路由器设置后的SSID、信道和加密设置信息

第二台无线路由器设置

登入需要桥接的第二台无线路由器

在网络参数-LAN口设置中,修改IP地址和第一台路由器不同(防止IP地址冲突),

如192.168.1.2,保存,路由器会自动重启。

在无线设置-基本设置中勾选“开启WDS”。点击“扫描”,搜索周围无线信号。

在扫描到的信号列表中选择第一台路由器SSID号,如下图中SSID,点击“连接”。

将信道设置成与第一台路由器信道相同。同时设置加密信息和第一台路由器相同,点击“保存”。如下图:

关闭DHCP服务器。在DHCP服务器中,选择“不启用”,保存,重启路由器。  

此时第一台无线路由器与第二台无线路由器已成功建立WDS。  

软件设计之UML—UML中的六大关系

一、UML中的六大关系

在UML类图中,常见的有以下几种关系:

  • 泛化(Generalization)
  • 实现(Realization)
  • 关联(Association)
  • 聚合(Aggregation)
  • 组合(Composition)
  • 依赖(Dependency)。

1.1、 继承关系—泛化(Generalization)

指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系;在Java中用extends关键字。

【泛化关系】是一种继承关系,表示一般与特殊的关系,它指定了子类如何特化父类的所有特征和行为。例如:猫头鹰是鸟的一种,即有鸟的特性也有猫头鹰的共性。

【箭头指向】带三角箭头的实线,箭头指向父类。

【描述】上图中的类bird有嘴、翅膀、羽毛等属性。会飞、会唧唧喳喳的叫,那么就有这些方法。而猫头鹰有大眼睛和捕捉老鼠的本领,这则是自身的特性。

1.2、 实现关系(Realization)

指的是一个class类实现interface接口(可以是多个)的功能;实现是类与接口之间最常见的关系;在Java中此类关系通过关键字implements明确标识。

【实现关系】是一种类与接口的关系,表示类是接口所有特征和行为的实现.

【箭头指向】带三角箭头的虚线,箭头指向接口。

【描述】上图中IFly是一个接口,接口中有时间、速度等常量,还有一个fly方法。FlyImpl继承了这个IFly接口后,需要实现fly方法,同时实现类也可以拥有自己的属性和方法。

1.3、 依赖(Dependency)

可以简单的理解,就是一个类A使用到了另一个类B,而这种使用关系是具有偶然性的、临时性的、非常弱的,但是B类的变化会影响到A;比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖;表现在代码层面,为类B作为参数、属性被类A在某个method方法中使用;

【依赖关系】是一种使用的关系,即一个类的实现需要另一个类的协助,所以要尽量不使用双向的互相依赖。

【代码表现】局部变量、方法的参数或者对静态方法的调用

【箭头及指向】带箭头的虚线,指向被使用者

【描述】Bird类中有一个setFly方法,它需要使用者用到IFly接口的实现,那么这种关系就是依赖关系。

1.4、 关联

他体现的是两个类、或者类与接口之间语义级别的一种强依赖关系,比如我和我的朋友;这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的、关联可以是单向、双向的;表现在代码层面,为被关联类B以类属性的形式出现在关联类A中,也可能是关联类A引用了一个类型为被关联类B的全局变量;

【关联关系】是一种拥有的关系,它使一个类知道另一个类的属性和方法;如:老师与学生,丈夫与妻子关联可以是双向的,也可以是单向的。双向的关联可以有两个箭头或者没有箭头,单向的关联有一个箭头。

【代码体现】成员变量

【箭头及指向】带普通箭头的实心线,指向被拥有者

【描述】在Bird类中有一个IFly类型的fly属性,需要提供IFly的接口实现。Bird对象会利用IFly接口的实现完成fly方法。

1.4.1、双向关联

双方都知道对方的存在,都可以调用对方的公共属性、方法。

【关联关系】双方都有关联的关系,通过自身对对方关联的属性来访问对方的属性和方法。

【代码体现】成员变量

【箭头及指向】用不带箭头的实线连接双方

【描述】在中国一个妻子只能嫁给一个丈夫,一个丈夫也只能取一个妻子。

 

1.4.2、自身关联

自己关联自己,这种情况比较少出现但是也有用到。

【自关联关系】双方都有关联的关系,通过自身对自身关联的属性引用来访问对方的属性和方法。

【代码体现】成员变量

【箭头及指向】用带普通箭头的实线连接自己

【描述】在盗梦空间中,演员需要在梦中再造梦,这种梦中梦的情况跟上图描述很符合。

1.5、 聚合(Aggregation)

聚合是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即has-a的关系,此时整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享;比如计算机与CPU、公司与员工的关系等;表现在代码层面,和关联关系是一致的,只能从语义级别来区分;

【聚合关系】是整体与部分的关系,且部分可以离开整体而单独存在。如车和轮胎是整体和部分的关系,轮胎离开车仍然可以存在。聚合关系是关联关系的一种,是强的关联关系;关联和聚合在语法上无法区分,必须考察具体的逻辑关系。

【代码体现】成员变量

【箭头及指向】带空心菱形的实心线,菱形指向整体

【描述】birdChild一只鸟有很多鸟宝宝,所以自引用。鸟有很多不同数量和颜色的羽毛,所以引用关系是0~*。

1.6、 组合(Composition)

组合也是关联关系的一种特例,他体现的是一种contains-a的关系,这种关系比聚合更强,也称为强聚合;他同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束;比如你和你的大脑;表现在代码层面,和关联关系是一致的,只能从语义级别来区分;

【组合关系】是整体与部分的关系,但部分不能离开整体而单独存在。如公司和部门是整体和部分的关系,没有公司就不存在部门。组合关系是关联关系的一种,是比聚合关系还要强的关系,它要求普通的聚合关系中代表整体的对象负责代表部分的对象的生命周期。

【代码体现】成员变量

【箭头及指向】带实心菱形的实线,菱形指向整体

【描述】一个学校由多个班级组成,班级离开学校也就不存在、而学校离开班级也不成立。像这种不可分离的关系就需要用组合。

综合示例

对于继承、实现这两种关系没多少疑问,他们体现的是一种类与类、或者类与接口间的纵向关系;

其他的四者关系则体现的是类与类、或者类与接口间的引用、横向关系,是比较难区分的,有很多事物间的关系要想准备定位是很难的,前面也提到,这几种关系都是语义级别的,所以从代码层面并不能完全区分各种关系;

但总的来说,后几种关系所表现的强弱程度依次为:泛化 = 实现 > 组合 > 聚合 > 关联 > 依赖。

https://www.cnblogs.com/hoojo/p/uml_design.html

mac上访问samba服务器

原文章地址:https://blog.csdn.net/h106140873/article/details/76680545

打开safari输入smb://ip,回车后出现输入用户名密码对话框,若是匿名则选择作为“客人”选项

打开Finder(或在桌面),CMD + k,可以得到以下页面:
在smb://后面,输入你的服务器地址或域名

三方登录几个概念的理解

1)外部标识
用来使用用户身份的标志,可以是用户名,手机号,邮箱等,每一个外部标识一定和一个内部标识相关联用以确定一个用户。
需要注意的是,外部标识可能改变,例如小明12年用的是手机号A,17年用的是手机号B,在APP内更改了登录的手机号,就完成了外部标识的改变。
所以,外部标识的作用有两个
1. 让用户通过自己熟知且占有的外部标识来登录产品
2. 可以通过校验外部标识来实现找回或转移数据资产(用户注册帐号后使用产品过程中产生的所有数据,例如在微信,数据资产是你的好友,历史聊天记录,所发的朋友圈;而在游戏中是你的游戏角色,拥有的装备,好友等)

2)内部标识
即产品中用于标识用户唯一性的标志,例如user_id,必须有,不可更改且唯一,用户一般接触不到内部标识。
当一个内部标识建立后,用户所有的数据资产都会绑定到这个内部标识上。

3)user_id
一个常用的内部标识,类似你的18位身份证ID

4)app_id
用于区别不同APP的ID,具有唯一性。

5)open_id
第三方平台为了用户信息的安全,一般不会直接将用户的内部标识给到其他产品,而是选择了给一个外部标识,这个open_id就是微信给各个APP用以区分微信用户身份的外部标识
需要注意的是,即使是同一个用户,微信给不同的APP的open_id也是不同的,这会导致一个问题。
例如我有两款产品,一款美团,一款美团外卖。
对微信而言,这是两个app_id,所以当用户授权美团使用微信信息后,微信给的是open_id_1,用户再来到美团外卖公众号,这时候还要再授权一次,微信给的是open_id_2。
但实际上在美团的帐号体系中,这都是同一个用户,却绑定了两个open_id,可能导致用户注册了两个帐号,数据不会互通,这显然是有问题的。
如何解决?往下看

6)union_id
为了解决上面的问题,union_id诞生了。
不同的产品的可以使用同一个union_id来确认用户的身份。
对于使用同一套用户数据的不同产品,可以向微信申请union_id,当A产品获得了用户授权后,拿到该用户的union_id,这时候再使用B产品再进行授权后,发现这是同一个union_id,直接登录之前的帐户,避免了同个微信用户注册了两个帐号数据不同步的问题。

7)access_token
可以理解为通行证,有了这个通行证,就能获取到第三方平台指定用户的有限信息。

常见单词

Association关联关系 英 [[ə,soʃɪ’eʃən]] n. 协会,联盟,社团;联合;联想

Directed Association单向关联

Aggregation聚合 英 [,æɡrɪ’ɡeɪʃən] n. [地质][数] 聚合,聚集;聚集体,集合体

Composition组合 英 [ˌkɒmpəˈzɪʃn] n. 作文,作曲;[材] 构成;合成物

Dependency依赖 英 [dɪ’pend(ə)nsɪ] n. 属国;从属;从属物

Generalization泛化,继承 英 [,dʒenrələ’zeɪʃən] n. 概括;普遍化;一般化

Interface Realization接口实现

Token 认证的来龙去脉

不久前,我在在前后端分离实践中提到了基于 Token 的认证,现在我们稍稍深入一些。

通常情况下,我们在讨论某个技术的时候,都是从问题开始。那么第一个问题:

为什么要用 Token?

而要回答这个问题很简单——因为它能解决问题!

可以解决哪些问题呢?

  1. Token 完全由应用管理,所以它可以避开同源策略
  2. Token 可以避免 CSRF 攻击
  3. Token 可以是无状态的,可以在多个服务间共享

Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位。如果这个 Token 在服务端持久化(比如存入数据库),那它就是一个永久的身份令牌。

于是,又一个问题产生了:需要为 Token 设置有效期吗?

需要设置有效期吗?

对于这个问题,我们不妨先看两个例子。一个例子是登录密码,一般要求定期改变密码,以防止泄漏,所以密码是有有效期的;另一个例子是安全证书。SSL 安全证书都有有效期,目的是为了解决吊销的问题,对于这个问题的详细情况,来看看知乎的回答。所以无论是从安全的角度考虑,还是从吊销的角度考虑,Token 都需要设有效期。

那么有效期多长合适呢?

只能说,根据系统的安全需要,尽可能的短,但也不能短得离谱——想像一下手机的自动熄屏时间,如果设置为 10 秒钟无操作自动熄屏,再次点亮需要输入密码,会不会疯?如果你觉得不会,那就亲自试一试,设置成可以设置的最短时间,坚持一周就好(不排除有人适应这个时间,毕竟手机厂商也是有用户体验研究的)。

然后新问题产生了,如果用户在正常操作的过程中,Token 过期失效了,要求用户重新登录……用户体验岂不是很糟糕?

为了解决在操作过程不能让用户感到 Token 失效这个问题,有一种方案是在服务器端保存 Token 状态,用户每次操作都会自动刷新(推迟) Token 的过期时间——Session 就是采用这种策略来保持用户登录状态的。然而仍然存在这样一个问题,在前后端分离、单页 App 这些情况下,每秒种可能发起很多次请求,每次都去刷新过期时间会产生非常大的代价。如果 Token 的过期时间被持久化到数据库或文件,代价就更大了。所以通常为了提升效率,减少消耗,会把 Token 的过期时保存在缓存或者内存中。

还有另一种方案,使用 Refresh Token,它可以避免频繁的读写操作。这种方案中,服务端不需要刷新 Token 的过期时间,一旦 Token 过期,就反馈给前端,前端使用 Refresh Token 申请一个全新 Token 继续使用。这种方案中,服务端只需要在客户端请求更新 Token 的时候对 Refresh Token 的有效性进行一次检查,大大减少了更新有效期的操作,也就避免了频繁读写。当然 Refresh Token 也是有有效期的,但是这个有效期就可以长一点了,比如,以天为单位的时间。

时序图表示

使用 Token 和 Refresh Token 的时序图如下:

1)登录

2)业务请求

3)Token 过期,刷新 Token

上面的时序图中并未提到 Refresh Token 过期怎么办。不过很显然,Refresh Token 既然已经过期,就该要求用户重新登录了。

当然还可以把这个机制设计得更复杂一些,比如,Refresh Token 每次使用的时候,都更新它的过期时间,直到与它的创建时间相比,已经超过了非常长的一段时间(比如三个月),这等于是在相当长一段时间内允许 Refresh Token 自动续期。

到目前为止,Token 都是有状态的,即在服务端需要保存并记录相关属性。那说好的无状态呢,怎么实现?

无状态 Token

如果我们把所有状态信息都附加在 Token 上,服务器就可以不保存。但是服务端仍然需要认证 Token 有效。不过只要服务端能确认是自己签发的 Token,而且其信息未被改动过,那就可以认为 Token 有效——“签名”可以作此保证。平时常说的签名都存在一方签发,另一方验证的情况,所以要使用非对称加密算法。但是在这里,签发和验证都是同一方,所以对称加密算法就能达到要求,而对称算法比非对称算法要快得多(可达数十倍差距)。更进一步思考,对称加密算法除了加密,还带有还原加密内容的功能,而这一功能在对 Token 签名时并无必要——既然不需要解密,摘要(散列)算法就会更快。可以指定密码的散列算法,自然是 HMAC。

上面说了这么多,还需要自己去实现吗?不用!JWT 已经定义了详细的规范,而且有各种语言的若干实现。

不过在使用无状态 Token 的时候在服务端会有一些变化,服务端虽然不保存有效的 Token 了,却需要保存未到期却已注销的 Token。如果一个 Token 未到期就被用户主动注销,那么服务器需要保存这个被注销的 Token,以便下次收到使用这个仍在有效期内的 Token 时判其无效。有没有感到一点沮丧?

在前端可控的情况下(比如前端和服务端在同一个项目组内),可以协商:前端一但注销成功,就丢掉本地保存(比如保存在内存、LocalStorage 等)的 Token 和 Refresh Token。基于这样的约定,服务器就可以假设收到的 Token 一定是没注销的(因为注销之后前端就不会再使用了)。

如果前端不可控的情况,仍然可以进行上面的假设,但是这种情况下,需要尽量缩短 Token 的有效期,而且必须在用户主动注销的情况下让 Refresh Token 无效。这个操作存在一定的安全漏洞,因为用户会认为已经注销了,实际上在较短的一段时间内并没有注销。如果应用设计中,这点漏洞并不会造成什么损失,那采用这种策略就是可行的。

在使用无状态 Token 的时候,有两点需要注意:

  1. Refresh Token 有效时间较长,所以它应该在服务器端有状态,以增强安全性,确保用户注销时可控
  2. 应该考虑使用二次认证来增强敏感操作的安全性

到此,关于 Token 的话题似乎差不多了——然而并没有,上面说的只是认证服务和业务服务集成在一起的情况,如果是分离的情况呢?

分离认证服务

当 Token 无状态之后,单点登录就变得容易了。前端拿到一个有效的 Token,它就可以在任何同一体系的服务上认证通过——只要它们使用同样的密钥和算法来认证 Token 的有效性。就样这样:

当然,如果 Token 过期了,前端仍然需要去认证服务更新 Token:

可见,虽然认证和业务分离了,实际即并没产生多大的差异。当然,这是建立在认证服务器信任业务服务器的前提下,因为认证服务器产生 Token 的密钥和业务服务器认证 Token 的密钥和算法相同。换句话说,业务服务器同样可以创建有效的 Token。

如果业务服务器不能被信任,该怎么办?

不受信的业务服务器

遇到不受信的业务服务器时,很容易想到的办法是使用不同的密钥。认证服务器使用密钥1签发,业务服务器使用密钥2验证——这是典型非对称加密签名的应用场景。认证服务器自己使用私钥对 Token 签名,公开公钥。信任这个认证服务器的业务服务器保存公钥,用于验证签名。幸好,JWT 不仅可以使用 HMAC 签名,也可以使用 RSA(一种非对称加密算法)签名。

不过,当业务服务器已经不受信任的时候,多个业务服务器之间使用相同的 Token 对用户来说是不安全的。因为任何一个服务器拿到 Token 都可以仿冒用户去另一个服务器处理业务……悲剧随时可能发生。

为了防止这种情况发生,就需要在认证服务器产生 Token 的时候,把使用该 Token 的业务服务器的信息记录在 Token 中,这样当另一个业务服务器拿到这个 Token 的时候,发现它并不是自己应该验证的 Token,就可以直接拒绝。

现在,认证服务器不信任业务服务器,业务服务器相互也不信任,但前端是信任这些服务器的——如果前端不信任,就不会拿 Token 去请求验证。那么为什么会信任?可能是因为这些是同一家公司或者同一个项目中提供的若干服务构成的服务体系。

但是,前端信任不代表用户信任。如果 Token 不没有携带用户隐私(比如姓名),那么用户不会关心信任问题。但如果 Token 含有用户隐私的时候,用户得关心信任问题了。这时候认证服务就不得不再啰嗦一些,当用户请求 Token 的时候,问上一句,你真的要授权给某某某业务服务吗?而这个“某某某”,用户怎么知道它是不是真的“某某某”呢?用户当然不知道,甚至认证服务也不知道,因为公钥已经公开了,任何一个业务都可以声明自己是“某某某”。

为了得到用户的信任,认证服务就不得不帮助用户来甄别业务服务。所以,认证服器决定不公开公钥,而是要求业务服务先申请注册并通过审核。只有通过审核的业务服务器才能得到认证服务为它创建的,仅供它使用的公钥。如果该业务服务泄漏公钥带来风险,由该业务服务自行承担。现在认证服务可以清楚的告诉用户,“某某某”服务是什么了。如果用户还是不够信任,认证服务甚至可以问,某某某业务服务需要请求 A、B、C 三项个人数据,其中 A 是必须的,不然它不工作,是否允许授权?如果你授权,我就把你授权的几项数据加密放在 Token 中……

废话了这么多,有没有似曾相识……对了,这类似开放式 API 的认证过程。开发式 API 多采用 OAuth 认证,而关于 OAuth 的探讨资源非常丰富,这里就不深究了。

TortoiseSVN客户端重新设置用户名和密码

办法一:在TortoiseSVN的设置对话框中,选择“已保存数据”,在“认证数据”那一行点击“清除”按钮,清楚保存的认证数据,再检出的时候就会重新跳出用户名密码输入框。

 

如果方法一不起作用,则可以采用方法二:

Tortoise的用户名密码等认证信息都是缓存在客户端文件系统的这个目录:

C:/Documents and Settings/Administrator/Application Data/Subversion/auth

删除auth下面的所有文件夹,重新连接远程服务器进行检出,对话框就会出现!

git 分支管理 推送本地分支到远程分支等

1、创建本地分支 local_branch

git branch local_branch

2、创建本地分支local_branch 并切换到local_branch分支

git checkout -b local_branch

3、切换到分支local_branch

git checkout local_branch

4、推送本地分支local_branch到远程分支 remote_branch并建立关联关系

a.远程已有remote_branch分支并且已经关联本地分支local_branch且本地已经切换到local_branch

git push

b.远程已有remote_branch分支但未关联本地分支local_branch且本地已经切换到local_branch

git push -u origin/remote_branch

c.远程没有有remote_branch分支并,本地已经切换到local_branch

git push origin local_branch:remote_branch

5、删除本地分支local_branch

git branch -d local_branch

6、删除远程分支remote_branch

git push origin  :remote_branch

git branch -m | -M oldbranch newbranch 重命名分支,如果newbranch名字分支已经存在,则需要使用-M强制重命名,否则,使用-m进行重命名。

git branch -d | -D branchname 删除branchname分支

git branch -d -r branchname 删除远程branchname分支

7、查看本地分支

git branch

8、查看远程和本地分支

git branch -a

线性表(LinearList)

InitList(&L)
DestroyList(&L)

ClearList(&L)

ListEmpty(L)
ListLength(L)

GetElem(L, i, &e)
LocateElem(L, e, compare())

PriorElem(L, e, &pre)
NextElem(L, e, &next)

ListInsert(&L, i, e)
ListDelete(L, i, &e)
ListTraverse(L, visit())

线性表

#define LIST_INIT_SIZE 100
#define LISTINCREMENT 10
typedef struct {
    ElemType *elem;
    int length;
    int listsize;
}SqList;

Status InitList_Sq(SqList &L){
    L.elem = (ElemType*) malloc(LIST_INIT_SIZE * sizeof(ElemType));
    if(!L.elem) exit(OVERFLOW);

    L.length = 0;
    L.listsize = LIST_INIT_SIZE;

    return OK;
}

Status ListInsert_Sq(SqList &L, int i, ElemType e){
    if(i < 1 || i > L.length + 1) return ERROR;

    if(L.length >= L.listsize){
        newbase = (ElemType*) realloc(L.elem, (L.listsize + LISTINCREMENT) * sizeof(ElemType));
        if(!newbase) exit(OVERFLOW);

        L.elem = newbase;
        L.listsize += LISTINCREMENT;
    }

    q = &(L.elem[i - 1];
    for(p = &(L.elem[L.length - 1]; p >= q; —p){
        *(p + 1) = *p;
    }
    *q = e;
    ++L.length;

    return OK;
}

Status ListDelete_Sq(SqList &L, int i, ElemType &e){
    if( i < 1 || i > L.length) return ERROR;

    p = &(L.elem[i - 1];
    e = *p;
    q = L.elem + L.length - 1;

    for(++p; p <= q; ++p){
        *(p - 1) = *p;
    }

    —L.length;
    return OK;
}

线性链表

  • 单链表
typedef struct LNode {
    ElemType data;
    struct LNode *next;
} LNode, *LinkList;

void CreateList_L(LinkList &L, int n){
    L = (LinkList)malloc(sizeof(LNode));
    L->next = NULL;

    for(i = n; i > 0; --i){
        p = (LinkList)malloc(sizeof(LNode));
        scanf(&p->data);
        p->next = L->next;
        L->next = p;
    }
}