Java岗大厂面试百日冲刺 - 日积月累,每日三题【Day36】—— 实战那些事儿1

2021-12-31 15:33:40 浏览数 (1)

车票

  • 面试题1:你们是怎样保存用户密码等敏感数据的?
  • 面试题2:怎么控制用户请求的幂等性的?
  • 面试题3:你们是如何预防SQL注入问题的?
  • 每日小结

  本栏目Java开发岗高频面试题主要出自以下各技术栈:Java基础知识集合容器并发编程JVMSpring全家桶MyBatis等ORMapping框架MySQL数据库Redis缓存RabbitMQ消息队列Linux操作技巧等。

面试题1:你们是怎样保存用户密码等敏感数据的?

  本题回答参考朱晔的《Java业务开发常见错误100例》

  我们知道,用户名、密码、身份证等都属于用户敏感信息,其中最敏感的数据恐怕就是用户的密码了。黑客一旦窃取了用户密码,就可以登录进用户的账号,消耗其资产、发布不良信息等;更可怕的是,有些用户至始至终都是使用一套密码,密码一旦泄露,就可以被黑客通过撞库来登录全网各大平台,嘿嘿嘿。

  为了防止密码泄露,最重要的原则是不要在数据库保存用户原始密码

  大家经常说,不要明文保存用户密码,应该把密码通过 MD5 加密后保存。这的确是一个正确的方向,但这个说法并不准确。

  首先,MD5 其实不是真正的加密算法。所谓加密算法,是可以使用密钥把明文加密为密文,随后还可以使用密钥解密出明文,是双向的。而 MD5 是散列、哈希算法或者摘要算法。不管多长的数据,使用 MD5 运算后得到的都是固定长度的摘要信息或指纹信息,无法再解密为原始数据。所以,MD5 是单向的。最重要的是,仅仅使用 MD5 对密码进行摘要,并不安全。

  比如,使用如下代码在保持用户信息时,对密码进行了 MD5 计算:

代码语言:javascript复制
UserData userData = new UserData();
userData.setId(1L);
userData.setName(name);
//密码字段使用MD5哈希后保存
userData.setPassword(DigestUtils.md5Hex(password));
return userRepository.save(userData);

通过输出,可以看到密码是 32 位的 MD5:

代码语言:javascript复制
"password": "325a2cc052914ceeb8c19016c091d2ac"

  然后,再去破解网站试一下这个 MD5,就可以得到原始密码是 salt,也就知道了盐值是 salt

  其实,知道盐是什么没什么关系,关键的是我们是在代码里写死了盐,并且盐很短、所有用户都是这个盐。这么做就会有三个问题:

  • 因为盐太短、太简单了,如果用户原始密码也很简单,那么整个拼起来的密码也很短,这样一般的 MD5 破解网站都可以直接解密这个 MD5除去盐就是原始密码了。
  • 相同的盐,意味着使用相同密码的用户 MD5 值是一样的,知道了一个用户的密码就可能知道了多个
  • 黑客也可以使用这个盐来构建一张彩虹表,也就是字典表,虽然会花不少代价,但是一旦构建完成,所有人的密码都可以被破解。

  所以,最好是每一个密码都有独立的盐,并且盐要长一点,比如超过 20 位。

  第二,虽然说每个人的盐最好不同,但也不建议将一部分用户数据作为盐。比如,使用用户名作为盐:

代码语言:javascript复制
userData.setPassword(DigestUtils.md5Hex(name   password));

  如果世界上所有的系统都是按照这个方案来保存密码,那么 root、admin 这样的用户使用再复杂的密码也总有一天会被破解,因为黑客们完全可以针对这些常用用户名来做彩虹表。所以,盐最好是随机的值,并且是全球唯一的,意味着全球不可能有现成的彩虹表给你用。

  正确的做法是,使用全球唯一的、和用户无关的、足够长的随机值作为盐。比如,可以使用 UUID 作为盐,把盐一起保存到数据库中:

代码语言:javascript复制
userData.setSalt(UUID.randomUUID().toString());
userData.setPassword(DigestUtils.md5Hex(userData.getSalt()   password));

  并且每次用户修改密码的时候都重新计算盐,重新保存新的密码。

  需要注意的是,这么做虽然黑客已经很难通过彩虹表来破解密码了,但是仍然有可能暴力破解密码,也就是对于同一个用户名使用常见的密码逐一尝试登录。因此,除了做好密码哈希保存的工作外,我们还要建设一套完善的安全防御机制,在感知到暴力破解危害的时候,开启短信验证、图形验证码、账号暂时锁定等防御机制来抵御暴力破解

