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登录表单加密_第1张图片

如何产生密钥对,在文章: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 keyPair = new HashMap();

        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模板为:


<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 = $("
) var $inputUser = $("") $inputUser.val(username) var $inputPwd = $("") $inputPwd.val(pwdEncrypt) var $key = $(""); $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 keyMap = (Map) 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)