需求:用户在登录时密码经常使用明文传输,在不安全的网络环境下,很容易造成密码泄露,而密码直接存储在数据库中,如果数据泄露,也会造成安全问题。
解决方法:前端给后端传输密码关键信息时,进行加密后再传输,后端解密验证,然后将密码加密后再存储到数据库中。
实现思路:
采用RSA非对称加密加密和解密密码传输,采用哈希加盐算法加密密码并存储
1.前端需要传输密码时,先向服务器获取一个加密公钥(加密密钥对由后端生成,以公钥为key,私钥为value存储在redis中)。
2.获取到公钥后,将密码进行加密,并将加密后的密码以及公钥一起传给后端。
3.后端根据公钥从redis中得到配对的密钥,然后对密码解密。
4.将密码加盐后进行哈希加密,然后把加密的盐和加密后的密码一起存入数据库。
5.下次比较时,只需要比较密码+盐进行hash计算后的值是不是和数据库的加密密码相同即可。
代码:
RSA加密与解密:
package com.zong.zongmail.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.stereotype.Repository;
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
/**
* @return
* @Author: Zongmao on 2021/1/8 15:44
* @description:非对称加密方法
*/
@Slf4j
@Repository
public class RSAConfig {
/**
* @Author ZongMao
* @Description //使用Java标准库提供的算法生成密钥对
* @Date 2021/1/8 15:49
**/
public Map createKeyPair() throws GeneralSecurityException {
// 生成公钥/私钥对:
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA");
//它的密钥有256/512/1024/2048/4096等不同的长度。长度越长,密码强度越大,当然计算速度也越慢。
//此处我设置256会报错:RSA keys must be at least 512 bits long
kpGen.initialize(512);
KeyPair kp = kpGen.generateKeyPair();
PublicKey publicKey = kp.getPublic();
PrivateKey privateKey = kp.getPrivate();
//将公钥和私钥转换为base64编码便于存储和返回至前端
byte[] publicKeyByte = publicKey.getEncoded();
byte[] privateKeyByte = privateKey.getEncoded();
String publicKeyStr = Base64.encodeBase64String(publicKeyByte);
String privateKeyStr = Base64.encodeBase64String(privateKeyByte);
Map map = new HashMap();
map.put("publicKeyStr", publicKeyStr);
map.put("privateKeyStr", privateKeyStr);
return map;
}
/**
* @Author ZongMao
* @Description
* 使用私钥解密(前端加密后传入的base64编码的信息,base64编码的私钥),
* return 解密后的密码
* @Date 2021/1/8 16:29
**/
public String decrypt(String passwordBase64, String privateKeyStr) throws GeneralSecurityException{
Cipher cipher = Cipher.getInstance("RSA");
//将Base64编码的私钥转为Private类型
byte[] privateKeyByte = Base64.decodeBase64(privateKeyStr);
KeyFactory kf = KeyFactory.getInstance("RSA"); // or "EC" or whatever
PrivateKey privateKey = kf.generatePrivate(new PKCS8EncodedKeySpec(privateKeyByte));
cipher.init(Cipher.DECRYPT_MODE, privateKey);
//将base64编码的加密信息转为byte[]数据
byte[] passwordByte = Base64.decodeBase64(passwordBase64);
//获取解密后的byte[]数据
byte[] resByte = cipher.doFinal(passwordByte);
//将byte[]转为字符串,即为解密后的信息
String resStr = new String(resByte, StandardCharsets.UTF_8);
return resStr;
}
}
哈希加盐加密以及验证方法:
package com.zongmao.schoolsystem.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* @return
* @Author: Zongmao on 2021/1/8 16:56
* @description:使用Md5对明文口令进行加密(加盐)
*/
@Slf4j
@Repository
public class HashConfigWithSalt {
/**
* @Author ZongMao
* @Description //加盐进行hash加密
* @Date 2021/1/8 16:56
**/
public String HashWithSalt(String message, String salt) throws NoSuchAlgorithmException, UnsupportedEncodingException {
// 创建一个MessageDigest实例:
MessageDigest md = MessageDigest.getInstance("MD5");
// 反复调用update输入数据:
String endMessage = message + salt;
md.update(endMessage.getBytes("UTF-8"));
byte[] result = md.digest();
return new BigInteger(1, result).toString(16);
}
/**
* @Author ZongMao
* @Description //比较是否相等(加密后的字符串,盐,需要验证的信息)
* @Date 2021/1/8 16:59
**/
public boolean isCommon(String mdStr, String salt, String message) throws UnsupportedEncodingException, NoSuchAlgorithmException {
String nowStr = HashWithSalt(message, salt);
if (nowStr.equals(mdStr)){
return true;
}
return false;
}
}
请求获取公钥:
@PostMapping("/common/getPublicKey")
public FormatResultData getPublicKey(){
log.info("请求获取加密公钥");
String publicKey = "";
try {
Map map = rsaConfig.createKeyPair();
publicKey = (String) map.get("publicKeyStr");
String privateKey = (String) map.get("privateKeyStr");
//将publicKey作为key,privateKey作为value存入redis,并且设置过期时间
stringRedisTemplate.opsForValue().set(publicKey, privateKey, 5 * 60 * 1000, TimeUnit.MILLISECONDS);
}catch (GeneralSecurityException e){
log.error(e.toString());
return FormatResultData.error("获取公钥失败", 0);
}catch (Exception e){
log.info(e.toString());
return FormatResultData.systemError();
}
return FormatResultData.success("获取公钥成功", publicKey);
}
前端加密密码:
(使用jsencrypt)
import {JSEncrypt} from 'jsencrypt'
//加密
export function encryptedData(publicKey, data) {
let encryptor = new JSEncrypt();
/*此处可以指定公钥的编码,也可以不指定*/
encryptor.setPublicKey(publicKey, "base64");
return encryptor.encrypt(data);
}
//解密
export function decryptData(privateKey, data) {
let decrypt = new JSEncrypt();
decrypt.setPrivateKey(privateKey);
return decrypt.decrypt(data);
}
//获取publicKey
getPublicKey(){
this.$http("/common/getPublicKey").then(res =>{
if (res.code === 1){
this.loginForm.publicKey = res.data;
this.password = this.loginForm.password;
this.loginForm.password = encryptedData(this.loginForm.publicKey, this.password);
this.$http("/commonLogin/allUserLogin",this.loginForm).then(res =>{
if (res.code === 1){
this.Cookie.set("token", res.data, { expires: 1});
this.$store.commit("changeUserType", this.loginForm.userType);
localStorage.setItem("userName", res.extend);
localStorage.setItem("account", this.loginForm.account);
localStorage.setItem("userType", this.loginForm.userType);
this.$router.push(this.fromPath);
}else {
this.loginForm.password = this.password;
Message({
message: res.msg,
showClose: true,
type:"error"
});
this.getCode();
}
}).catch(err =>{
this.getCode();
})
}else {
this.loginForm.publicKey = "";
Message({
message: "获取公钥失败,请稍后再试!",
showClose: true,
type:"error"
});
}
}).catch(err =>{
})
},
注册新增账号时加密存储:
/**
* @Author ZongMao
* @Description //新增管理员账号
* @Date 2021/1/12 16:05
**/
@Override
public String addAccount(AdminUser adminUser) throws Exception{
String account = adminUser.getAccount();
if (!account.equals("admin")){
log.info("非法用户名!");
throw new IllegalArgumentException("非法用户名");
}
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("account", account);
AdminUser hasUser = adminUserMapper.selectOne(wrapper);
if (null != hasUser) {
throw new IllegalArgumentException("管理员账号已存在");
}
String password = adminUser.getPassword(); //前端传的加密后的代码
String publicKey = adminUser.getPublicKey(); //加密使用的公钥
String privateKey = stringRedisTemplate.opsForValue().get(publicKey); //从redis中取出对应的密钥
if (null == privateKey){
throw new IllegalArgumentException("安全校验失败!");
}
String resPassword = rsaConfig.decrypt(password, privateKey); //使用密钥解密得到真实的密码
String salt = randomConfig.randomData("", 6, false); //随机生成一个盐
String hashPassword = hashConfigWithSalt.HashWithSalt(resPassword, salt); //把密码加盐后hash加密
adminUser.setPassword(hashPassword); //把加密后的密码以及盐存入数据库
adminUser.setSalt(salt);
String id = UUID.randomUUID().toString();
adminUser.setId(id);
int addUser = adminUserMapper.insert(adminUser);
if (addUser == 0){
log.info("新增失败!");
throw new IllegalArgumentException("新增失败!");
}
log.info("新增成功!");
stringRedisTemplate.delete(publicKey);//新增成功后删除密钥对
return "新增成功";
}
在登录情况下进行验证:
/**
* @Author ZongMao
* @Description //登录(adminUser)
* @Date 2021/1/12 11:57
**/
@Override
public String login(String account, String password) throws Exception{
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("account", account);
AdminUser adminUser = adminUserMapper.selectOne(wrapper); //从数据库中查出对应的用户
if (null == adminUser){
throw new IllegalArgumentException("账号不存在!");
}
Date lockTime = adminUser.getLockTime();
if (null != lockTime){
lockAccount.checkLock(lockTime);
}
String salt = adminUser.getSalt(); //从数据库中得到盐
String okPassword = adminUser.getPassword(); //数据库中存储的加密的密码
boolean okLogin = hashConfigWithSalt.isCommon(okPassword, salt, password); //比较是否相等,password已经是解密了的
if (!okLogin){
boolean needLock = lockAccount.lockTime(account);
if (needLock){
Date date = new Date();
date.setTime(date.getTime() + needLockTime * 60 * 1000);
adminUser.setLockTime(date);
UpdateWrapper<AdminUser> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("account", account)
.set("lock_time", date);
int i = adminUserMapper.update(adminUser, wrapper);
if (i == 1){
throw new IllegalArgumentException("你的账号已被锁定,请在" + needLockTime + "分钟后重试");
}
throw new IllegalArgumentException("你已多次尝试登录失败,账号可能会被锁定");
}
throw new IllegalArgumentException("账号或密码错误!");
}
String userId = adminUser.getId();
String token = tokenConfig.createToken(userId);
return token;
}