Java安全系列-RSA登录表单加密

Posted Zip Zou

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java安全系列-RSA登录表单加密相关的知识,希望对你有一定的参考价值。

Java安全系列-RSA登录表单加密

在Java Web开发项目中,经常会接触到有关于登录问题。在一般的开发过程中,由于没有申请CA证书,我们只能基于HTTP进行开发,然而对于Http数据连接而言,请求数据在进行传输时,采用的为明文不加密的方式进行传输,这便对数据安全造成了很大的威胁。

同样的,由于是基于Http进行构建,并且未对表单数据进行处理,数据出了被监听而被窃取外,还可以通过使用抓包分析的方式,将表单所提交的内容进行获取,从而窃取用户的信息,包括登录表单中所含的密码等信息。

而与Http相对而言,Https连接则更为安全,Https在Http连接的基础上,增加了SSL的特性,从而使得数据在传输过程中,经过加密传输,保证了数据安全。

为此我们可以基于Https的思想,在像服务器传输数据时,采用相类似的方式进行数据加密,则可以很大程度上的保证数据安全,并且防止抓包工具对数据进行获取。

项目构建

在该项目中,我们采用Spring Boot微框架进行搭建,从而减少配置文件的编写,若采用JSP+Servlet或SSM或SSH等框架搭建,也同样适用。

该项目依赖于其父项目:RSAEncrypt进行依赖,请安装好该项目后再进行该项目的构建!

该项目的基本原理为:

  1. 客户端发送GET请求,以访问登录页面;
  2. 服务器接收到该请求后,利用RSA技术,协商产生密钥对,用于加密及解密;
  3. 服务器将产生的公钥经由Base64编码后,传入页面进行渲染,并且将公钥的公共指数值16进制字符串传入页面待渲染,并且传入该公钥的modulus的Base64编码,该密钥对存储到服务器的session中;
  4. 浏览器根据服务器所设定的属性进行渲染,用户填写好信息,并请求提交
  5. 浏览器在前端,获取到公钥的modulus的Base64编码,并解码为16进制串,获取到公钥对应的公共指数,利用rsa进行密码加密;
  6. 将加密的数据重新设置为表单值,进行提交;
  7. 服务器接收到该表单,解析,并获取session中所存储的密钥对,对密文进行解密,并验证数据,回传结果即可。

以上为整个项目的基本流程。

如何产生密钥对,在文章:Java安全系列-RSA加密 已有说明,不做重复介绍。

在Spring中,我们定义一个PageController,用来响应用户页面的请求:

package site.franksite.encrypt.controller;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.interfaces.RSAPublicKey;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpSession;

import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import site.franksite.encrpt.rsaencrypt.KeyManager;
import site.franksite.encrpt.rsaencrypt.RSAKeyGenerator;
import site.franksite.encrpt.rsaencrypt.RSAValidator;

@Controller
public class PageController 

    @GetMapping("/")
    public String indexPage(Model mdl, HttpSession session) 

        RSAKeyGenerator generator = new RSAKeyGenerator();
        byte[] privateKeyEncoded = generator.getPrivateKeyEncoded();
        byte[] publicKeyEncoded = generator.getPublicKeyEncoded();

        KeyManager keyMan = new RSAValidator();
        RSAPublicKey rsaPubKey = (RSAPublicKey) keyMan.restorePublicKey(Base64.decodeBase64(publicKeyEncoded));
        BigInteger publicExponent = rsaPubKey.getPublicExponent();
        BigInteger modulus = rsaPubKey.getModulus();

        Map<String, String> keyPair = new HashMap<String, String>();

        Object obj = session.getAttribute("keys"); // 原始数据
        if (null != obj) 
            // 移除原始session
            session.removeAttribute("keys");
        

        session.setAttribute("keys", keyPair);
        try 
            String pubKeyStr = new String(publicKeyEncoded, "utf-8");
            String priKeyStr = new String(privateKeyEncoded, "utf-8");

            System.out.println(pubKeyStr);
            System.out.println(priKeyStr);

            keyPair.put(pubKeyStr, priKeyStr);
            mdl.addAttribute("pubKey", pubKeyStr);
            mdl.addAttribute("modulus", modulus.toString(16));
            mdl.addAttribute("pubExep", publicExponent.toString(16));
         catch (UnsupportedEncodingException e) 
            e.printStackTrace();
        


        return "login";
    


如此,在系统启动后,将会响应根目录的请求,直接跳转为登录页面。登录页面的html模板为:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<script type="text/javascript" th:src="@/js/jquery-2.1.4.min.js"></script>
<script type="text/javascript" th:src="@/js/jsbn.js"></script>
<script type="text/javascript" th:src="@/js/prng4.js"></script>
<script type="text/javascript" th:src="@/js/rng.js"></script>
<script type="text/javascript" th:src="@/js/jsbn2.js"></script>
<script type="text/javascript" th:src="@/js/base64.js"></script>
<script type="text/javascript" th:src="@/js/rsa.js"></script>
<script type="text/javascript" th:src="@/js/rsa2.js"></script>
<title>登录</title>
</head>
<body>
<form id="login-form" name="login" onsubmit="return false;">
<fieldset>

<input type="hidden" name="pubKey" id="pubKey" th:value="$pubKey" value="keys">

<input type="hidden" name="pubExep" id="pubExep" th:value="$pubExep">

<input type="hidden" name="modulus" id="modulus" th:value="$modulus" value="modulus">

<label for="username">用户名:</label><input id="username" name="username" placeholder="username">

<br>

<label for="password">密码:</label><input id="password" name="password" type="password" placeholder="password">

