目录
一、加密知识回顾
AES加密模式
二、Java 自定义 AES 工具类
三、SpringBoot 实现 AES 加密登陆
controller 层
server 层
四、Vue 实现 AES 加密登陆
五、前端AES工具类
六、实现结果
一、加密知识回顾
密钥是AES算法实现加密和解密的根本。对称加密算法之所以对称,是因为这类算法对明文的加密和解密需要使用同一个密钥。
AES支持三种长度的密钥: 128位,192位,256位
平时大家所说的AES128,AES192,AES256,实际上就是指的AES算法对不同长度密钥的使用。从安全性来看,AES256 安全性最高。从性能来看,AES128 性能最高。本质原因是它们的加密处理轮数不同。
对称加密与非对称加密有什么区别,敏感数据怎么加解密和传输
AES加密模式
1 : ECB(Electronic Code Book电子密码本)模式
ECB模式是最早采用和最简单的模式,它将加密的数据分成若干组,每组的大小跟加密密钥长度相同,然后每组都用相同的密钥进行加密。
优点:1、简单;2:有利于并行计算;3、误差不会被传送;
缺点:1、不能隐藏明文的模式;2、可能对明文进行主动攻击;
因此,此模式适于加密小消息。
2 : CBC(Cipher Block Chaining,加密块链)模式
优点:1、不容易主动攻击,安全性好于ECB,适合传输长度长的报文,是SSL、IPSec的标准。
缺点:1、不利于并行计算;2、误差传递;3、需要初始化向量IV
3 : CFB(Cipher FeedBack Mode,加密反馈)模式
优点:1、隐藏了明文模式;2、分组密码转化为流模式;3、可以及时加密传送小于分组的数据;
缺点:1、不利于并行计算;2、误差传送:一个明文单元损坏影响多个单元;3、唯一的IV;
4 : OFB(Output FeedBack,输出反馈)模式
优点:1、隐藏了明文模式;2、分组密码转化为流模式;3、可以及时加密传送小于分组的数据;
缺点:1、不利于并行计算;2、对明文的主动攻击是可能的;3、误差传送:一个明文单元损坏影响多个单元;
ECB 不够安全,只适合于短数据的加密,而 CBC 和 CFB 相较于 ECB 更加安全,因为前一个密文块会影响当前明文块,使攻击者难以预测密文的结构。总的来说,选择 AES 的算法模式需要根据加密需要的安全性和速度来进行选择,通常推荐使用CBC 或 CFB 模式,而不是 ECB 模式。
二、Java 自定义 AES 工具类
AES 算法在 Java 的 javax.crypto 包里有很好的封装,我们来看一下调用的方法:
public class AesSingleUtil {
private static volatile AesSingleUtil aESUtil;
private static String encodeRules = "9JLsB1ukY3o4mdTuuE90+Q==";
private AesSingleUtil() {
}
public static AesSingleUtil getSingleton() {
if (aESUtil == null) {
Class var0 = AesSingleUtil.class;
synchronized(AesSingleUtil.class) {
if (aESUtil == null) {
aESUtil = new AesSingleUtil();
}
}
}
return aESUtil;
}
public String AESEncode(String content) throws Exception {
KeyGenerator keygen = KeyGenerator.getInstance("AES");
keygen.init(128, new SecureRandom(encodeRules.getBytes()));
SecretKey original_key = keygen.generateKey();
byte[] raw = original_key.getEncoded();
SecretKey key = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(1, key);
byte[] byte_encode = content.getBytes("utf-8");
byte[] byte_AES = cipher.doFinal(byte_encode);
String AES_encode = new String(Base64.getEncoder().encode(byte_AES));
return AES_encode;
}
public String AESDecode(String content) throws Exception {
KeyGenerator keygen = KeyGenerator.getInstance("AES");
keygen.init(128, new SecureRandom(encodeRules.getBytes()));
SecretKey original_key = keygen.generateKey();
byte[] raw = original_key.getEncoded();
SecretKey key = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(2, key);
byte[] byte_content = Base64.getDecoder().decode(content);
byte[] byte_decode = cipher.doFinal(byte_content);
String AES_decode = new String(byte_decode, "utf-8");
return AES_decode;
}
}
AesSingleUtil 类:这是一个单例类,使用了双重检查锁(double-checked locking)实现按需延迟初始化。在 getSingleton
方法中,通过检查静态变量 aESUtil
是否为 null,来确定是否需要创建新的实例。通过单例模式确保了整个应用程序中只有一个实例存在,节省了资源并避免了不必要的重复创建对象。
encodeRules 变量:存储着用于生成 AES 密钥的编码规则。
AESEncode 方法:接受一个字符串作为参数,使用该字符串作为密钥的种子,生成 AES 密钥,并对输入的内容进行 AES 加密,最终返回 Base64 编码的加密结果。
AESDecode 方法:与 AESEncode 方法相对应,接受一个经过 Base64 编码的加密字符串作为参数,使用同样的密钥规则生成 AES 密钥,并对传入的加密字符串进行 AES 解密,返回解密后的原始字符串。
import com.ss.check.NotNullCheck;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AesWebUtil {
private static Logger log = LoggerFactory.getLogger(AesWebUtil.class);
private static String KEY = "kl9o56u98gvjhw9i";
private static String IV = "grjei564389tj843";
public AesWebUtil() {
}
public String encrypt(String data, String key, String iv) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
int blockSize = cipher.getBlockSize();
byte[] dataBytes = data.getBytes("UTF-8");
int plaintextLength = dataBytes.length;
if (plaintextLength % blockSize != 0) {
plaintextLength += blockSize - plaintextLength % blockSize;
}
byte[] plaintext = new byte[plaintextLength];
System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
cipher.init(1, keyspec, ivspec);
byte[] encrypted = cipher.doFinal(plaintext);
return (new Base64()).encodeToString(encrypted);
} catch (Exception var12) {
return null;
}
}
public String desEncrypt(String data, String key, String iv) {
try {
byte[] encrypted1 = (new Base64()).decode(data.getBytes("UTF-8"));
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
cipher.init(2, keyspec, ivspec);
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original);
return originalString;
} catch (Exception var10) {
return null;
}
}
public static String jiaMi(String data) {
String mm = "";
try {
if (NotNullCheck.str(data)) {
mm = (new AesWebUtil()).encrypt(data, KEY, IV).trim();
}
} catch (Exception var3) {
}
return mm;
}
public static String jieMi(String data) {
String mm = "";
try {
if (NotNullCheck.str(data)) {
mm = (new AesWebUtil()).desEncrypt(data, KEY, IV).trim();
}
} catch (Exception var3) {
}
return mm;
}
public static void main(String[] args) throws Exception {
String jiemi = (new AesWebUtil()).desEncrypt("oLlc7AJnsgl93vaAEYMgGd2/G8zLFX8HrCN9HaBwS9rLJYFT8+RmlReuNHHdX+NacpP6MAByLIdxSeMBQ/9a5HA1oikanQY5I1kTCxpaN639WvA/Rj8I74PicVYemYqiMr/W6dDwpyJt7H5jL7Sofw6bnNmQAtsF9UNQthDS73ddxo19ThLpxOajVE5LF+Mu", KEY, IV).trim();
System.out.println("解密:" + jiemi);
new AesWebUtil();
String jiami = jiaMi("123456");
System.out.println("加密:" + jiami);
}
}
三、SpringBoot 实现 AES 加密登陆
controller 层
@PostMapping("login")
public String userLogin(@RequestBody String user, HttpServletRequest request) {
return userService.userLogin(user, request);
}
server 层
public String userLogin(String user, HttpServletRequest request) {
String jiemiUserStr = null;
try {
jiemiUserStr = AesWebUtil.jieMi(user);
} catch (Exception e) {
throw new SSError(GlobalCodeEnum.SystemLogicError, "非法登录!");
}
UserAll userAll = GsonUtil.jsonToObject(jiemiUserStr, UserAll.class);
if (userAll != null && userAll.getLoginType() != null) {
UserAll loginBack = null;
if (userAll.getLoginType() == 1 && NotNullCheck.str(userAll.getUserLoginName())
&& NotNullCheck.str(userAll.getUserPassword())) {
loginBack = userMapper.getUserByLoginName(userAll.getUserLoginName());
if(loginBack==null){
throw new SSError(GlobalCodeEnum.SystemLogicError, "用户不存在");
}else if(loginBack!=null&&!AesWebUtil.jiaMi(userAll.getUserPassword()).equals(loginBack.getUserPassword())){
throw new SSError(GlobalCodeEnum.SystemLogicError, "密码不正确");
}
} else if (userAll.getLoginType() == 2 && NotNullCheck.str(userAll.getUserCard())) {
loginBack = userMapper.getUserByUserCard(userAll);
}
if (loginBack == null) {
throw new SSError(GlobalCodeEnum.SystemLogicError, "登录信息错误");
} else {
checkUserRightState(loginBack);
try {
loginBack.setUserPassword(JwtUtil.generateToken());
} catch (Exception e) {
e.printStackTrace();
}
// log.info("****************登录成功:" + loginBack);
//埋点登录
return AesWebUtil.jiaMi(GsonUtil.objectToJson(loginBack));
}
}
throw new SSError(GlobalCodeEnum.SystemLogicError, "登录信息不完整");
}
四、Vue 实现 AES 加密登陆
<template>
<common-layout>
<div class="top">
<div class="header">
<img alt="logo" class="logo" src="@/assets/img/logo.png" />
<span class="title">{{ systemName }}</span>
</div>
<div class="desc"></div>
</div>
<div class="login">
<a-form @submit.native="onSubmit" :form="form">
<a-tabs
size="large"
:tabBarStyle="{ textAlign: 'center' }"
style="padding: 0 2px"
@change="userLoginTypeChange"
>
<a-tab-pane tab="账户密码登录" key="1">
<a-alert
type="error"
:closable="true"
v-show="error"
:message="error"
showIcon
style="margin-bottom: 24px"
/>
<a-form-item>
<a-input
ref="userNameRef"
autocomplete="autocomplete"
size="large"
allowClear
placeholder="登录用户名"
v-decorator="[
'name',
{
rules: [
{
required: true,
message: '请输入账户名',
whitespace: true,
},
],
},
]"
>
<a-icon slot="prefix" type="user" />
</a-input>
</a-form-item>
<a-form-item>
<a-input
size="large"
placeholder="登录密码"
autocomplete="autocomplete"
type="password"
allowClear
v-decorator="[
'password',
{
rules: [
{
required: true,
message: '请输入密码',
whitespace: true,
},
],
},
]"
>
<a-icon slot="prefix" type="lock" />
</a-input>
</a-form-item>
</a-tab-pane>
<a-tab-pane tab="工号登录" key="2">
<a-form-item>
<a-input
size="large"
placeholder="员工工号"
v-model="userCard"
ref="userCardRef"
allowClear
>
<a-icon slot="prefix" type="solution" />
</a-input>
</a-form-item>
<!-- <a-form-item> -->
<!-- <a-row :gutter="8" style="margin: 0 -4px">
<a-col :span="16"> -->
<!-- <a-input size="large" placeholder="captcha">
<a-icon slot="prefix" type="lock" />
</a-input> -->
<!-- </a-col> -->
<!-- <a-col :span="8" style="padding-left: 4px">
<a-button
style="width: 100%"
class="captcha-button"
size="large"
>获取验证码</a-button
>
</a-col> -->
<!-- </a-row> -->
<!-- </a-form-item> -->
</a-tab-pane>
</a-tabs>
<!-- <div>
<a-checkbox :checked="true">自动登录</a-checkbox>
</div> -->
<a-form-item>
<a-button
:loading="logging"
style="width: 100%; margin-top: 24px"
size="large"
htmlType="submit"
type="primary"
>登录</a-button
>
</a-form-item>
</a-form>
</div>
</common-layout>
</template>
<script>
import CommonLayout from "@/layouts/CommonLayout";
// import { login, getRoutesConfig } from "@/services/user";
// import { setAuthorization } from "@/utils/request";
// import { loadRoutes } from "@/utils/routerUtil";
import { mapMutations } from "vuex";
import { userLogin } from "@/api/user";
import { aesJiami, aesJiemi } from "@/utils/aes";
const timeList = ["早上好", "上午好", "中午好", "下午好", "晚上好"];
export default {
name: "Login",
components: { CommonLayout },
data() {
return {
logging: false,
error: "",
form: this.$form.createForm(this),
userLgoinType: 1,
userCard: "",
};
},
computed: {
systemName() {
return this.$store.state.setting.systemName;
},
},
mounted() {
// 禁用浏览器返回键
history.pushState(null, null, document.URL);
window.addEventListener("popstate", this.disableBrowserBack);
if (this.$refs.userNameRef) {
this.$refs.userNameRef.focus();
}
},
destroyed() {
// 清除popstate事件 否则会影响到其他页面
window.removeEventListener("popstate", this.disableBrowserBack, false);
},
methods: {
...mapMutations("account", ["setUser", "setPermissions", "setRoles"]),
disableBrowserBack() {
history.pushState(null, null, document.URL);
},
userLoginTypeChange(key) {
setTimeout(() => {
if (key === "1") {
this.$refs.userNameRef.focus();
} else if (key === "2") {
this.$refs.userCardRef.focus();
}
}, 100);
this.userLgoinType = key;
},
onSubmit(e) {
e.preventDefault();
let checkUserLoginInput = false;
if (this.userLgoinType === 1) {
this.form.validateFields((err) => {
if (!err) {
checkUserLoginInput = true;
}
});
} else {
if (
this.userCard &&
this.userCard.replace(/↵/g, "").replace(/[/n/r]/g, "")
) {
checkUserLoginInput = true;
}
}
if (!checkUserLoginInput) {
return;
}
this.logging = true;
const name = this.form.getFieldValue("name");
const password = this.form.getFieldValue("password");
// login(name, password).then(this.afterLogin);
const userLoginInfo = {
userLoginName: name,
userPassword: password,
userCard: this.userCard,
loginType: this.userLgoinType,
};
userLogin(aesJiami(JSON.stringify(userLoginInfo)))
.then((res) => {
if (!res) {
return;
}
try {
res = JSON.parse(aesJiemi(res));
} catch (error) {
this.$message.error("登录失败,服务器校验不通过");
return;
}
let permissions = null;
if (res) {
permissions = [{ id: res.userAuth, operation: ["add", "edit"] }];
if (!permissions) {
this.$message.error("用户权限校验失败");
return;
}
this.setPermissions(aesJiami(JSON.stringify(permissions)));
this.$router.push({ name: "主页" });
const time = new Date();
const hour = time.getHours();
const welcomeTime =
hour < 9
? timeList[0]
: hour <= 11
? timeList[1]
: hour <= 13
? timeList[2]
: hour <= 17
? timeList[3]
: timeList[4];
this.$message.success(welcomeTime + "," + res.userName);
const roles = [
{ id: res.userAuth, operation: ["add", "edit", "delete"] },
];
res.name = res.userName;
this.setRoles(aesJiami(JSON.stringify(roles)));
this.setUser(aesJiami(JSON.stringify(res)));
}
})
.finally(() => {
this.logging = false;
});
},
/* afterLogin(res) {
this.logging = false;
const loginRes = res.data;
if (loginRes.code >= 0) {
const { user, permissions, roles } = loginRes.data;
this.setUser(user);
this.setPermissions(permissions);
this.setRoles(roles);
setAuthorization({
token: loginRes.data.token,
expireAt: new Date(loginRes.data.expireAt),
});
// 获取路由配置
getRoutesConfig().then((result) => {
const routesConfig = result.data.data;
loadRoutes(routesConfig);
this.$router.push("/dashboard/workplace");
this.$message.success(loginRes.message, 3);
});
} else {
this.error = loginRes.message;
}
}, */
},
};
</script>
<style lang="less" scoped>
.common-layout {
.top {
text-align: center;
.header {
height: 44px;
line-height: 44px;
a {
text-decoration: none;
}
.logo {
height: 44px;
vertical-align: top;
margin-right: 16px;
}
.title {
font-size: 33px;
color: @title-color;
font-family: "Myriad Pro", "Helvetica Neue", Arial, Helvetica,
sans-serif;
font-weight: 600;
position: relative;
top: 2px;
}
}
.desc {
font-size: 14px;
color: @text-color-second;
margin-top: 12px;
margin-bottom: 40px;
}
}
.login {
width: 368px;
margin: 0 auto;
@media screen and (max-width: 576px) {
width: 95%;
}
@media screen and (max-width: 320px) {
.captcha-button {
font-size: 14px;
}
}
.icon {
font-size: 24px;
color: @text-color-second;
margin-left: 16px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: @primary-color;
}
}
}
}
</style>
五、前端AES工具类
import CryptoJS from "crypto-js";
const KEY = CryptoJS.enc.Utf8.parse("kl9o56u98gvjhw9i");
const IV = CryptoJS.enc.Utf8.parse("grjei564389tj843");
export const aesJiami = (word, keyStr, ivStr) => {
let key = KEY;
let iv = IV;
if (keyStr) {
key = CryptoJS.enc.Utf8.parse(keyStr);
iv = CryptoJS.enc.Utf8.parse(ivStr);
}
let srcs = CryptoJS.enc.Utf8.parse(word);
var encrypted = CryptoJS.AES.encrypt(srcs, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding,
});
// console.log("-=-=-=-", encrypted.ciphertext)
return CryptoJS.enc.Base64.stringify(encrypted.ciphertext);
};
export const aesJiemi = (word, keyStr, ivStr) => {
let key = KEY;
let iv = IV;
if (keyStr) {
key = CryptoJS.enc.Utf8.parse(keyStr);
iv = CryptoJS.enc.Utf8.parse(ivStr);
}
let base64 = CryptoJS.enc.Base64.parse(word);
let src = CryptoJS.enc.Base64.stringify(base64);
var decrypt = CryptoJS.AES.decrypt(src, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding,
});
var decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
return decryptedStr.toString();
};
这段前端代码的逻辑是:
-
导入 CryptoJS 库:通过
import CryptoJS from "crypto-js";
这行代码导入了 CryptoJS 库,用于进行加密和解密操作。 -
定义加密所需的密钥和向量:
KEY
和IV
分别是用于加密和解密的密钥和初始化向量。在这段代码中,它们被硬编码为固定值。 -
定义
aesJiami
函数:aesJiami
函数用于对传入的字符串进行加密操作。如果有自定义的密钥和向量参数,则使用传入的参数,否则使用预设的KEY
和IV
。将传入的字符串转换为 UTF-8 编码格式。使用 AES 加密算法对字符串进行加密,加密模式为 CBC,填充方式为 ZeroPadding。将加密后的结果转换为 Base64 字符串并返回。 -
定义
aesJiemi
函数:aesJiemi
函数用于对传入的 Base64 加密字符串进行解密操作。如果有自定义的密钥和向量参数,则使用传入的参数,否则使用预设的KEY
和IV
。将传入的 Base64 加密字符串解析为 Base64 对象。使用 AES 解密算法对 Base64 对象进行解密,解密模式为 CBC,填充方式为 ZeroPadding。将解密后的结果转换为 UTF-8 编码格式的字符串并返回。
总的来说,这段代码实现了对字符串的 AES 加密和解密操作,使用了 CryptoJS 库提供的加密算法和相关函数。通过传入不同的参数,可以实现自定义密钥和向量进行加密解密的功能
六、实现结果
{
"reqID": 1,
"method": "",
"sender": "ss-smartdata-web-browser Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0",
"token": "Bearer eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE3MTg5MzkzNzl9.FcPMd60PZQJ8TBrkRZLf3gL76Vr1w9paSfgZPYZlDKNT0h74_zF8Wa8DwrB0Gciw-zneaBtUc1CBAa6yF2RmRQ",
"reqData": "cThk+vaRL/B41LkCUVrshuT0R8xfnKGh56D+NeqC6HC2PdfcWwFL5RU3gY/sjsc39e156rj29ryANn1LZ29pL5bfN6EIq2oVuqC34GQeu7g="
}
{
result: 1, reqID: -1, respID: -1, sender: "", sendee: "", msg: "请求成功",…
}
msg : "请求成功"
reqID : -1
respData:"nWOD3Sw2iLf1xevNYnHuHGvPFr/TAJpviyh/M22qLYuiaHoqvxhCn9OF2WGitiKr+LxTbEJvuBsbCmetAPCo7b48WG0PzsHz3fQxlOsQKeh629Z18gyBKho4zEAayHHjhP4rrHjSNNTwoZzAD0n6Cj2HTx+COXH9KtL905HZC3y0+mn1n72BCN2nxVExAu+8cBP+N66MJWVQLyj+7ENWJ0Euq22v3xWWoX2fFKe2XZr8Y7taMRkSNEyfYpgyq3Tl1az7A3I6+eYdpuYndBlxe0m7K6qOgckjni4l9ApIN16P7947Y5LXPY3wHsKVs1t0NKERnNzUTE/aD5einOv/pE3HdZAujtigOHwmihwrhHbTeumSYTC02kbtKUHNlR3lw+GsKR/727sCvkNhYnqz3uXjfJHYmghJJjL20XmPzpr5GJaohr3f4ZcoABv5G3dy"
respID : -1
result : 1
sendee : ""
sender: ""