四要素用户身份认证

This commit is contained in:
谭凯凯
2025-01-08 19:05:22 +08:00
committed by Tankaikai
parent ea9c4299e2
commit 0dd28536e1
11 changed files with 575 additions and 23 deletions

View File

@@ -3,14 +3,17 @@ package com.sqx.modules.app.controller.app;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.IdcardUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.sqx.common.annotation.Debounce;
import com.sqx.common.utils.ApiAccessLimitUtil;
import com.sqx.common.utils.DataLimitUtil;
import com.sqx.common.utils.DesensitizedUtil;
import com.sqx.common.utils.Result;
import com.sqx.modules.app.annotation.Login;
import com.sqx.modules.app.annotation.LoginUser;
@@ -35,6 +38,7 @@ import java.util.Map;
/**
* APP登录授权
*
* @author mac
*/
@RestController
@@ -101,8 +105,13 @@ public class AppController {
@ApiOperation("用户修改个人信息")
@ResponseBody
@Debounce(interval = 3000, value = "#userId")
public Result updateUserImageUrl(@RequestAttribute("userId") Long userId, @RequestParam(required = false) String zhiFuBao,
@RequestParam String certName, @RequestParam(required = false) String certNum) {
public Result updateUserImageUrl(@RequestAttribute("userId") Long userId,
@RequestParam(required = false) String zhiFuBao,
@RequestParam String certName,
@RequestParam(required = false) String certNum,
@RequestParam(required = false) String accountNo,
@RequestParam(required = false) String mobile
) {
if (StrUtil.isAllBlank(zhiFuBao, certNum)) {
return Result.error("支付宝账号或实名身份证号码必须传递一个");
}
@@ -140,7 +149,11 @@ public class AppController {
}
if (!certNum.equals(userInfo.getCertNo()) || !certName.equals(userInfo.getCertName())) {
if (!certNum.equals(userInfo.getCertNo())
|| !certName.equals(userInfo.getCertName())
|| !accountNo.equals(userInfo.getAccountNo())
|| !mobile.equals(userInfo.getMobile())
) {
if (StrUtil.isNotBlank(userEntity.getZhiFuBaoName()) && !certName.equals(userEntity.getZhiFuBaoName())) {
return Result.error("实名修改失败: 姓名与绑定支付宝信息不相符");
}
@@ -153,14 +166,6 @@ public class AppController {
return Result.error("实名修改失败: 此身份证信息已绑定过");
}
if (!ApiAccessLimitUtil.getCertAuthIsAccessAllowed(String.valueOf(userId), "updateAuthCertInfo", 1)) {
return Result.error("实名修改失败: 每月可修改次数已用完,请联系管理员");
}
if (!ApiAccessLimitUtil.getCertAuthIsAccessAllowed(certNum, "updateAuthCertInfoByIdCard", 1)) {
return Result.error("实名修改失败: 每月可修改次数已用完,请联系管理员");
}
try {
// 校验实名信息是否在黑名单里面
Integer count = tbUserBlacklistMapper.selectCount(new LambdaQueryWrapper<TbUserBlacklist>().eq(TbUserBlacklist::getRealName, certName)
@@ -172,16 +177,17 @@ public class AppController {
return Result.error("异常行为: 您的实名信息存在异常行为");
}
aliService.authCertNo(certName, certNum);
String respJson = aliService.auth(certName, certNum, accountNo, mobile);
userInfo.setCertName(certName);
userInfo.setCertNo(certNum);
userInfo.setAccountNo(accountNo);
userInfo.setMobile(mobile);
userInfo.setRespJson(respJson);
userInfo.setUpdateTime(DateUtil.date());
boolean update = userInfoService.update(userInfo, new LambdaQueryWrapper<UserInfo>().eq(UserInfo::getUserId, userId).eq(UserInfo::getId, userInfo.getId()));
if (!update) {
return Result.error("实名修改失败: 请稍后重试");
}
ApiAccessLimitUtil.setCertAuthIsAccessAllowed(String.valueOf(userId), "updateAuthCertInfo", 1, "month");
ApiAccessLimitUtil.setCertAuthIsAccessAllowed(certNum, "updateAuthCertInfoByIdCard", 1, "month");
return Result.success();
} catch (Exception e) {
return Result.error("实名修改失败: 身份证信息不匹配");
@@ -198,7 +204,10 @@ public class AppController {
@ResponseBody
public Result updateUsers(@RequestAttribute("userId") Long userId, @RequestBody UserEntity userEntity) {
userEntity.setUserId(userId);
userService.update(userEntity, new LambdaQueryWrapper<UserEntity>().eq(UserEntity::getUserId, userId));
userService.update(null, Wrappers.<UserEntity>lambdaUpdate().eq(UserEntity::getUserId, userId)
.set(StrUtil.isNotBlank(userEntity.getAvatar()), UserEntity::getAvatar, userEntity.getAvatar())
.set(StrUtil.isNotBlank(userEntity.getUserName()), UserEntity::getUserName, userEntity.getUserName())
);
return Result.success();
}
@@ -228,6 +237,23 @@ public class AppController {
@ResponseBody
public Result selectUserById(@LoginUser UserEntity user) {
UserInfo userInfo = userInfoService.getByUserId(user.getUserId());
userInfo.setRespJson(null);
if (StrUtil.isNotEmpty(userInfo.getAccountNo())) {
userInfo.setAccountNo(DesensitizedUtil.bankCard(userInfo.getAccountNo()));
}
if (StrUtil.isNotEmpty(userInfo.getMobile())) {
userInfo.setMobile(DesensitizedUtil.mobilePhone(userInfo.getMobile()));
}
if (StrUtil.isNotEmpty(userInfo.getCertNo())) {
userInfo.setCertNo(DesensitizedUtil.idCardNum(userInfo.getCertNo(), 3, 2));
}
if (StrUtil.isNotEmpty(user.getZhiFuBao())) {
if (Validator.isEmail(user.getZhiFuBao())) {
user.setZhiFuBao(DesensitizedUtil.email(user.getZhiFuBao()));
} else {
user.setZhiFuBao(DesensitizedUtil.mobilePhone(user.getZhiFuBao()));
}
}
Map<String, Object> map = BeanUtil.beanToMap(user);
map.putAll(BeanUtil.beanToMap(userInfo));
if (StrUtil.isBlank(user.getZhiFuBaoName()) && StrUtil.isNotBlank(userInfo.getCertName())) {

View File

@@ -0,0 +1,19 @@
package com.sqx.modules.app.dao;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
public class AuthDTO {
@NotBlank
private String name;
@NotBlank
private String idNum;
@NotBlank
private String accountNo;
@NotBlank
private String mobile;
}

View File

@@ -39,6 +39,21 @@ public class UserInfo implements Serializable {
*/
private String certNo;
/**
* 银行账号
*/
private String accountNo;
/**
* 银行预留手机号
*/
private String mobile;
/**
* 四要素接口响应报文
*/
private String respJson;
/**
* 修改时间
*/

View File

@@ -3,4 +3,6 @@ package com.sqx.modules.app.service;
public interface AliService {
void authCertNo(String name, String idCard);
String auth(String name, String idCard, String accountNo, String bankPreMobile);
}

View File

@@ -1,7 +1,7 @@
package com.sqx.modules.app.service;
import com.sqx.modules.app.entity.UserInfo;
import com.baomidou.mybatisplus.extension.service.IService;
import com.sqx.modules.app.entity.UserInfo;
/**
* @author Administrator
@@ -14,4 +14,6 @@ public interface UserInfoService extends IService<UserInfo> {
Integer countCertCount(String name, String idNum);
Integer countCertCount(String name, String idNum, String accountNo, String mobile);
}

View File

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.sqx.common.utils.PageUtils;
import com.sqx.common.utils.Result;
import com.sqx.modules.app.dao.AuthCertNoDTO;
import com.sqx.modules.app.dao.AuthDTO;
import com.sqx.modules.app.entity.UserEntity;
import javax.servlet.http.HttpServletRequest;
@@ -232,6 +233,12 @@ public interface UserService extends IService<UserEntity> {
*/
Object authCertNo(long userId, AuthCertNoDTO authCertNoDTO);
/**
* 四要素身份证认证
*/
Object auth(long userId, AuthDTO authDTO);
/**
* 封禁拉黑用户
*/

View File

@@ -1,5 +1,12 @@
package com.sqx.modules.app.service.impl;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.IdcardUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.aliyun.dytnsapi20200217.Client;
import com.aliyun.dytnsapi20200217.models.CertNoTwoElementVerificationRequest;
import com.aliyun.dytnsapi20200217.models.CertNoTwoElementVerificationResponse;
@@ -12,6 +19,10 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
@Service
@Slf4j
@@ -64,4 +75,64 @@ public class AliServiceImpl implements AliService {
throw new SqxException(e.getMessage());
}
}
public static final Pattern CHINESE_NAME = Pattern.compile("^[\u2E80-\u9FFF·]{2,60}$");
/**
* 阿里云四要素认证
*
* @param name
* @param idCard
* @param accountNo
* @param bankPreMobile
*/
@Override
public String auth(String name, String idCard, String accountNo, String bankPreMobile) {
log.info("阿里云四要素认证请求参数: {} {} {} {}", name, idCard, accountNo, bankPreMobile);
if (StrUtil.isBlank(name)) {
throw new SqxException("持卡人姓名不能为空");
}
if (StrUtil.isBlank(idCard)) {
throw new SqxException("身份证号码不能为空");
}
if (StrUtil.isBlank(accountNo)) {
throw new SqxException("银行卡卡号不能为空");
}
if (StrUtil.isBlank(bankPreMobile)) {
throw new SqxException("银行预留手机号码不能为空");
}
if (!Validator.isMactchRegex(CHINESE_NAME, name)) {
throw new SqxException("持卡人姓名不合法");
}
boolean validCard = IdcardUtil.isValidCard(idCard);
if (!validCard) {
throw new SqxException("身份证号码不合法");
}
validCard = Validator.isMobile(bankPreMobile);
if (!validCard) {
throw new SqxException("银行预留手机号码格式不正确");
}
String appcode = "f98606b602564d209f37fc02b0bd590c";
Map<String, String> headers = new HashMap<>(2);
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.put("Authorization", "APPCODE " + appcode);
//根据API的要求定义相对应的Content-Type
headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
Map<String, Object> params = new HashMap<>();
params.put("accountNo", accountNo);
params.put("name", name);
params.put("idCardCode", idCard);
params.put("bankPreMobile", bankPreMobile);
String reqBody = URLUtil.encode(StrUtil.format("accountNo={}&name={}&idCardCode={}&bankPreMobile={}", accountNo, name, idCard, bankPreMobile), Charset.defaultCharset());
String respBody = HttpUtil.createPost("https://bkvip.market.alicloudapi.com/v3/bcheck").headerMap(headers, true).body(reqBody).timeout(15 * 1000).execute().body();
// {"error_code":0,"reason":"成功","result":{"respCode":"0","respMsg":"银行卡鉴权成功","bancardInfor":{"bankName":"招商银行","BankCode":"03080000","BankId":5,"type":"借记卡","cardname":"一卡通(银联卡)","tel":"95555","Icon":"2014121619271052743.gif"}},"sn":"010817431025283426800706871"}
// {"error_code":10028,"reason":"成功","result":{"respCode":"6","respMsg":"身份证格式有误","bancardInfor":{"bankName":"招商银行","BankCode":"03080000","BankId":5,"type":"借记卡","cardname":"一卡通(银联卡)","tel":"95555","Icon":"2014121619271052743.gif"}},"sn":"010817575524183118006799233"}
JSONObject ret = JSONUtil.parseObj(respBody);
Integer errorCode = ret.getInt("error_code");
if (errorCode != 0) {
JSONObject result = ret.getJSONObject("result");
throw new SqxException(result.getStr("respMsg"));
}
return respBody;
}
}

View File

@@ -32,7 +32,20 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo>
public Integer countCertCount(String name, String idNum) {
return count(new LambdaQueryWrapper<UserInfo>()
.eq(UserInfo::getCertName, name)
.eq(UserInfo::getCertNo, idNum));
.eq(UserInfo::getCertNo, idNum)
.isNotNull(UserInfo::getAccountNo)
.isNotNull(UserInfo::getMobile)
);
}
@Override
public Integer countCertCount(String name, String idNum, String accountNo, String mobile) {
return count(new LambdaQueryWrapper<UserInfo>()
.eq(UserInfo::getCertName, name)
.eq(UserInfo::getCertNo, idNum)
.eq(UserInfo::getAccountNo, accountNo)
.eq(UserInfo::getMobile, mobile)
);
}
}

View File

@@ -41,10 +41,7 @@ import com.sqx.common.exception.SqxException;
import com.sqx.common.utils.DateUtils;
import com.sqx.common.utils.PageUtils;
import com.sqx.common.utils.Result;
import com.sqx.modules.app.dao.AuthCertNoDTO;
import com.sqx.modules.app.dao.MsgDao;
import com.sqx.modules.app.dao.UserDao;
import com.sqx.modules.app.dao.UserVipDao;
import com.sqx.modules.app.dao.*;
import com.sqx.modules.app.entity.*;
import com.sqx.modules.app.mapper.TbUserBlacklistMapper;
import com.sqx.modules.app.service.*;
@@ -1670,6 +1667,39 @@ public class UserServiceImpl extends ServiceImpl<UserDao, UserEntity> implements
return userInfoService.updateById(userInfo);
}
@Override
public Object auth(long userId, AuthDTO authDTO) {
authDTO.setName(StrUtil.trim(authDTO.getName()));
authDTO.setIdNum(StrUtil.trim(authDTO.getIdNum()));
authDTO.setAccountNo(StrUtil.trim(authDTO.getAccountNo()));
authDTO.setMobile(StrUtil.trim(authDTO.getMobile()));
UserEntity userEntity = baseMapper.selectById(userId);
if (userEntity == null) {
throw new SqxException("用户信息不存在");
}
UserInfo userInfo = userInfoService.getByUserId(userId);
if (StrUtil.isNotEmpty(userInfo.getCertName()) && StrUtil.isNotEmpty(userInfo.getAccountNo()) && StrUtil.isNotEmpty(userInfo.getMobile())) {
throw new SqxException("此账号已认证");
}
Integer count = userInfoService.countCertCount(authDTO.getName(), authDTO.getIdNum(), authDTO.getAccountNo(), authDTO.getMobile());
if (count > 1) {
throw new SqxException("此实名信息已存在");
}
String respJson = aliService.auth(authDTO.getName(), authDTO.getIdNum(), authDTO.getAccountNo(), authDTO.getMobile());
userInfo.setCertName(authDTO.getName());
userInfo.setCertNo(authDTO.getIdNum());
userInfo.setAccountNo(authDTO.getAccountNo());
userInfo.setMobile(authDTO.getMobile());
userInfo.setRespJson(respJson);
userInfo.setUpdateTime(DateUtil.date());
return userInfoService.updateById(userInfo);
}
@Override
public void addBlackUser(Long userId) {
log.info("异常用户id, 异常操作: {}", userId);