1、需要实现的功能介绍
注册 登录 用户名校验
忘记密码 提交问题答案 重置密码
获取用户信息 更新用户信息 退出登录
目标:
- 避免横向越权,纵向越权的安全漏洞
- MD5明文加密级增加的salt值
- Guava缓存的使用
- 高复用服务响应对象的设计思想级抽象封装
- session的使用
横向越权:攻击者尝试访问与他人拥有相同权限的用户。
纵向越权:低级别攻击者尝试访问高级别用户的资源
当用户在未登录的状态下修改密码时,用户回答了忘记密码的答案然后生成一个具有时间限制的token,这里用UUID表示,之后再跳转到重置密码界面输入新的密码提交,这里设置token的作用就是避免横向越权,如果一个网络攻击者直接进入重置密码页面,这里会进行检测他提交时是不是有正确的token,并且我们可以设置token的生效时间,这样就提高了安全性,当然密码的存储是经过MD5加密的,所以数据库看到的是加密后的密码。如果用户在登陆成功后,进行修改的话,我们不需要这么做。
用户重置密码阶段
这个阶段是在用户正确回答自己设置的问题后,拿到了服务器生成的token后进行密码重置,这时该进行怎样的处理呢?
首先前台表单中我们可以获取到用户名,提交表单携带的token,和新的密码。其次,我们先要验证的是传过来的token,如果他为空或者是个空串,或者包含空格,那就不合法,返回参数错误信息;然后,校验用户名,这时候为什么要再次校验username呢?因为在本地保存token时,我把username当做key,生成的token当做了value,所以在后面获取本地储存的token时,要保证username的正确,才能获取本地的token,校验完username后。下一步,我们开始从本地获取token,获取后需要校验一下本地的是不是为空或者是空串,这时才把两个token进行比较,当前台传过来的token与本地服务器端的token一样时,我们才开始更新密码,记得在更新前对新的密码进行加密,HDMC或者MD5方式,加密后才根据username更新密码字段,记得要带上更新时间,也就是一起把update_time也更新了。如果两个token不一样,那就设置错误的状态吗,提示token错误,请重新回答密保问题后再次进行更改。
核心代码如下
public ServerResponse<String> forgetResetPassword(String username, String passwordNew, String forgetToken) {
if (StringUtils.isBlank(forgetToken)) {
return ServerResponse.createByErrorMessage("参数错误,token需要传递");
}
//需要校验用户名,因为tokenCache的key包含username
ServerResponse<String> checkValid = this.checkValid(username, Const.USERNAME);
if (checkValid.isSuccess()) {
//用户名不存在
return ServerResponse.createByErrorMessage("用户不存在");
}
String token = TokenCache.getKey(TokenCache.TOKEN_PREFIX + username);
if (StringUtils.isBlank(token)) {
return ServerResponse.createByErrorMessage("token无效或过期");
}
if (StringUtils.equals(forgetToken,token)){
//这时开始更新password,先加密在保存
String md5Password = MD5Util.MD5EncodeUtf8(passwordNew);
int rowCount = userMapper.updatePasswordByUsername(username, md5Password);
if (rowCount>0){
return ServerResponse.createBySuccessMessage("修改密码成功");
}
}else{
//如果两个token不一样,那就重新获取新的token
return ServerResponse.createByErrorMessage("token错误,请重新回答问题后获取重置密码的token");
}
return ServerResponse.createByErrorMessage("修改密码失败");
}
当用户回答完忘记密码的问题后,提交的用户名,问题,答案要在数据库中进行查询如果查到的记录数为1,代表问题与答案相匹配,然后生成token,这里也就是一个UUID,把这个token放在本地的token缓存中,以键值对的形式存储,token_username代表key,生成的UUID代表value,最后返回前端的数据是生成的token。代码如下
public ServerResponse<String> checkAnswer(String username, String question, String answer) {
int resultCount = userMapper.checkAnswer(username, question, answer);
if (resultCount > 0) {
//问题答案匹配成功
String forgetToken = UUID.randomUUID().toString();
TokenCache.setKey(TokenCache.TOKEN_PREFIX + username, forgetToken);
return ServerResponse.createBySuccessData(forgetToken);
}
return ServerResponse.createByErrorMessage("问题的答案错误");
}
TokenCache类:
private static Logger logger = LoggerFactory.getLogger(TokenCache.class);
public static final String TOKEN_PREFIX = "token_";
//LRU算法
private static LoadingCache<String, String> localCache =
CacheBuilder.newBuilder()
.initialCapacity(1000)
.maximumSize(10000)
.expireAfterAccess(12, TimeUnit.HOURS)
.build(new CacheLoader<String, String>() {
//默认的数据加载实现,当key没有对应的值时,就调用这个方法进行加载
@Override
public String load(String s) throws Exception {
return "null";
}
});
public static void setKey(String key, String value) {
localCache.put(key, value);
}
public static String getKey(String key){
String value = null;
try {
value = localCache.get(key);
if ("null".equals(value)){
return null;
}
return value;
} catch (ExecutionException e) {
logger.error("localCache get error",e);
}
return null;
}
现在纯粹的MD5加密已经不安全了,因为有专门解密的相关网站,只要将加密后的密文粘贴上去就可以看到解密的密码。所以为了保存数据传入的安全性,我们需要加上盐值,再用MD5生成密文,只要对方获取不到盐值,他解密是很难的。来看一下怎样使用加盐的方式进行加密。
例如在注册的接口中:
注册过程
首先前台提交过来了一大堆内容,例如用户名,密码,邮箱等等,这时我们防止用户名重复,首先校验用户名不能重复,然后校验邮箱也不能重复,方法和校验用户名一样,再然后就开始对用户的密码进行加密。
public ServerResponse<String> register(User user) {
//校验用户名
ServerResponse<String> checkValid = this.checkValid(user.getUsername(), Const.USERNAME);
if (!checkValid.isSuccess()) {
return checkValid;
}
//校验email
checkValid = this.checkValid(user.getEmail(), Const.EMAIL);
if (!checkValid.isSuccess()) {
return checkValid;
}
user.setRole(Const.Role.ROLE_CUSTOMER);
//MD5加密
user.setPassword(MD5Util.MD5EncodeUtf8(user.getPassword()));
int i = userMapper.insert(user);
if (i == 0) {
return ServerResponse.createByErrorMessage("注册失败");
}
return ServerResponse.createBySuccessMessage("注册成功");
}
这里开始查看MD5Util类的实现;
public static String MD5EncodeUtf8(String origin) {
origin = origin + PropertiesUtil.getProperty("password.salt", "");
return MD5Encode(origin, "utf-8");
}
这里就是加盐的过程了,如果在配置文件中没有设置盐默认就是一个空的字符串。password+salt的字符串在进行加密。
private static String MD5Encode(String origin, String charsetname) {
String resultString = null;
try {
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if (charsetname == null || "".equals(charsetname)) {
resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
} else {
resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
}
} catch (Exception exception) {
}
return resultString.toUpperCase();
}
先将字符串转化为字节数组,然后整体对字节数组进行处理,对与字节数组中的每一个字节将他自身对16进行取余,整除运算,得到的两个结果,再从预先定义的private static final
数组中获取值,最终返回。
private static String byteArrayToHexString(byte b[]) {
StringBuffer resultSb = new StringBuffer();
for (int i = 0; i < b.length; i++) {
resultSb.append(byteToHexString(b[i]));
}
return resultSb.toString();
}
private static String byteToHexString(byte b) {
int n = b;
if (n < 0) {
n += 256;
}
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};