一、明文密码加密处理
1.1 介绍
使用BCryptPasswordEncoder进行密码加密。
spring security中的BCryptPasswordEncoder方法采用SHA-256 +随机盐+密钥对密码进行加密。SHA系列是Hash算法,不是加密算法,使用加密算法意味着可以解密(这个与编码/解码一样),但是采用Hash处理,其过程是不可逆的。
(1)加密(encode):注册用户时,使用SHA-256+随机盐+密钥把用户输入的密码进行hash处理,得到密码的hash值,然后将其存入数据库中。
(2)密码匹配(matches):用户登录时,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是使用相同的算法把用户输入的密码进行hash处理,得到密码的hash值,然后将其与从数据库中查询到的密码hash值进行比较。如果两者相同,说明用户输入的密码正确。
这正是为什么处理密码时要用hash算法,而不用加密算法。因为这样处理即使数据库泄漏,黑客也很难破解密码(破解密码只能用彩虹表)。
1.2 加密
BCryptPasswordEncoder的构造方法,主要是用来初始化strength和随机数random
public BCryptPasswordEncoder() {
this(-1);
}
public BCryptPasswordEncoder(int strength) {
this(strength, (SecureRandom)null);
}
public BCryptPasswordEncoder(int strength, SecureRandom random) {
this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}");
this.logger = LogFactory.getLog(this.getClass());
if (strength == -1 || strength >= 4 && strength <= 31) {
this.strength = strength;
this.random = random;
} else {
throw new IllegalArgumentException("Bad strength");
}
}
strength的取值范围:-1、[4,31]
random是一个随机数
我们知道,Random类中实现的随机算法是伪随机,也就是有规则的随机。在进行随机时,随机算法的起源数字称为种子数(seed),在种子数的基础上进行一定的变换,从而产生需要的随机数字。相同种子数的Random对象,相同次数生成的随机数字是完全相同的。也就是说,两个种子数相同的Random对象,生成的随机数字完全相同。所以在需要频繁生成随机数,或者安全要求较高的时候,不要使用Random,因为其生成的值其实是可以预测的。
SecureRandom和Random都是,也是如果种子一样,产生的随机数也一样: 因为种子确定,随机数算法也确定,因此输出是确定的。只是说,SecureRandom类收集了一些随机事件,比如鼠标点击,键盘点击等等,SecureRandom 使用这些随机事件作为种子。这意味着,种子是不可预测的,而不像Random默认使用系统当前时间的毫秒数作为种子,有规律可寻。
SecureRandom内置两种随机数算法,NativePRNG和SHA1PRNG。通过new来初始化,默认来说会使用NativePRNG算法生成随机数,但是也可以配置参数来修改调用的算法。
在这里strength的默认值为-1,random默认为空。
encode函数
public String encode(CharSequence rawPassword) {
String salt;
if (this.strength > 0) {
if (this.random != null) {
salt = BCrypt.gensalt(this.strength, this.random);
} else {
salt = BCrypt.gensalt(this.strength);
}
} else {
salt = BCrypt.gensalt();
}
return BCrypt.hashpw(rawPassword.toString(), salt);
}
内部生成salt,使用初始化后的strength和random来进行随机盐的生成。
gensalt函数
因为如果不指定strength的话默认为-1,那么就调用gensalt(),然后将10赋值给strength
public static String gensalt(int log_rounds, SecureRandom random) {
if (log_rounds >= 4 && log_rounds <= 31) {
StringBuilder rs = new StringBuilder();
byte[] rnd = new byte[16];
//可以获取随机的一个byte数组,注意这里不是返回,这个方法是void返回类型,是直接随机改变了rnd
random.nextBytes(rnd);
rs.append("$2a$");
if (log_rounds < 10) {
rs.append("0");
}
rs.append(log_rounds);
rs.append("$");
encode_base64(rnd, rnd.length, rs);
return rs.toString();
} else {
throw new IllegalArgumentException("Bad number of rounds");
}
}
public static String gensalt(int log_rounds) {
return gensalt(log_rounds, new SecureRandom());
}
public static String gensalt() {
return gensalt(10);
}
通过调用decode_base64方法来生成随机盐值,然后返回。得到盐值salt后,调用hashpw方法进行密码加密,具体加密算法就不介绍了。
测试
import org.junit.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* @Author: 98050
* @Time: 2018-11-06 15:13
* @Feature: BCryptPasswordEncoder测试
*/
public class BCTest {
@Test
public void BcTest(){
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String password = "123123";
System.out.println("加密前:" + password);
System.out.println("加密后:" + bCryptPasswordEncoder.encode(password));
}
}
打断点查看一下生成的盐值,如下:
发现最后的结果中保存了随机生成的盐值,主要作用是用来进行密码匹配的。
1.3 密码匹配
密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是使用相同的算法把用户输入的密码进行hash处理,得到密码的hash值,然后将其与从数据库中查询到的密码hash值进行比较。因为盐值就存放在密文的前半部分,得到盐值后对原密码进行加密,结果是和第一次一样的,这样就完成了密码的匹配。
匹配方法:matches
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword != null && encodedPassword.length() != 0) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
} else {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
} else {
this.logger.warn("Empty encoded password");
return false;
}
}
通过正则表达式判断加密后的密文是否符合规则
符合的话进入checkpw方法中:
将盐值取出来后进行加密,然后进行字符串的匹配就可以返回结果了。
二、注册异常处理
使用vue-validator来进行表单数据验证。
2.1 安装
npm install vee-validate@next --save
2.2 引用
import Vue from 'vue';
import VeeValidate from 'vee-validate';
Vue.use(VeeValidate, {
events: 'blur',
dictionary: {
zh: {
messages: {
required: (field) => field + '不能为空!',
min: (field, args) => field + '长度不能小于' + args[0],
max: (field, args) => field + '长度不能大于' + args[0],
alpha_dash: (field) => field + '只能包含数字、字母或下划线',
regex: (field) => field + "格式不正确",
is: () => "两次密码不一致"
}
}
},
locale: 'zh'
});
2.3 使用自定义验证规则
this.$validator.extend('useful', {
getMessage(field, args, data) {
// will be added to default locale messages.
// Returns a message.
return args[0] === '1' ? '用户名' + data : '手机' + data;
},
validate(value, args) {
return new Promise(resolve => {
leyou.http.get("/user/check/" + value + "/" + args[0])
.then(resp => {
resolve({
valid: resp.data,
data: "已存在!"
})
})
});
}
});
this.$validator.extend('confirm', {
getMessage() {
return "两次密码不一致"
},
validate(val, args) {
return val === args[0]
}
})
2.4 表单验证
<div class="info" style="width: 650px">
<form class="sui-form form-horizontal">
<div class="control-group">
<label class="control-label">用户名:</label>
<div class="controls">
<input type="text" placeholder="请输入你的用户名" class="input-xfat input-xlarge"
v-model.lazy="user.username" name="username" data-vv-as="用户名"
v-validate="'required|alpha_dash|min:4|max:15|useful:1'">
</div>
<span style="color: red;">{{ errors.first('username') }}</span>
</div>
<div class="control-group">
<label class="control-label">登录密码:</label>
<div class="controls">
<input type="password" placeholder="设置登录密码" class="input-xfat input-xlarge"
v-model="user.password" name="password" data-vv-as="密码"
v-validate="'required|alpha_dash|min:6|max:25'">
</div>
<span style="color: red;">{{ errors.first('password') }}</span>
</div>
<div class="control-group">
<label class="control-label">确认密码:</label>
<div class="controls">
<input type="password" placeholder="再次确认密码" class="input-xfat input-xlarge"
v-model="user.confirmPassword" name="confirmPass" data-vv-as="确认密码"
v-validate="{required:true,confirm:user.password}">
</div>
<span style="color: red;">{{ errors.first('confirmPass') }}</span>
</div>
<div class="control-group">
<label class="control-label">手机号:</label>
<div class="controls">
<input type="text" placeholder="请输入你的手机号" class="input-xfat input-xlarge"
v-model="user.phone" name="phone" data-vv-as="手机号"
v-validate="{required:true,regex:/^1[35678]\d{9}$/,useful:2}">
</div>
<span style="color: red;">{{ errors.first('phone') }}</span>
</div>
<div class="control-group">
<label class="control-label">短信验证码:</label>
<div class="controls">
<input type="text" placeholder="短信验证码" class="input-xfat input-xlarge" style="width: 120px;"
v-model="user.code" name="code" v-validate="'required'" data-vv-as="验证码">
<span class="code-span" @click="createVerifyCode">
获取短信验证码
</span>
</div>
<span style="color: red;">{{ errors.first('code') }}</span>
</div>
<div class="control-group">
<label class="control-label"> </label>
<div class="controls">
<input name="m1" type="checkbox" value="2" checked=""><span>同意协议并注册《乐优用户协议》</span>
</div>
</div>
<div class="control-group">
<label class="control-label"></label>
<div class="controls btn-reg">
<a class="sui-btn btn-block btn-xlarge btn-danger" href="javascript:void(0)" target="_blank"
@click.stop="submit"
>完成注册</a>
</div>
</div>
</form>
<div class="clearfix"></div>
</div>
errors.first('field')
获取关于当前field的第一个错误信息
2.5 执行过程
以用户名的验证为例:
v-validate中有字段:(useful:1)表示使用自定义规则的过滤器:useful,并且传入值1。
自定义过滤规则:
this.$validator.extend('useful', {
getMessage(field, args, data) {
return field + data;
},
validate(value, args) {
console.log(value);
console.log(args)
return new Promise(resolve => {
leyou.http.get("/user/check/" + value + "/" + args[0])
.then(resp => {
resolve({
valid: resp.data,
data: "已存在!"
})
})
});
}
});
其中validate方法用来接收当前字段的值,以及传入的数据1,查询输出结果如下所示:
因为这里的验证是通过后台api接口来进行的,所以在返回提示信息的时候就需要包含两个字段:可选属性data和valid属性。这里面的data将作为第三个参数传递给消息生成器函数即getMessage。然后在getMessage中将data输出。这里面在封装属性时使用Promise函数,来进行异步操作。
Promise的构造函数接收一个参数,是函数,并且传入两个参数:resolve,reject,分别表示异步操作执行成功后的回调函数和异步操作执行失败后的回调函数。其实这里用“成功”和“失败”来描述并不准确,按照标准来讲,resolve是将Promise的状态置为fullfiled,reject是将Promise的状态置为rejected。
举例:
this.runAsync1().then(function(data){ console.log(data); }); runAsync1(){ let p = new Promise(function(resolve, reject){ //做一些异步操作 setTimeout(function(){ console.log('异步任务1执行完成'); resolve('随便什么数据1'); }, 1000); }); return p; },
结果:
分析:
在runAsync()的返回上直接调用then方法,then接收一个参数,是函数,并且会拿到我们在runAsync中调用resolve时传的的参数。运行这段代码,会在1秒后输出“异步任务1执行完成”,紧接着输出“随便什么数据1”。
then里面的函数就跟我们平时的回调函数一个意思,能够在runAsync这个异步任务执行完成之后被执行。这就是Promise的作用了,简单来讲,就是能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。
validate方法中返回一个Promise对象用来异步封装valid和data属性,以便在getMessage中获取。官方文档:
getMessage方法用来返回错误信息。
输出getMessage中的field、args、data三个字段:
结果如下:
其中filed字段接收的是data-vv-as中的值
args接收传入的值:1
data接收validate方法的返回值data。
最终错误信息用errors.first('username')来显示。