@
接着上一篇 《千億級互聯網平台背后那些事-欲上青天攬明月》,今天我們來聊一聊關於用戶隱私信息的事。
隨着時代及互聯網的發展,人們對個人隱私越來越重視,但隱私信息泄露及濫用的問題依然屢見不鮮。之前有一份《中國個人信息安全和隱私保護報告》曾抽取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。 整個規范文檔主要體現了七大原則:權責一致原則、目的明確原則、選擇同意原則、最少夠用原則、公開透明原則、確保安全原則、主體參與原則。
1.1 用戶信息、敏感信息定義及判斷依據
1.1.1 個人信息
個人信息,personal information。指以電子或者其他方式記錄的能夠單獨或者與其他信息結合識別特定自然人身份或者反映特定自然人活動情況的各種信息。
判定方式:
- 識別:即從信息到個人,由信息本身的特殊性識別出特定自然人,個人信息應有助於識別出特定個人。
- 關聯:即從個人到信息,如已知特定自然人,由該特定自然人在其活動中產生的信息(如個人位置信息、個人通話記錄、個人瀏覽記錄等)即為個人信息。
符合上述兩種情形之一的信息,均應判定為個人信息。
個人信息舉例:
注:個人信息控制者通過個人信息或其他信息加工處理后形成的信息,例如,用戶畫像或特征標簽,能夠單獨或者與其他信息結合識別特定自然人身份或者反映特定自然人活動情況的,也屬於個人信息。
1.1.2 個人敏感信息
個人敏感信息,personal sensitive information。指一旦泄露、非法提供或濫用可能危害人身和財產安全,極易導致個人名譽、身心健康受到損害或歧視性待遇等的個人信息。通常情況下,14歲以下(含)兒童的個人信息和涉及自然人隱私的信息屬於個人敏感信息。
判定方式:
-
泄露:個人信息一旦泄露,將導致個人信息主體及收集、使用個人信息的組織和機構喪失對個人信息的控制能力,造成個人信息擴散范圍和用途的不可控。某些個人信息在泄漏后,被以違背個人信息主體意願的方式直接使用或與其他信息進行關聯分析,可能對個人信息主體權益帶來重大風險,應判定為個人敏感信息。例如,個人信息主體的身份證復印件被他人用於手機號卡實名登記、銀行賬戶開戶辦卡等。
-
非法提供:某些個人信息僅因在個人信息主體授權同意范圍外擴散,即可對個人信息主體權益帶來重大風險,應判定為個人敏感信息。例如,性取向、存款信息、傳染病史等。
-
濫用:某些個人信息在被超出授權合理界限時使用(如變更處理目的、擴大處理范圍等),可能對個人信息主體權益帶來重大風險,應判定為個人敏感信息。例如,在未取得個人信息主體授權時,將健康信息用於保險公司營銷和確定個體保費高低。
個人敏感信息舉例
注:個人信息控制者通過個人信息或其他信息加工處理后形成的信息,如一旦泄露、非法提供或濫GB/T 35273—20206用可能危害人身和財產安全,極易導致個人名譽、身心健康受到損害或歧視性待遇等的,屬於個人敏感信息。
1.2 用戶信息存儲的注意事項
- 個人信息存儲時間最小化,超過個人信息存儲期限后,應對個人信息進行刪除或匿名化處理。
- 傳輸和存儲個人敏感信息時,應采用加密等安全措施;采用密碼技術時宜遵循密碼管理相關國家標准。
- 個人生物識別信息應與個人身份信息分開存儲;
- 原則上不應存儲原始個人生物識別信息(如樣本、圖像等),可采取的措施包括但不限於:僅存儲個人生物識別信息的摘要信息;在采集終端中直接使用個人生物識別信息實現身份識別、認證等功能; 在使用面部識別特征、指紋、掌紋、虹膜等實現識別身份、認證等功能后刪除可提取個人生物識別信息的原始圖像。
整個規范文件中,還提到了用戶信息的使用、展示、第三方接入、安全管理等等,有興趣的小伙伴可以自定搜索了解一下。
二、框架技術實現
2.1 用戶敏感信息自動加解密
正如第一章節提到的,用戶的真實姓名、手機號、銀行卡號、包括住宿等敏感信息需要加密存儲到數據庫中,業務正常使用的時候再轉化為明文數據。從技術實現角度來看,無非就是新增、編輯時進行加密,查詢時解密,這樣一個個操作起來還是比較low的,而且很可能哪天新增了一個方法又忘記加解密了。所以大部分會通過框架來實現,實現的原理無外乎反射機器+攔截器。接下來以Mybatis為例,原理如下圖,具體可參考:https://blog.csdn.net/weixin_39494923/article/details/91534658
2.1.1 通過Interceptor實現數據的自動加解密
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();
}
}
2.1.2 通過BaseTypeHandler實現數據的自動加解密
一般情況下不會通過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}</if>
<!--這個地方只需要指定javaType=crypt,如果上面沒有重命名,這個地方需要寫成javaType= cn.itmds.plugin.dbcrypt.Crypt,寫起來比較麻煩 -->
<if test=realName != null">and real_name = #{realName,javaType=crypt}</if>
</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>
2.1.3 MybatisPlus實現數據的自動加解密
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;
}
2.2 日志文件自動過濾用戶敏感信息
為了便於開發調試及產線問題定位,開發框架基本都會定義日志攔截器,對所有的controller層和service層的方法進行攔截,打印詳細等入參、出參。在2.1中我們提到了用戶的敏感信息的加解密是在dao底層自動完成的,所以也就導致了日志中還會打印了用戶的敏感信息,那么此時該如何處理呢?接下來提供一個完整的案例。
- 定義一個注解@ServiceLog,可以作用在類上或者方法上。提供一個參數:ignore,默認為false。如果為true,表示該方法不需要打印日志。比如某一個類里有很多個方法需要日志,但其中某個方法是用於文件上傳的或者定時任務每秒都會執行1次,這些場景下不需要打印日志,則可以設置ignore=true。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ServiceLog {
boolean ignore() default false;
}
- 定義一個全局攔截器,打印入參、出參日志,在這里使用的是FastJson將對象轉化為字符串。
@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
- 接下來,我們可以利用FastJSON的過濾器特性來實現日志的過濾。
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);
}
- Controller層同樣,攔截所有的controller目錄下的文件即可。
@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));
2.3 密碼加密和《密碼法》
關於密碼,國家也是有一部《密碼法》的,最近好像也在推廣宣傳。當然我們平時常說的用戶名“密碼”只是“口令”,並不是密碼法中的“密碼”。《密碼法》中的密碼使用范圍包含二代身份證、電子簽名、增值稅發票密碼區之類的,具體大家可以去看看全文,不做多講。
2.3.1 密碼加密的注意事項
現在的開發人員基本都具備一定的安全知識,很少有明文存儲密碼的了,甚至直接md5的也很少,大部分都開始采用sha1,sha256了,也有一些公司開始使用用Argon2
Argon2 是一種慢哈希函數,在 2015 年獲得 Password Hashing Competition 冠軍,利用大量內存計算抵御GPU 和其他定制硬件的破解,提高哈希結果的安全性。
這里主要講幾點:
- 每一個密碼都要加上不同的鹽,確保相同的密碼也產生不同的hash。比如兩個人的密碼都是abcd1234,生成的hash一定要是不同的。
- 不要使用普通的隨機算法生成鹽,一定要使用CSPRNG(Cryptographically Secure Pseudo-Random Number Generator);對應java就是Java.security.SecureRandom,對應C/C++ CryptGenRandom。
- 有些系統使用用戶的id、手機號等來作為鹽加密密碼,這其實不符合鹽的生成規則要求。但對於一般性的安全性要求並不是那么高的網站,也基本能用。
2.3.2 使用BCrypt實現密碼加密
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);
}
}
加密后的字符串值組成:
- $是分割符,無意義;
- 2a是bcrypt加密版本號;
- 10是cost的值;
- 后面的字符串中,前22位是salt值;再然后的字符串就是密碼的密文了。
有興趣的可以看下源碼
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();
}
2.3.3 Dropbox密碼加密存儲防范
Dropbox是提供文件在線存儲的著名廠商,曾在其官方技術博客發表名為《How Dropbox securely stores your passwords》的文章,講述了他們的用戶密碼加密存儲方案。
- 首先使用sha512,將用戶密碼歸一化為64字節hash值。因為兩個原因:一個是Bcrypt算對輸入敏感,如果用戶輸入的密碼較長,可能導致Bcrypt計算過慢從而影響響應時間;另一個是有些Bcrypt算法的實現會將長輸入直接截斷為72字節,從信息論的角度講,這導致用戶信息的熵變小;
- 然后使用Bcrypt算法。選擇Bcrypt的原因,是Dropbox的工程師對這個算法更熟悉調優更有經驗,參數選擇的標准,是Dropbox的線上API服務器可以在100ms左右的時間可計算出結果。另外,關於Bcrypt和Scrypt哪個算法更優,密碼學家也沒有定論。同時,Dropbox也在關注密碼hash算法新秀Argon2,並表示會在合適的時機引入;
- 最后使用AES加密。因為Bcrypt不是完美的算法,所以Dropbox使用AES和全局密鑰進一步降低密碼被破解的風險,為了防止密鑰泄露,Dropbox采用了專用的密鑰保存硬件。Dropbox還提到了最后使用AES加密的另一個好處,即密鑰可定時更換,以降低用戶信息/密鑰泄露帶來的風險。
用戶隱私保護,遠不是開發人員加解密這么簡單,還需要運營、運維團隊各方面的配合,任重而道遠!
【人總要給自己留一些隱私的空間,就像你總是會站在你的影子前擋住了光的視線】
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
架構師,十年戎【碼】,老【叔】開花。個人微信號:qiaojs,關注架構設計、大數據、微服務、技術管理。