当前位置: 首页 > news >正文

若依框架集成国密SM2加密登录:从原理到实践的完整指南

1. 项目概述:为什么要在若依框架中引入SM2加密?

最近在重构一个老项目的登录模块,客户对安全性提出了明确要求,希望登录密码的传输和存储能采用国密算法。若依(RuoYi)作为国内广泛使用的开源后台管理系统,其默认的密码处理方式是前端MD5加密后传输,后端再进行一次加盐的MD5或SHA-256加密存储。这个流程本身是安全的,但为了满足特定合规性要求(比如等保测评中对国密算法的建议),或者应对某些对传输层安全性有极高要求的场景,将前端加密方式从MD5替换为SM2,就成了一次典型的技术改造。

SM2是一种基于椭圆曲线密码学的非对称加密算法。与MD5这种哈希算法(不可逆)不同,SM2加密是可逆的,它需要一对公私钥:前端用公钥加密密码,后端用私钥解密。这样做最大的好处是,即使在网络传输过程中被截获,攻击者没有私钥也无法解密出明文密码,安全性比单纯的哈希传输要高一个维度。但引入非对称加密也带来了新的复杂度:密钥管理、前后端加解密逻辑的协同、以及如何平滑地集成到若依现有的认证流程中。

我花了几天时间,从零开始把若依前后端分离版(Vue3 + Spring Boot)的登录密码加密改造成了SM2。过程中踩了不少坑,比如前端加密库的选型、后端Java解密时遇到的BC库版本冲突、以及如何优雅地处理新老用户密码兼容问题。这篇文章,我就把这些实操步骤、核心配置和避坑经验,整理成一份保姆级的指南。无论你是为了项目合规,还是单纯想提升系统安全性,跟着做一遍,都能让你的若依框架用上国密SM2加密登录。

2. 核心思路与整体设计

在动手写代码之前,我们必须把改造的核心思路理清楚。若依框架的登录认证核心在LoginService和相关的过滤器链中,我们的目标是“无感”替换其中的加密环节,同时保证系统其他功能(如用户管理、修改密码)不受影响。

2.1 技术选型与依赖确认

首先,要确定前后端使用的SM2算法库。国密算法虽然标准统一,但不同语言、不同库的实现细节和默认参数可能有细微差别,前后端必须匹配,否则解密一定会失败。

后端(Java)选型:Java生态中,最常用、最权威的国密算法实现是Bouncy Castle(BC)库。若依框架基于Spring Boot,我们通常通过引入bcprov-jdk15on或更高版本(如bcprov-jdk18on)来提供SM2能力。这里有一个关键点:Spring Boot或项目其他依赖(比如某些安全框架)可能会间接引入不同版本的BC库,导致类冲突(ClassNotFoundExceptionNoSuchMethodError)。因此,在pom.xml中必须显式声明并统一版本。

前端(Vue)选型:前端没有官方标准的国密库,社区中有多个选择。经过实测,sm-crypto这个库是目前最稳定、最易用的选择。它支持SM2、SM3、SM4,API清晰,且与后端Bouncy Castle的默认参数兼容性好。另一个常见选择是sm2,但sm-crypto的文档和示例更友好。我们将使用sm-crypto

整体流程设计:

  1. 密钥生成:在后端或使用工具生成一对SM2公私钥(通常为PEM格式)。公钥给前端,私钥留在后端并妥善保管(如放在配置中心或环境变量中,绝不能硬编码在代码里)。
  2. 前端改造:登录页面,用户输入密码后,调用sm-crypto的SM2加密方法,使用公钥对密码进行加密,得到加密后的密文(通常为Base64或16进制字符串)。
  3. 传输:前端将密文作为password参数,替换原来的MD5值,发送登录请求。
  4. 后端改造
    • 解密:在请求进入业务逻辑前(例如在自定义的过滤器或LoginService中),拦截登录请求,使用私钥对密文进行解密,得到明文密码。
    • 兼容处理:考虑到系统可能有已存在的、用旧方式(MD5)加密存储的用户,我们需要一个策略来判断当前传入的密码是SM2密文还是旧格式。一个简单的办法是约定SM2密文带有特定前缀(如SM2:),或者通过密码字段的长度、字符特征进行判断。
    • 后续处理:将解密得到的明文密码,交给若依原有的密码验证逻辑(即调用SecurityUtils.matchesPassword进行加盐哈希比对)。这样,用户密码的存储格式完全不变,只是传输过程变了。

