rsa加密解密原理
RSA加密解密的原理基于非对称加密技术,它使用了一对数学上相关的密钥:公钥和私钥。这两个密钥在数学上是相互关联的,但知道其中一个并不能轻易推导出另一个。以下是RSA加密解密的基本原理:
密钥生成
选择两个大素数:首先选择两个足够大的不同素数p和q。这两个素数的选择是随机的,但需要足够大以确保安全性。
计算乘积n:计算两个素数的乘积n = p * q。这个乘积n将作为公钥和私钥的基础。
计算欧拉函数φ(n):欧拉函数φ(n)表示小于n且与n互质的正整数的个数。对于两个素数p和q,φ(n) = (p - 1) * (q - 1)。
选择加密指数e:选择一个整数e,使得1 < e < φ(n)且e与φ(n)互质。这个e将作为公钥的一部分,用于加密数据。
计算解密指数d:使用扩展欧几里得算法计算e关于φ(n)的模逆元d,即ed ≡ 1 (mod φ(n))。这个d将作为私钥的一部分,用于解密数据。
现在,公钥为(e, n),私钥为(d, n)。
加密过程
对于待加密的明文m(假设m是一个小于n的整数),加密过程如下:
计算密文c = me mod n。这里的e是公钥中的加密指数,n是公钥中的模数。
解密过程
对于接收到的密文c,解密过程如下:
计算明文m = cd mod n。这里的d是私钥中的解密指数,n是私钥中的模数。由于ed ≡ 1 (mod φ(n)),所以cd ≡ m (mod n),从而可以正确地恢复出明文m。
安全性原理
RSA的安全性基于以下两个数学难题:
大数分解:给定一个大的合数n,很难在合理的时间内将其分解为两个素数的乘积。RSA正是利用了这一难题,因为知道n的值并不足以推导出p和q,进而无法计算出私钥d。
离散对数问题:在模n的乘法群中找到一个数的模逆元是困难的。即使知道公钥(e, n)和密文c,攻击者也很难在不知道私钥d的情况下计算出明文m。
由于这两个数学难题的存在,RSA算法被认为是安全的,并被广泛应用于各种加密和身份验证场景。然而,随着计算能力的提升和新的攻击方法的出现,RSA的安全性也在不断受到挑战。因此,在实际应用中,通常会选择足够长的密钥长度(如2048位或更长)来确保安全性。
密钥存储
密钥存储:在代码中,私钥和公钥是在静态代码块中生成的,并且被编码为Base64字符串存储在静态变量中。这意味着每次类被加载时(例如,在应用启动时),都会生成新的密钥对。如果服务器重启,并且直接使用这些静态变量中的密钥来验证JWT,那么之前签名的JWT将无法通过验证,因为用于签名的私钥已经改变。
解决方案:应该将私钥安全地存储在一个持久化的位置(如文件、数据库或密钥管理服务),并在应用启动时从该位置加载它。公钥可以发送给客户端或存储在配置中,因为它不需要保密。
jwt支持两种类型的签名
JWT的MAC签名通常使用的是对称密钥(SecretKey),而不是RSA这样的非对称密钥。
JWT支持两种类型的签名:
HMAC(基于哈希的消息认证码):使用对称密钥(如HS256、HS384、HS512)。
RSA(或其他公钥密码系统):使用非对称密钥对中的私钥进行签名,公钥用于验证(如RS256、RS384、RS512)。
MacSigner类如果期望一个SecretKey类型的密钥,若传递了一个RSA私钥。这通常发生在使用JWT库(如jjwt)时,错误地配置了签名算法或密钥类型。
为了解决这个问题,需要确保:
在JwtUtil.createToken方法中,使用正确的签名算法。如果想要使用RSA签名,应该使用类似RS256的算法,而不是HMAC算法(如HS256)。
确保传递给JWT生成方法的密钥是正确的类型。对于RSA签名,需要传递RSA私钥;对于HMAC签名,需要传递一个SecretKey。
rsa+token思路
RSA加密结合Token的具体思路通常涉及几个关键步骤,以确保用户身份验证和数据传输的安全性。以下是该思路的详细讲解:
- RSA密钥对生成
服务器端:
应用启动时,服务器生成一对RSA密钥:公钥和私钥。
公钥用于加密数据,可以公开给任何人;私钥用于解密数据,必须严格保密。
2. 公钥分发
服务器端:
服务器通过安全的渠道(如HTTPS)将公钥分发给客户端。
公钥可以嵌入到网页中、通过API请求返回,或者以其他方式提供给客户端。
3. 客户端使用公钥加密密码
客户端:
当用户尝试登录时,客户端从服务器获取公钥。
用户输入密码后,客户端使用RSA公钥对密码进行加密。
加密后的密码(密文)随后被发送到服务器进行验证。
4. 服务器端验证密码
服务器端:
服务器接收到客户端发送的加密密码后,使用私钥对密文进行解密。
解密后得到明文密码,服务器将其与数据库中存储的密码(或密码哈希)进行比较。
如果密码匹配,服务器认为用户身份验证成功。
5. 生成并发送Token
服务器端:
一旦用户身份验证成功,服务器生成一个Token(如JWT)。
Token通常包含用户ID、用户名、过期时间等信息,并使用服务器的私钥进行签名以确保其完整性和真实性。
服务器将生成的Token发送给客户端。
6. 客户端存储并使用Token
客户端:
客户端接收到Token后,将其存储在本地(如浏览器的localStorage或cookie中)。
在后续的请求中,客户端将Token包含在请求头中发送给服务器,以证明其身份。
7. 服务器端验证Token
服务器端:
服务器接收到客户端发送的请求后,从请求头中提取Token。
服务器使用公钥验证Token的签名,确保Token未被篡改。
服务器检查Token的过期时间和其他相关信息,以确认其有效性。
如果Token有效,服务器处理请求并返回响应。
安全性考虑
HTTPS:确保所有与Token相关的请求和响应都通过HTTPS进行传输,以防止中间人攻击和数据泄露。
Token过期时间:设置合理的Token过期时间,以减少Token被盗用或滥用的风险。
刷新Token:可以考虑使用刷新Token机制,允许用户在Token过期后重新获取新的访问Token,而无需重新登录。
私钥安全:私钥是服务器端的关键资产,必须严格保密。私钥的泄露将导致Token签名验证失效,从而危及整个系统的安全性。因此,私钥应存储在安全的环境中,如硬件安全模块(HSM)或密钥管理服务(KMS)。
使用私钥签名token
当使用RSA算法来签名JWT(JSON Web Token)时,原理主要涉及到公钥密码学的基础概念。以下是签名和验证过程的简要概述:
生成RSA密钥对
选择密钥长度:通常选择2048位或更长的密钥长度来确保安全性。
生成密钥对:使用RSA算法生成一个包含公钥和私钥的密钥对。公钥用于加密数据或验证签名,而私钥用于解密数据或生成签名。
使用私钥签名JWT
构建JWT:JWT由三部分组成:Header(头部)、Payload(载荷)和Signature(签名)。Header和Payload被编码为Base64 URL格式,然后用一个点(.)连接起来。
生成签名:签名部分是通过使用私钥对Header和Payload的编码字符串应用某种哈希函数(如SHA-256),然后用私钥加密哈希值来生成的。这个加密的哈希值就是签名。
附加签名:将签名附加到Header和Payload之后,用另一个点(.)连接起来,形成完整的JWT。
使用公钥验证签名
分割JWT:接收者收到JWT后,首先将其分割成Header、Payload和Signature三部分。
重新计算哈希值:接收者使用与签名者相同的哈希函数对Header和Payload的编码字符串重新计算哈希值。
解密签名:接收者使用公钥对签名部分进行解密,得到原始的哈希值。
比较哈希值:接收者比较重新计算的哈希值和从签名中解密出来的哈希值。如果它们相同,那么签名就是有效的,JWT是未被篡改的;否则,签名无效,JWT可能被篡改。
安全性原理
非对称加密:RSA是一种非对称加密算法,意味着公钥和私钥在数学上是相关的,但从一个无法轻易推导出另一个。这使得公钥可以公开分发,而私钥保持私密。
数字签名:签名是一种确保消息完整性和发件人身份的方法。通过使用私钥对消息的哈希值进行加密来生成签名,接收者可以使用公钥解密签名并验证消息的完整性。
哈希函数:哈希函数将任意长度的数据映射为固定长度的哈希值。它们被设计为单向的(即,从哈希值无法反向推导出原始数据),并且对于不同的输入产生不同的输出。这使得哈希函数非常适合用于生成签名。
通过结合这些原理,RSA签名提供了JWT的完整性和发件人身份验证,确保消息在传输过程中未被篡改,并且确实来自预期的发送者。
好处:
使用RSA私钥签名JWT(JSON Web Token)具有多个好处,这些好处不仅超越了使用固定私钥的范围,还增强了整体的安全性和灵活性:
非对称性:RSA是一种非对称加密算法,意味着它使用一对密钥:公钥和私钥。公钥用于加密数据,而私钥用于解密数据(在RSA中,虽然通常不直接用于解密,但私钥用于签名,公钥用于验证签名)。这种非对称性提供了更高的安全性,因为私钥可以安全地保存在服务器上,而公钥可以公开给任何人。
签名验证:使用RSA私钥对JWT进行签名可以确保token的完整性和真实性。当客户端发送JWT给服务器时,服务器可以使用相应的公钥来验证签名,从而确认token在传输过程中没有被篡改,并且确实是由持有私钥的合法服务器签发的。
防止重放攻击:由于JWT通常包含过期时间信息,服务器可以拒绝接受已过期的token,从而防止重放攻击。即使攻击者截获了一个有效的token,如果该token已经过期,他们也无法使用它来访问受保护的资源。
密钥轮换:使用RSA私钥签名还允许更容易地进行密钥轮换。如果私钥泄露或怀疑被泄露,你可以生成一个新的私钥并更新服务器配置,而无需通知所有客户端或更改已分发的公钥。只需确保新公钥在所有需要验证JWT的地方都可用即可。
兼容性和灵活性:RSA是一种广泛使用的加密算法,得到了许多库和框架的支持。这意味着你可以轻松地将JWT集成到你的现有系统中,并利用现有的工具和库来处理签名和验证操作。此外,由于公钥是公开的,因此你可以根据需要与多个客户端或服务共享JWT,而无需担心密钥分发问题。
总之,使用RSA私钥签名JWT提供了一种安全、灵活且易于管理的身份验证和授权机制。与固定私钥相比,它提供了更高的安全性和更好的密钥管理选项。
私钥安全
私钥生成:
避免在应用程序代码中生成私钥:私钥应该在应用程序部署之前生成,并且最好是在一个安全的环境中生成,例如使用硬件安全模块(HSM)或安全的密钥管理服务(KMS)。
使用强随机数生成器:确保私钥是使用强随机数生成器生成的,以增加其不可预测性。
私钥存储:
不要将私钥存储在源代码中:这包括硬编码在类文件、配置文件或任何可以被版本控制或轻易访问的地方。
使用HSM或KMS:HSM提供了硬件级别的密钥保护,而KMS提供了集中管理和访问控制。
文件系统存储:如果必须将私钥存储在文件系统中,确保文件权限设置得足够严格,只有必要的服务或用户才能访问。
加密存储:即使私钥存储在安全的位置,也可以考虑使用额外的加密来保护它,特别是当私钥需要备份或传输时。
私钥使用:
仅在需要时使用私钥:避免在应用程序中长时间持有私钥,特别是在内存中。完成签名或解密操作后,应立即从内存中清除私钥。
限制私钥访问:确保只有经过授权的服务或代码可以访问私钥。这可能需要使用访问控制列表(ACL)或类似的机制。
密钥轮换:
定期更换私钥,以减少密钥泄露的风险。轮换周期应根据您的安全需求和风险评估来确定。
审计和监控:
实施审计和监控机制,以跟踪谁访问了私钥以及何时访问了私钥。这有助于检测任何异常行为或潜在的安全事件。
需改善
私钥硬编码:私钥PRIVATE_KEY_B64被硬编码在类中,这是不安全的。私钥应该在服务器端安全地存储,而不是直接编码在源代码中。
UserServiceImpl 类
直接解密密码:在login方法中,尝试使用RSA私钥直接解密客户端发送的密码。这通常不是最佳实践,因为密码应该存储为哈希值而不是明文或加密形式。应该解密密码,哈希它,然后与数据库中存储的哈希值进行比较。
异常处理:在捕获解密异常时,你抛出了一个运行时异常。这可能会导致上层调用者无法适当处理这个特定类型的错误。考虑使用更具体的异常类型,或者提供额外的上下文信息。
其他
安全性考虑:确保所有与Token相关的请求和响应都通过HTTPS进行传输,以防止中间人攻击和数据泄露。
Token存储:虽然将Token存储在客户端的localStorage中,但请确保遵循了最佳实践来保护它(例如,使用HTTPS-only cookies)。
错误处理:在前端和后端都添加适当的错误处理逻辑,以处理请求失败、验证失败等情况。
日志记录:考虑添加日志记录功能,以跟踪和调试潜在的问题。
代码组织:考虑将RSA相关的代码移到单独的模块或库中,以保持代码的清晰和组织。
测试:编写单元测试和功能测试来验证代码逻辑和安全性。
密钥管理:使用安全的密钥管理服务来存储和管理私钥。不要将私钥硬编码在代码中或存储在不安全的地方。
密码哈希:确保在数据库中存储的是密码的哈希值,而不是明文或加密后的密码。使用强哈希算法(如bcrypt、Argon2或PBKDF2)和唯一的盐值来哈希密码。
代码
sql
/*
Navicat Premium Data Transfer
Source Server : rootWindows
Source Server Type : MySQL
Source Server Version : 80036
Source Host : localhost:3306
Source Schema : dict
Target Server Type : MySQL
Target Server Version : 80036
File Encoding : 65001
Date: 05/05/2024 01:50:41
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`user_id` int(0) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`user_name` varchar(20) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '用户名',
`user_password_hash` char(32) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '用户密码,加密',
`user_avatar_url` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '头像URL',
`user_phone` varchar(20) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '手机号码',
`user_status` tinyint(0) UNSIGNED NOT NULL COMMENT '用户状态:已注销0、已冻结1、已激活2',
`version` int(0) UNSIGNED NOT NULL DEFAULT 1 COMMENT '版本',
`user_create_time` datetime(0) NOT NULL COMMENT '创建(注册)时间',
`update_time` datetime(0) NOT NULL COMMENT '最近更新时间',
`other1` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
`other2` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`user_id`) USING BTREE,
UNIQUE INDEX `user_name`(`user_name`) USING BTREE,
UNIQUE INDEX `user_phone`(`user_phone`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'abc', '123456', '1', '13300000000', 1, 1, '2024-05-05 00:37:34', '2024-05-05 00:37:40', NULL, NULL);
SET FOREIGN_KEY_CHECKS = 1;
UserController.java
package com.fshop.controller;
import com.fshop.common.R;
import com.fshop.dto.LoginUserDto;
import com.fshop.entity.User;
import com.fshop.rsa.RSAKeyPairGenerator;
import com.fshop.service.IUserService;
import com.fshop.util.JwtUtil;
import com.fshop.util.ServerResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
/**
* <p>
* 用户表 前端控制器
* </p>
*
* @author dev
* @since 2024-04-23
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private IUserService userService;
@GetMapping("{userId}")
@ResponseBody
public User getById(@PathVariable("userId") Integer userId){
return userService.getById(userId);
}
@PostMapping("login")
@ResponseBody
public ServerResult login(User user){
String userPasswordHash = user.getUserPasswordHash();
String userPhone = user.getUserPhone();
// System.out.println("controller层"+userName+userPasswordHash);
// System.out.println(result);
return userService.login(userPhone,userPasswordHash);
}
// 用户名回显
@GetMapping("loginUserName")
public R<String> loginUserName(HttpServletRequest request){
String token = request.getHeader("token");
LoginUserDto loginUserDto = null;
try {
loginUserDto = JwtUtil.parseToken(token);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (loginUserDto != null){
return R.ok("解析成功", loginUserDto.getUserName());
}
return R.error("解析失败");
}
@GetMapping("/public-key")
@ResponseBody
public String getPublicKey() {
// 返回Base64编码的公钥
return RSAKeyPairGenerator.publickey64();
}
}
LoginUserDto.java
package com.fshop.dto;
import lombok.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUserDto {
private Integer userId;
private String userName;
}
User.java
package com.fshop.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
* 用户表
* </p>
*
* @author dev
* @since 2024-04-23
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
@TableId(value = "user_id", type = IdType.AUTO)
private Integer userId;
/**
* 用户名
*/
private String userName;
/**
* 用户密码,md5加密
*/
private String userPasswordHash;
/**
* 头像URL
*/
private String userAvatarUrl;
/**
* 手机号码
*/
private String userPhone;
/**
* 用户状态:已注销0、已冻结1、已激活2
*/
private Byte userStatus;
/**
* 版本
*/
private Integer version;
/**
* 创建(注册)时间
*/
private LocalDateTime userCreateTime;
/**
* 最近更新时间
*/
private LocalDateTime updateTime;
private String other1;
private String other2;
}
JwtInterceptor.java
package com.fshop.interceptor;
import com.fshop.util.JwtUtil;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class JwtInterceptor implements HandlerInterceptor {
// 在访问前拦截
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("登录检查拦截器正在拦截URL>>>"+request.getRequestURI());
// 获取token
String token = request.getHeader("token");
System.out.println("拦截到的token:" + token);
if (token == null || token.isEmpty()) {
System.out.println("拦截到请求,跳转登录");
// 跳转到登录页面
//response.sendRedirect(request.getContextPath() + "/html/user/login.html");
return false;
}
// 检查token
return JwtUtil.checkToken(token);
}
}
UserMapper.java
package com.fshop.mapper;
import com.fshop.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 用户表 Mapper 接口
* </p>
*
* @author dev
* @since 2024-04-23
*/
public interface UserMapper extends BaseMapper<User> {
}
RSAKeyPairGenerator.java
package com.fshop.rsa;
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RSAKeyPairGenerator {
private static String PUBLIC_KEY_B64 = null;
private static String PRIVATE_KEY_B64 = null;
private static PublicKey PUBLIC_KEY = null;
private static PrivateKey PRIVATE_KEY = null;
private static final String RSA = "RSA";
private static final int KEY_SIZE = 2048; // 通常使用2048位或更长的密钥长度
static {
try {
// 生成RSA密钥对
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA);
keyPairGenerator.initialize(KEY_SIZE);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 获取公钥和私钥
PUBLIC_KEY = keyPair.getPublic();
PRIVATE_KEY = keyPair.getPrivate();
// 将公钥编码为Base64字符串,以便发送给客户端
PUBLIC_KEY_B64 = Base64.getEncoder().encodeToString(PUBLIC_KEY.getEncoded());
PRIVATE_KEY_B64 = Base64.getEncoder().encodeToString(PRIVATE_KEY.getEncoded());
// 将Base64编码的公钥字符串解码为字节数组,并转换为X509EncodedKeySpec对象
//byte[] encodedPublicKey = Base64.getDecoder().decode(PUBLIC_KEY_B64);
//X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(encodedPublicKey);
// 使用KeyFactory将公钥规范转换为PublicKey对象
//KeyFactory kf = KeyFactory.getInstance("RSA");
//PUBLIC_KEY = kf.generatePublic(publicKeySpec);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// public static String encrypt(String data) throws Exception {
// Cipher cipher = Cipher.getInstance("RSA");
// cipher.init(Cipher.ENCRYPT_MODE, PUBLIC_KEY);
// byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
// return Base64.getEncoder().encodeToString(encrypted);
// }
public static String decrypt(String encryptedData) throws Exception {
System.out.println("解析service层密码的私钥PRIVATE_KEY_B64=========="+PRIVATE_KEY_B64);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, PRIVATE_KEY);
byte[] decodedValue = Base64.getDecoder().decode(encryptedData);
byte[] decryptedValue = cipher.doFinal(decodedValue);
return new String(decryptedValue, StandardCharsets.UTF_8);
}
public static String publickey64(){
return PUBLIC_KEY_B64;
}
public static String privatekey64(){
return PRIVATE_KEY_B64;
}
public static void main(String[] args) throws Exception{
// try {
// 生成RSA密钥对
// KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA);
// keyPairGenerator.initialize(KEY_SIZE);
// KeyPair keyPair = keyPairGenerator.generateKeyPair();
//
// // 获取公钥和私钥
// PublicKey publicKey = keyPair.getPublic();
// PrivateKey privateKey = keyPair.getPrivate();
//
// // 将公钥编码为Base64字符串,以便发送给客户端
// String encodedPublicKey = Base64.getEncoder().encodeToString(publicKey.getEncoded());
// System.out.println("Encoded Public Key: " + encodedPublicKey);
// 私钥保留在服务器端,通常不会将其编码为字符串,而是直接用于解密操作
// 但如果需要,也可以将其编码为Base64字符串并安全地存储
// 示例:将私钥编码为Base64字符串(不推荐直接这样做,仅用于演示目的)
// String encodedPrivateKey = Base64.getEncoder().encodeToString(privateKey.getEncoded());
// 注意:不要在生产环境中打印或存储私钥的Base64编码!
// } catch (NoSuchAlgorithmException e) {
// e.printStackTrace();
// }
// String original = "Hello, RSA!";
// String encrypted = encrypt(original);
// System.out.println("Encrypted: " + encrypted);
//
// String decrypted = decrypt(encrypted);
// System.out.println("Decrypted: " + decrypted);
}
}
UserServiceImpl.java
package com.fshop.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fshop.entity.User;
import com.fshop.mapper.UserMapper;
import com.fshop.rsa.RSAKeyPairGenerator;
import com.fshop.service.IUserService;
import com.fshop.util.JwtUtil;
import com.fshop.util.ServerResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>
* 用户表 服务实现类
* </p>
*
* @author dev
* @since 2024-04-23
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private UserMapper userMapper;
@Override
public ServerResult login(String userPhone, String userPassword){
try {
System.out.println("seivice层");
System.out.println("userpassBefore:"+userPassword);
userPassword = RSAKeyPairGenerator.decrypt(userPassword);
System.out.println("userpassAfter:"+userPassword);
} catch (Exception e) {
throw new RuntimeException(e);
}
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("user_phone",userPhone).eq("user_password_hash",userPassword);
User user = userMapper.selectOne(wrapper);
if(user != null){
// System.out.println("service层user 登录");
String token = null;
try {
token = JwtUtil.createToken(user.getUserId(),user.getUserName());
} catch (Exception e) {
throw new RuntimeException(e);
}
return ServerResult.loginSuccess(token);
}
return ServerResult.loginFail("用户登录失败");
}
}
JwtUtil.java
package com.fshop.util;
import com.fshop.dto.LoginUserDto;
import com.fshop.rsa.RSAKeyPairGenerator;
import io.jsonwebtoken.*;
import java.security.Key;
import java.security.KeyFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtUtil {
private static final String privateKeyBase64 = RSAKeyPairGenerator.privatekey64(); // 私钥Base64字符串
private static final String publicKeyBase64 = RSAKeyPairGenerator.publickey64(); // 公钥Base64字符串
private static long expireTime = 1000*60*60*24;
// 将Base64编码的私钥转换为RSAPrivateKey对象
private static Key getPrivateKey() throws Exception {
byte[] encodedKey = Base64.getDecoder().decode(privateKeyBase64);
KeyFactory kf = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec keySpecPKCS8 = new PKCS8EncodedKeySpec(encodedKey);
return kf.generatePrivate(keySpecPKCS8);
}
// 将Base64编码的公钥转换为RSAPublicKey对象
private static Key getPublicKey() throws Exception {
byte[] encodedKey = Base64.getDecoder().decode(publicKeyBase64);
KeyFactory kf = KeyFactory.getInstance("RSA");
X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(encodedKey);
return kf.generatePublic(keySpecX509);
}
/**
* 创建新token
* @param userId 用户id
* @param userName 用户名
* @return token
*/
public static String createToken(Integer userId,String userName) throws Exception{
System.out.println("privateKeyBase64===="+privateKeyBase64);
Map<String,Object> claims = new HashMap<>();
claims.put("userId",userId);
claims.put("userName",userName);
// System.out.println("创建token"+userId);
// System.out.println("创建token"+userName);
Key privateKey = getPrivateKey();
JwtBuilder jwtBuilder = Jwts.builder().signWith(SignatureAlgorithm.RS256,privateKey)//签发算法(head部分),使用RS256算法和私钥签名
.setClaims(claims)//body数据,要唯一,自行设。payload部分数据 //1.(customerId,customerName)--token
.setIssuedAt(new Date())//设置签发时间:保证每次生成的token不同
.setExpiration(new Date(System.currentTimeMillis()+expireTime));//一天的有效时间
String token = jwtBuilder.compact();
return token;
}
//验证token是否有效
/**
* 验证token是否有效
* @param token 客户端携带的token
* @return 返回是否有效
*/
//2.验证token是否过期
public static boolean checkToken(String token) throws Exception{
System.out.println("checkToken publicKeyBase64===="+publicKeyBase64);
Key publicKey = getPublicKey();
if(token != null && !token.isEmpty()){
try {
Jwt parse = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
return true;
}catch (ExpiredJwtException e){
// System.out.println("token已经过期了");
return false;
} catch (Exception e) {
// System.out.println("无效的token");
return false;
}
}else
return false;
}
/**
* 解析token中的用户数据
* @param token 客户端携带的token
* @return 返回登录用户信息(customerIs)
*/
public static LoginUserDto parseToken(String token) throws Exception{
System.out.println("parseToken publicKeyBase64==="+publicKeyBase64);
Key publicKey = getPublicKey();
if(token != null && !token.equals("")) {
try {
Jwt parse = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
Map<String,Object> map = (Map<String, Object>)parse.getBody();
if(map != null){
Integer userId = (Integer)map.get("userId");//注意,此处的key 要与当初绑定进去的key一致
String userName = (String)map.get("userName");//注意,此处的key 要与当初绑定进去的key一致
LoginUserDto loginCustomer = new LoginUserDto(userId,userName);
// System.out.println("获得到的登录用户的信息是:" + loginCustomer);
return loginCustomer;
}else{
// System.out.println("获得到的登录用户的信息失败");
return null;
}
} catch (Exception e){
e.printStackTrace();
return null;
}
}else
return null;
}
}
R(重复)
package com.fshop.common;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import java.io.Serializable;
// @JsonInclude 保证序列化json的时候, 如果是null的对象, key也会消失
@Getter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class R<T> implements Serializable {
private static final long serialVersionUID = 7735505903525411467L;
// 成功值,默认为1
private static final int SUCCESS_CODE = 1;
// 失败值,默认为0
private static final int ERROR_CODE = 0;
// 状态码
private final int code;
// 消息
private String msg;
// 返回数据
private T data;
private R(int code) {
this.code = code;
}
private R(int code, T data) {
this.code = code;
this.data = data;
}
private R(int code, String msg) {
this.code = code;
this.msg = msg;
}
private R(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public static <T> R<T> ok() {
return new R<T>(SUCCESS_CODE, "success");
}
public static <T> R<T> ok(String msg) {
return new R<T>(SUCCESS_CODE, msg);
}
public static <T> R<T> ok(T data) {
return new R<T>(SUCCESS_CODE, data);
}
public static <T> R<T> ok(String msg, T data) {
return new R<T>(SUCCESS_CODE, msg, data);
}
public static <T> R<T> error() {
return new R<T>(ERROR_CODE, "error");
}
public static <T> R<T> error(String msg) {
return new R<T>(ERROR_CODE, msg);
}
public static <T> R<T> error(int code, String msg) {
return new R<T>(code, msg);
}
public static <T> R<T> error(ResponseCode res) {
return new R<T>(res.getCode(), res.getMessage());
}
@Override
public String toString() {
return "R{" +
"code=" + code +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
}
ResponseCode.java
package com.fshop.common;
import lombok.Getter;
@Getter
public enum ResponseCode{
ERROR(0,"操作失败"),
SUCCESS(1,"操作成功"),
DATA_ERROR(0,"参数异常"),
NO_RESPONSE_DATA(0,"无响应数据"),
CHECK_CODE_NOT_EMPTY(0,"验证码不能为空"),
CHECK_CODE_ERROR(0,"验证码错误"),
USERNAME_OR_PASSWORD_ERROR(0,"用户名或密码错误"),
ACCOUNT_EXISTS_ERROR(0,"该账号已存在"),
ACCOUNT_NOT_EXISTS(0,"该账号不存在"),
TOKEN_ERROR(2,"用户未登录,请先登录"),
NOT_PERMISSION(3,"没有权限访问该资源"),
ANONMOUSE_NOT_PERMISSION(0,"匿名用户没有权限访问"),
INVALID_TOKEN(0,"无效的票据"),
OPERATION_MENU_PERMISSION_CATALOG_ERROR(0,"操作后的菜单类型是目录,所属菜单必须为默认顶级菜单或者目录"),
OPERATION_MENU_PERMISSION_MENU_ERROR(0,"操作后的菜单类型是菜单,所属菜单必须为目录类型"),
OPERATION_MENU_PERMISSION_BTN_ERROR(0,"操作后的菜单类型是按钮,所属菜单必须为菜单类型"),
OPERATION_MENU_PERMISSION_URL_CODE_NULL(0,"菜单权限的按钮标识不能为空"),
ROLE_PERMISSION_RELATION(0, "该菜单权限存在子集关联,不允许删除");
private final int code;
private final String message;
ResponseCode(int code, String message) {
this.code = code;
this.message = message;
}
}
FshopAppApplication.java
package com.fshop;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.fshop.mapper")
public class FshopAppApplication {
public static void main(String[] args) {
SpringApplication.run(FshopAppApplication.class, args);
}
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fshop.mapper.UserMapper">
</mapper>
application.yaml
server:
port: 8080
servlet:
context-path: /fshop
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/dict?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
initial-size: 5 # 初始化连接池大小
max-active: 20 # 最大连接数
min-idle: 10 # 最小连接数
max-wait: 60000 # 超时等待时间
min-evictable-idle-time-millis: 600000 # 连接在连接池中的最小生存时间
max-evictable-idle-time-millis: 900000 # 连接在连接池中的最大生存时间
time-between-eviction-runs-millis: 2000 # 配置间隔多久进行一次检测,检测需要关闭的空闲连接
test-while-idle: true # 从连接池中获取连接时,当连接空闲时间大于timeBetweenEvictionRunsMillis时检查连接有效性
phy-max-use-count: 1000 # 配置一个连接最大使用次数,避免长时间使用相同连接造成服务器端负载不均衡
redis:
host: 127.0.0.1
port: 6379
database: 0
connect-timeout: 1800000
logging:
file:
name: logs/fshop.log
level:
com.fshop: info
login.html
<!-- 登录 -->
<!DOCTYPE html>
<html lang="ch">
<head>
<meta charset="UTF-8">
<title>登陆注册</title>
<script src="../../common/jquery-3.3.1.min.js"></script>
<script src="../../common/jquery.form.js"></script>
<script src="../../js/user/jsencrypt.min.js"></script>
</head>
<body>
<div class="content">
<div class="form sign-in">
<h2 class="h2">欢迎回来,果友们!</h2><br/>
<label>
<span>电话</span>
<input type="phone" id="L_phone" />
</label>
<label>
<span>密码</span>
<input type="password" id="L_pwd"/>
</label>
<button type="button" class="submit" id="submit" >登 录</button>
<a class="login_fg_a" href="../user/login-pwd.html">忘记密码?</a>
</div>
<div class="sub-cont">
<div class="img">
<div class="img__text m--up">
<h1>还未注册?</h1>
<p>立即注册,尝果味人生。</p>
</div>
<div class="img__text m--in">
<h2>已有帐号?</h2>
<p>好久不见了!快进入果粒世界。</p>
</div>
<div class="img__btn">
<span class="m--up">注 册</span>
<span class="m--in">登 录</span>
</div>
</div>
<div class="form sign-up">
<h2>立即注册,果友们!</h2>
<label>
<span>用户名</span>
<input type="text" id="R_user"/>
</label>
<label>
<span>密码</span>
<input type="password" id="R_passwors"/>
</label>
<label>
<span>确认密码</span>
<input type="password" id="R_tpwd"/>
</label>
<label class="label">
<span class="span">短信验证码</span><br>
<span><input type="text" class="L_note" ></input> </span>
<button class="L_but" type="button">获取验证码</button>
</label>
<a href="" class="mml">
<button type="button" class="submit" onclick="">注 册</button>
</a>
</div>
</div>
</div>
<script src="../../js/user/login.js"></script>
</body>
</html>
login.js
document.querySelector('.img__btn').addEventListener('click', function() {
document.querySelector('.content').classList.toggle('s--signup')
})
$(document).ready(function() {
// 当DOM加载完成后执行此函数
$.ajax({
url: 'http://localhost:8080/fshop/user/public-key', // 假设这是返回公钥的API端点
type: 'GET',
dataType: 'text', // 明确指定期望接收的数据类型为文本
success: function(publicKeyData) {
// publicKeyData现在包含从服务器获取的公钥数据,可能是Base64编码的字符串
console.log(publicKeyData);
// 你可以将其解码并存储在某个变量中以备后用
//var publicKey = atob(publicKeyData); // 如果publicKeyData是Base64编码的
var publicKey = publicKeyData;
// 现在你可以将publicKey存储在一个全局变量中,以便在页面的其他地方使用
window.publicKey = publicKey;
// 如果你需要在页面加载后立即使用公钥进行某些操作,可以在这里添加代码
// ...
},
error: function(jqXHR, textStatus, errorThrown) {
// 处理错误情况
console.error("获取公钥时发生错误: " + textStatus, errorThrown);
}
});
});
// function btn(){
// window.location.href = '../index.html';
// }
$("#submit").click(function (){
let encrypt = new JSEncrypt();
encrypt.setPublicKey(window.publicKey);
console.log(window.publicKey);
let userphone = $('#L_phone').val();
let password = $('#L_pwd').val();
// 使用公钥加密密码
let encryptedPassword = encrypt.encrypt(password);
console.log(userphone);
console.log(password);
console.log(encryptedPassword);
let formData = {
userPhone: userphone,
userPasswordHash: encryptedPassword
};
//let formDataSerialized = $.param(formData);
$.ajax({
url: 'http://localhost:8080/fshop/user/login', //
type: 'POST',
data: formData,
contentType: 'application/x-www-form-urlencoded; charset=UTF-8', // 直接发送对象
success: function(result) {
console.log(result)
if(result.code ==200){
localStorage.setItem("token",result.data); // 1.保存token
window.location.href = "../../index.html";
}else{
// $(".login_tip").text(result.data);
}
},
error: function() {
// $('#response').text('Error occurred: ' + error);
// 在此处处理 AJAX 请求错误。
}
});
})
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>果粒优选</title>
<script src="./common/jquery-3.3.1.min.js"></script>
<script src="./common/vue.js"></script>
<script src="./common/element-ui/lib/index.js"></script>
</head>
<body>
<div id="app">
<!-- 顶部工具栏 -->
<div id="tool-nav">
<div class="center clearfix">
<ul class="fl">
<li class="tool-nav-li"><span>欢迎来到果粒优选!</span></li>
<li class="tool-nav-li enable-click">
<a id="login-if" href="./html/user/login.html">登录/注册</a>
</li>
</ul>
</div>
</div>
</div>
</body>
<script type="text/javascript" src="./js/index.js"></script>
<script>
</script>
</html>
index.js
let token = localStorage.getItem('token');
console.log(token);
let loginIf = document.getElementById('login-if');
if (token != null && token !== '') {
// 已经登陆,在工具栏显示用户名
$.ajax({
url: '/fshop/user/loginUserName',
type: 'GET',
headers: {'token': token},
success: function (result) {
if (result.code === 1) {
loginIf.innerText = result.data;
//loginIf.setAttribute('href', './html/user/user-evaluate.html');
}
}
});
}
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.fshop</groupId>
<artifactId>fshop-app</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>fshop-app</name>
<description>fshop-app</description>
<packaging>war</packaging>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- SPRINGBOOT -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- JDBC -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- MYBATIS PLUS -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- DRUID -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.9</version>
</dependency>
<!--REDIS-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- ALIPAY -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.39.58.ALL</version>
</dependency>
<!-- LOMBOK -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.6</version>
</plugin>
</plugins>
</build>
</project>
login-pwd.html
<!-- 找回密码 -->
<!DOCTYPE html>
<html>
<head>
<title>忘记密码</title>
</head>
<body>
<div class="login-box">
<a class="fg_lg" href="./login.html">< 返回</a>
<h1>忘记密码</h1>
<form class="form_fg" action="">
<input type="email" placeholder="请输入您的电话">
<input type="password" placeholder="请输入修改后的密码">
<button id="L_but" type="button">获取验证码</button>
<input type="text" placeholder="请输入验证码">
<button type="submit">提交</button>
</form>
</div>
</body>
</html>
<script src="https://cdn.bootcdn.net/ajax/libs/jsencrypt/3.3.2/jsencrypt.min.js"></script>