You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

82 lines
2.6 KiB

package idverify
import (
"crypto/sha256"
"encoding/hex"
"strings"
)
// 身份证号的本地处理:格式校验、掩码、加盐指纹。
//
// 本地校验的意义不只是"友好提示"——服务商核验是**按次计费**的,把明显不合法的号码
// (位数不对、校验位算不上)挡在调用之前,既省额度也避免把垃圾请求算进失败次数。
// 加权因子与校验码表(GB 11643-1999 附录A,ISO 7064:1983 MOD 11-2)。
var (
idWeights = [17]int{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}
idCheckCode = [11]byte{'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'}
)
// ValidIdCard 校验 18 位二代身份证号:前 17 位为数字,末位为数字或 X/x,且校验位正确。
// 只支持二代证——阿里云 Id2MetaVerify 与腾讯云 IdCardVerification 都只认二代证。
func ValidIdCard(no string) bool {
no = strings.TrimSpace(no)
if len(no) != 18 {
return false
}
sum := 0
for i := 0; i < 17; i++ {
c := no[i]
if c < '0' || c > '9' {
return false
}
sum += int(c-'0') * idWeights[i]
}
last := no[17]
if last == 'x' {
last = 'X'
}
return last == idCheckCode[sum%11]
}
// MaskIdCard 生成用于展示与落库的掩码:保留前 4 位与后 4 位,中间一律 *。
// 例:440301199001011234 -> 4403**********1234
// 非 18 位的输入原样返回掩码化的兜底(全 *),避免意外把原文写进库。
func MaskIdCard(no string) string {
no = strings.TrimSpace(no)
if len(no) != 18 {
return strings.Repeat("*", len(no))
}
return no[:4] + strings.Repeat("*", 10) + no[14:]
}
// HashIdCard 生成加盐 SHA-256 指纹(小写十六进制),用于后台排查"同一证件绑了多个账号"。
//
// 必须加盐:身份证号空间有限(约 10^17,且前 6 位地区码、中间 8 位生日高度可枚举),
// 裸 SHA-256 可以被彻底反查,等同于明文存储。salt 由 ID_HASH_SALT 环境变量提供。
// salt 为空时返回空串——调用方据此跳过写指纹,绝不退化成裸哈希。
func HashIdCard(no, salt string) string {
no = strings.TrimSpace(no)
if no == "" || salt == "" {
return ""
}
sum := sha256.Sum256([]byte(salt + "|" + strings.ToUpper(no)))
return hex.EncodeToString(sum[:])
}
// GenderFromIdCard 从身份证号解析性别:第 17 位(顺序码末位)奇数为男、偶数为女。
// 返回值对齐 pb.DBUser.Gender 的口径:1=男,2=女,0=解析不出。
func GenderFromIdCard(no string) int32 {
no = strings.TrimSpace(no)
if len(no) != 18 {
return 0
}
c := no[16]
if c < '0' || c > '9' {
return 0
}
if (c-'0')%2 == 1 {
return 1
}
return 2
}