14-SpringSecurity:前后端分离项目中用户名与密码通过RSA加密传输

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 14-SpringSecurity:前后端分离项目中用户名与密码通过RSA加密传输

背景


登录认证几乎是所有互联网应用的必备功能,传统的用户名-密码认证方式依然流行,如何避免用户名、密码这类敏感信息在认证过程中被嗅探、破解?

image.png

这里将传统的用户名、密码明文传输方式改为采用 RSA 的非对称加密算法密文传输,即使认证请求被网络抓包,只要私钥安全,则认证流程中的用户信息相对安全;


  1. 一般是生成RSA的密钥对之后,公钥存储在前端或后端(登录时每次请求后端返回公钥)进行加密,私钥存储在后端用于解密;


  1. 曾在实际的应用中看到过动态生成密钥对的做法,即公钥-私钥都是动态生成,每次请求都不一样,这与固定公钥-私钥的做法相比,性能上损耗较大,而在安全性上的收益并没有增加多少;因此这里采用固定密钥对的方式进行演示。


生成RSA密钥对


主要涉及三条命令:


# 生成RSA私钥
genrsa -out rsa_private_key.pem 1024
# 把RSA私钥转换成PKCS8格式
pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt
# 生成RSA公钥
rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem


  • Windows操作系统: Win10


下载安装 OpenSSLslproweb.com/products/Wi…

打开 openssl.exe 所在目录,我这里是: D:\Program Files\OpenSSL-Win64\bin ,运行exe,执行上述三行命令实现 RSA 密钥对生成:

OpenSSL> genrsa -out rsa_private_key.pem 1024
Generating RSA private key, 1024 bit long modulus (2 primes)
.....................................+++++
................+++++
e is 65537 (0x010001)
OpenSSL> pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt
-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAL/KFpxZ2ZJq4/f8
1oM2LX/aX1llPL6SlFbk5pBw1ESuQDVrcA8T4grdrFoEY6T2mNQAMiuzRKfYkS1l
Qx1C+L0HruqOPhFwDL7rxrDQU+8g/trCv+DQoMAbIcteqgxLQrvMZs1OuJrK0XpG
p4Ca7Wxfuk8HUynjQ9fhXIjWzWTjAgMBAAECgYBMUAARNFszPF77RNqiGQOftOdt
ra+u8KofrTLk1FBSB7e6ycYr6bBuvGeg5dA0Sn7jFDTiWJF/69dQZdN/qC9Kb0OV
jRtXDCSMHe1oRlvDr8tZKn9h9UljJHXrIapXJi5Z1eNQ3DW8ltgJbx/DpQrsSTYJ
JiWWpwfb6e+ub09JEQJBAOt+DAxec2h1Gq43Fc/fJ6hUmVl0VI0d5WkeVHezhutE
gYj29gkHkQin5VIMbXtutB/083vUm+Fxqc5EXdxzYIsCQQDQfb+gNZgBzeNhF/j5
IdqW68PpSOmWj2z9sVvAktSS9VzTt46haBvnjzIbES+uzJXoW0LI0H1zDlbvbtRV
HQAJAkEAz+kQMBdvowjIzok5y7ZEqBxQ66aGQ7TiZ2Vsw+YPt0VbbBZF8IDqro61
KzRnsLNzekdkdK6oFWmptr+rcse2swJARN10QSfSqK3n7/cqHqgm+nivgku6FCgV
uQovI0Gcg1oWKjxUGU45AVhUFYqstFERJumV+pybAzj2UCnMarykeQJAAkXb5Z7A
sb7wmLCDMoyfzJCn54k1VDEvGVcrn4SiME53wEyGnrYkyg8R84hO7rHLOnwz0PtZ
iLWuHpqd2OovmA==
-----END PRIVATE KEY-----
OpenSSL> rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
writing RSA key
  • Linux操作系统:CentOS7


同样,执行上述三行命令实现 RSA 密钥对生成image.png


