java结合node.js非对称加密,实现密文登录传参——让前后端分离的项目更安全

前言


 

  在参考互联网大厂的登录、订单、提现这类对安全性操作要求较高的场景操作时发现,传输的都是密文。而为了目前项目安全,我自己负责的项目也需要这方面的技术。由于,我当前的项目是使用了前后端分离技术,即node.js做前端,spring boot做后端。于是,我开始搜索有关node.js与java实现非对称加密的资料,然而,我却没有得到一个满意的答案。因此,我有了写本篇博客的想法,并希望给用到这类技术的朋友提供帮助。

 

一、明文密码传输对比


 

首先、 构建spring boot 2.0项目

 

引入web依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

  

App启动类

 

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

 

控制器类,编写两个方法:

1.模拟获取登录后的用户信息

2.明文登录方法

 

/**
 * java与node.js非对称加密
 * 
 * 出自:http://www.cnblogs.com/goodhelper
 * 
 * @author 刘冬
 *
 */
@RestController
public class MainController {

    /**
     * 存储用户信息
     */
    private Map<String, String> users = new ConcurrentHashMap<>();

    @GetMapping("getUser")
    public String getUser(@RequestHeader(value = "Authorization", required = false) String token) {
        if (token == null) {
            return null;
        }
        return users.containsKey(token) ? users.get(token) : null;
    }

    @PostMapping("login")
    public Map<String, Object> login(@RequestBody Map<String, String> params) {
        Map<String, Object> result = new HashMap<>();
        if (!params.containsKey("account") || !params.containsKey("password")) {
            result.put("success", false);
            result.put("message", "请输入账号和密码");
            return result;
        }
        if (!"123456".equals(params.get("password"))) {
            result.put("success", false);
            result.put("message", "密码错误");
            return result;
        }

        String token = UUID.randomUUID().toString();
        users.put(token, params.get("account"));

        result.put("success", true);
        result.put("message", "登录成功");
        result.put("data", token);
        return result;
    }
}

 

其次、使用vue脚手架构建项目

 

安装依赖 

 

vue init webpack demo-rsa
npm install
npm install --save axios

  

main.js入口

 

import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

import axios from 'axios'
Vue.prototype.$axios = axios
axios.defaults.baseURL = '/api'

new Vue({
  el: '#app',
  router,
  components: {
    App
  },
  template: '<App/>'
})
main.js

 

路由的钩子函数

 

import Vue from 'vue'
import Router from 'vue-router'
import Main from '@/components/Main'
import Login from '@/components/Login'

Vue.use(Router)
let routes = [{
    path: '/',
    name: '首页',
    component: Main
  },
  {
    path: '/login',
    name: '登录',
    component: Login
  }
]

const router = new Router({
  routes: routes
})

router.beforeEach((to, from, next) => {
  if (to.path == '/login') {
    sessionStorage.removeItem('Authorization')
  }

  let token = sessionStorage.getItem('Authorization')
  if (!token && to.path != '/login') {
    next({
      path: '/login'
    })
    return
  }
  next()
})

export default router;
router/index.js

 

登录后的页面

 

<template>
<div class="hello">
  当前用户:{{user}}
</div>
</template>

<script>
export default {
  data() {
    return {
      user: null
    }
  },
  mounted() {
    this.$axios.get('/getUser').then(res => {
      this.user = res.data
    })
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>

</style>
components/Main.vue

 

登录页面

 

<template>
<div class="hello">
  <table>
    <tr>
      <td>
        用户名:
      </td>
      <td>
        <input type="text" v-model="form.account" />
      </td>
    </tr>
    <tr>
      <td>
        密码:
      </td>
      <td>
        <input type="password" v-model="form.password" />
      </td>
    </tr>
    <tr>
      <td>
        <input type="button" value="登录" @click="login" />
      </td>
      <td>
        <font v-if="message">{{message}}</font>
      </td>
    </tr>
  </table>
</div>
</template>

<script>

export default {
  data() {
    return {
      message: null,
      form: {
        account: null,
        password: null
      }
    }
  },
  methods: {
    //明文登录
    login() {
      this.message = null
      this.$axios.post('/login', this.form).then(res => {
        if (!res.data.success) {
          this.message = res.data.message
          return
        }
        let token = res.data.data
        sessionStorage.setItem('Authorization', token)
        this.$axios.defaults.headers.common['Authorization'] = token
        this.$router.push({
          path: '/'
        });
      })
    }
  }
}
</script>

<style scoped>

</style>

 

设置开发模式反向代理 ,便于js跨域

 

 proxyTable: {
      '/api': {
        target: 'http://localhost:8080/',
        changeOrigin: true,
        pathRewrite: {
          '^/api': '/'
        }
      }
    }

 

输入用户名和密码

 

 观察得知,传递的密码是明文

如果打开浏览器的调试模式,就能看到输入的密码。这无疑会导致系统的不安全。并且,在非https协议下,传输的密码也有被截取风险。 

 

二、实现密文登录


 

思路是:

  首先、java后端生成公私钥对。node.js前端调用获取公钥的方法,然后对密码进行公钥加密,再把加密过的密文发送到java后端。最后,java后端用私钥对密文解密。

俗称:公钥加密,私钥加密。而这就是非对称加密的流程。

 

在spring boot项目中的pom.xml引入bouncycastle

 

        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcpkix-jdk15on</artifactId>
            <version>1.60</version>
        </dependency>

 

完整的pom.xml文件如下:

 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.demo</groupId>
    <artifactId>rsa</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>rsa</name>
    <description>java与node.js非对称加密</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- bouncycastle -->
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcpkix-jdk15on</artifactId>
            <version>1.60</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>
pom.xml

 

在MainController类中增加如下代码: 

1.生成公私钥对,模拟session实现存储私钥,返回公钥

2.实现私钥解密

 

    /**
     * 存储session私钥
     */
    private Map<String, String> session = new ConcurrentHashMap<>();

    static {
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
    }


/**
     * 获取session公钥
     * 
     * @return
     */
    @GetMapping("getSession")
    public Map<String, String> getSession() throws Exception {
        String sessionId = UUID.randomUUID().toString();
        Map<String, String> result = new HashMap<>();
        result.put("sessionId", sessionId);

        String algorithm = "RSA";
        String privateKey = null, publicKey = null;

        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(algorithm);
        keyPairGen.initialize(512);
        KeyPair keyPair = keyPairGen.generateKeyPair();

        byte[] encoded = keyPair.getPrivate().getEncoded();
        PrivateKeyInfo pkInfo = PrivateKeyInfo.getInstance(encoded);
        ASN1Encodable encodable = pkInfo.parsePrivateKey();
        ASN1Primitive primitive = encodable.toASN1Primitive();
        byte[] privateKeyPKCS1 = primitive.getEncoded();
        PemObject pemObject = new PemObject("RSA PRIVATE KEY", privateKeyPKCS1);
        try (StringWriter stringWriter = new StringWriter()) {
            try (PemWriter pemWriter = new PemWriter(stringWriter)) {
                pemWriter.writeObject(pemObject);
                pemWriter.flush();
                String pemString = stringWriter.toString();
                privateKey = pemString;
            }
        }

        encoded = keyPair.getPublic().getEncoded();
        SubjectPublicKeyInfo spkInfo = SubjectPublicKeyInfo.getInstance(encoded);
        primitive = spkInfo.parsePublicKey();
        byte[] publicKeyPKCS1 = primitive.getEncoded();

        pemObject = new PemObject("RSA PUBLIC KEY", publicKeyPKCS1);
        try (StringWriter stringWriter = new StringWriter()) {
            try (PemWriter pemWriter = new PemWriter(stringWriter)) {
                pemWriter.writeObject(pemObject);
                pemWriter.flush();
                String pemString = stringWriter.toString();
                publicKey = pemString;
            }
        }

        // 记录私钥
        session.put(sessionId, privateKey);
        // 返回公钥
        result.put("publicKey", publicKey);

        return result;
    }

    @SuppressWarnings("unchecked")
    @PostMapping("loginByEncrypt")
    public Map<String, Object> loginByEncrypt(@RequestBody Map<String, String> params) {
        Map<String, Object> result = new HashMap<>();

        if (!params.containsKey("sessionId")) {
            result.put("success", false);
            result.put("message", "sessionId是必填参数");
            return result;
        }

        if (!params.containsKey("playload")) {
            result.put("success", false);
            result.put("message", "playload是必填参数");
            return result;
        }

        String sessionId = params.get("sessionId");

        if (!session.containsKey(sessionId)) {
            result.put("success", false);
            result.put("message", "无效session");
            return result;
        }

        Map<String, String> json = null;
        try {
            String privateKey = session.get(sessionId);
            String playload = params.get("playload");
            String text = decrypt(playload, privateKey);

            ObjectMapper mapper = new ObjectMapper();
            json = mapper.readValue(text, Map.class);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (json == null) {
            result.put("success", false);
            result.put("message", "非法请求");
            return result;
        }

        if (!json.containsKey("account") || !json.containsKey("password")) {
            result.put("success", false);
            result.put("message", "请输入账号和密码");
            return result;
        }
        if (!"123456".equals(json.get("password"))) {
            result.put("success", false);
            result.put("message", "密码错误");
            return result;
        }

        String token = UUID.randomUUID().toString();
        users.put(token, json.get("account"));

        result.put("success", true);
        result.put("message", "登录成功");
        result.put("data", token);
        return result;
    }

    /**
     * 私钥解密
     * 
     * @param encode
     * @param privateKey
     * @return
     * @throws Exception
     */
    private String decrypt(String text, String privateKey) throws Exception {
        String algorithm = "RSA";
        String keyText = privateKey.split("-----")[2].replaceAll("\n", "").replaceAll("\r", "");
        byte[] bytes = Base64.decode(keyText.getBytes());
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(bytes);
        PrivateKey key = keyFactory.generatePrivate(privateKeySpec);

        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.DECRYPT_MODE, key);

        byte[] doFinal = cipher.doFinal(Base64.decode(text));
        return new String(doFinal, "utf-8");
    }

 

完整的MainController为: 

 

package com.demo.rsa;

import java.io.StringWriter;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.Security;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import javax.crypto.Cipher;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemWriter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * java与node.js非对称加密
 * 
 * 出自:http://www.cnblogs.com/goodhelper
 * 
 * @author 刘冬
 *
 */
@RestController
public class MainController {

    /**
     * 存储用户信息
     */
    private Map<String, String> users = new ConcurrentHashMap<>();

    /**
     * 存储session私钥
     */
    private Map<String, String> session = new ConcurrentHashMap<>();

    static {
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
    }

    @GetMapping("getUser")
    public String getUser(@RequestHeader(value = "Authorization", required = false) String token) {
        if (token == null) {
            return null;
        }
        return users.containsKey(token) ? users.get(token) : null;
    }

    @PostMapping("login")
    @Deprecated
    public Map<String, Object> login(@RequestBody Map<String, String> params) {
        Map<String, Object> result = new HashMap<>();
        if (!params.containsKey("account") || !params.containsKey("password")) {
            result.put("success", false);
            result.put("message", "请输入账号和密码");
            return result;
        }
        if (!"123456".equals(params.get("password"))) {
            result.put("success", false);
            result.put("message", "密码错误");
            return result;
        }

        String token = UUID.randomUUID().toString();
        users.put(token, params.get("account"));

        result.put("success", true);
        result.put("message", "登录成功");
        result.put("data", token);
        return result;
    }

    /**
     * 获取session公钥
     * 
     * @return
     */
    @GetMapping("getSession")
    public Map<String, String> getSession() throws Exception {
        String sessionId = UUID.randomUUID().toString();
        Map<String, String> result = new HashMap<>();
        result.put("sessionId", sessionId);

        String algorithm = "RSA";
        String privateKey = null, publicKey = null;

        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(algorithm);
        keyPairGen.initialize(512);
        KeyPair keyPair = keyPairGen.generateKeyPair();

        byte[] encoded = keyPair.getPrivate().getEncoded();
        PrivateKeyInfo pkInfo = PrivateKeyInfo.getInstance(encoded);
        ASN1Encodable encodable = pkInfo.parsePrivateKey();
        ASN1Primitive primitive = encodable.toASN1Primitive();
        byte[] privateKeyPKCS1 = primitive.getEncoded();
        PemObject pemObject = new PemObject("RSA PRIVATE KEY", privateKeyPKCS1);
        try (StringWriter stringWriter = new StringWriter()) {
            try (PemWriter pemWriter = new PemWriter(stringWriter)) {
                pemWriter.writeObject(pemObject);
                pemWriter.flush();
                String pemString = stringWriter.toString();
                privateKey = pemString;
            }
        }

        encoded = keyPair.getPublic().getEncoded();
        SubjectPublicKeyInfo spkInfo = SubjectPublicKeyInfo.getInstance(encoded);
        primitive = spkInfo.parsePublicKey();
        byte[] publicKeyPKCS1 = primitive.getEncoded();

        pemObject = new PemObject("RSA PUBLIC KEY", publicKeyPKCS1);
        try (StringWriter stringWriter = new StringWriter()) {
            try (PemWriter pemWriter = new PemWriter(stringWriter)) {
                pemWriter.writeObject(pemObject);
                pemWriter.flush();
                String pemString = stringWriter.toString();
                publicKey = pemString;
            }
        }

        // 记录私钥
        session.put(sessionId, privateKey);
        // 返回公钥
        result.put("publicKey", publicKey);

        return result;
    }

    @SuppressWarnings("unchecked")
    @PostMapping("loginByEncrypt")
    public Map<String, Object> loginByEncrypt(@RequestBody Map<String, String> params) {
        Map<String, Object> result = new HashMap<>();

        if (!params.containsKey("sessionId")) {
            result.put("success", false);
            result.put("message", "sessionId是必填参数");
            return result;
        }

        if (!params.containsKey("playload")) {
            result.put("success", false);
            result.put("message", "playload是必填参数");
            return result;
        }

        String sessionId = params.get("sessionId");

        if (!session.containsKey(sessionId)) {
            result.put("success", false);
            result.put("message", "无效session");
            return result;
        }

        Map<String, String> json = null;
        try {
            String privateKey = session.get(sessionId);
            String playload = params.get("playload");
            String text = decrypt(playload, privateKey);

            ObjectMapper mapper = new ObjectMapper();
            json = mapper.readValue(text, Map.class);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (json == null) {
            result.put("success", false);
            result.put("message", "非法请求");
            return result;
        }

        if (!json.containsKey("account") || !json.containsKey("password")) {
            result.put("success", false);
            result.put("message", "请输入账号和密码");
            return result;
        }
        if (!"123456".equals(json.get("password"))) {
            result.put("success", false);
            result.put("message", "密码错误");
            return result;
        }

        String token = UUID.randomUUID().toString();
        users.put(token, json.get("account"));

        result.put("success", true);
        result.put("message", "登录成功");
        result.put("data", token);
        return result;
    }

    /**
     * 私钥解密
     * 
     * @param encode
     * @param privateKey
     * @return
     * @throws Exception
     */
    private String decrypt(String text, String privateKey) throws Exception {
        String algorithm = "RSA";
        String keyText = privateKey.split("-----")[2].replaceAll("\n", "").replaceAll("\r", "");
        byte[] bytes = Base64.decode(keyText.getBytes());
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(bytes);
        PrivateKey key = keyFactory.generatePrivate(privateKeySpec);

        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.DECRYPT_MODE, key);

        byte[] doFinal = cipher.doFinal(Base64.decode(text));
        return new String(doFinal, "utf-8");
    }
}
MainController

 

在node.js项目中安装node-rsa依赖 

 

npm install --save node-rsa

 

增加密文登录的方法:

首先,调用getSession接口获取后端生成的公钥

其次,调用node-rsa封装的方法,实现公钥加密。注意的是,需要设置密钥格式为pkcs1,否则后端解密出的字符会是乱码。

 

<template>
<div class="hello">
  <table>
    <tr>
      <td>
        用户名:
      </td>
      <td>
        <input type="text" v-model="form.account" />
      </td>
    </tr>
    <tr>
      <td>
        密码:
      </td>
      <td>
        <input type="password" v-model="form.password" />
      </td>
    </tr>
    <tr>
      <td>
        <input type="button" value="登录" @click="loginByEncrypt" />
      </td>
      <td>
        <font v-if="message">{{message}}</font>
      </td>
    </tr>
  </table>
</div>
</template>

<script>
import NodeRSA from 'node-rsa'

export default {
  data() {
    return {
      message: null,
      sessionId: null,
      publicKey: null,
      form: {
        account: null,
        password: null
      }
    }
  },
  methods: {
    //明文登录
    login() {
      this.message = null
      this.$axios.post('/login', this.form).then(res => {
        if (!res.data.success) {
          this.message = res.data.message
          return
        }
        let token = res.data.data
        sessionStorage.setItem('Authorization', token)
        this.$axios.defaults.headers.common['Authorization'] = token
        this.$router.push({
          path: '/'
        });
      })
    },
    //获取session公钥
    getSession() {
      this.$axios.get('/getSession', this.form).then(res => {
        this.sessionId = res.data.sessionId
        this.publicKey = res.data.publicKey
      })
    },
    //密文登录
    loginByEncrypt() {
      let key = new NodeRSA(this.publicKey)
      key.setOptions({
        encryptionScheme: 'pkcs1'
      })

      this.message = null
      let playload = key.encrypt(JSON.stringify(this.form), 'base64', 'utf8')
      let param = {
        sessionId: this.sessionId,
        playload: playload
      }
      this.$axios.post('/loginByEncrypt', param).then(res => {
        if (!res.data.success) {
          this.message = res.data.message
          return
        }
        let token = res.data.data
        sessionStorage.setItem('Authorization', token)
        this.$axios.defaults.headers.common['Authorization'] = token
        this.$router.push({
          path: '/'
        });
      })
    }
  },
  mounted() {
    this.getSession()
  }
}
</script>

<style scoped>

</style>

 

 获取公钥的效果:

 

 

 

 传输密文的效果:

 

 

 


 

好了,到这里,java结合node.js非对称加密的密文登录传参就实现了。不过,这篇博客仅仅是个例子。如果是在正式项目中,则需要考虑很多问题,如,私钥存在数据库或redis,而非java内存中。

 

参考:https://www.npmjs.com/package/node-rsa

 

代码地址:https://github.com/carter659/java-node-rsa-demo

 

如果你觉得我的博客对你有帮助,可以给我点儿打赏,左侧微信,右侧支付宝。

有可能就是你的一点打赏会让我的博客写的更好:)

 

玩转spring boot系列目录

 

作者:刘冬.NET 博客地址:http://www.cnblogs.com/GoodHelper/ 欢迎转载,但须保留版权
posted @ 2018-09-10 14:11  冬子哥  阅读(3240)  评论(2编辑  收藏  举报