目录
一、加密知识回顾
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: ""
复制