使用非对称加密(RSA)实现前端加密后端解密。

1、前言

现有比较熟知的加密方式有:MD5、对称加密(单密钥加密)、非对称加密(双密钥)。其中MD5使用最广泛,但是安全性最高的还是RSA。
MD5:可以将任意长度的输入串经过计算得到固定长度的输出,而且只有在明文相同的情况下,才能等到相同的密文,并且这个算法是不可逆的,即便得到了加密以后的密文,也不可能通过解密算法反算出明文。这样就可以把用户的密码以MD5值(或类似的其它算法)的方式保存起来,用户注册的时候,系统是把用户输入的密码计算成 MD5 值,然后再去和系统中保存的 MD5 值进行比较,如果密文相同,就可以认定密码是正确的,否则密码错误。通过这样的步骤,系统在并不知道用户密码明码的情况下就可以确定用户登录系统的合法性。
对称加密:双方拥有共同的密钥,一方使用密钥加密明文,另一方使用相同的密钥解密密文,缺点也很明显,只要一方泄露了密钥,对于数据都是不安全的。常用的算法有:DES、3DES、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK…。
RSA:生成两把密钥(公钥、私钥),一般情况下私钥留给自己,公钥作为提供 - 公钥加密数据成密文(每次加密的结果都不相同),密文只能用生成的对应的私钥解密。RSA算法的保密强度随其密钥的长度增加而增强。但是,密钥越长,其加解密所耗用的时间也越长。因此,要根据所保护信息的敏感程度与攻击者破解所要花费的代价值不值得以及系统所要求的反应时间来综合考虑。

2、例子 - 实现简单的登录和注册功能

这里我使用一个简单的登录、注册功能来介绍RSA的使用。
技术:spring boot+mybatis+Ajax

2.1、创建spring项目

在这里插入图片描述
每次创建完一个项目需要测试一下,我一般习惯写一个“hello word”测试运行环境,这里我不做详细介绍,不知道怎么测试或者有兴趣的小伙伴可以看我以往有关spring boot的博客

2.2、dao层、service层、entity层、mapper.xml文件(熟悉一般业务的可以跳过)

这是我的数据库表结构和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

注意:不要把usernamepassword写成data-usernamedata-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

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<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>

2.3、RSA关键代码

导入相关依赖:

<!--下面两个是RSA需要的依赖-->
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
     <groupId>commons-io</groupId>
     <artifactId>commons-io</artifactId>
     <version>2.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
     <groupId>commons-codec</groupId>
     <artifactId>commons-codec</artifactId>
     <version>1.10</version>
</dependency>

前端 - jsencrypt.js
后端 - RSAUtils.java:

这里的jsencrypt.js我是用的是 jsencrypt的 cdn

<script src="https://cdn.bootcss.com/jsencrypt/3.0.0-beta.1/jsencrypt.js"></script>

获取公钥、私钥、以及公钥加密解密,私钥加密解密,分段加密、解密

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

<!DOCTYPE html>
<html lang="en" xmlns:th="http://thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form id="doLogin">
    <!--页面放置一个隐藏的input标签,用于存放公钥-->
    <input type="hidden" th:value="${session.publicKey}" name="publicKey" id="publicKey">

    <!--用户名;autofocus=""--页面加载时有光标-->
    <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>
<!--jquery cdn-->
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<!--引入jsencrypt.js  cdn-->
<script src="https://cdn.bootcss.com/jsencrypt/3.0.0-beta.1/jsencrypt.js"></script>
<!--SweetAlert-->
<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>

首页:

在这里插入图片描述
注册页代码:add.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>注册</title>
</head>
<body>
<form id="doRegister">
    <!--页面放置一个隐藏的input标签,用于存放公钥-->
    <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>
<!--jquery-->
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<!--JSEncrypt-->
<script src="https://cdn.bootcss.com/jsencrypt/3.0.0-beta.1/jsencrypt.js"></script>
<!--SweetAlert-->
<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;
    }
}

2.4、运行结果

进入首页,首先存入publicKey到session中,用户进入前端后,获取session中的key;
点击注册(我原先在数据库已经存放了一个username为xiaozhu的用户。)
在这里插入图片描述

在这里插入图片描述
所以,我们另想一个用户名:desiy,密码:123456

在这里插入图片描述
使用新注册的username和password进行登录

在这里插入图片描述
这是我的项目结构:
在这里插入图片描述

3、总结

注册:
用户在前端输入真实姓名、用户名、密码(明文)进行注册,经过jsencrypt.js将密码加密成密文。通过Ajax传送给后端,那么数据库中存储的就是用户输入姓名、用户名、密文。注册我们需要注意的是用户名重复的问题;我的想法是用户名和密码可以相同,但是用户名不能相同。sql语句中的selectPassword是根据username查询的。
登录:由上面的结果看出,登录时,前端只给后端传递了username和密文。后端如何判断?由于每次使用公钥加密同一明文的结果都不一样,也就是说,假设明文如果是1,第一次加密后密文是A,第二次加密1后的密文就不再是A。这里我用私钥解密密文(数据库中的密文以及用户登录时前端输入经加密后的密文),解密后再进行判断。

  • 10
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值