003 登录rsa+token

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的具体思路通常涉及几个关键步骤,以确保用户身份验证和数据传输的安全性。以下是该思路的详细讲解:

  1. 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>

  • 27
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

简 洁 冬冬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值