一、SpringBoot使用自定义注解实现返回数据脱敏操作
在实际项目中,对于敏感数据的保护十分重要,数据脱敏又称数据去隐私化或数据变形,是在给定的规则、策略下对敏感数据进行变换、修改的技术机制,能够在很大程度上解决敏感数据在非可信环境中使用的问题。
本文使用自定义注解,在返回数据给前端的时候,根据给定的脱敏规则实现敏感数据脱敏操作,实现过程非常简单,一起来看看吧!
1、引入依赖
<!-- hutool工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.7</version>
</dependency>
2、自定义注解 - Desensitize
package com.ltkj.common.annotation;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.ltkj.common.enums.SensitiveStrategy;
import com.ltkj.common.jackson.SensitiveJsonSerializer;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 数据脱敏注解
*
* @author wangl
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive {
SensitiveStrategy strategy();
}
3、脱敏规则枚举类
package com.ltkj.common.enums;
import cn.hutool.core.util.DesensitizedUtil;
import lombok.AllArgsConstructor;
import java.util.function.Function;
/**
* 脱敏策略
*
* @author wangl
*/
@AllArgsConstructor
public enum SensitiveStrategy {
/**
* 身份证脱敏
*/
ID_CARD(s -> DesensitizedUtil.idCardNum(s, 3, 4)),
/**
* 手机号脱敏
*/
PHONE(DesensitizedUtil::mobilePhone),
/**
* 地址脱敏
*/
ADDRESS(s -> DesensitizedUtil.address(s, 8)),
/**
* 邮箱脱敏
*/
EMAIL(DesensitizedUtil::email),
/**
* 银行卡
*/
BANK_CARD(DesensitizedUtil::bankCard);
//可自行添加其他脱敏策略
private final Function<String, String> desensitizer;
public Function<String, String> desensitizer() {
return desensitizer;
}
}
这其中的脱敏规则全都依赖 hutool 的 DesensitizedUtil 工具类,有其它的脱敏规则可以自定义实现
4、数据脱敏 JSON 序列化工具
package com.ltkj.common.jackson;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.ltkj.common.annotation.Sensitive;
import com.ltkj.common.core.service.SensitiveService;
import com.ltkj.common.enums.SensitiveStrategy;
import com.ltkj.common.utils.spring.SpringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import java.io.IOException;
import java.util.Objects;
/**
* 数据脱敏json序列化工具
*
* @author wangl
*/
@Slf4j
public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
private SensitiveStrategy strategy;
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
try {
SensitiveService sensitiveService = SpringUtils.getBean(SensitiveService.class);
if (ObjectUtil.isNotNull(sensitiveService) && sensitiveService.isSensitive()) {
gen.writeString(strategy.desensitizer().apply(value));
} else {
gen.writeString(value);
}
} catch (BeansException e) {
log.error("脱敏实现不存在, 采用默认处理 => {}", e.getMessage());
gen.writeString(value);
}
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
Sensitive annotation = property.getAnnotation(Sensitive.class);
if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) {
this.strategy = annotation.strategy();
return this;
}
return prov.findValueSerializer(property.getType(), property);
}
}
SpringBoot 中默认使用 jackson 作为 JSON 序列化工具,我们获取到自定义的脱敏注解(@Sensitive ),就对该数据实现我们的脱敏操作,就完成了我们的敏感数据脱敏操作。
import com.ltkj.common.core.service.SensitiveService;
import com.ltkj.common.helper.LoginHelper;
import org.springframework.stereotype.Service;
/**
* 脱敏服务
* 默认管理员不过滤
* 需自行根据业务重写实现
*
* @author wangl脱敏服务
* @date 2023-10-10
*/
@Service
public class SysSensitiveServiceImpl implements SensitiveService {
/**
* 动态判断是否脱敏
* 默认管理员不过滤
*/
@Override
public boolean isSensitive() {
return !LoginHelper.isAdmin();
}
}
5 测试结果
访问接口:
http://localhost:8090/test
得到数据:
可以看出,对应的敏感数据被进行脱敏了,证明我们的脱敏注解生效了
二、vue + SpringBoot密码加密传输(前后分离板)
前言:目前登录接口密码是明文传输 为了提高安全性 调整为加密方式传输( 这里选择Rsa加密算法)
修改前
修改后
大概加密流程:
- 后台生成随机公钥私钥 (原本是固定公私秘钥 我改为了随机生成)
- 前台拿到公钥 集成jsencrypt实现密码加密
- 传输加密后的密码给后台
- 后台通过私钥对加密后的密码进行解密
后台改动
- 首先新建RsaUtils类
/*
* Copyright (c) 2023. ltkj.com 石家庄旅投集团科技有限公司 All rights reserved.
*/
package com.ltkj.common.utils;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* 加密工具类
*
* @author wangl
* @date 2023-10-10
*/
public class RsaUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(RsaUtil.class);
private static final String RSA = "RSA";
// Rsa 私钥 也可固定秘钥对 若依原写法(不安全)
public static String privateKeys = "";
private static String publicKeyStr = "";
private static String privateKeyStr = "";
private static final RsaKeyPair rsaKeyPair = new RsaKeyPair();
/**
* 构建RSA密钥对
*
* @return 生成后的公私钥信息
*/
static {
KeyPairGenerator keyPairGenerator = null;
try {
keyPairGenerator = KeyPairGenerator.getInstance(RSA);
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded());
String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded());
rsaKeyPair.setPrivateKey(privateKeyString);
rsaKeyPair.setPublicKey(publicKeyString);
publicKeyStr = publicKeyString;
privateKeyStr = privateKeyString;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
/**
* 私钥解密
*
* @param text 待解密的文本
* @return 解密后的文本
*/
public static String decryptByPrivateKey(String text) {
LOGGER.info("rsaKeyPair.privateKey[{}]", rsaKeyPair.getPrivateKey());
return decryptByPrivateKey(rsaKeyPair.getPrivateKey(), text);
}
/**
* 公钥解密
*
* @param publicKeyString 公钥
* @param text 待解密的信息
* @return 解密后的文本
*/
public static String decryptByPublicKey(String publicKeyString, String text) throws Exception {
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString));
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
Cipher cipher = Cipher.getInstance(RSA);
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] result = cipher.doFinal(Base64.decodeBase64(text));
return new String(result);
}
/**
* 私钥加密
*
* @param privateKeyString 私钥
* @param text 待加密的信息
* @return 加密后的文本
*/
public static String encryptByPrivateKey(String privateKeyString, String text) throws Exception {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString));
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Cipher cipher = Cipher.getInstance(RSA);
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] result = cipher.doFinal(text.getBytes());
return Base64.encodeBase64String(result);
}
/**
* 私钥解密
*
* @param privateKeyString 私钥
* @param text 待解密的文本
* @return 解密后的文本
*/
public static String decryptByPrivateKey(String privateKeyString, String text) {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString));
KeyFactory keyFactory = null;
try {
keyFactory = KeyFactory.getInstance(RSA);
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5);
Cipher cipher = Cipher.getInstance(RSA);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] result = cipher.doFinal(Base64.decodeBase64(text));
return new String(result);
} catch (Exception e) {
LOGGER.error("decryptByPrivateKey error, privateKeyString[{}], text[{}]", privateKeyString, text);
LOGGER.error("decryptByPrivateKey error: ", e);
throw new RuntimeException("decryptByPrivateKey error");
}
}
/**
* 公钥加密
*
* @param publicKeyString 公钥
* @param text 待加密的文本
* @return 加密后的文本
*/
public static String encryptByPublicKey(String publicKeyString, String text) throws Exception {
X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString));
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2);
Cipher cipher = Cipher.getInstance(RSA);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] result = cipher.doFinal(text.getBytes());
return Base64.encodeBase64String(result);
}
public static String getPublicKey() {
return publicKeyStr;
}
public static String getPrivateKey() {
return privateKeyStr;
}
public static RsaKeyPair rsaKeyPair() {
return rsaKeyPair;
}
/**
* RSA密钥对对象
*/
public static class RsaKeyPair {
private String publicKey;
private String privateKey;
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
public RsaKeyPair() {
}
public RsaKeyPair(String publicKey, String privateKey) {
this.publicKey = publicKey;
this.privateKey = privateKey;
}
public String getPublicKey() {
return publicKey;
}
public String getPrivateKey() {
return privateKey;
}
}
}
RsaUtils添加了 generateKeyPair()构建密钥对 在第一次类加载时,通过static代码块 生成, 每次调用接口会是一样的公私秘钥 每次重启后公私钥不同
- 在controller层添加获取公钥接口
/**
* 获取公钥前端用来密码加密
*/
@SaIgnore
@GetMapping("/inner/publicKey")
@Operation(summary = "获取公钥", description = "获取公钥接口")
public R<RsaUtil.RsaKeyPair> publicKey() {
// 测试 公私秘钥都传了 后期改为只传公钥
// RsaUtils.RsaKeyPair rsaKeyPair = new RsaUtils.RsaKeyPair();
// rsaKeyPair.setPublicKey(RsaUtils.getPublicKey());
// return rsaKeyPair;
return R.ok(RsaUtil.rsaKeyPair());
}
- 登录接口类 修改(重点)
@SaIgnore
@PostMapping("/login")
@Operation(summary = "账号密码", description = "账号密码登录")
public R<Map<String, Object>> login(@Validated @RequestBody LoginBody loginBody) {
Map<String, Object> ajax = new HashMap<>();
// 新增加解密密码 20240105
String pwd = RsaUtil.decryptByPrivateKey(loginBody.getPassword());
// 生成令牌
String token = loginService.login(loginBody.getDeptId(), loginBody.getUsername(), pwd, loginBody.getCode(), loginBody.getUuid());
ajax.put(Constants.TOKEN, token);
return R.ok(ajax);
}
前端改动
- login.js添加获取公钥接口路径
export function getPublicKey() {
return request({
url: '/inner/publicKey',
method: 'get',
})
}
- user.js更改登陆方法
getPublicKey() {
return new Promise((resolve, reject) => {
getPublicKey()
.then(res => {
resolve(res)
})
.catch(error => {
reject(error)
})
})
},
// 登录
Login({commit, dispatch}, userInfo) {
const username = userInfo.username.trim()
// const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
const deptId = userInfo.deptId
return new Promise((resolve, reject) => {
dispatch('getPublicKey').then(res => {
let publicKey = res.data.publicKey
const password = encrypt(userInfo.password, publicKey)
login(username, password, deptId, code, uuid)
.then(res => {
setToken(res.data.token)
commit('SET_TOKEN', res.data.token)
resolve()
}).catch(error => {
reject(error)
})
})
})
},
- JSEncrypt.js更改加解密方法
// 加密
export function encrypt(txt, pubKey) {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(pubKey) // 设置公钥
return encryptor.encrypt(txt) // 对数据进行加密
}
// 解密
export function decrypt(txt, priKey) {
const encryptor = new JSEncrypt()
encryptor.setPrivateKey(privateKey) // 设置私钥
return encryptor.decrypt(txt) // 对数据进行解密
}
好啦,登陆验证下吧~
或者把pubKey 写在encrypt 里获取 :
这样外面调用 encrypt时,就不用在每次都得在外面 先获取 pub-key了
import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
import request from '@/utils/request'
// 密钥对生成 http://web.chacuo.net/netrsakeypair
const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' +
'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' +
'7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' +
'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' +
'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' +
'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' +
'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' +
'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' +
'UP8iWi1Qw0Y='
const key = {
pubKey: 'ltkj123',
priKey: 'ltkj321'
}
export function getPublicKey() {
return request({
url: '/inner/publicKey',
method: 'get',
})
}
// 加密
export async function encrypt(txt) {
console.log("encrypt init==" + (key.pubKey == 'ltkj123'));
if (key.pubKey == 'ltkj123') {
await getPublicKey().then(res => {
key.pubKey = res.data.publicKey;
key.priKey = res.data.privateKey
});
}
const encryptor = new JSEncrypt()
encryptor.setPublicKey(key.pubKey) // 设置公钥
return encryptor.encrypt(txt) // 对数据进行加密
}
// 解密
export async function decrypt(txt) {
const encryptor = new JSEncrypt()
console.log("decrypt key.priKey==" + (key.priKey == 'ltkj321'));
if (key.priKey == 'ltkj321') {
await getPublicKey().then(res => {
key.pubKey = res.data.publicKey;
key.priKey = res.data.privateKey
});
}
encryptor.setPrivateKey(key.priKey) // 设置私钥
return encryptor.decrypt(txt) // 对数据进行解密
}
user.js
// 登录
Login({commit, dispatch}, userInfo) {
const username = userInfo.username.trim()
// const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
const deptId = userInfo.deptId
return new Promise((resolve, reject) => {
encrypt(userInfo.password).then(res => {
console.log("Login res==" + res);
login(username, res, deptId, code, uuid)
.then(res => {
setToken(res.data.token)
commit('SET_TOKEN', res.data.token)
resolve()
}).catch(error => {
reject(error)
})
})
})
},
login.vue
import { getCodeImg, getCompanyInfo } from "@/api/login";
import Cookies from "js-cookie";
import { encrypt, decrypt } from '@/utils/jsencrypt'
handleLogin () {
//表单验证-Element-ui的$refs.loginForm.validate()接口
this.$refs.loginForm.validate(valid => {
if (valid) {
// 切换为显示登陆中
this.loading = true;
//是否记住密码
if (this.loginForm.rememberMe) {
// 记住密码-设置到Cookie中-[过期时间为30天]
Cookies.set("username", this.loginForm.username, { expires: 30 });
// 密码加密
encrypt(this.loginForm.password).then(res => {
console.log("handleLogin res==" + res);
Cookies.set("password", res, { expires: 30 });
Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });
Cookies.set('deptId', this.loginForm.deptId, { expires: 30 });
});
} else {
// 不记住密码-将上一次设置到Cookie中的值移除
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove('rememberMe');
}
// 提交用户信息到 Vuex中
// Vuex全局状态管理相关的内容
this.$store.dispatch("Login", this.loginForm).then(() => {
// 路由跳转相关的内容
this.$router.push({ path: this.redirect || "/" }).catch(() => { });
}).catch(() => {
// 登陆失败时-取消loading显示,并尝试重新获取验证码图片资源
this.loading = false;
if (this.captchaEnabled) {
this.getCode();
}
});
}
});
}