Note:


  1. 后续编码实现时,使用Windows上生成的秘钥进行演示;


  1. 公钥、私钥用的是下图中红色椭圆标注出来的内容。

image.png

公钥:MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/yhacWdmSauP3/NaDNi1/2l9ZZTy+kpRW5OaQcNRErkA1a3APE+IK3axaBGOk9pjUADIrs0Sn2JEtZUMdQvi9B67qjj4RcAy+68aw0FPvIP7awr/g0KDAGyHLXqoMS0K7zGbNTriaytF6RqeAmu1sX7pPB1Mp40PX4VyI1s1k4wIDAQAB
私钥:MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAL/KFpxZ2ZJq4/f81oM2LX/aX1llPL6SlFbk5pBw1ESuQDVrcA8T4grdrFoEY6T2mNQAMiuzRKfYkS1lQx1C+L0HruqOPhFwDL7rxrDQU+8g/trCv+DQoMAbIcteqgxLQrvMZs1OuJrK0XpGp4Ca7Wxfuk8HUynjQ9fhXIjWzWTjAgMBAAECgYBMUAARNFszPF77RNqiGQOftOdtra+u8KofrTLk1FBSB7e6ycYr6bBuvGeg5dA0Sn7jFDTiWJF/69dQZdN/qC9Kb0OVjRtXDCSMHe1oRlvDr8tZKn9h9UljJHXrIapXJi5Z1eNQ3DW8ltgJbx/DpQrsSTYJJiWWpwfb6e+ub09JEQJBAOt+DAxec2h1Gq43Fc/fJ6hUmVl0VI0d5WkeVHezhutEgYj29gkHkQin5VIMbXtutB/083vUm+Fxqc5EXdxzYIsCQQDQfb+gNZgBzeNhF/j5IdqW68PpSOmWj2z9sVvAktSS9VzTt46haBvnjzIbES+uzJXoW0LI0H1zDlbvbtRVHQAJAkEAz+kQMBdvowjIzok5y7ZEqBxQ66aGQ7TiZ2Vsw+YPt0VbbBZF8IDqro61KzRnsLNzekdkdK6oFWmptr+rcse2swJARN10QSfSqK3n7/cqHqgm+nivgku6FCgVuQovI0Gcg1oWKjxUGU45AVhUFYqstFERJumV+pybAzj2UCnMarykeQJAAkXb5Z7Asb7wmLCDMoyfzJCn54k1VDEvGVcrn4SiME53wEyGnrYkyg8R84hO7rHLOnwz0PtZiLWuHpqd2OovmA==


后端服务


基于 SpringBoot , SpringSecurity 实现用户认证功能。


项目依赖


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


SpringSecurity配置


注意放行认证接口,否则报错:403。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/auth/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // turn off csrf, or will be 403 forbidden
                .csrf().disable();
    }
}


用户信息配置


为了集中焦点在本篇的用户名-密码加密传输上,避免引入其他复杂性,这里采用内存型用户信息来演示,关于从数据库中获取用户信息,可参考6-SpringSecurity:数据库存储用户信息

@Component
public class CustomUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return User.withUsername("dev").password(new BCryptPasswordEncoder().encode("123")).authorities("p1", "p2").build();
    }
}


认证接口


这里将私钥配置在 applicaiton.yml 中。

@RestController
@RequestMapping("auth")
@Slf4j
public class LoginController {
    @Value("${rsa.private_key}")
    private String privateKey;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;
    public LoginController(AuthenticationManagerBuilder authenticationManagerBuilder) {
        this.authenticationManagerBuilder = authenticationManagerBuilder;
    }
    @PostMapping("/login")
    public String login(@RequestBody FormUser formUser, HttpServletRequest request) {
        log.info("formUser encrypted: {}", formUser);
        // 用户信息RSA私钥解密,方法一:自定义工具类:RSAEncrypt
//        String username = RSAEncrypt.decrypt(formUser.getUsername(), privateKey);
//        String password = RSAEncrypt.decrypt(formUser.getPassword(), privateKey);
//        log.info("Userinfo decrypted: {}, {}", username, password);
        // 用户信息RSA私钥解密,方法二:使用hutool中的工具类进行解密
        RSA rsa = new RSA(privateKey, null);
        String username = new String(rsa.decrypt(formUser.getUsername(), KeyType.PrivateKey));
        String password = new String(rsa.decrypt(formUser.getPassword(), KeyType.PrivateKey));
        log.info("Userinfo decrypted: {}, {}", username, password);
        // 核验用户名密码
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        log.info("authentication: {}", authentication);
        return SecurityContextHolder.getContext().getAuthentication().getPrincipal().toString();
    }
}


