随着时代及互联网的发展,人们对个人隐私越来越重视,但隐私信息泄露及滥用的问题依然屡见不鲜。之前有一份《中国个人信息安全和隐私保护报告》曾抽取100万份调查数据,80%用户遭遇隐私泄露,还比如万豪在18年遭遇3.83亿隐私数据泄露后于2020年3月31日再次爆出520万客户信息泄露。这背后的缘由咱们就不做多讲,除了一些流氓公司的恶意行为,肯定还有很多的商业利益的驱使。今天我们来聊一聊开发人员该如何处理用户隐私,想起半年前在知乎上爆出的某省普通话水平测试查询系统开发人员把身份证直接写在了js里,有网友笑称这才是真正的前后端分离,支撑亿级并发完全不是事。
文章开始之前,先抛出一个小问题:除了姓名、身份证、银行卡、手机号外,你觉得还有哪些是用户的敏感信息,需要加密存储?
什么叫个人信息,哪些又算敏感信息?个人信息该如何存储,又该如何展示?游戏中的兑换码是不是敏感信息?住宿信息是不是敏感信息??作为一名优秀的开发人员,我们不能把目光仅仅聚焦在代码上,不能永远是产品经理或者项目经理让我这么做,还应该掌握所在行业的业务知识,包括法律及政策规范等,提升拓宽我们的业务知识面。
关于信息系统建设这一块,国家及行业其实有很多的标准和规范的,比如国家标准全文公开系统(http://openstd.samr.gov.cn/))。关于个人信息,最新的是今年发布的《GB/T 35273-2020 信息安全技术-个人信息安全规范 》,将于2020-10-01正式实施,取代老的标准GB/T 35273-2017。 整个规范文档主要体现了七大原则:权责一致原则、目的明确原则、选择同意原则、最少够用原则、公开透明原则、确保安全原则、主体参与原则。
个人信息,personal information。指以电子或者其他方式记录的能够单独或者与其他信息结合识别特定自然人身份或者反映特定自然人活动情况的各种信息。
判定方式:
个人信息举例:
注:个人信息控制者通过个人信息或其他信息加工处理后形成的信息,例如,用户画像或特征标签,能够单独或者与其他信息结合识别特定自然人身份或者反映特定自然人活动情况的,也属于个人信息。
个人敏感信息,personal sensitive information。指一旦泄露、非法提供或滥用可能危害人身和财产安全,极易导致个人名誉、身心健康受到损害或歧视性待遇等的个人信息。通常情况下,14岁以下(含)儿童的个人信息和涉及自然人隐私的信息属于个人敏感信息。
判定方式:
泄露:个人信息一旦泄露,将导致个人信息主体及收集、使用个人信息的组织和机构丧失对个人信息的控制能力,造成个人信息扩散范围和用途的不可控。某些个人信息在泄漏后,被以违背个人信息主体意愿的方式直接使用或与其他信息进行关联分析,可能对个人信息主体权益带来重大风险,应判定为个人敏感信息。例如,个人信息主体的身份证复印件被他人用于手机号卡实名登记、银行账户开户办卡等。
非法提供:某些个人信息仅因在个人信息主体授权同意范围外扩散,即可对个人信息主体权益带来重大风险,应判定为个人敏感信息。例如,性取向、存款信息、传染病史等。
滥用:某些个人信息在被超出授权合理界限时使用(如变更处理目的、扩大处理范围等),可能对个人信息主体权益带来重大风险,应判定为个人敏感信息。例如,在未取得个人信息主体授权时,将健康信息用于保险公司营销和确定个体保费高低。
个人敏感信息举例
注:个人信息控制者通过个人信息或其他信息加工处理后形成的信息,如一旦泄露、非法提供或滥GB/T 35273—20206用可能危害人身和财产安全,极易导致个人名誉、身心健康受到损害或歧视性待遇等的,属于个人敏感信息。
整个规范文件中,还提到了用户信息的使用、展示、第三方接入、安全管理等等,有兴趣的小伙伴可以自定搜索了解一下。
正如第一章节提到的,用户的真实姓名、手机号、银行卡号、包括住宿等敏感信息需要加密存储到数据库中,业务正常使用的时候再转化为明文数据。从技术实现角度来看,无非就是新增、编辑时进行加密,查询时解密,这样一个个操作起来还是比较low的,而且很可能哪天新增了一个方法又忘记加解密了。所以大部分会通过框架来实现,实现的原理无外乎反射机器+拦截器。接下来以Mybatis为例,原理如下图,具体可参考:https://blog.csdn.net/weixin_39494923/article/details/91534658
Mybatis默认提供了一个拦截器接口Interceptor,大部分Mybatis的增强工具都是通过该接口实现的。如果要实现自定义的拦截器,只需要实现 org.apache.ibatis.plugin.Interceptor 接口,该接口有三个方法:
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
首先以自定义一个注解@Crypt,作用在字段上,用于告诉拦截器那个字段需要加解密。
@Target({ ElementType.FIELD,ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Crypt {
}
接下来添加一个自定义拦截器,selelct方法时进行解密,update和add方法时进行加密。
@Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class, }),
@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class }),
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = { Statement.class }) })
public class CryptInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
if (args.length <= 0 || invocation.getMethod() == null || args[0] == null) {
return invocation.proceed();
}
String methodName = invocation.getMethod().getName();
if ("update".equals(methodName) && args[1] != null) {
return this.interceptUpdate(invocation);
} else if ("query".equals(methodName) && args[1] != null) {
return this.interceptQuery(invocation);
} else if ("handleResultSets".equals(methodName)) {
return this.interceptHandleResultSets(invocation);
}
return invocation.proceed();
}
private Object interceptHandleResultSets(Invocation invocation) throws Throwable {
Object resultCollection = invocation.proceed();
// 略 将resultCollection的对象中有@Crypt注解的Feild进行解密
return newObject;
}
private Object interceptUpdate(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
Object args1Obj = args[1];
// 略 将args1Obj的对象进行加密
args[1] = newObject;
return invocation.proceed();
}
private Object interceptQuery(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
Object condition = args[1];
// 略 将condition对象进行解密
args[1] = newObject;
return invocation.proceed();
}
}
一般情况下不会通过Interceptor接口对Mybatis的请求进行拦截,除非类似于“读写分离”这样的一些复杂的需求。参见上面的mybatis的执行过程,我们发现最后一步调用了TypeHander,这个类的作用就是把数据库与实体之间进行类型转换,比如把MySql的varchar转为Java的Long,把Java的Integer转为Mysql的int,所以我们可以借助于BaseTypeHandler类。
@Component
@Alias("CryptHandler")
@MappedTypes(value = {Crypt.class})
public class EncryptHandler extends BaseTypeHandler {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, encrypt(parameter.toString()));
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
String columnValue = rs.getString(columnName);
return decrypt(columnValue);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String columnValue = rs.getString(columnIndex);
return decrypt(columnValue);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String columnValue = cs.getString(columnIndex);
return decrypt(columnValue);
}
private String encrypt(String parameter) {
// 加密
return parameter;
}
private String decrypt(String columnValue) {
// 解密
return columnValue;
}
}
完整 代码见上面,不做多讲。接下来需要告诉Mybatis哪些字段需要加解密,为了简化书写,定义一个类Crypt重命名为crypt,上面的类EncryptHandler也重命名为EncryptHandler
@Alias("crypt")
public final class Crypt {
}
上面的两个类都放在cn.itmds.plugin目录下,配置yml文件告诉Mybatis读取重命名的配置
mybatis:
type-aliases-Package: cn.itmds.plugin.dbcrypt
接下来,假设有一张member表的realname(真实姓名)字段需要加解密,写起来就很简单了:
<sql id="memberConditionSql">
<where>
<if test="id != null">and id = #id}
<!--这个地方只需要指定javaType=crypt,如果上面没有重命名,这个地方需要写成javaType= cn.itmds.plugin.dbcrypt.Crypt,写起来比较麻烦 -->
<if test=realName != null">and real_name = #{realName,javaType=crypt}
</where>
</sql>
<resultMap id="memberDOResultMap" type="MemberDO">
<!--这个地方只需要指定typeHandler=CryptHandler,如果上面没有重命名,这个地方需要写成javaType= cn.itmds.plugin.dbcrypt.CryptHandler,写起来比较麻烦 -->
<!--另外,只需要将需要解密的字段写到这个resultMap里即可,不需要写全部的字段,其他字段系统会自动映射为MemberDO -->
<result column="phone" property="phone" typeHandler="CryptHandler"/>
</resultMap>
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生。
MyBatis-Plus只需简单配置,即可快速进行 CRUD 操作,从而节省大量时间。而且还支持Lambda表达式,通过对象来操作sql等,所以现在使用的人越来越多。那么它如何来实现数据的自动加解密呢,超级简单。实现原理和2.1.2一样,也是通过BaseTypeHandler来实现。
1、增加@TableField(typeHandler = EncryptHandler.class)
,其中EncryptHandler就是2.1.2定义的EncryptHandler.java
,此时新增、修改时就实现了自动加密。
2、在@TableName上设置autoResultMap = true
,此时就实现了返回值的自动解密。
Done!示例:
@Data
@TableName(value = "user_info",autoResultMap = true)
public class UserPO {
/** */
@TableId(type = IdType.AUTO)
private Long id;
/** 真实姓名 */
@TableField(typeHandler = EncryptHandler.class)
private String realName;
}
为了便于开发调试及产线问题定位,开发框架基本都会定义日志拦截器,对所有的controller层和service层的方法进行拦截,打印详细等入参、出参。在2.1中我们提到了用户的敏感信息的加解密是在dao底层自动完成的,所以也就导致了日志中还会打印了用户的敏感信息,那么此时该如何处理呢?接下来提供一个完整的案例。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ServiceLog {
boolean ignore() default false;
}
@Aspect
@Component
public class ServiceLogAspect {
@Around("@within(cn.itmds.log.ServiceLog)")
protected Object aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
Method method = signature.getMethod();
ServiceLog serviceLog = method.getAnnotation(ServiceLog.class);
if (null != serviceLog && serviceLog.ignore()) {
return joinPoint.proceed();
}
long beginTime = System.currentTimeMillis();
Class clazz = joinPoint.getTarget().getClass();
String methodName = clazz.getSimpleName() + "." + method.getName();
// 打印请求所有的入参
log.info("Begin|{}|{}", methodName, jsonString(joinPoint.getArgs()));
Object result = null;
try {
result = joinPoint.proceed();
} finally {
// 打印所有的出参
log.info("End|{}|{}ms|{}", methodName, System.currentTimeMillis(),
- beginTime, jsonString(result));
}
return result;
}
}
logging:
sensitiveChars: realName,phoneNumber,idCard,mail,password
private ValueFilter valueFilter = (object, name, value) -> {
if (null == value || "".equals(value)) {
return value;
}
if (value instanceof byte[]) {
// 如果是byte字节,直接打印长度
return "byte length:" + ((byte[])value).length;
} else if (value instanceof String) {
// 在该方法里检查name,如果name包含我们配置的敏感信息,则将value设置为加*隐藏。
return stringValueProcess(name, (String)value);
} else {
return value;
}
};
在第二步拦截器的方法aroundJoinPoint中,对象转化为String时,使用FastJSON的过滤器。
protected String jsonString(Object object) {
return JSON.toJSONString(object, valueFilter);
}
@Around("execution(public * cn.itmds.controller..*(..) )")
Controller通过该方法实现时要注意,http请求和response请求有些字段是无法序列化的,所以务必要进行过滤。
public static <T> Stream<T> streamOf(T[] array) {
return ArrayUtils.isEmpty(array) ? Stream.empty() : Arrays.asList(array).stream();
}
//... 拦截器的方法中增加过滤
List<Object> logArgs = (List)streamOf(args).filter((arg) -> {
return !(arg instanceof HttpServletRequest) && !(arg instanceof HttpServletResponse);
}).collect(Collectors.toList());
// 打印请求所有的入参
log.info("Begin|{}|{}", methodName, jsonString(logArgs));
关于密码,国家也是有一部《密码法》的,最近好像也在推广宣传。当然我们平时常说的用户名“密码”只是“口令”,并不是密码法中的“密码”。《密码法》中的密码使用范围包含二代身份证、电子签名、增值税发票密码区之类的,具体大家可以去看看全文,不做多讲。
现在的开发人员基本都具备一定的安全知识,很少有明文存储密码的了,甚至直接md5的也很少,大部分都开始采用sha1,sha256了,也有一些公司开始使用用Argon2
Argon2 是一种慢哈希函数,在 2015 年获得 Password Hashing Competition 冠军,利用大量内存计算抵御GPU 和其他定制硬件的破解,提高哈希结果的安全性。
这里主要讲几点:
Bcrypt是一个跨平台的文件加密工具,SpringSecurity默认使用了该算法。如果项目中没有依然SpringSecurity,也可以单独引入jar包。 bcrypt算法与md5/sha算法有一个很大的区别,就是每次生成的hash值都是不同的,不需要我们自行指定盐。加密后的字符长度比较长,有60位,数据库字段设计时务必要注意。示例如下:
public static void main(String[] args) {
BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder();
String pwd = "abcd1234";
for (int i = 0; i < 5; i++) {
String encodePwd = bcrypt.encode(pwd);
boolean result = bcrypt.matches(pwd, encodePwd);
System.out.println(encodePwd + "|" + result);
}
}
有兴趣的可以看下源码
public static String gensalt(int log_rounds, SecureRandom random) {
if (log_rounds < MIN_LOG_ROUNDS || log_rounds > MAX_LOG_ROUNDS) {
throw new IllegalArgumentException("Bad number of rounds");
}
StringBuilder rs = new StringBuilder();
byte rnd[] = new byte[BCRYPT_SALT_LEN];
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();
}
Dropbox是提供文件在线存储的著名厂商,曾在其官方技术博客发表名为《How Dropbox securely stores your passwords》的文章,讲述了他们的用户密码加密存储方案。
用户隐私保护,远不是开发人员加解密这么简单,还需要运营、运维团队各方面的配合,任重而道远!
【人总要给自己留一些隐私的空间,就像你总是会站在你的影子前挡住了光的视线】
People always want to give yourself some privacy space, just like you will always be standing in front of the shadow of you blocking the line of sight of the light.
参考:
https://www.cnblogs.com/xinzhao/p/6035847.html
https://blog.csdn.net/weixin_39494923/article/details/91534658