那么姓名和身份证又是怎么保存的?

  我们把姓名和身份证,叫做二要素。

  现在互联网非常发达,很多服务都可以在网上办理,很多网站仅仅依靠二要素来确认你是谁。所以,二要素是比较敏感的数据,如果在数据库中明文保存,那么数据库被攻破后,黑客就可能拿到大量的二要素信息。如果这些二要素被用来申请贷款等,后果不堪设想。

  之前我们提到的单向散列算法(MD5),显然不适合用来加密保存二要素,因为数据无法解密。这个时候,我们需要选择真正的加密算法。可供选择的算法,包括对称加密非对称加密算法两类。

  • 对称加密算法:是使用相同的密钥进行加密和解密。使用对称加密算法来加密双方的通信的话,双方需要先约定一个密钥,加密方才能加密,接收方才能解密。如果密钥在发送的时候被窃取,那么加密就是白忙一场。因此,这种加密方式的特点是,加密速度比较快,但是密钥传输分发有泄露风险。
  • 非对称加密算法:或者叫公钥密码算法。公钥密码是由一对密钥对构成的,使用公钥或者说加密密钥来加密,使用私钥或者说解密密钥来解密,公钥可以任意公开,私钥不能公开。使用非对称加密的话,通信双方可以仅分享公钥用于加密,加密后的数据没有私钥无法解密。因此,这种加密方式的特点是,加密速度比较慢,但是解决了密钥的配送分发安全问题。

  但是,对于保存敏感信息的场景来说,加密和解密都是我们的服务端程序,不太需要考虑密钥的分发安全性,也就是说使用非对称加密算法没有太大的意义。我们一般会使用对称加密算法来加密数据。常见的对称加密算法有:DES3DESAES


面试题2:怎么控制用户请求的幂等性的?

幂等性:对于同一笔业务操作,不管调用多少次,得到的结果都是一样的。

当然,有些操作是天然幂等的,如:

  • 查询操作:查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作;
  • 删除操作:删除操作也是幂等的,删除一次和多次删除都是把数据删除。

后台控制幂等性的几种途径:

  1. 设置唯一索引:防止新增脏数据

  比如支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建资金账户多个,那么给资金账户表中的用户ID加唯一索引,所以一个用户新增成功一个资金账户记录。要点:唯一索引或唯一组合索引来防止新增数据存在脏数据(当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可);

  1. token机制:防止页面重复提交

  原理上通过session token来实现的(也可以通过redis来实现)。当客户端请求页面时,服务器会生成一个随机数Token,并且将Token放置到session当中,然后将Token发给客户端(一般通过构造hidden表单)。下次客户端提交请求时,Token会随着表单一起提交到服务器端

  服务器端第一次验证相同过后,会将session中的Token值更新下,若用户重复提交,第二次的验证判断将失败,因为用户提交的表单中的Token没变,但服务器端session中Token已经改变了。

  1. 悲观锁

获取数据的时候加锁获取。

代码语言:javascript复制
select * from table_xxx where id='xxx' for update; 

注意:id字段一定是主键或者唯一索引,不然是锁表,会死人的;悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用;

  1. 乐观锁

  乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。乐观锁的实现方式多种多样,可以通过version或者其他状态条件判断:

  • 通过版本号实现
代码语言:javascript复制
update table_xxx set name=#name#,version=version 1 where version=#version#;
  • 通过条件限制
代码语言:javascript复制
update table_xxx set avai_amount=avai_amount where avai_amount >= 0
  1. 分布式锁

  如果是分布式系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。

  要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID 后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供)


面试题3:你们是如何预防SQL注入问题的?

SQL注入攻击的总体思路

  1. 寻找到SQL注入的位置
  2. 判断服务器类型和后台数据库类型
  3. 针对不通的服务器和数据库特点进行SQL注入攻击

预防方式:

1、PreparedStatement(简单有效)

  采用预编译语句集,它内置了处理SQL注入的能力,只要使用它的setXXX方法传值即可。

  sql注入只对sql语句的准备(编译)过程有破坏作用,而PreparedStatement在执行阶段只是把输入串作为数据处理,不再对sql语句进行解析,因此也就避免了sql注入问题。

2、使用正则表达式过滤传入的参数

要引入的包:

代码语言:javascript复制
import java.util.regex.*;

正则表达式:

代码语言:javascript复制
private String CHECKSQL = “^(. )\sand\s(. )|(. )\sor(. )\s$”;

判断是否匹配:

代码语言:javascript复制
Pattern.matches(CHECKSQL,targerStr);

下面是常用来过滤参数是否存在SQL注入的正则表达式:

  • 检测SQL meta-characters的正则表达式 : /(')|(')|(--)|(#)|(#)/ix
  • 修正检测SQL meta-characters的正则表达式 : /((=)|(=))[^n]*((')|(')|(--)|(;)|(:))/i
  • 典型的SQL 注入攻击的正则表达式 : /w*((')|('))((o)|o|(O))((r)|r|(R))/ix
  • 检测SQL注入,UNION查询关键字的正则表达式 : /((')|('))union/ix(')|(')

3.使用正则表达式过滤传入的URL

  比较通用的一个方法,jsp中调用该函数检查是否包函非法字符,防止SQL从URL注入:

(||之间的参数可以根据自己程序的需要添加)

代码语言:javascript复制
public static boolean sql_inj(String str){
 
    String inj_str = "'|and|exec|insert|select|delete|update|
    
    count|*|%|chr|mid|master|truncate|char|declare|;|or|-| |,";
    
    String inj_stra[] = split(inj_str,"|");
    
    for (int i=0 ; i < inj_stra.length ; i   ){
    
        if (str.indexOf(inj_stra[i])>=0){
        
        return true;
        
        }
     
    }
 
    return false;
 
}

每日小结

  今天我们复习了面试中常问到的网络安全方面的三个问题,你做到心中有数了么?对了,如果你的朋友也在准备面试,请将这个系列扔给他,如果他认真对待,肯定会感谢你的!!好了,今天就到这里,学废了的同学,记得在评论区留言:打卡。,给同学们以激励。

0 人点赞