2.2 密钥生成与管理策略

密钥安全是整个方案的基础。绝对不要将私钥写在代码的常量里或提交到Git仓库。

生成密钥对:你可以使用OpenSSL(需支持国密)或在线工具生成,但更推荐用Java代码生成,以确保格式兼容。这里给出一个用Bouncy Castle生成密钥对并导出为PEM格式的示例代码片段。在实际项目中,这段代码可以作为一个一次性运行的工具类,生成后便将密钥对保存到安全的地方。

import org.bouncycastle.asn1.gm.GMNamedCurves; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.AsymmetricCipherKeyPair; import org.bouncycastle.crypto.generators.ECKeyPairGenerator; import org.bouncycastle.crypto.params.ECDomainParameters; import org.bouncycastle.crypto.params.ECKeyGenerationParameters; import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.encoders.Base64; import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemWriter; import java.io.StringWriter; import java.security.SecureRandom; public class Sm2KeyGenerator { static { // 确保BC Provider已注册 if (java.security.Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { java.security.Security.addProvider(new BouncyCastleProvider()); } } public static void main(String[] args) throws Exception { // 获取SM2椭圆曲线参数 X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1"); ECDomainParameters domainParameters = new ECDomainParameters( sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN(), sm2ECParameters.getH() ); // 生成密钥对 ECKeyPairGenerator keyPairGenerator = new ECKeyPairGenerator(); ECKeyGenerationParameters keyGenerationParameters = new ECKeyGenerationParameters(domainParameters, new SecureRandom()); keyPairGenerator.init(keyGenerationParameters); AsymmetricCipherKeyPair keyPair = keyPairGenerator.generateKeyPair(); ECPrivateKeyParameters privateKeyParams = (ECPrivateKeyParameters) keyPair.getPrivate(); ECPublicKeyParameters publicKeyParams = (ECPublicKeyParameters) keyPair.getPublic(); // 转换为PEM格式(示例,实际需根据库的API调整) // 注意:这里只是示意,BC库对SM2的PEM格式支持可能需要额外处理 // 更简单的做法是直接导出为16进制或Base64的原始公私钥数据 String publicKeyHex = org.bouncycastle.util.encoders.Hex.toHexString(publicKeyParams.getQ().getEncoded(false)); // 非压缩格式 String privateKeyHex = privateKeyParams.getD().toString(16); System.out.println("公钥(16进制,非压缩,04开头): " + publicKeyHex); System.out.println("私钥(16进制): " + privateKeyHex); // 在实际项目中,你可能需要将公钥(publicKeyHex)发给前端,私钥(privateKeyHex)配置在后端应用环境变量中。 } }

密钥管理:

  • 公钥:可以硬编码在前端代码中,或通过接口在登录页加载时动态获取。由于公钥本身就是公开的,即使泄露也不会影响安全性(但可能被用来加密垃圾数据攻击你的接口)。
  • 私钥:必须保密。推荐做法是:
    1. 将私钥的16进制字符串或PEM内容存入服务器的环境变量中(如SM2_PRIVATE_KEY)。
    2. 在Spring Boot的application.yml中通过${SM2_PRIVATE_KEY:默认值}引用,并确保生产环境的配置与开发/测试环境分离。
    3. 绝对不要将私钥提交到版本控制系统。

3. 后端Java代码配置与核心实现

这是改造的核心部分。我们需要在若依的后端工程中,添加SM2解密能力,并将其嵌入到登录流程中。

3.1 引入依赖与版本管理

打开你的Spring Boot项目的pom.xml文件,添加Bouncy Castle依赖。关键点:要声明版本,并注意排除其他依赖可能引入的旧版本BC库。

<properties> <!-- 建议使用较新且稳定的版本 --> <bouncycastle.version>1.78</bouncycastle.version> </properties> <dependencies> <!-- 其他依赖... --> <!-- Bouncy Castle Provider --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk18on</artifactId> <version>${bouncycastle.version}</version> </dependency> <!-- 如果需要处理PEM格式密钥,可能还需要bcpkix --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk18on</artifactId> <version>${bouncycastle.version}</version> </dependency> </dependencies>

避坑指南1:依赖冲突若依框架可能间接引入了bcprov-jdk15to18等不同artifactId或版本的BC库。你需要在pom.xml中搜索bcprovbouncycastle,在所有引入它的依赖中,使用<exclusions>标签排除旧的,确保整个项目只使用我们显式声明的版本。例如,如果spring-security-core引入了旧版本:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <exclusions> <exclusion> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15to18</artifactId> </exclusion> </exclusions> </dependency>

3.2 编写SM2工具类

创建一个Sm2Utils工具类,封装加密和解密方法。这里我们假设公私钥都是以16进制字符串形式存储。

import lombok.extern.slf4j.Slf4j; import org.bouncycastle.asn1.gm.GMNamedCurves; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.engines.SM2Engine; import org.bouncycastle.crypto.params.ECDomainParameters; import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.params.ParametersWithRandom; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.spec.ECParameterSpec; import org.bouncycastle.jce.spec.ECPublicKeySpec; import org.bouncycastle.math.ec.ECPoint; import org.bouncycastle.util.encoders.Hex; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; @Slf4j public class Sm2Utils { static { // 静态代码块注册BC Provider,确保只注册一次 if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { Security.addProvider(new BouncyCastleProvider()); } } /** * SM2解密 (使用BC库原生方式,兼容sm-crypto的默认加密结果) * @param privateKeyHex 私钥的16进制字符串(不带04开头) * @param encryptedDataHex 加密后的密文(16进制字符串,通常是C1C3C2格式) * @return 解密后的明文 */ public static String decrypt(String privateKeyHex, String encryptedDataHex) { try { // 1. 获取SM2椭圆曲线参数 X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1"); ECDomainParameters domainParameters = new ECDomainParameters( sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN(), sm2ECParameters.getH() ); // 2. 将16进制私钥转换为BigInteger,并创建私钥参数 java.math.BigInteger privateKeyD = new java.math.BigInteger(privateKeyHex, 16); ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyD, domainParameters); // 3. 初始化SM2解密引擎 // SM2Engine.Mode.C1C3C2 是默认模式,与sm-crypto库加密结果匹配 SM2Engine engine = new SM2Engine(SM2Engine.Mode.C1C3C2); engine.init(false, privateKeyParameters); // false 表示解密模式 // 4. 将16进制密文转换为字节数组并进行解密 byte[] encryptedData = Hex.decode(encryptedDataHex); byte[] decryptedBytes = engine.processBlock(encryptedData, 0, encryptedData.length); // 5. 返回解密后的明文字符串 return new String(decryptedBytes, "UTF-8"); } catch (Exception e) { log.error("SM2解密失败,私钥: {}, 密文: {}", privateKeyHex, encryptedDataHex, e); throw new RuntimeException("SM2解密失败", e); } } /** * 加密方法(可选,主要用于测试或后端生成加密数据) * @param publicKeyHex 公钥的16进制字符串(04开头,非压缩格式) * @param plainText 明文 * @return 加密后的密文(16进制) */ public static String encrypt(String publicKeyHex, String plainText) { try { // 类似解密过程,但使用公钥并初始化引擎为true(加密模式) X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1"); ECDomainParameters domainParameters = new ECDomainParameters( sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN(), sm2ECParameters.getH() ); // 解析公钥(04 || X || Y) byte[] publicKeyBytes = Hex.decode(publicKeyHex); ECPoint publicKeyPoint = sm2ECParameters.getCurve().decodePoint(publicKeyBytes); ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(publicKeyPoint, domainParameters); SM2Engine engine = new SM2Engine(SM2Engine.Mode.C1C3C2); engine.init(true, new ParametersWithRandom(publicKeyParameters, new SecureRandom())); byte[] plainBytes = plainText.getBytes("UTF-8"); byte[] encryptedBytes = engine.processBlock(plainBytes, 0, plainBytes.length); return Hex.toHexString(encryptedBytes); } catch (Exception e) { log.error("SM2加密失败", e); throw new RuntimeException("SM2加密失败", e); } } }

避坑指南2:加密模式与格式sm-crypto默认使用C1C3C2的ASN.1编码顺序,并且公钥格式是“04”开头的非压缩格式。我们的工具类必须与之匹配。SM2Engine.Mode.C1C3C2这个参数至关重要,如果选错(比如用C1C2C3),解密一定会失败。如果你发现解密报错或结果乱码,首先检查前后端的模式是否一致。

3.3 集成到若依登录逻辑

若依的登录逻辑主要在SysLoginService类的login方法中。我们不能直接修改这个方法的内部密码比对逻辑,更优雅的方式是在请求到达LoginService之前,对密码参数进行预处理。

方案一:自定义过滤器(Filter)在Spring Security的过滤器链中,在UsernamePasswordAuthenticationFilter之前插入一个自定义过滤器,专门处理登录请求,解密SM2密码。

  1. 创建过滤器
import com.ruoyi.common.core.domain.model.LoginBody; import com.ruoyi.common.utils.StringUtils; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; @Component @Slf4j public class Sm2PasswordDecryptFilter extends OncePerRequestFilter { @Value("${sm2.private-key}") // 从配置文件中读取私钥 private String privateKey; private static final String LOGIN_URL = "/login"; private static final String SM2_PREFIX = "SM2:"; // 约定SM2密文前缀,用于识别 @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 只处理登录POST请求 if (LOGIN_URL.equals(request.getServletPath()) && "POST".equalsIgnoreCase(request.getMethod())) { // 缓存请求体,因为InputStream只能读一次 CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(request); String body = getRequestBody(cachedRequest); ObjectMapper mapper = new ObjectMapper(); LoginBody loginBody = mapper.readValue(body, LoginBody.class); String password = loginBody.getPassword(); // 判断是否为SM2加密的密码(根据前缀或特征) if (StringUtils.isNotEmpty(password) && password.startsWith(SM2_PREFIX)) { try { // 去掉前缀,获取真正的密文(假设前端传的是Base64,这里需要根据实际情况调整) String cipherText = password.substring(SM2_PREFIX.length()); // 如果前端传的是Base64,需要先解码。这里假设前端传的是16进制。 // String cipherTextHex = Hex.toHexString(Base64.getDecoder().decode(cipherText)); // 我们假设前端直接传16进制密文,并去掉了`04`公钥头(因为加密时只用公钥,密文里不含04) // 注意:sm-crypto加密后的密文已经是16进制字符串,无需再转换。 String plainPassword = Sm2Utils.decrypt(privateKey, cipherText); loginBody.setPassword(plainPassword); // 替换为明文密码 // 将修改后的登录体重新写入请求 String newBody = mapper.writeValueAsString(loginBody); cachedRequest.setBody(newBody.getBytes()); log.debug("SM2密码解密成功,用户: {}", loginBody.getUsername()); } catch (Exception e) { log.error("登录请求SM2密码解密失败", e); // 解密失败,可以返回错误响应,或者继续传递密文(会导致登录失败) response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("{\"code\":500,\"msg\":\"密码解析错误\"}"); return; } } // 将包装后的请求传递下去 filterChain.doFilter(cachedRequest, response); return; } filterChain.doFilter(request, response); } private String getRequestBody(HttpServletRequest request) throws IOException { StringBuilder stringBuilder = new StringBuilder(); BufferedReader bufferedReader = null; try { bufferedReader = request.getReader(); char[] charBuffer = new char[128]; int bytesRead; while ((bytesRead = bufferedReader.read(charBuffer)) != -1) { stringBuilder.append(charBuffer, 0, bytesRead); } } finally { if (bufferedReader != null) { bufferedReader.close(); } } return stringBuilder.toString(); } } // 需要配合一个可以缓存和修改请求体的 HttpServletRequestWrapper // 这里省略 CachedBodyHttpServletRequest 的实现,它是一个标准的Servlet Wrapper,用于缓存请求体字节流。
  1. 配置过滤器:在Spring Security的配置类(通常是SecurityConfig)中,将这个过滤器添加到UsernamePasswordAuthenticationFilter之前。
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private Sm2PasswordDecryptFilter sm2PasswordDecryptFilter; @Override protected void configure(HttpSecurity http) throws Exception { http // ... 其他配置 .addFilterBefore(sm2PasswordDecryptFilter, UsernamePasswordAuthenticationFilter.class) // ... 其他配置 } }

方案二:在Controller层或Service层进行解密(更简单直接)如果觉得过滤器太复杂,也可以直接在处理登录的Controller方法中,或者在SysLoginService.login方法的一开始,对密码进行判断和解密。这样更直观,但耦合度稍高。

SysLoginControllerlogin方法中:

@PostMapping("/login") public AjaxResult login(@RequestBody LoginBody loginBody) { String password = loginBody.getPassword(); // 判断是否为SM2加密密码 if (password != null && password.startsWith("SM2:")) { String cipherText = password.substring(4); // 去掉“SM2:”前缀 try { String plainPassword = Sm2Utils.decrypt(sm2PrivateKey, cipherText); loginBody.setPassword(plainPassword); } catch (Exception e) { log.error("密码解密失败", e); return AjaxResult.error("登录失败:密码格式错误"); } } // 调用原有的service方法 String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), loginBody.getUuid()); return AjaxResult.success("登录成功").put("token", token); }

避坑指南3:密码字段的识别与兼容我们给SM2密文加了一个SM2:前缀。这样做有两个好处:一是后端能明确识别这是需要解密的密文;二是为系统兼容性留了后路。如果未来有其他加密方式(比如SM9),可以定义不同的前缀。对于已经存在的、使用旧MD5方式登录的用户(或者通过其他接口创建的用户),他们的密码字段不会有这个前缀,后端会直接将其交给原有的密码验证逻辑处理,从而实现平滑过渡。

4. 前端Vue代码改造

前端改造相对简单,核心是引入sm-crypto库,并在登录时对密码进行加密。

4.1 安装依赖

在Vue项目的根目录下执行:

npm install sm-crypto --save # 或 yarn add sm-crypto

4.2 封装加密函数

src/utils目录下创建一个sm2.js文件(或者在你认为合适的工具目录):

import { sm2 } from 'sm-crypto' // 这里填写后端生成的公钥(16进制,04开头) const publicKey = '04xxxxxxxx...你的公钥...' /** * 使用SM2加密密码 * @param {string} password 明文密码 * @returns {string} 加密后的密文,格式为 'SM2:' + 16进制密文 */ export function encryptPassword(password) { // sm2.doEncrypt 默认输出为16进制字符串,使用C1C3C2模式 const encryptedData = sm2.doEncrypt(password, publicKey, 1) // 第二个参数是公钥,第三个参数1表示输出为16进制 // 添加前缀,方便后端识别 return 'SM2:' + encryptedData } // 可选:解密函数(主要用于测试,前端一般不需要) export function decryptPassword(encryptedDataHex, privateKey) { // 注意:私钥绝不能出现在前端代码中! // 这个函数仅用于本地测试或非常特殊的场景 return sm2.doDecrypt(encryptedDataHex, privateKey, 1) // 1表示输入是16进制 }

避坑指南4:公钥格式与加密模式确保这里的publicKey变量值,与后端生成的公钥16进制字符串完全一致,并且是以04开头的非压缩格式。sm2.doEncrypt的第三个参数1指定输出为16进制字符串,这与我们后端工具类的解密输入格式匹配。

4.3 修改登录页面逻辑

找到你的登录页面组件(通常是src/views/login.vue或类似位置),在提交登录前调用加密函数。

<template> <!-- 你的登录表单 --> </template> <script> import { encryptPassword } from '@/utils/sm2' // 引入加密函数 import { login } from '@/api/user' // 引入登录API export default { name: 'Login', data() { return { loginForm: { username: '', password: '', // 这个字段将存储明文,用于表单绑定 // ... 其他字段 } } }, methods: { handleLogin() { this.$refs.loginForm.validate(valid => { if (valid) { this.loading = true // 1. 对密码进行SM2加密 const encryptedPassword = encryptPassword(this.loginForm.password) // 2. 构造登录参数,使用加密后的密码 const loginParams = { username: this.loginForm.username, password: encryptedPassword, // 这里传的是 'SM2:xxxx...' // ... 其他参数如验证码 } // 3. 调用登录API login(loginParams).then(response => { // ... 登录成功处理 }).catch(error => { // ... 登录失败处理 }).finally(() => { this.loading = false }) } }) } } } </script>

注意:这样修改后,用户在前端输入密码,点击登录时,密码在内存中被加密,然后密文被发送到后端。前端代码和网络请求中都不会出现明文密码。

5. 配置、测试与上线

5.1 配置文件

application.yml中配置私钥:

# 开发环境配置 sm2: private-key: 你的私钥16进制字符串(不带04) # 生产环境建议使用环境变量 # sm2: # private-key: ${SM2_PRIVATE_KEY:默认值}

5.2 完整测试流程

  1. 单元测试:为Sm2Utils编写单元测试,验证加密解密功能是否正常。
    @SpringBootTest class Sm2UtilsTest { @Value("${sm2.private-key}") private String privateKey; private static final String publicKey = "你的公钥"; @Test void testEncryptAndDecrypt() { String plainText = "TestPassword123!"; String cipherText = Sm2Utils.encrypt(publicKey, plainText); assertNotNull(cipherText); String decryptedText = Sm2Utils.decrypt(privateKey, cipherText); assertEquals(plainText, decryptedText); } }
  2. 集成测试
    • 启动前后端项目。
    • 打开登录页,输入用户名密码,点击登录。
    • 使用浏览器的开发者工具(F12)查看网络请求,确认password字段的值是SM2:开头的长字符串。
    • 在后端应用日志中,查看自定义过滤器或Controller的解密日志,确认解密成功。
    • 登录成功后,验证后续的权限、菜单加载等功能是否正常。
  3. 兼容性测试
    • 测试一个新用户注册(如果注册功能也用了同一套密码逻辑,也需要同步改造)。
    • 测试一个已存在的、用旧MD5密码存储的老用户,是否还能用旧密码登录(如果老密码没有SM2:前缀,应该走原有验证流程)。这可能需要你手动在数据库里创建一个MD5密码的用户来测试。
    • 测试修改密码功能,确保新密码也经过SM2加密传输。

5.3 常见问题与排查技巧实录

即使按照步骤操作,你也可能会遇到一些问题。下面是我在实施过程中遇到的一些典型问题及解决方法:

问题1:后端解密失败,报错如Invalid point encodingMalformed input

  • 可能原因1:公私钥不匹配。
    • 检查:确认前端使用的公钥和后端用来解密的私钥是同一对。重新运行密钥生成程序,确保公私钥对应。
  • 可能原因2:密文格式错误。
    • 检查:前端加密后传给后端的密文是什么格式?我们的工具类期望的是16进制字符串。确保前端sm2.doEncrypt的第三个参数是1(输出16进制)。如果前端传了Base64,后端需要先做Base64解码再转16进制解密。
    • 检查:密文是否包含了SM2:前缀?后端代码在解密前是否正确地去掉了这个前缀?
  • 可能原因3:加密/解密模式不一致。
    • 检查:前后端是否都使用了C1C3C2模式?sm-crypto默认是C1C3C2,后端SM2Engine初始化时也必须指定Mode.C1C3C2
  • 排查技巧
    • 日志输出:在解密函数的第一行,打印出接收到的密文长度和前几十个字符,与前端发送的进行比对。
    • 编写一个简单的测试接口:创建一个临时Controller,接收前端传来的密文,直接调用Sm2Utils.decrypt并返回结果,排除业务逻辑干扰。

问题2:登录时系统提示“用户名或密码错误”,但数据库密码看起来没错。

  • 可能原因1:解密成功,但解密后的明文密码与数据库存储的哈希值不匹配。
    • 检查:数据库存储的是password + salt再哈希的结果。你解密得到的是明文密码。若依的SecurityUtils.matchesPassword方法会自动处理加盐哈希比对。确保解密后的明文密码是正确的(没有多余空格、换行符)。
    • 检查:是否在解密后,又无意中对密码进行了二次处理(比如trim)?
  • 可能原因2:用户密码本身就是旧MD5格式,但被错误地加上了SM2:前缀,导致解密失败后系统尝试用密文去比对哈希,必然失败。
    • 检查:你的识别逻辑。如果密码没有SM2:前缀,是否正确地跳过了解密步骤,直接交给了原有验证流程?在过滤器中添加详细日志,打印密码字段和识别结果。
  • 排查技巧
    • 调试模式:在SysLoginServicelogin方法开始处打上断点,查看传入的password参数到底是什么(是明文还是SM2:密文)。
    • 数据库对比:手动计算一下。用你解密得到的明文密码,加上该用户的盐(sys_user表中的salt字段),用相同的哈希算法(若依默认是SHA-256)计算一次,看结果是否与数据库的password字段一致。

问题3:项目启动时报NoSuchMethodErrorClassNotFoundException,与Bouncy Castle相关。

  • 可能原因:依赖冲突。
    • 检查:执行mvn dependency:tree | findstr bcprovgradle dependencies | grep bcprov,查看所有引入BC库的路径。确保最终生效的是你指定的版本(如1.78)。
    • 解决:在pom.xml中,对所有引入旧版本BC的依赖进行排除(<exclusions>),如前面“避坑指南1”所示。

问题4:前端加密后,密码字段过长,导致HTTP 400错误或数据库字段长度不足。

  • 可能原因:SM2加密后的密文(16进制格式)比原来的MD5(32位)长很多,可能超过数据库字段或某些中间件(如Nginx)的默认限制。
  • 解决
    • 数据库:检查sys_user表的password字段长度。若依默认是100,SM2密文(带前缀)可能超过100。建议将其改为varchar(255)或更长。
    • HTTP服务器:如果使用Nginx,检查client_max_body_sizelarge_client_header_buffers配置,确保能接收较长的请求头或体(虽然密码在请求体中,但整体请求体可能变大)。
    • 考虑压缩:如果确实很长,可以考虑在前端将16进制密文再进行一次Base64编码,体积会减少约1/3。后端则需要先Base64解码再解密。

问题5:如何同时支持SM2新用户和MD5老用户?

这是我们采用SM2:前缀方案的核心目的。在解密逻辑中:

if (password != null && password.startsWith("SM2:")) { // 走SM2解密流程 String plain = decrypt(password.substring(4)); // ... 用plain去验证 } else { // 走原有流程(可能是MD5,也可能是明文,取决于系统原有设计) // 若依默认是前端MD5,后端再次加盐哈希。所以这里传入的password应该是前端MD5后的值。 // 直接调用原有的验证逻辑即可。 }

这样,新用户登录时用带前缀的SM2密文,老用户登录时用无前缀的MD5值,系统可以自动路由,实现无缝兼容。

整个改造过程,从技术上看并不复杂,核心是理解SM2非对称加密的原理、选对前后端兼容的库、处理好密钥管理和新旧密码的兼容逻辑。最大的坑往往在细节:依赖冲突、格式不一致、模式不匹配。按照这份指南一步步走,遇到问题对照“常见问题”部分排查,你应该能顺利地在若依框架中实现SM2加密登录。这不仅是满足合规要求,更是对系统安全传输层的一次实质性加固。

http://www.jsqmd.com/news/1232621/

相关文章:

  • 2026年7月永磁大风扇品牌推荐,工业吊扇/工业散热风扇/工业大风扇/大型工业风扇/工业风扇,永磁大风扇厂商选哪家 - 品牌推荐师
  • C++轻量级XML解析库CMarkup:单文件集成与实战应用
  • Unity AssetBundle打包配置深度解析:策略、优化与实战管理
  • AI辅助开发实战:从诊断到重构,根治API速率限制难题
  • 高通8295车机卡顿原因与优化方案
  • SpringBoot仓库管理系统实战:从环境搭建到功能测试完整指南
  • 智能PPT革命:RAG技术如何提升学术汇报效率
  • C++项目集成DuckDB实战:从环境配置到性能调优全指南
  • 辛普森案证据审查与陪审团筛选策略分析
  • Unity开发Android动态壁纸:从原理到实战的完整指南
  • 如何用Python高效实现B站会员购自动化抢票:biliTickerBuy深度解析
  • Unity WebGL工业监控大屏:AVProVideo与XCharts实战整合方案
  • 零跑A10车机系统:SA8295芯片与智能座舱技术解析
  • 深入解析DRA7xP SoC CORE_PRM模块:电源管理与上下文恢复实战
  • Spek音频频谱分析器终极指南:免费开源的声音可视化神器
  • 提升团队协作效率的7大策略与工具链实践
  • 2026年 重庆打木箱/打木架包装公司推荐榜单:专业定制、防震加固与高效物流服务深度解析 - 甄选服务推荐
  • DeepSeek 一键安装:零基础本地部署与 API 代理实战指南
  • C++智能调度系统性能优化实战:从测试到调优的全流程解析
  • RAG知识库质量评估与优化实战指南
  • C++并发编程实战:从进程线程原理到线程池实现
  • AutoCAD 2020:新手入门黄金版本选择与学习路径全指南
  • SolidWorks大国工匠插件安装指南:从环境准备到功能验证
  • Luma AI单眼串联全片:从照片到AI时尚大片的完整指南
  • C++分布式系统性能优化:OOP设计原则与高效实现策略
  • PaddleOCR C++与Python结果差异排查:从数值精度到部署一致性的深度解析
  • C++递归实现十进制转二进制:从原理到代码的完整解析
  • 哈希标签如何提升AI编码成功率与行业争议
  • 企业规模化创新战略与全球化研发管理实践
  • C语言结构体内存对齐机制详解与优化实践