2.2. 用户-注册-业务层
消息摘要
当计算机之间通信时,假设X计算机向Y计算机发送数据,中间可能经过多层路由等等中继设备,当Y计算机想要核实接收到的数据确实是X所发出的,而没有在中途被篡改,可行的做法是当X计算机发出数据的同时,使用某种摘要算法,对原数据进行运算,得到一条摘要数据,且将摘要数据也发送给Y计算机,而当Y计算机收到数据时,也使用相同的摘要算法,得到摘要数据,并且,和接收到的摘要数据进行对比,如果运算得到的摘要数据和接收到的摘要数据是一致的,则视为数据没有被篡改。
通常使用的摘要算法是某种哈希算法,常用的有SHA系列和MD系列的算法,这些算法的特征有:
无论原数据有多长,得到的摘要数据的长度是固定的,以MD5为例,默认得到32位长度的十六进制数;
对于同样的原数据,一定可以得到相同的摘要数据;
对于不同的原数据,有可能会得到相同的摘要数据;
在密码的应用中,其实,就是把原密码进行摘要运算,把得到的摘要数据作为加密后的密码,当用户注册时,会把加密后的密码存储下来,后续,当需要验证密码时(例如登录、修改密码等等功能),会把用户后续输入的原密码使用同样的算法计算得到摘要数据,与数据库存储的摘要数据进行对比即可。
常见的摘要算法都是公开的(包括其运算步骤和运算公式),所以,单纯的只使用摘要算法对原数据进行摘要,实现密码的加密,也存在不安全因素,尽管例如MD5这种运算是不可逆的运算,但是,可以通过穷举方式,记录大量的“原始密码”与“使用MD5加密得到的密码”的对应关系,实现密码的伪破解!
为了进一步加强密码的安全性,通常,会使用“盐”,“盐”是某个字符串,可以由设计者自由的将它应用在密码的加密过程中,例如:
将原密码和盐拼接,然后将拼接的结果进行加密;
将原密码加密,得到结果A,然后将结果A与盐拼接,得到结果B,最终使用结果B作为最终存储的密码;
将原密码加密,得到结果A,然后将盐加密,得到结果B,然后将A与B拼接,再将拼接的结果进行加密……
所以,盐的具体使用方式没有固定的约定,但是,只要用了盐,肯定可以增强密码的安全性,理论上来说:
盐越复杂,密码的安全性越高,甚至可以为每个用户分配一个随机的盐,例如UUID值,UUID是根据时间、硬件信息等运算得到的36位长度的随机字符串;
盐的使用方式(例如不是单纯的拼接,可能是打散后再拼接……)越复杂,密码的安全性越高;
使用摘要算法加密的次数越多,密码的安全性越高。
在实际的应用中,Spring框架提供了DigestUtils
工具类,其中的静态方法String md5DigestAsHex(byte[])
可以将原密码的字节数据加密得到32位长度的十六进制字符串,在加密时,使用这个方法即可。
在项目中实现加密
当前项目中使用随机盐,所以,首先在t_user
数据表中必须添加新的字段salt CHAR(36)
,用于存储UUID盐,由于数据表发生了变化,所以,实体类User
也应该添加新的属性,并为新的属性添加SET/GET方法,然后,在UserMapper.xml
文件中,无论是<insert>
还是<select>
,都应该添加对salt
字段数据的处理。
然后,在UserServiceImpl
业务层实现类中,在插入数据之前,必须完成密码的加密:
/**
* 获取随机的盐值
* @return 随机的盐值
*/
private String getRandomSalt() {
return UUID.randomUUID()
.toString().toUpperCase();
}
/**
* 获取加密后的密码
* @param src 原始密码
* @param salt 盐
* @return 加密后的密码
* @see #md5(String)
*/
private String getEncrpytedPassword(
String src, String salt) {
// 将原密码加密
String s1 = md5(src);
// 将盐加密
String s2 = md5(salt);
// 将2次加密结果拼接,再加密
String s3 = s1 + s2;
String result = md5(s3);
// 将以上结果再加密5次
for (int i = 0; i < 5; i++) {
result = md5(result);
}
// 返回
return result;
}
/**
* 使用MD5算法对数据进行加密
* @param src 原文
* @return 密文
*/
private String md5(String src) {
return DigestUtils
.md5DigestAsHex(src.getBytes())
.toUpperCase();
}
public void insert(User user)
throws InsertDataException {
// 加密密码
String salt = getRandomSalt();
String md5Password
= getEncrpytedPassword(
user.getPassword(), salt);
user.setSalt(salt);
user.setPassword(md5Password);
// ... 原有其它代码 ...
}
2.3. 用户-注册-控制器层
关于处理注册请求
首先,设计请求:
请求路径:/user/handle_reg.do
请求类型:POST
请求参数:username(*),password(*),phone,email,gender(def:1)
响应方式:ResponseBody
是否拦截:否
创建com.company.store.entity.ResponseResult
类,用于表示响应结果,当控制器中的方法返回这个类的对象时,加上Jackson框架的处理,会返回这个类对应的JSON字符串!在这个类中,至少应该添加Integer state = 200;
和String message
属性。
创建com.company.store.controller.BaseController
,声明为抽象类,作为当前项目中所有控制器类的基类。
创建com.company.store.controller.UserController
,继承自BaseController
,添加@Controller
和@RequestMapping("/user")
注解。
在类中添加业务层对象作为属性@Autowired private IUserService userService;
。
请检查spring-mvc.xml中组件扫描的包是否正确!
然后,在UserController
中添加处理请求的方法:
@RequestMapping("/handle_reg.do")
@ResponseBody
public ResponseResult handleReg(
@RequestParam("username") String username,
@RequestParam("password") String password,
String phone,
String email,
@RequestParam(value="gender", required=false, defaultValue="1") Integer gender) {
// 将5个参数封装到User对象中
// 调用业务层对象的User reg(User user)方法
// 返回ResponseResult对象
}
由于业务层的许多方法都是会抛出异常来表示业务错误的,为了便于统一处理,所以,在BaseController
中添加处理异常的方法:
@ExceptionHandler(ServiceException.class)
@ResponseBody
public ResponseResult handleException(Exception e) {
// 判断异常的类型
if (e instanceof UsernameConflictException) {
return new ResponseResult(401, e);
} else if (e instanceof InsertDataException) {
return new ResponseResult(501, e);
} else {
return new ResponseResult(600, e);
}
}
注:以上代码需要ResponseResult中自定义构造方法!
关于显示注册页面
2.4. 用户-注册-前端页面
将共享的pages.zip
解压,然后将5个子级文件夹复制到项目的webapp
下,即可直接访问相关html文件,例如:http://localhost:8080/TeduStore/web/register.html
。
接下来,应该编辑注册页面register.html
,实现点击注册按钮后,能将用户输入的数据提交到服务端,并获取服务端返回的结果,进行处理!
$('#bt-register').click(function(){
var url = "../user/handle_reg.do";
var username = $("#uname").val();
var password = $("#upwd").val();
var phone = $("#phone").val();
var email = $("#email").val();
var data = "username=" + username
+ "&password=" + password
+ "&phone=" + phone
+ "&email=" + email;
$.ajax({
"url": url,
"data": data,
"type": "post",
"dataType": "json",
"success": function(json) {
if (json.state == 200) {
alert("注册成功!");
} else if (json.state == 401) {
alert("注册失败!" + json.message);
} else if (json.state == 501) {
alert("严重错误!" + json.message);
} else {
alert("莫名其妙!!!");
}
}
});
});
3. 用户-登录
3.1. 用户-登录-持久层
关于“登录”功能的处理,需要能够根据用户名查询用户信息,并且,查询到结果的话,结果中至少需要包括密码、盐、用户id、用户名……
该查询功能在此前完成“注册”时已经实现!所以,无须再开发!
3.2. 用户-登录-业务层
先创建UserNotFoundException
和PasswordNotMatchException
这2个异常类。
在IUserService
中声明抽象方法:
User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException;
然后,在UserServiceImpl
中实现以上方法:
public User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException {
// 根据用户名查询用户信息
// 判断是否查询到用户信息
// 是:用户名有匹配的数据,即用户名正确,则获取查询结果中的盐
// -- 对用户输入的密码执行加密
// -- 判断以上加密密码与数据库的是否匹配
// -- 是:登录成功,返回查询到的对象
// -- 否:密码错误,抛出异常:PasswordNotMatchException
// 否:没有与用户名匹配的数据,即用户名错误,抛出异常:UserNotFoundException
}
3.3. 用户-登录-控制器层
3.4. 用户-登录-前端页面
其它
1. 关于密码加密
加密规则是自定义的,使用哪种加密算法、如何使用盐、加密多少次,都可以自由设计。
2. 关于访问权限
在设计属性、方法时,都应该尽量使用更加严格的权限,也就是:能用private就不要用默认权限,能用默认权限就不要使用protected,能用protected就不要用public,如果一定要使用更加宽松的访问权限,应该是有必须要这样用的原因的!
3. 什么时候需要自行添加构造方法
为了限制对象的创建过程,例如在某些设计模式中。
为了快速创建对象,在创建过程中就为其中的某些属性直接赋值。
3. 哪些数据需要放在Session中
Session是占用服务器内存空间的,所以,能不放在Session中的,就不要放到Session中!
登录后,在Session中存放用户的唯一标识,通常是用户的id;
常用数据也可以放在Session中,例如许多页面都会显示当前登录的用户的用户名,则用户名也可以放在Session中;
处理数据时所必须的数据,且不适合使用其它方式进行存储的。