使用 RSA 在 Web 前端加密传输至后台解密

在自定义登录过程中,用户名和密码通常是作为 form 表单中的 input 控件进行输入,然后明文传输到服务端进行校验登录的。

此时,可能会出现密码明文传输问题。如下图示例所示:

image-20201217163137695

通常跟服务器的交互中,为保障数据传输的安全性,避免被人抓包篡改数据,除了 https 的应用,还需要对传输数据进行加解密。

本文介绍一种 RSA 加密 web 前端用户名密码加密传输至后台并解密 的方法。

公共类 RSAUtils

可能需要引入 Jar 包:

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.67</version>
</dependency>

编写加解密公共方法类 RSAUtils

import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.security.*;
import java.security.interfaces.RSAPublicKey;

public class RSAUtils {
	private static final KeyPair keyPair = initKey();

	private static KeyPair initKey() {
		try {
			Provider provider = new org.bouncycastle.jce.provider.BouncyCastleProvider();
			Security.addProvider(provider);
			SecureRandom random = new SecureRandom();
			KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", provider);
			generator.initialize(1024, random);
			return generator.generateKeyPair();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static String generateBase64PublicKey() {
		PublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
		return new String(Base64.encodeBase64(publicKey.getEncoded()));
	}

	public static String decryptBase64(String string) {
		return new String(decrypt(Base64.decodeBase64(string.getBytes())));
	}

	private static byte[] decrypt(byte[] byteArray) {
		try {
			Provider provider = new org.bouncycastle.jce.provider.BouncyCastleProvider();
			Security.addProvider(provider);
			Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", provider);
			PrivateKey privateKey = keyPair.getPrivate();
			cipher.init(Cipher.DECRYPT_MODE, privateKey);
			byte[] plainText = cipher.doFinal(byteArray);
			return plainText;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
}

后台生成公钥方法

@RequestMapping(value = "getPublicKey")
public @ResponseBody String getPublicKey(HttpServletRequest request){
    String publicKey = RSAUtils.generateBase64PublicKey();
    return publicKey;
}

前端获取公钥

前端在向后台发起登录请求之前,先请求后台获取公钥的方法

var publicKey;
$.ajax({
    url: "getPublicKey",
    type: "get",
    async: false,
    dataType: "text",
    success: function(data) {
        if(data){
            publicKey = data;
        };
    }
});

前端引入加密依赖

前端引入 jsencrypt.min.js 文件:

<script src="/js/jsencrypt.min.js"></script>

npm 引入:

npm i jsencrypt

npm 使用:

import JsEncrypt from 'jsencrypt'

let encrypt = new JsEncrypt();
encrypt.setPublicKey(publicKey);

username = encrypt.encrypt(username.trim());
  • 官网:http://travistidwell.com/jsencrypt/
  • GitHub:https://github.com/travist/jsencrypt
  • npm:https://www.npmjs.com/package/jsencrypt

前端加密

通过公钥对用户名和密码加密

var encrypt = new JSEncrypt();
encrypt.setPublicKey(publicKey);
var loginaccount = $('#loginaccount').val();
var loginpassword = $('#loginpassword').val();
loginaccount = encrypt.encrypt(loginaccount.trim());
loginpassword = encrypt.encrypt(loginpassword.trim());

加密请求后台

方法一:ajax 请求

通过 ajax 用加密后的用户名密码请求后台

$.ajax({
	type: "POST",
	url: "xxxxxx",
	data: {
		"username":username,
		"password":password,
	},
	dataType: "json",
	success: function (result) {
		if (result.code == 0) {//登录成功
			parent.location.href = 'index.html';
		} else {
			vm.error = true;
			vm.errorMsg = result.msg;
			vm.refreshCode();
		}
	}
});

方法二:form 表单

使用 form 表单请求后台

<form method="post" action="login" id="loginForm" onsubmit="return checkLoginTask()">
	<input type="hidden" id="account" name="account" />
	<input type="hidden" id="password" name="password" />
</form>

<button onclick="$('#loginForm').submit()">登录</button>

其中 checkLoginTask 方法如下:

function checkLoginTask(){
	var publicKey;
	$.ajax({
        url: "getPublicKey",
        type: "get",
        async: false,
        dataType: "text",
        success: function(data) {
            if(data){
                publicKey = data;
			};
        }
    });
    if(publicKey == null){
    	return false;
    }
    var encrypt = new JSEncrypt();
    encrypt.setPublicKey(publicKey);
	var loginaccount = $('#loginaccount').val();
	var loginpassword = $('#loginpassword').val();
	loginaccount = encrypt.encrypt(loginaccount.trim());
	loginpassword = encrypt.encrypt(loginpassword.trim());
    $('#account').val(loginaccount);
    $('#password').val(loginpassword);
    return true;
}

方法三:axios 请求

const { data: publicKey } = await this.$http.get("/user/key");
var username = this.loginForm.username;
var password = this.loginForm.password;
let encrypt = new JsEncrypt();
encrypt.setPublicKey(publicKey);
username = encrypt.encrypt(username.trim());
password = encrypt.encrypt(password.trim());
console.log(username)
//发起登入请求
const { data: res } = await this.$http.post(
    "user/login?username="+encodeURIComponent(username)+"&password="+encodeURIComponent(password)
);

后端解密

String account = request.getParameter("account");
String password = request.getParameter("password");
if (StringUtils.isNotBlank(account)) {
    account = RSAUtils.decryptBase64(account.trim());
}
if (StringUtils.isNotBlank(password)) {
    password = RSAUtils.decryptBase64(password.trim());
}

最终效果

image-20201217164715019

从而可以避免出现密码明文传输问题。

测试代码

String key = HttpClient4.doGet("http://127.0.0.1:8181/user/key");
log.info(key);
Map<String, Object> paramMap = new HashMap<>();
try{
    paramMap.put("username", RSAUtils.encryptByPublicKey(key,"admin"));
    paramMap.put("password", RSAUtils.encryptByPublicKey(key,"123456"));
    String result = HttpClient4.doPost("http://127.0.0.1:8181/user/login", paramMap);
    log.info(result);
    JsonObject jsonObject = new JsonParser().parse(result).getAsJsonObject();
    if (jsonObject.get("code").getAsInt()==200){
        log.info(jsonObject.get("data").getAsString());
    }
} catch (Exception e){
    log.error(e.getMessage(),e);
}

其中:

/**
 * 公钥加密
 * @param publicKeyText 公钥
 * @param text 待加密的文本
 * @return /
 */
public static String encryptByPublicKey(String publicKeyText, String text) throws Exception {
	X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText));
	KeyFactory keyFactory = KeyFactory.getInstance("RSA");
	PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2);
	Cipher cipher = Cipher.getInstance("RSA");
	cipher.init(Cipher.ENCRYPT_MODE, publicKey);
	byte[] result = cipher.doFinal(text.getBytes());
	return Base64.encodeBase64String(result);
}

/**
 * 私钥解密
 * @param privateKeyText
 * @param text
 * @return
 * @throws Exception
 */
public static String decryptByPrivateKey(String privateKeyText, String text) throws Exception {
	PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText));
	KeyFactory keyFactory = KeyFactory.getInstance("RSA");
	PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5);
	Cipher cipher = Cipher.getInstance("RSA");
	cipher.init(Cipher.DECRYPT_MODE, privateKey);
	byte[] result = cipher.doFinal(Base64.decodeBase64(text));
	return new String(result);
}