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/ 歡迎轉載,但須保留版權


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM