若依框架集成国密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 CastleBC库。若依框架基于Spring Boot我们通常通过引入bcprov-jdk15on或更高版本如bcprov-jdk18on来提供SM2能力。这里有一个关键点Spring Boot或项目其他依赖比如某些安全框架可能会间接引入不同版本的BC库导致类冲突ClassNotFoundException或NoSuchMethodError。因此在pom.xml中必须显式声明并统一版本。前端Vue选型前端没有官方标准的国密库社区中有多个选择。经过实测sm-crypto这个库是目前最稳定、最易用的选择。它支持SM2、SM3、SM4API清晰且与后端Bouncy Castle的默认参数兼容性好。另一个常见选择是sm2但sm-crypto的文档和示例更友好。我们将使用sm-crypto。整体流程设计密钥生成在后端或使用工具生成一对SM2公私钥通常为PEM格式。公钥给前端私钥留在后端并妥善保管如放在配置中心或环境变量中绝不能硬编码在代码里。前端改造登录页面用户输入密码后调用sm-crypto的SM2加密方法使用公钥对密码进行加密得到加密后的密文通常为Base64或16进制字符串。传输前端将密文作为password参数替换原来的MD5值发送登录请求。后端改造解密在请求进入业务逻辑前例如在自定义的过滤器或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配置在后端应用环境变量中。 } }密钥管理公钥可以硬编码在前端代码中或通过接口在登录页加载时动态获取。由于公钥本身就是公开的即使泄露也不会影响安全性但可能被用来加密垃圾数据攻击你的接口。私钥必须保密。推荐做法是将私钥的16进制字符串或PEM内容存入服务器的环境变量中如SM2_PRIVATE_KEY。在Spring Boot的application.yml中通过${SM2_PRIVATE_KEY:默认值}引用并确保生产环境的配置与开发/测试环境分离。绝对不要将私钥提交到版本控制系统。3. 后端Java代码配置与核心实现这是改造的核心部分。我们需要在若依的后端工程中添加SM2解密能力并将其嵌入到登录流程中。3.1 引入依赖与版本管理打开你的Spring Boot项目的pom.xml文件添加Bouncy Castle依赖。关键点要声明版本并注意排除其他依赖可能引入的旧版本BC库。properties !-- 建议使用较新且稳定的版本 -- bouncycastle.version1.78/bouncycastle.version /properties dependencies !-- 其他依赖... -- !-- Bouncy Castle Provider -- dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk18on/artifactId version${bouncycastle.version}/version /dependency !-- 如果需要处理PEM格式密钥可能还需要bcpkix -- dependency groupIdorg.bouncycastle/groupId artifactIdbcpkix-jdk18on/artifactId version${bouncycastle.version}/version /dependency /dependencies避坑指南1依赖冲突若依框架可能间接引入了bcprov-jdk15to18等不同artifactId或版本的BC库。你需要在pom.xml中搜索bcprov或bouncycastle在所有引入它的依赖中使用exclusions标签排除旧的确保整个项目只使用我们显式声明的版本。例如如果spring-security-core引入了旧版本dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId exclusions exclusion groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk15to18/artifactId /exclusion /exclusions /dependency3.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密码。创建过滤器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用于缓存请求体字节流。配置过滤器在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方法的一开始对密码进行判断和解密。这样更直观但耦合度稍高。在SysLoginController的login方法中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-crypto4.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 完整测试流程单元测试为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); } }集成测试启动前后端项目。打开登录页输入用户名密码点击登录。使用浏览器的开发者工具F12查看网络请求确认password字段的值是SM2:开头的长字符串。在后端应用日志中查看自定义过滤器或Controller的解密日志确认解密成功。登录成功后验证后续的权限、菜单加载等功能是否正常。兼容性测试测试一个新用户注册如果注册功能也用了同一套密码逻辑也需要同步改造。测试一个已存在的、用旧MD5密码存储的老用户是否还能用旧密码登录如果老密码没有SM2:前缀应该走原有验证流程。这可能需要你手动在数据库里创建一个MD5密码的用户来测试。测试修改密码功能确保新密码也经过SM2加密传输。5.3 常见问题与排查技巧实录即使按照步骤操作你也可能会遇到一些问题。下面是我在实施过程中遇到的一些典型问题及解决方法问题1后端解密失败报错如Invalid point encoding或Malformed 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:前缀是否正确地跳过了解密步骤直接交给了原有验证流程在过滤器中添加详细日志打印密码字段和识别结果。排查技巧调试模式在SysLoginService的login方法开始处打上断点查看传入的password参数到底是什么是明文还是SM2:密文。数据库对比手动计算一下。用你解密得到的明文密码加上该用户的盐sys_user表中的salt字段用相同的哈希算法若依默认是SHA-256计算一次看结果是否与数据库的password字段一致。问题3项目启动时报NoSuchMethodError或ClassNotFoundException与Bouncy Castle相关。可能原因依赖冲突。检查执行mvn dependency:tree | findstr bcprov或gradle dependencies | grep bcprov查看所有引入BC库的路径。确保最终生效的是你指定的版本如1.78。解决在pom.xml中对所有引入旧版本BC的依赖进行排除exclusions如前面“避坑指南1”所示。问题4前端加密后密码字段过长导致HTTP 400错误或数据库字段长度不足。可能原因SM2加密后的密文16进制格式比原来的MD532位长很多可能超过数据库字段或某些中间件如Nginx的默认限制。解决数据库检查sys_user表的password字段长度。若依默认是100SM2密文带前缀可能超过100。建议将其改为varchar(255)或更长。HTTP服务器如果使用Nginx检查client_max_body_size和large_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加密登录。这不仅是满足合规要求更是对系统安全传输层的一次实质性加固。