爲了支撐高併發,我把身份證存到了JS裏


接着上一篇《千億級互聯網平臺背後那些事-欲上青天攬明月》,今天我們來聊一聊關於用戶隱私信息的事。

隨着時代及互聯網的發展,人們對個人隱私越來越重視,但隱私信息泄露及濫用的問題依然屢見不鮮。之前有一份《中國個人信息安全和隱私保護報告》曾抽取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. 識別:即從信息到個人,由信息本身的特殊性識別出特定自然人,個人信息應有助於識別出特定個人。
  2. 關聯:即從個人到信息,如已知特定自然人,由該特定自然人在其活動中產生的信息(如個人位置信息、個人通話記錄、個人瀏覽記錄等)即爲個人信息。
    符合上述兩種情形之一的信息,均應判定爲個人信息。

個人信息舉例個人信息舉例
:個人信息控制者通過個人信息或其他信息加工處理後形成的信息,例如,用戶畫像或特徵標籤,能夠單獨或者與其他信息結合識別特定自然人身份或者反映特定自然人活動情況的,也屬於個人信息。

1.1.2 個人敏感信息

個人敏感信息,personal sensitive information。指一旦泄露、非法提供或濫用可能危害人身和財產安全,極易導致個人名譽、身心健康受到損害或歧視性待遇等的個人信息。通常情況下,14歲以下(含)兒童的個人信息和涉及自然人隱私的信息屬於個人敏感信息

判定方式

  1. 泄露:個人信息一旦泄露,將導致個人信息主體及收集、使用個人信息的組織和機構喪失對個人信息的控制能力,造成個人信息擴散範圍和用途的不可控。某些個人信息在泄漏後,被以違背個人信息主體意願的方式直接使用或與其他信息進行關聯分析,可能對個人信息主體權益帶來重大風險,應判定爲個人敏感信息。例如,個人信息主體的身份證複印件被他人用於手機號卡實名登記、銀行賬戶開戶辦卡等。

  2. 非法提供:某些個人信息僅因在個人信息主體授權同意範圍外擴散,即可對個人信息主體權益帶來重大風險,應判定爲個人敏感信息。例如,性取向、存款信息、傳染病史等。

  3. 濫用:某些個人信息在被超出授權合理界限時使用(如變更處理目的、擴大處理範圍等),可能對個人信息主體權益帶來重大風險,應判定爲個人敏感信息。例如,在未取得個人信息主體授權時,將健康信息用於保險公司營銷和確定個體保費高低。

個人敏感信息舉例
個人敏感信息舉例
:個人信息控制者通過個人信息或其他信息加工處理後形成的信息,如一旦泄露、非法提供或濫GB/T 35273—20206用可能危害人身和財產安全,極易導致個人名譽、身心健康受到損害或歧視性待遇等的,屬於個人敏感信息。

1.2 ​用戶信息存儲的注意事項

  1. 個人信息存儲時間最小化,超過個人信息存儲期限後,應對個人信息進行刪除或匿名化處理。
  2. 傳輸和存儲個人敏感信息時,應採用加密等安全措施;採用密碼技術時宜遵循密碼管理相關國家標準。
  3. 個人生物識別信息應與個人身份信息分開存儲
  4. 原則上不應存儲原始個人生物識別信息(如樣本、圖像等),可採取的措施包括但不限於:僅存儲個人生物識別信息的摘要信息;在採集終端中直接使用個人生物識別信息實現身份識別、認證等功能; 在使用面部識別特徵、指紋、掌紋、虹膜等實現識別身份、認證等功能後刪除可提取個人生物識別信息的原始圖像。

整個規範文件中,還提到了用戶信息的使用、展示、第三方接入、安全管理等等,有興趣的小夥伴可以自定搜索瞭解一下。

二、​框架技術實現

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底層自動完成的,所以也就導致了日誌中還會打印了用戶的敏感信息,那麼此時該如何處理呢?接下來提供一個完整的案例。

  1. 定義一個註解@ServiceLog,可以作用在類上或者方法上。提供一個參數:ignore,默認爲false。如果爲true,表示該方法不需要打印日誌。比如某一個類裏有很多個方法需要日誌,但其中某個方法是用於文件上傳的或者定時任務每秒都會執行1次,這些場景下不需要打印日誌,則可以設置ignore=true。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ServiceLog {

    boolean ignore() default false;
}
  1. 定義一個全局攔截器,打印入參、出參日誌,在這裏使用的是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;
    }
}

  1. 增加一個配置項,定義需要過濾的敏感信息,比如真實姓名、手機號、身份證、密碼等
logging:
  sensitiveChars: realName,phoneNumber,idCard,mail,password
  1. 接下來,我們可以利用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);
    }
  1. 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 和其他定製硬件的破解,提高哈希結果的安全性。

這裏主要講幾點:

  1. 每一個密碼都要加上不同的鹽,確保相同的密碼也產生不同的hash。比如兩個人的密碼都是abcd1234,生成的hash一定要是不同的。
  2. 不要使用普通的隨機算法生成鹽,一定要使用CSPRNG(Cryptographically Secure Pseudo-Random Number Generator);對應java就是Java.security.SecureRandom,對應C/C++ CryptGenRandom。
  3. 有些系統使用用戶的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》的文章,講述了他們的用戶密碼加密存儲方案。
在這裏插入圖片描述

  1. 首先使用sha512,將用戶密碼歸一化爲64字節hash值。因爲兩個原因:一個是Bcrypt算對輸入敏感,如果用戶輸入的密碼較長,可能導致Bcrypt計算過慢從而影響響應時間;另一個是有些Bcrypt算法的實現會將長輸入直接截斷爲72字節,從信息論的角度講,這導致用戶信息的熵變小;
  2. 然後使用Bcrypt算法。選擇Bcrypt的原因,是Dropbox的工程師對這個算法更熟悉調優更有經驗,參數選擇的標準,是Dropbox的線上API服務器可以在100ms左右的時間可計算出結果。另外,關於Bcrypt和Scrypt哪個算法更優,密碼學家也沒有定論。同時,Dropbox也在關注密碼hash算法新秀Argon2,並表示會在合適的時機引入;
  3. 最後使用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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章