自定义工具类进行解密


<dependency>
  <groupId>commons-codec</groupId>
  <artifactId>commons-codec</artifactId>
  <version>1.12</version>
</dependency>
public class RSAEncrypt {
    /**
     * RSA公钥加密
     * @param str       待加密字符串
     * @param publicKey 公钥
     * @return 密文
     */
    public static String encrypt(String str, String publicKey) {
        try {
            //base64编码的公钥
            byte[] decoded = Base64.decodeBase64(publicKey);
            RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
            //RSA加密
            Cipher cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.ENCRYPT_MODE, pubKey);
            return Base64.encodeBase64String(cipher.doFinal(str.getBytes("UTF-8")));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * RSA私钥解密
     * @param str        已加密字符串
     * @param privateKey 私钥
     * @return 明文
     */
    public static String decrypt(String str, String privateKey) {
        try {
            //64位解码加密后的字符串
            byte[] inputByte = Base64.decodeBase64(str.getBytes("UTF-8"));
            //base64编码的私钥
            byte[] decoded = Base64.decodeBase64(privateKey);
            RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
            //RSA解密
            Cipher cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.DECRYPT_MODE, priKey);
            return new String(cipher.doFinal(inputByte));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}


使用hutool中的工具类进行解密


<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-all</artifactId>
  <version>5.0.6</version>
</dependency>


前端工程


基于 Vue3.0 , axios 实现极简登录页面。


Note:


  1. 前提需要有 Node.js 环境,可使用 nvm 进行 Node.js 的多版本管理;可参考heartsuit.blog.csdn.net/article/det…

  1. npm install <package>默认会在依赖安装完成后将其写入package.json,因此安装依赖的命令都未附加save参数。
$ node -v
v12.16.1


安装vue-cli并创建项目


npm install -g @vue/cli
vue --version
vue create hello-world

刚开始的 package.json 依赖是这样:

"dependencies": {
    "core-js": "^3.6.5",
    "vue": "^3.0.0"
  },


集成Axios


  • 安装依赖
npm install axios

此时, package.json 的依赖变为:

"dependencies": {
    "axios": "^0.21.1",
    "core-js": "^3.6.5",
    "vue": "^3.0.0"
  },
  • 按需引入

在需要使用axios的组件中引入 import axios from "axios";


集成jsencrypt


此时, package.json 的依赖变为:

"dependencies": {
    "axios": "^0.21.1",
    "core-js": "^3.6.5",
    "jsencrypt": "^3.2.1",
    "vue": "^3.0.0"
  },
  • 按需引入

在需要使用JSEncrypt的组件中引入 import JSEncrypt from "jsencrypt";


最终的前端登录组件代码


<template>
  <div>
    <span>用户名</span><input type="text" v-model="user.username" />
    <span>密码</span><input type="text" v-model="user.password" />
    <input type="submit" v-on:click="login" value="登录" />
  </div>
</template>
<script>
import { defineComponent } from "vue";
import axios from "axios";
import JSEncrypt from "jsencrypt";
export default defineComponent({
  name: "RSADemo",
  setup() {},
  data() {
    return {
      user: { username: "dev", password: 123 },
      publicKey: `MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/yhacWdmSauP3/NaDNi1/2l9Z
ZTy+kpRW5OaQcNRErkA1a3APE+IK3axaBGOk9pjUADIrs0Sn2JEtZUMdQvi9B67q
jj4RcAy+68aw0FPvIP7awr/g0KDAGyHLXqoMS0K7zGbNTriaytF6RqeAmu1sX7pP
B1Mp40PX4VyI1s1k4wIDAQAB`,
    };
  },
  mounted() {
    this.login();
  },
  methods: {
    login: function () {
      let userinfo = {
        username: this.encrypt(this.user.username),
        password: this.encrypt(this.user.password),
      };
      axios.post("http://localhost:8000/auth/login", userinfo).then(
        function (res) {
          if (res.status == 200) {
            console.log(res.data);
          } else {
            console.error(res);
          }
        },
        function (res) {
          console.error(res);
        }
      );
    },
    encrypt: function (str) {
      let jsEncrypt = new JSEncrypt();
      // 设置加密公钥,一般通过后端接口获取,这里写在前端代码中
      jsEncrypt.setPublicKey(this.publicKey);
      let encrypted = jsEncrypt.encrypt(str.toString());
      return encrypted;
    },
  },
});
</script>


RSA加密传输效果


image.png

image.png


可能遇到的问题


  • 开发环境跨域


方法一:通过开发环境(生产环境可通过Nginx实现)的代理服务进行请求转发,新建 vue.config.js 文件,内容如下:

module.exports = {
    devServer: {
        proxy: {
            '/api': {
                target: 'http://localhost:8000/',
                changeOrigin: true,
                ws: true,
                secure: true,
                pathRewrite: {
                    '^/api': ''
                }
            }
        }
    }
};

方法二:因为后端服务是我们自己开发的,所以可以在后端进行CORS配置,允许跨域

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**").
                        allowedOriginPatterns("*").
                        allowedMethods("*").
                        allowedHeaders("*").
                        allowCredentials(true).
                        exposedHeaders(HttpHeaders.SET_COOKIE).maxAge(3600L);
            }
        };
    }
}


