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/>' })
路由的钩子函数
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;
登录后的页面
<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>
登录页面
<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>
在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"); } }
在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
如果你觉得我的博客对你有帮助,可以给我点儿打赏,左侧微信,右侧支付宝。
有可能就是你的一点打赏会让我的博客写的更好:)