文章目录
一、前言
一个健壮的后端,需要能够校验前端传来的参数,直接拦截掉不合法的参数。在一些涉及敏感字段的接口中,需要对特定字段进行加密。
简单的实现,可以在common中写一个Util,在各controller中对参数进行判断或转换。但毫无疑问,这样的写法会造成大量重复代码。
本章我们介绍使用注解实现。
二、参数校验
Spring框架 提供了参数校验功能。
2.1、pom引用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2.2、涉及注解
2.2.1、注解位置
2.2.2、注解使用
我们以用户注册为例子,展示该注解的使用
@Data
public class BaseSignInDto {
@NotNull(message = "账号名不可为空")
@Length(min=3,max=20,message = "用户名需要在3~20个字符内")
@Schema(title = "用户名")
private String name;
@Pattern(
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[A-Za-z\\d$@$!%*?&]{8,16}",
message = "密码必须同时包含大小写字母、数字及特殊字符,长度7-32位"
)
@Schema(title = "密码")
private String password;
}
@Data
public class SignInDto extends BaseSignInDto {
@Length(min = 2,max = 20,message = "昵称需在2~20个字符内")
@Schema(title = "昵称")
private String nickName;
@Schema(title = "自我描述")
private String description;
@Email(message = "邮箱格式不正确")
@Schema(title = "邮箱地址")
private String email;
@Pattern(regexp = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$",message = "手机号不合法")
@Schema(title = "电话号码")
private String phone;
@Schema(title = "性别")
private Gender gender;
@Schema(title = "生日")
private LocalDate birthday;
}
2.2.3 拦截器处理
在framework-web-common的全局异常中,加入这段代码即可
package indi.zhifa.recipe.bailan.framework.web.common.filter;
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(value = {ConstraintViolationException.class, MethodArgumentNotValidException.class, BindException.class})
@ResponseBody
public RestResponse<Object> argumentValidException(HttpServletRequest req, Exception ex) {
StringBuilder sb = new StringBuilder("参数验证失败:");
//Controller上 @Valid + 校验标签(例如@NotBlank()) 在方法形参 中直接校验参数抛出异常
if(ex instanceof ConstraintViolationException){
ConstraintViolationException ce = (ConstraintViolationException) ex;
Set<ConstraintViolation<?>> errors = ce.getConstraintViolations();
errors.forEach(error->{
sb.append(error.getMessage()).append(";");
});
//@Valid + @RequestBody 校验出错抛出的异常
}else if(ex instanceof MethodArgumentNotValidException){
MethodArgumentNotValidException me = (MethodArgumentNotValidException) ex;
List<ObjectError> errors = me.getBindingResult().getAllErrors();
errors.forEach(error->{
sb.append(error.getDefaultMessage()).append(";");
});
//@Valid User user校验实体类中字段出错时抛出的异常
}else if (ex instanceof BindException){
BindException be = (BindException) ex;
List<ObjectError> errors = be.getAllErrors();
errors.forEach(error->{
sb.append(error.getDefaultMessage()).append(";");
});
}
log.debug("op=global_exception_handler_log_ServiceException", ex);
return RestResponse.error(sb.toString());
}
@ExceptionHandler(JsonParseException.class)
@ResponseBody
public RestResponse jsonValidException() {
return RestResponse.error("Json格式错误!");
}
@ExceptionHandler(ServiceException.class)
@ResponseBody
public RestResponse otherException(ServiceException ex) {
RestResponse result = null;
if (null != ex){
log.debug("op=global_exception_handler_log_ServiceException", ex);
int code = ex.getCode();
if(code ==0 || code == 500){
result = RestResponse.error(ex.getMsg());
}else{
result = RestResponse.error(code,ex.getMsg());
}
}
return result;
}
@ResponseBody
@ExceptionHandler(Exception.class)
public RestResponse handle(Exception e) {
log.error("发生未知错误", e);
return RestResponse.error("发生未知错误 "+e.toString());
}
}
三、参数加密
3.1 加密方法介绍
我们程序员日常的开发中,常用的加密有以下几种。
3.1.1 编码
所谓编码,是把一个字符串按一定规则做编码,编码后的字符串可以较为轻松的推出原串。常见的编码有base64,哈夫曼编码等。
3.1.2 对称加密
对称加密算法也称私钥加密算法,是指加密密钥和解密密钥相同,或者虽然不同,但从其中的任意一个可以很容易地推导出另一个。
本篇实现AES,DESede算法,其他算法由读者根据项目要求自行实现
3.1.3 非对称加密
非对称加密算法也成为公钥加密算法,是指加密密钥和解密密钥完全不同,其中一个为公钥,另一个为私钥,并且不可能从任何一个推导出另一个。
本篇实现RSA,其他算法由读者根据项目要求自行实现
3.1.4 摘要算法
摘要算法是一种能产生特殊输出格式的算法,这种算法的特点是:无论用户输入什么长度的原始数据,经过计算后输出的密文都是固定长度的,这种算法的原理是根据一定的运算规则对原数据进行某种形式的提取,这种提取就是摘要,被摘要的数据内容与原数据有密切联系,只要原数据稍有改变,输出的“摘要”便完全不同,因此,基于这种原理的算法便能对数据完整性提供较为健全的保障。
3.1.5 数字签名
数字签名就是在原有的信息中追加一个笔迹,来判断原文是否经过修改。
3.2 加密的简单实现
hutool工具包非常Nice,可以用来做加密的实现
3.2.1 接口
public interface IZfEncryptUtil {
/*aes*/
String AES_HTTP_SESSION_KEY = "ASC_KEY";
int AES_KEY_LEN = 256;
String aesKey();
String aesEncode(String pContent);
String aesDecode(String pCiphertext);
/*des*/
String DES_HTTP_SESSION_KEY = "DES_KEY";
int DES_KEY_LEN = 192;
String desKey();
String desEncode(String pContent);
String desDecode(String pCiphertext);
/*RSA*/
String RSA_HTTP_SESSION_KEY = "RSA_KEY";
int RSA_KEY_LEN = 768;
String rsaKey();
String rsaEncode(String pContent);
String rsaDecode(String pCiphertext);
/*SM2*/
String SM2_HTTP_SESSION_KEY = "SM2_KEY";
String sm2Key();
String sm2Encode(String pContent);
String sm2Decode(String pCiphertext);
/*SM3*/
String sm3Digest(String pContent);
/*MD5*/
String md5Digest(String pContent);
/*userName*/
String USER_ENCRYPT_HTTP_SESSION_KEY = "USER_ENCRYPT_KEY";
String userNameKey();
String userNameEncode(String pUserName);
String userNameDecode(String pCiphertext);
/*passwd*/
String PASSWD_ENCRYPT_HTTP_SESSION_KEY = "USER_ENCRYPT_KEY";
int PASSWD_SALT_LEN = 8;
PasswdEncryptKeyVo passwdKey();
String passwdEncode(String pPasswd);
String passwdDecode(String pCiphertext);
}
3.2.2 实现
@Slf4j
@Component
public class ZfEncryptUtilImpl implements IZfEncryptUtil {
@Override
public String aesKey() {
return Base64.encode(genAesKeyByte());
}
protected byte[] genAesKeyByte(){
HttpSession httpSession = ZFHttpUtil.getHttpSession();
byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.AES.getValue(), AES_KEY_LEN).getEncoded();
httpSession.setAttribute(AES_HTTP_SESSION_KEY,key);
return key;
}
protected byte[] getAesKey(){
HttpSession httpSession = ZFHttpUtil.getHttpSession();
Object obj = httpSession.getAttribute(AES_HTTP_SESSION_KEY);
if(null == obj){
throw new ServiceException("还未生成aesKey");
}
return (byte[])obj;
}
@Override
public String aesEncode(String pContent) {
byte[] key = getAesKey();
String encodeStr = SecureUtil.aes(key).encryptBase64(pContent.getBytes(StandardCharsets.UTF_8));
return encodeStr;
}
@Override
public String aesDecode(String pCiphertext) {
byte[] key = getAesKey();
String decryptStr = SecureUtil.aes(key).decryptStr(pCiphertext);
return decryptStr;
}
protected byte[] genDesKeyByte(){
HttpSession httpSession = ZFHttpUtil.getHttpSession();
byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DESede.getValue(), DES_KEY_LEN).getEncoded();
httpSession.setAttribute(DES_HTTP_SESSION_KEY,key);
return key;
}
protected byte[] getDesKey(){
HttpSession httpSession = ZFHttpUtil.getHttpSession();
Object obj = httpSession.getAttribute(DES_HTTP_SESSION_KEY);
if(null == obj){
throw new ServiceException("还未生成desKey");
}
return (byte[])obj;
}
@Override
public String desKey() {
return Base64.encode(genDesKeyByte());
}
@Override
public String desEncode(String pContent) {
byte[] key = getDesKey();
String decodeStr = SecureUtil.desede(key).encryptBase64(pContent);
return decodeStr;
}
@Override
public String desDecode(String pCiphertext) {
byte[] key = getDesKey();
String decodeStr = SecureUtil.desede(key).decryptStr(pCiphertext);
return decodeStr;
}
protected KeyPair getRsaKey(){
HttpSession httpSession = ZFHttpUtil.getHttpSession();
Object oKeyPair = httpSession.getAttribute(RSA_HTTP_SESSION_KEY);
if(null == oKeyPair){
throw new ServiceException("还未生成rsa-key");
}
return (KeyPair)oKeyPair;
}
@Override
public String rsaKey() {
KeyPair keyPair = SecureUtil.generateKeyPair(AsymmetricAlgorithm.RSA.getValue());
HttpSession httpSession = ZFHttpUtil.getHttpSession();
httpSession.setAttribute(RSA_HTTP_SESSION_KEY,keyPair);
return Base64.encode(keyPair.getPublic().getEncoded());
}
@Override
public String rsaEncode(String pContent) {
KeyPair keyPair = getRsaKey();
byte[] publicKey = keyPair.getPublic().getEncoded();
byte[] privateKey = keyPair.getPrivate().getEncoded();
RSA rsa = new RSA(privateKey,publicKey);
String ciphertext = rsa.encryptBase64(pContent, KeyType.PublicKey);
return ciphertext;
}
@Override
public String rsaDecode(String pCiphertext) {
KeyPair keyPair = getRsaKey();
byte[] publicKey = keyPair.getPublic().getEncoded();
byte[] privateKey = keyPair.getPrivate().getEncoded();
String content = SecureUtil.rsa(privateKey,publicKey).decryptStr(pCiphertext,KeyType.PrivateKey);
return content;
}
@Override
public String sm2Key() {
SM2 sm2 = new SM2();
KeyPair keyPair = new KeyPair(sm2.getPublicKey(),sm2.getPrivateKey());
HttpSession httpSession = ZFHttpUtil.getHttpSession();
httpSession.setAttribute(SM2_HTTP_SESSION_KEY,keyPair);
return Base64.encode(keyPair.getPublic().getEncoded());
}
protected KeyPair getSm2Key(){
HttpSession httpSession = ZFHttpUtil.getHttpSession();
Object oKeyPair = httpSession.getAttribute(SM2_HTTP_SESSION_KEY);
if(null == oKeyPair){
throw new ServiceException("还未生成sm2-key");
}
return (KeyPair)oKeyPair;
}
@Override
public String sm2Encode(String pContent) {
KeyPair keyPair = getSm2Key();
SM2 sm2 = new SM2(keyPair.getPrivate(),keyPair.getPublic());
byte[] cipherCode = sm2.encrypt(pContent,KeyType.PublicKey);
byte[] signCode = sm2.sign(pContent.getBytes(StandardCharsets.UTF_8));
String ciphertext = Base64.encode(cipherCode)+"."+Base64.encode(signCode);
return ciphertext;
}
@Override
public String sm2Decode(String pCiphertext) {
String[] cipherAndSign = pCiphertext.split("\\.");
if(null == cipherAndSign|| cipherAndSign.length != 2){
throw new ServiceException("密文格式不对");
}
KeyPair keyPair = getSm2Key();
SM2 sm2 = new SM2(keyPair.getPrivate(),keyPair.getPublic());
String content = sm2.decryptStr(cipherAndSign[0],KeyType.PrivateKey);
if(!sm2.verify(content.getBytes(StandardCharsets.UTF_8),Base64.decode(cipherAndSign[1]))){
throw new ServiceException("签名验证不通过");
}
return content;
}
@Override
public String sm3Digest(String pContent) {
return SmUtil.sm3(pContent);
}
@Override
public String md5Digest(String pContent) {
return SecureUtil.md5(pContent);
}
@Override
public String userNameKey() {
SM2 sm2 = new SM2();
UserNameEncryptKey userNameEncryptKey = new UserNameEncryptKey();
userNameEncryptKey.setPublicKey(sm2.getPublicKey().getEncoded());
userNameEncryptKey.setPrivateKey(sm2.getPrivateKey().getEncoded());
HttpSession httpSession = ZFHttpUtil.getHttpSession();
httpSession.setAttribute(USER_ENCRYPT_HTTP_SESSION_KEY,userNameEncryptKey);
return Base64.encode(userNameEncryptKey.getPublicKey());
}
protected UserNameEncryptKey getUserNameKey(){
HttpSession httpSession = ZFHttpUtil.getHttpSession();
Object oUserNameEncryptKey = httpSession.getAttribute(USER_ENCRYPT_HTTP_SESSION_KEY);
if(null == oUserNameEncryptKey){
throw new ServiceException("用户名编码没有生成加密密钥");
}
return (UserNameEncryptKey)oUserNameEncryptKey;
}
@Override
public String userNameEncode(String pUserName) {
UserNameEncryptKey userNameEncryptKey = getUserNameKey();
SM2 sm2 = new SM2(userNameEncryptKey.getPrivateKey(),userNameEncryptKey.getPublicKey());
String cipherText = sm2.encryptBase64(pUserName,KeyType.PublicKey);
return cipherText;
}
@Override
public String userNameDecode(String pCiphertext) {
UserNameEncryptKey userNameEncryptKey = getUserNameKey();
SM2 sm2 = new SM2(userNameEncryptKey.getPrivateKey(),userNameEncryptKey.getPublicKey());
String contentTest = sm2.decryptStr(pCiphertext,KeyType.PrivateKey);
return contentTest;
}
@Override
public PasswdEncryptKeyVo passwdKey() {
PasswdEncryptKey passwdEncryptKey = new PasswdEncryptKey();
SM2 sm2 = new SM2();
passwdEncryptKey.setPrivateKey(sm2.getPrivateKey().getEncoded());
passwdEncryptKey.setPublicKey(sm2.getPublicKey().getEncoded());
passwdEncryptKey.setSalt(RandomUtil.randomString(PASSWD_SALT_LEN));
HttpSession httpSession = ZFHttpUtil.getHttpSession();
httpSession.setAttribute(PASSWD_ENCRYPT_HTTP_SESSION_KEY,passwdEncryptKey);
PasswdEncryptKeyVo passwdEncryptKeyVo = new PasswdEncryptKeyVo();
passwdEncryptKeyVo.setPublicKey( Base64.encode(sm2.getPrivateKey().getEncoded()));
passwdEncryptKeyVo.setSalt(passwdEncryptKey.getSalt());
return passwdEncryptKeyVo;
}
protected PasswdEncryptKey getPasswdKey(){
HttpSession httpSession = ZFHttpUtil.getHttpSession();
Object oPasswdEncryptKey = httpSession.getAttribute(PASSWD_ENCRYPT_HTTP_SESSION_KEY);
if(null == oPasswdEncryptKey){
throw new ServiceException("没有生成密码的加密密钥");
}
return (PasswdEncryptKey)oPasswdEncryptKey;
}
@Override
public String passwdEncode(String pPasswd) {
PasswdEncryptKey passwdEncryptKey = getPasswdKey();
SM2 sm2 = new SM2(passwdEncryptKey.getPrivateKey(), passwdEncryptKey.getPublicKey());
String cipherText = sm2.encryptBase64(pPasswd,KeyType.PublicKey);
String signStr = SmUtil.sm3(cipherText+passwdEncryptKey.getSalt());
return signStr+cipherText;
}
@Override
public String passwdDecode(String pCiphertext) {
PasswdEncryptKey passwdEncryptKey = getPasswdKey();
if(pCiphertext.length() <= 64){
throw new ServiceException("密码密文格式不对,缺失签名");
}
String signStr = pCiphertext.substring(0,64);
String cipherText = pCiphertext.substring(64);
String testSignStr = SmUtil.sm3(cipherText+passwdEncryptKey.getSalt());
if(!testSignStr.equals(signStr)){
throw new ServiceException("签名验证不通过");
}
SM2 sm2 = new SM2(passwdEncryptKey.getPrivateKey(),passwdEncryptKey.getPublicKey());
String passwd = sm2.decryptStr(cipherText,KeyType.PrivateKey);
return passwd;
}
}
3.3 注解设计
3.3.1 PasswdEncode
该注解加载到controller的方法上,表示该方法需要做加密。
毕竟加密解密比较消耗效率。
@Target({ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface PasswdEncode {
}
3.3.2 UserName
该注解加载到用户名参数上
@Target({ElementType.PARAMETER,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserName {
}
3.3.3 Passwd
该注解加载到密钥参数上
@Target({ElementType.PARAMETER,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Passwd {
}
3.4 注解实现
package indi.zhifa.recipe.bailan.framework.auth.filter;
@Order(103)
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class PasswdEncodeFilters {
private final SecurityConfig mSecurityConfig;
private final IZfEncryptUtil mZfEncryptUtil;
@Around("@annotation(passwdEncode)")
public Object spAdminAop(ProceedingJoinPoint pJoinPoint, PasswdEncode passwdEncode) throws Throwable {
MethodSignature signature = (MethodSignature)pJoinPoint.getSignature();
Annotation[][] annotations = signature.getMethod().getParameterAnnotations();
Object[] params = pJoinPoint.getArgs();
String[] paramNames = signature.getParameterNames();
for(int i=0;i<params.length;i++){
Object param = params[i];
Annotation[] paramAnnotations = annotations[i];
for(Annotation an : paramAnnotations){
if(an.annotationType().equals(Passwd.class)){
if(param instanceof String){
String rawPasswd = (String)param;
try{
params[i] = decodePasswd(rawPasswd);
}catch (Exception ex){
throw new ServiceException(paramNames[i]+" "+ex.getMessage());
}
}
break;
}
else if(an.annotationType().equals(UserName.class)){
if(param instanceof String){
String rawUserName = (String)param;
try{
params[i] = decodeUserName(rawUserName);
}catch (Exception ex){
throw new ServiceException(paramNames[i]+" "+ex.getMessage());
}
}
break;
}else if(an.annotationType().equals(RequestBody.class)){
Field[] fields = param.getClass().getDeclaredFields();
for(Field field : fields){
handlePasswd(param,field);
handleName(param,field);
}
}
}
}
return pJoinPoint.proceed(params);
}
protected void handlePasswd(Object pDto, Field pField) throws IllegalAccessException {
Passwd passwd = pField.getAnnotation(Passwd.class);
if(null != passwd){
Class fieldType = pField.getType();
if(null != fieldType && fieldType == String.class){
boolean accessible = pField.isAccessible();
pField.setAccessible(true);
Object objPsd = pField.get(pDto);
if(null != objPsd){
String psd = (String) objPsd;
if(StringUtils.hasText(psd)){
String decodePsd = decodePasswd(psd);
pField.set(pDto,decodePsd);
}
}
pField.setAccessible(accessible);
}
}
}
protected void handleName(Object pDto, Field pField) throws IllegalAccessException {
UserName userName = pField.getAnnotation(UserName.class);
if(null != userName){
Class fieldType = pField.getType();
if(null != fieldType && fieldType == String.class){
boolean accessible = pField.isAccessible();
pField.setAccessible(true);
Object objUsr = pField.get(pDto);
if(null != objUsr){
String usr = (String) objUsr;
if(StringUtils.hasText(usr)){
String decodeUsr = decodeUserName(usr);
pField.set(pDto,decodeUsr);
}
}
pField.setAccessible(accessible);
}
}
}
private String decodePasswd(String pPasswd){
String originPwd = pPasswd;
PasswdConfig passwordConfig = mSecurityConfig.getPasswd();
if(passwordConfig.isEncrypt()){
originPwd = mZfEncryptUtil.passwdDecode(pPasswd);
}
return originPwd;
}
private String decodeUserName(String pUserName){
String originUserName= pUserName;
PasswdConfig passwordConfig = mSecurityConfig.getPasswd();
if(passwordConfig.isEncrypt()){
originUserName = mZfEncryptUtil.userNameDecode(pUserName);
}
return originUserName;
}
}
3.5 修改登陆接口
@Api(tags = "LoginAip-登录接口")
@RequestMapping("/api/login")
@Slf4j
@ZfRestController
@RequiredArgsConstructor
public class LoginAip {
private final ILoginService mLoginService;
@PasswdEncode
@UnLogin
@Operation(summary = "登录")
@PostMapping
public BaseLoginInfoVo login(
@Validated @Parameter(description = "登录信息") @RequestBody LoginDto pLoginDto){
BaseLoginInfoVo baseLoginInfoVo = mLoginService.login(pLoginDto);
return baseLoginInfoVo;
}
}