附:代码生成RSA密钥对


当然,除了使用Windows、Linux上的openssl工具生成密钥对之外,我们也可以使用代码来直接生成。

<dependency>
  <groupId>org.bouncycastle</groupId>
  <artifactId>bcprov-jdk15on</artifactId>
  <version>1.64</version>
</dependency>
public class RSAEncrypt {
    private static final KeyPair keyPair = genKeyPair() ;
    private static org.bouncycastle.jce.provider.BouncyCastleProvider bouncyCastleProvider = null;
    public static synchronized org.bouncycastle.jce.provider.BouncyCastleProvider getInstance() {
        if (bouncyCastleProvider == null) {
            bouncyCastleProvider = new org.bouncycastle.jce.provider.BouncyCastleProvider();
        }
        return bouncyCastleProvider;
    }
    /**
     * 随机生成密钥对
     */
    public static KeyPair  genKeyPair()  {
        try {
//            Provider provider =new org.bouncycastle.jce.provider.BouncyCastleProvider();
//            Security.addProvider(DEFAULT_PROVIDER);
            SecureRandom random = new SecureRandom();
            KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", getInstance());
            generator.initialize(1024,random);
            return generator.generateKeyPair();
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 获取公钥字符串(base64字符串)
     * @return
     */
    public static String generateBase64PublicKey() {
        PublicKey  publicKey = (RSAPublicKey) keyPair.getPublic();
        return new String(Base64.encodeBase64(publicKey.getEncoded()));
    }
    /**
     * 获取私钥字符串(base64字符串)
     * @return
     */
    public static String generateBase64PrivateKey() {
        PrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        // 得到私钥字符串
        return new String(Base64.encodeBase64((privateKey.getEncoded())));
    }
    ...
}


目录
相关文章
|
2月前
|
存储 安全 数据安全/隐私保护
打造安全防线!Python AES&RSA加密工具,黑客绕道走的秘籍
【9月更文挑战第9天】随着数字化时代的到来,信息安全问题日益凸显。本文将介绍如何使用Python结合AES与RSA两种加密算法,构建强大的加密工具。AES以其高效性和强安全性著称,适用于大量数据的快速加密;RSA作为非对称加密算法,在加密小量数据及实现数字签名方面表现卓越。通过整合两者,可以构建既安全又灵活的加密系统。首先,需要安装pycryptodome库。接着,实现AES加密与解密功能,最后利用RSA加密AES密钥,确保其安全传输。这种设计不仅提高了数据传输效率,还增强了密钥交换的安全性,为敏感数据提供坚实保护。
225 43
|
2月前
|
安全 算法 网络安全
浅谈非对称加密(RSA)
浅谈非对称加密(RSA)
|
1月前
|
存储 Java 数据库
密码专辑:对密码加盐加密,对密码进行md5加密,封装成密码工具类
这篇文章介绍了如何在Java中通过加盐和加密算法(如MD5和SHA)安全地存储密码,并提供了一个密码工具类PasswordUtils和密码编码类PasswordEncoder的实现示例。
32 10
密码专辑:对密码加盐加密,对密码进行md5加密,封装成密码工具类
|
1月前
|
NoSQL Java Redis
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
这篇文章介绍了如何使用Spring Boot整合Apache Shiro框架进行后端开发,包括认证和授权流程,并使用Redis存储Token以及MD5加密用户密码。
30 0
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
|
1月前
|
算法 安全 Go
RSA加密算法详解与Python和Go实现
RSA加密算法详解与Python和Go实现
103 1
|
1月前
|
算法 安全 网络安全
使用 Python 实现 RSA 加密
使用 Python 实现 RSA 加密
61 2
|
1月前
|
安全 算法 Java
数据库信息/密码加盐加密 —— Java代码手写+集成两种方式,手把手教学!保证能用!
本文提供了在数据库中对密码等敏感信息进行加盐加密的详细教程,包括手写MD5加密算法和使用Spring Security的BCryptPasswordEncoder进行加密,并强调了使用BCryptPasswordEncoder时需要注意的Spring Security配置问题。
143 0
数据库信息/密码加盐加密 —— Java代码手写+集成两种方式,手把手教学!保证能用!
|
2月前
|
存储 安全 算法
RSA在手,安全我有!Python加密解密技术,让你的数据密码坚不可摧
【9月更文挑战第11天】在数字化时代,信息安全至关重要。传统的加密方法已难以应对日益复杂的网络攻击。RSA加密算法凭借其强大的安全性和广泛的应用场景,成为保护敏感数据的首选。本文介绍RSA的基本原理及在Python中的实现方法,并探讨其优势与挑战。通过使用PyCryptodome库,我们展示了RSA加密解密的完整流程,帮助读者理解如何利用RSA为数据提供安全保障。
127 5
|
2月前
|
安全 算法 数据安全/隐私保护
深度揭秘!Python加密技术的背后,AES与RSA如何守护你的数据安全
【9月更文挑战第10天】随着数字化时代的到来,数据安全成为企业和个人面临的重大挑战。Python 作为功能强大的编程语言,在数据加密领域扮演着重要角色。AES 和 RSA 是两种主流加密算法,分别以对称和非对称加密方式保障数据安全。AES(Advanced Encryption Standard)因其高效性和安全性,在数据加密中广泛应用;而 RSA 则利用公钥和私钥机制,在密钥交换和数字签名方面表现卓越。
81 3
|
2月前
|
存储 安全 数据库
双重防护,无懈可击!Python AES+RSA加密方案,构建最强数据安全堡垒
【9月更文挑战第11天】在数字时代,数据安全至关重要。AES与RSA加密技术相结合,构成了一道坚固防线。AES以其高效性保障数据加密,而RSA则确保密钥安全传输,二者相辅相成,提供双重保护。本文通过Python代码示例展示了这一加密方案的魅力,强调了其在实际应用中的重要性和安全性。使用HTTPS等安全协议传输加密密钥和密文,确保数据在数字世界中自由流通而无忧。
68 1