现有比较熟知的加密方式有:MD5、对称加密(单密钥加密)、非对称加密(双密钥)。其中MD5使用最广泛,但是安全性最高的还是RSA。
MD5:可以将任意长度的输入串经过计算得到固定长度的输出,而且只有在明文相同的情况下,才能等到相同的密文,并且这个算法是不可逆的,即便得到了加密以后的密文,也不可能通过解密算法反算出明文。这样就可以把用户的密码以MD5值(或类似的其它算法)的方式保存起来,用户注册的时候,系统是把用户输入的密码计算成 MD5 值,然后再去和系统中保存的 MD5 值进行比较,如果密文相同,就可以认定密码是正确的,否则密码错误。通过这样的步骤,系统在并不知道用户密码明码的情况下就可以确定用户登录系统的合法性。
对称加密:双方拥有共同的密钥,一方使用密钥加密明文,另一方使用相同的密钥解密密文,缺点也很明显,只要一方泄露了密钥,对于数据都是不安全的。常用的算法有:DES、3DES、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK…。
RSA:生成两把密钥(公钥、私钥),一般情况下私钥留给自己,公钥作为提供 - 公钥加密数据成密文(每次加密的结果都不相同),密文只能用生成的对应的私钥解密。RSA算法的保密强度随其密钥的长度增加而增强。但是,密钥越长,其加解密所耗用的时间也越长。因此,要根据所保护信息的敏感程度与攻击者破解所要花费的代价值不值得以及系统所要求的反应时间来综合考虑。
这里我使用一个简单的登录、注册功能来介绍RSA的使用。
技术:spring boot+mybatis+Ajax
每次创建完一个项目需要测试一下,我一般习惯写一个“hello word”测试运行环境,这里我不做详细介绍,不知道怎么测试或者有兴趣的小伙伴可以看我以往有关spring boot的博客。
这是我的数据库表结构和yml配置
# 连接数据库
spring:
datasource:
username:
password:
url: jdbc:mysql://localhost:3306/cap?serverTimeZone=UTC&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.jdbc.Driver
# 清理thymeleaf缓存
thymeleaf:
cache: false
# 整合mybatis
mybatis:
type-aliases-package: com.desiy.entity
mapper-locations: classpath:mapper/**.xml
注意:不要把username和password写成data-username和data-password,不然会报错。
实体类Admin
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Admin {
private String rel_name;
private String username;
private String password;
}
dao层 - AdminDao
@Mapper
public interface AdminDao {
Admin selectAdmin(String username);
String selectPassword(String username);
String selectUsername(String username);
// 用户注册时需要
void add(String rel_name,String username,String password);
}
service层 - AdminService and AdminServiceImpl
public interface AdminService {
Admin login(String username, String password);
String selectPassword(String username);
String selectUsername(String username);
void add(String rel_name, String username, String password);
}
@Service
public class AdminServiceImpl implements AdminService {
@Autowired
AdminDao adminDao;
@Override
public Admin login(String username, String password) {
return adminDao.selectAdmin(username);
}
@Override
public String selectPassword(String username) {
return adminDao.selectPassword(username);
}
@Override
public String selectUsername(String username) {
return adminDao.selectUsername(username);
}
@Override
public void add(String rel_name, String username, String password) {
adminDao.add(rel_name, username, password);
}
}
AdminDao.xml
<mapper namespace="com.desiy.dao.AdminDao">
<select id="adminList" resultType="Admin">
select * from admin
select>
<select id="selectAdmin" resultType="Admin">
select * from admin where username = #{username}
select>
<select id="selectPassword" resultType="string">
select password from admin where username = #{username}
select>
<select id="selectUsername" resultType="string">
select username from admin where username = #{username}
select>
<insert id="add" parameterType="Admin">
insert into cap.admin(rel_name,username,password)
values (#{rel_name},#{username},#{password})
insert>
mapper>
导入相关依赖:
<dependency>
<groupId>commons-iogroupId>
<artifactId>commons-ioartifactId>
<version>2.4version>
dependency>
<dependency>
<groupId>commons-codecgroupId>
<artifactId>commons-codecartifactId>
<version>1.10version>
dependency>
前端 - jsencrypt.js
后端 - RSAUtils.java:
这里的jsencrypt.js我是用的是 jsencrypt的 cdn
获取公钥、私钥、以及公钥加密解密,私钥加密解密,分段加密、解密
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
public class RSAUtils {
RSA rsa = new RSA();
public static final String CHARSET = "UTF-8";
public static final String RSA_ALGORITHM = "RSA"; // ALGORITHM ['ælgərɪð(ə)m] 算法的意思
public static Map<String, String> createKeys(int keySize) {
// 为RSA算法创建一个KeyPairGenerator对象
KeyPairGenerator kpg;
try {
kpg = KeyPairGenerator.getInstance(RSA_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("No such algorithm-->[" + RSA_ALGORITHM + "]");
}
// 初始化KeyPairGenerator对象,密钥长度
kpg.initialize(keySize);
// 生成密匙对
KeyPair keyPair = kpg.generateKeyPair();
// 得到公钥
Key publicKey = keyPair.getPublic();
String publicKeyStr = Base64.encodeBase64String(publicKey.getEncoded());
// 得到私钥
Key privateKey = keyPair.getPrivate();
String privateKeyStr = Base64.encodeBase64String(privateKey.getEncoded());
// map装载公钥和私钥
Map<String, String> keyPairMap = new HashMap<String, String>();
keyPairMap.put("publicKey", publicKeyStr);
keyPairMap.put("privateKey", privateKeyStr);
// 返回map
return keyPairMap;
}
/**
* 得到公钥
*
* @param publicKey 密钥字符串(经过base64编码)
* @throws Exception
*/
public static RSAPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
// 通过X509编码的Key指令获得公钥对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
RSAPublicKey key = (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
return key;
}
/**
* 得到私钥
*
* @param privateKey 密钥字符串(经过base64编码)
* @throws Exception
*/
public static RSAPrivateKey getPrivateKey(String privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
// 通过PKCS#8编码的Key指令获得私钥对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
RSAPrivateKey key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
return key;
}
/**
* 公钥加密
*
* @param data
* @param publicKey
* @return
*/
public static String publicEncrypt(String data, RSAPublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return Base64.encodeBase64URLSafeString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET), publicKey.getModulus().bitLength()));
} catch (Exception e) {
throw new RuntimeException("加密字符串[" + data + "]时遇到异常", e);
}
}
/**
* 私钥解密
*
* @param data
* @param privateKey
* @return
*/
public static String privateDecrypt(String data, RSAPrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data), privateKey.getModulus().bitLength()), CHARSET);
} catch (Exception e) {
throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e);
}
}
/**
* 私钥加密
*
* @param data
* @param privateKey
* @return
*/
public static String privateEncrypt(String data, RSAPrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
//每个Cipher初始化方法使用一个模式参数opmod,并用此模式初始化Cipher对象。此外还有其他参数,包括密钥key、包含密钥的证书certificate、算法参数params和随机源random。
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return Base64.encodeBase64URLSafeString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET), privateKey.getModulus().bitLength()));
} catch (Exception e) {
throw new RuntimeException("加密字符串[" + data + "]时遇到异常", e);
}
}
/**
* 公钥解密
*
* @param data
* @param publicKey
* @return
*/
public static String publicDecrypt(String data, RSAPublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data), publicKey.getModulus().bitLength()), CHARSET);
} catch (Exception e) {
throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e);
}
}
//rsa切割解码 , ENCRYPT_MODE,加密数据 ,DECRYPT_MODE,解密数据
private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] datas, int keySize) {
int maxBlock = 0; //最大块
if (opmode == Cipher.DECRYPT_MODE) {
maxBlock = keySize / 8;
} else {
maxBlock = keySize / 8 - 11;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] buff;
int i = 0;
try {
while (datas.length > offSet) {
if (datas.length - offSet > maxBlock) {
//可以调用以下的doFinal()方法完成加密或解密数据:
buff = cipher.doFinal(datas, offSet, maxBlock);
} else {
buff = cipher.doFinal(datas, offSet, datas.length - offSet);
}
out.write(buff, 0, buff.length);
i++;
offSet = i * maxBlock;
}
} catch (Exception e) {
throw new RuntimeException("加解密阀值为[" + maxBlock + "]的数据时发生异常", e);
}
byte[] resultDatas = out.toByteArray();
IOUtils.closeQuietly(out);
return resultDatas;
}
public static void main(String[] args) {
// 创建密钥对
Map<String, String> keys = RSAUtils.createKeys(1024);
// 从Map中获取密钥对
String publicKey = keys.get("publicKey");
String privateKey = keys.get("privateKey");
// 获取公钥
System.out.println("publicKey:"+publicKey);
// 获取私钥
System.out.println("privateKey:"+privateKey);
}
}
由上面的RSAUtils先获得公钥和私钥:
接着我创建 一个RSA类,放我们的密钥对。
首页代码:index.html
<html lang="en" xmlns:th="http://thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<form id="doLogin">
<input type="hidden" th:value="${session.publicKey}" name="publicKey" id="publicKey">
<input type="text" name="username" required="" autofocus="">
<input type="password" name="password" required="">
<button type="button" id="bt">登录button>
<a th:href="@{/AddPage}">注册a>
form>
body>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js">script>
<script src="https://cdn.bootcss.com/jsencrypt/3.0.0-beta.1/jsencrypt.js">script>
<link href="https://cdn.bootcdn.net/ajax/libs/limonte-sweetalert2/0.0.1/sweetalert2.css" rel="stylesheet">
<script src="https://cdn.bootcdn.net/ajax/libs/limonte-sweetalert2/0.0.1/sweetalert2.min.js">script>
<script type="text/javascript">
$("#bt").click(function () {
let data = $("#doLogin").serializeArray();
let publicKey = data[0].value;
let username = data[1].value;
let oldPwd = data[2].value;
let encrypt_Pwd = encrypt(publicKey, oldPwd);
let data1 = {"username": username, "encrypt_Pwd": encrypt_Pwd};
$.ajax({
url: '/user/login',
type: 'POST',
data: data1,
dataType: 'json',
success: function (res) {
// 一旦设置的 dataType 选项,就不再关心 服务端 响应的 Content-Type 了
// 客户端会主观认为服务端返回的就是 JSON 格式的字符串
if (res.code == 200) {
swal({
title: "登录成功!",
type: "success",
closeOnConfirm: false,
}, function () {
window.location = "/go";
});
}
if (res.code == 101) {
swal({
title: "登录失败。",
text: "输入信息有误。",
type: "error",
confirmButtonText: "重新登录",
});
}
},
error: function () {
swal({
title: "登录失败",
text: "网络异常",
type: "error",
confirmButtonText: "重新登录",
});
}
});
});
// RSA前端加密
function encrypt(key, oldPwd) {
let encrypt = new JSEncrypt();
encrypt.setPublicKey(key);
let encrypted = encrypt.encrypt(oldPwd);
return encrypted;
}
script>
html>
首页:
<html lang="en" xmlns:th="http://thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>注册title>
head>
<body>
<form id="doRegister">
<input type="hidden" th:value="${session.publicKey}" name="publicKey" id="publicKey">
<div>
<label>真实姓名label>
<input type="text" name="name" >
div>
<div>
<label>用户名label>
<input type="text" name="username" >
div>
<div>
<label>密码label>
<input type="password" name="password" >
div>
<button type="button" id="register">注册button>
form>
body>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js">script>
<script src="https://cdn.bootcss.com/jsencrypt/3.0.0-beta.1/jsencrypt.js">script>
<link href="https://cdn.bootcdn.net/ajax/libs/limonte-sweetalert2/0.0.1/sweetalert2.css" rel="stylesheet">
<script src="https://cdn.bootcdn.net/ajax/libs/limonte-sweetalert2/0.0.1/sweetalert2.min.js">script>
<script type="text/javascript">
$("#register").click(function () {
let data = $("#doRegister").serializeArray();
console.log(data);
let publicKey = data[0].value;
let rel_name = data[1].value;
let username = data[2].value;
let old_Pwd = data[3].value;
let encrypt_Pwd = encrypt(publicKey, old_Pwd);
let rea_data = {"rel_name": rel_name, "username": username, "encrypt_Pwd": encrypt_Pwd};
$.ajax({
url: '/doAdd',
type: 'post',
dataType: 'json',
encoding: 'UTF-8',
data: rea_data,
success: function (res) {
// 一旦设置的 dataType 选项,就不再关心 服务端 响应的 Content-Type 了
// 客户端会主观认为服务端返回的就是 JSON 格式的字符串
console.log(res);
if (res.code == 20) {
swal({
title: "注册成功!",
text: "",
type: "success",
confirmButtonText: "立即登录",
closeOnConfirm: false,
}, function () {
window.location = "/";
})
}
if (res.code == 10) {
swal({
title: "注册失败。",
text: "该用户名已被注册,请重新输入。",
type: "error",
confirmButtonText: "确认",
});
}
return false;
},
error: function () {
alert("服务器忙碌...");
}
});
});
// RSA前端加密
function encrypt(key, oldPwd) {
let encrypt = new JSEncrypt();
encrypt.setPublicKey(key);
let encrypted = encrypt.encrypt(oldPwd);
return encrypted;
}
script>
html>
工具类 - MsgData.java
public class MsgData<T> {
private Integer code;
private String msg;
private T data;
public MsgData() {}
public MsgData(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public MsgData(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@Override
public String toString() {
return "MsgData{" +
"code=" + code +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
}
controller层 - AdminController.java
import com.desiy.service.AdminService;
import com.desiy.utils.MsgData;
import com.desiy.utils.RSA;
import com.desiy.utils.RSAUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpSession;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
@Controller
public class AdminController {
RSA rsa = new RSA();
@Autowired
AdminService adminService;
@GetMapping("/")
public String defaultIndex(HttpSession session) {
session.setAttribute("publicKey", rsa.publicKey);
return "index";
}
/**
* 跳转到首页(登录页面)
*
* @param
* @return
*/
@RequestMapping("/user/login")
@ResponseBody
public MsgData loginPage(String username,
String encrypt_Pwd) throws InvalidKeySpecException, NoSuchAlgorithmException {
MsgData msgData = new MsgData<>();
// 后端获取私钥
String privateKey = rsa.privateKey;
// 获得RSA类型的私钥
RSAPrivateKey rsaPrivateKey = RSAUtils.getPrivateKey(privateKey);
// 从数据库中获取密文
String select_Password = adminService.selectPassword(username);
// 使用私钥解密select_Password
String Decrypt_database = RSAUtils.privateDecrypt(select_Password, rsaPrivateKey);
// 使用私钥解密经过前端加密用户输入的密文
String Decrypt_web = RSAUtils.privateDecrypt(encrypt_Pwd, rsaPrivateKey);
if (Decrypt_database.equals(Decrypt_web)) {
msgData.setCode(200);
msgData.setMsg("success");
return msgData;
} else {
msgData.setCode(101);
msgData.setMsg("fails");
return msgData;
}
}
@GetMapping("/go")
public String loginPage() {
return "success";
}
//进入注册页面
@GetMapping("/AddPage")
public String addPage() {
return "admin/add";
}
//注册功能
@RequestMapping("/doAdd")
@ResponseBody
public MsgData add(String rel_name, String username, String encrypt_Pwd) {
MsgData msgData = new MsgData<>();
// 注册:真实姓名和密码可以重复,但是用户名不行。
String select_username = adminService.selectUsername(username);
// null:表示未能从数据库中找到与username值一样的数据
if (select_username == null) {
adminService.add(rel_name, username, encrypt_Pwd);
msgData.setCode(20);
} else {
msgData.setCode(10);
}
return msgData;
}
}
进入首页,首先存入publicKey到session中,用户进入前端后,获取session中的key;
点击注册(我原先在数据库已经存放了一个username为xiaozhu的用户。)
注册:
用户在前端输入真实姓名、用户名、密码(明文)进行注册,经过jsencrypt.js将密码加密成密文。通过Ajax传送给后端,那么数据库中存储的就是用户输入姓名、用户名、密文。注册我们需要注意的是用户名重复的问题;我的想法是用户名和密码可以相同,但是用户名不能相同。sql语句中的selectPassword是根据username查询的。
登录:由上面的结果看出,登录时,前端只给后端传递了username和密文。后端如何判断?由于每次使用公钥加密同一明文的结果都不一样,也就是说,假设明文如果是1,第一次加密后密文是A,第二次加密1后的密文就不再是A。这里我用私钥解密密文(数据库中的密文以及用户登录时前端输入经加密后的密文),解密后再进行判断。