在本人的日程管理项目中,既然涉及到了用户登录这一功能,并且是一个联网系统,那么安全策略是必不可少的,以下是本项目的一些安全策略实现:
1.MySQL数据存储加密
用户信息的安全始终是第一位的,数据库加密则是必不可少的一环。
具体策略如下:
·用户密码采用SHA1不可逆加密算法
·用户邮箱采用CBC对称加密算法
效果如下(摘自用户信息存储数据表):
| uid | username | password | email | created_at |
+-----+----------+------------------------------------------+--------------------------------------------------------------------+---------------------+
| 2 | test | af048c5e9f7684536de4544114a8e3292cde1740 | 0fafbc2ad0d54bc2ae0b27ecd4b2a7a2ccb67bff337be525cac0538f5a1741d474 | 2023-04-13 23:18:12 |
| 3 | test1 | af048c5e9f7684536de4544114a8e3292cde1740 | ff47471240fc0adfeacd6f656f49b7b3 | 2023-05-22 13:07:25 |
SHA1算法相对MD5等算法增加了破解成本,提高了安全性
CBC算法因其需要密钥,偏移量两个变量进行加密,也降低了在无法使用不可逆加密算法时的风险程度(因加密,解密均由服务器后端完成,故认为无需使用非对称加密)
实现方式如下(因此博客公开,故不展示CBC密钥及偏移量):
//CBC加密
public String cbc(String str){
String key="KEY HERE";
String iv="IV HERE";
if(str==null) return "";
CBC cbc=new CBC(key,iv);
cbc.setPlaintext(str);
cbc.Encrypt();
return cbc.showCiphertext();
}
//SHA1加密
public String sha1(String str) throws NoSuchAlgorithmException {
if(str==null) return "";
MessageDigest sha128=MessageDigest.getInstance("SHA-1");
sha128.update(str.getBytes());
byte[] res=sha128.digest();
StringBuilder sb = new StringBuilder();
for(byte bite : res) {
sb.append(String.format("%02x", bite));
}
return sb.toString();
}
//具体实现
info.password=encrypt.sha1(encrypt.cbc(info.password));
info.email=encrypt.cbc(info.email);
2.登录态验证
登录态验证是避免水平越权的重要途径,在本项目中的实现方式主要有:对未登录用户禁止访问部分接口,限定数据库查询语句仅可查询当前登录态用户的数据。当前登录用户采用uid作为唯一标识符。
实现方式如下:
服务器验证:
//验证登录态
@SaCheckLogin
//获取登录态
int uid = Integer.parseInt(StpUtil.getLoginId().toString());
仅是一行注解和代码的内容,这便是SaToken框架的简便之处
数据库验证:
select * from schedule where owner=#{uid} order by end_at DESC;
通过验证数据表中的owner字段来保证用户只能操作自己的内容
3.恶意请求过滤及服务器异常捕获
恶意请求是服务器运行的一大阻碍,可能造成服务器崩溃,数据泄露等安全问题。SpringBoot框架,MyBatis框架以及SaToken框架因其相对成熟,已经从框架层面避免了部分因恶意请求造成的漏洞,包括但不限于:
· SQL注入攻击
· 恶意cookie
· 不符合类型要求的URL参数
但是仍有部分恶意请求需要手动过滤,包括但不限于:
· 超长字符串
· 不合规的数字
其中,对于部分异常可以通过忽略或返回请求空体或者直接返回400/500状态码解决
其余的过滤方式如下:
//摘自新建日程处理部分
if(info.begin>info.end||info.event.length()>500||info.tag.length()>120)
throw new InvalidRequest();//超长字符串和非法起止时间的过滤
if(info.repeat<0||info.repeat>3650) info.repeat=0;//非法重复周期和超长重复周期的过滤
if(end_n<0||end>rt.size()) end_n=rt.size();
if(begin_n<0) begin_n=0;
//不合法的切片数据过滤
部分非法数据可以通过将其限制在合法范围内解决,剩余的交给异常捕获器处理。
异常捕获器避免了报错信息的直接反馈,同时避免了服务器崩溃,也能告知用户出现的问题
实现方式如下:
//运行时错误捕获并输出至日志
@ExceptionHandler
public Response globalFilter(Throwable e, HttpServletResponse res){
res.setStatus(500);
logger.error("Runtime error found at "+dateToStamp.stampToDate(System.currentTimeMillis())+
"with exception"+e.toString());
return new Response(ReturnCode.UnknownError);
}
//处理请求异常捕获并输出到日志
@ExceptionHandler
public Response filter(ResponseError e, HttpServletResponse res){
res.setStatus(e.status);
logger.warn("Server exception found at"+dateToStamp.stampToDate(System.currentTimeMillis())+"with code"+e.status);
return new Response(e.info);
}
@ExceptionHandler
public Response NoLogin(NotLoginException e,HttpServletResponse res){
res.setStatus(401);
return new Response(ReturnCode.NoPrivileges);
}