<br>
<input type="submit" value="登录">
</fieldset>
</form>

<script type="text/javascript">
$(document).ready(function() 
    $('#login-form').on('submit', function() 

        var pubKey = $('#pubKey').val()

        if (null != pubKey) 
            pubKey = pubKey.trim();
        

        var username = $('#username').val()

        var modulus = $('#modulus').val()

        var e = $('#pubExep').val()

        if (null != username) 
            username = username.trim()
        

        if (null != password) 
            password = password.trim()
        

        if (null != modulus) 
            modulus = modulus.trim(); // 模,BigInteger
        

        var password = $('#password').val()

        // 启用RSA加密
        var rsa = new RSAKey()
        rsa.setPublic(modulus, e)

        var pwdEncrypt = rsa.encrypt(password) // 密文

        pwdEncrypt=hex2b64(pwdEncrypt)

        var $form = $("<form></form")

        var $inputUser = $("<input name=\\"username\\">")

        $inputUser.val(username)

        var $inputPwd = $("<input name=\\"password\\">")
        $inputPwd.val(pwdEncrypt)

        var $key = $("<input name=\\"pubKey\\">");
        $key.val(pubKey)

        $form.append($inputUser)
        $form.append($inputPwd)
        $form.append($key)

        $form.attr('action', "/login")
        $form.attr('method', 'post')

        $('body').append($form)

        $form.submit()

        return false;
    )
)
</script>
</body>
</html>

从代码中,我们有使用thymeleaf进行模板的渲染,在js中,我们引用了如下的js进行前端的加密和编解码:

  • jquery-2.1.4.min.js
  • jsbn.js
  • prng4.js
  • rng.js
  • jsbn2.js
  • base64.js
  • rsa.js
  • rsa2.js

上述的js文件,为必须的文件,他们相互依赖,并且由于依赖关系,必须保证如上的顺序关系,其中rsa依赖于jsbn(BigInteger),base64依赖于rng,rng依赖于prng4。

为此,前端界面我们将获得到公钥的有关信息。

如图中,我们将由Base64编码的公钥字符串渲染到前端,并渲染了一个指数为10001,并且modulus也渲染到了表单中。

在用户选择提交后,前端将会利用上述信息,对其进行RSA加密,并发送到服务器后端,后端将对其进行验证:

package site.franksite.encrypt.controller;

import java.io.UnsupportedEncodingException;
import java.util.Map;

import javax.servlet.http.HttpSession;

import org.apache.commons.codec.binary.Base64;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import site.franksite.encrpt.rsaencrypt.RSAEncrypt;
import site.franksite.encrpt.rsaencrypt.RSAValidator;
import site.franksite.encrypt.entity.ResultEntity;

@Controller
public class LoginController 

    @PostMapping("/login")
    public @ResponseBody ResultEntity login(@RequestParam("username") String username, 
            @RequestParam("password") String passwordBased64, HttpSession session, @RequestParam("pubKey") String pubKey) 

        ResultEntity result = new ResultEntity();

        Object keys = session.getAttribute("keys");

        if (null == keys) 
            result.setStatus(false);
            result.setReason("该公钥已经失效!");
            result.setData(pubKey);
         else 
            @SuppressWarnings("unchecked")
            Map<String, String> keyMap = (Map<String, String>) keys;

            String priKey = keyMap.get(pubKey);

            RSAEncrypt validator = new RSAValidator();
            byte[] dencrypt = validator.dencrypt(Base64.decodeBase64(priKey), Base64.decodeBase64(passwordBased64));
            try 
                System.out.println(new String(dencrypt, "utf-8"));

                if (new String(dencrypt, "utf-8").equals("1234")) 
                    result.setStatus(true);
                    result.setData(username);
                 else 
                    result.setStatus(false);
                    result.setReason("密码错误!");
                    result.setData(username);
                

             catch (UnsupportedEncodingException e) 
                // TODO Auto-generated catch block
                e.printStackTrace();
            
        

        session.removeAttribute("keys"); // 移除session

        return result;

    


在这里,我们返回了一个Json实体,该实体包含属性为3个,分别为:状态,原因,数据。

实体如下:

package site.franksite.encrypt.entity;

public class ResultEntity 

    private boolean status;

    private String reason;

    private Object data;

    /**
     * @return the status
     */
    public boolean isStatus() 
        return status;
    

    /**
     * @param status the status to set
     */
    public void setStatus(boolean status) 
        this.status = status;
    

    /**
     * @return the reason
     */
    public String getReason() 
        return reason;
    

    /**
     * @param reason the reason to set
     */
    public void setReason(String reason) 
        this.reason = reason;
    

    /**
     * @return the data
     */
    public Object getData() 
        return data;
    

    /**
     * @param data the data to set
     */
    public void setData(Object data) 
        this.data = data;
    

    public ResultEntity() 
        super();
        // TODO Auto-generated constructor stub
    

    public ResultEntity(boolean status, String reason, Object data) 
        super();
        this.status = status;
        this.reason = reason;
        this.data = data;
    


在进行提交后,可以看到,表单提交了一系列加密的数据信息

验证成功后,将在浏览器中直接解析出json字符串:


  "status": true,
  "reason": null,
  "data": "hello"

项目获取

github项目地址:RSALoginEncrypt

以上是关于Java安全系列-RSA登录表单加密的主要内容,如果未能解决你的问题,请参考以下文章

如何实现用javascript实现rsa加解密

Java RSA为啥每次密文都不一样?

Java 进行 RSA 加解密时不得不考虑到的那些事儿

登录页面之RSA加密

SA密钥长度明文长度和密文长度

Android Okhttp/Retrofit网络请求加解密实现方案