基于 JWT 和 Redis 实现高效账号密码验证码登录功能

基于 JWT 和 Redis 实现高效账号密码验证码登录功能

一、为什么选择 JWT 和 Redis?

  1. JWT 的优势
    • JWT 是一种开放标准,它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息。在登录场景中,JWT 可以携带用户的身份信息,并且可以进行数字签名,确保信息的完整性和真实性。
    • 无状态特性:JWT 使得服务器无需在内存中存储用户的会话状态,从而减轻了服务器的负担,提高了可扩展性。
    • 跨平台性:JWT 可以在不同的平台和设备之间轻松传递,适用于多种客户端类型,如网页、移动应用等。
  2. Redis 的作用
    • 作为一种高性能的内存数据库,Redis 可以快速存储和检索临时数据。在登录过程中,我们可以利用 Redis 来存储验证码、用户会话信息等,提高系统的响应速度。

二、实现账号密码验证码登录的流程

  1. 用户输入账号密码和验证码
    • 用户在界面输入账号和密码,验证码方式登录。
    • 系统接收到用户的输入后,进行初步的校验,确保输入的格式正确。
  2. 生成验证码并存储到 Redis
    • 前端获取验证码接口,后端返回验证码图片的base64和一个uuid,并将uuid作为key其存储到 Redis 中。同时,设置一个过期时间,以确保验证码的有效性。
  3. 校验账号密码或验证码
    • 用户输入的是账号密码和验证码,检查验证码是否正确,用户账号和密码与数据库中的存储的用户信息进行比对。如果匹配成功,则用户身份验证通过。
  4. 生成 JWT 令牌
    • 一旦用户身份验证通过,系统会生成一个 JWT 令牌。这个令牌包含了用户的身份信息,如用户 ID。
    • JWT 令牌可以使用密钥进行签名,确保其安全性。
  5. 返回 JWT 令牌给客户端
    • 系统将生成的 JWT 令牌返回给前端,后续的请求中,前端可以将 JWT 令牌包含在请求头中,服务器可以通过验证 JWT 令牌来确定用户的身份。

三、拦截器中的 Token 验证

  1. 拦截器中的 Token 验证
  • 拦截器在接收到请求时,首先从请求头的 Authorization 字段中判断是否存在 Token。如果存在 Token,说明用户可能已经登录过,需要进一步验证 Token 的有效性。
  • 从 Token 中提取用户 ID,通过用户 ID 在 Redis 中获取存储的 Token。这样可以确保 Token 是由服务器生成并存储在可靠的地方,而不是被篡改或伪造的。
  • 比较从请求中获取的 Token 和从 Redis 中获取的 Token 是否一致。如果一致,说明 Token 有效;如果不一致,说明 Token 可能已经过期、被篡改或者用户未登录。
  1. 将 Token 信息放入请求属性
  • 如果 Token 验证通过,将 Token 中的相关信息(如用户 ID)放入请求属性中。这样,后续的业务逻辑可以方便地从请求属性中获取用户信息,而无需再次解析 Token。

四、优势分析

  1. 安全性高
    • 通过将 Token 存储在 Redis 中,并在每次请求时进行验证,可以有效地防止 Token 被篡改或伪造。
    • 利用用户 ID 在 Redis 中获取 Token 进行比较,可以确保 Token 的来源可靠。
  2. 性能优化
    • Redis 是一种内存数据库,具有高速读写的特点。将 Token 存储在 Redis 中可以快速地进行验证,提高系统的响应速度。
    • 拦截器在请求处理的早期阶段进行 Token 验证,可以避免不必要的业务逻辑处理,提高系统的整体性能。
  3. 易于扩展
    • 这种方式可以方便地与其他安全机制(如加密、签名等)结合使用,进一步提高系统的安全性。
    • 如果需要支持多用户、多角色等复杂场景,可以通过在 Token 中添加更多的信息,并在验证过程中进行相应的处理。

五、可能的问题及解决方案

  1. Token 过期处理
    • 问题:如果 Token 过期,用户需要重新登录,这可能会影响用户体验。
    • 解决方案:可以在 Token 中设置过期时间,并在拦截器中判断 Token 是否过期。如果 Token 即将过期,可以自动刷新 Token,并将新的 Token 返回给客户端。
  2. Redis 故障处理
    • 问题:如果 Redis 出现故障,无法获取存储的 Token,会导致系统无法正常验证用户身份。
    • 解决方案:可以考虑使用备份机制,将 Token 同时存储在其他可靠的地方,如数据库中。当 Redis 出现故障时,可以从备份中获取 Token 进行验证。
  3. 并发请求处理
    • 问题:在高并发情况下,多个请求同时进行 Token 验证,可能会导致 Redis 的压力过大。
    • 解决方案:可以使用缓存机制,将验证通过的 Token 缓存一段时间,减少对 Redis 的访问次数。同时,可以考虑使用分布式锁等技术,确保在并发情况下 Token 的验证过程是正确的。

六、以上实现总结

通过将 Token 存储在 Redis 中,并在拦截器中进行验证,可以实现一种安全、高效的用户身份验证机制。这种方式具有安全性高、性能优化、易于扩展等优点,但也需要注意处理可能出现的问题,如 Token 过期、Redis 故障和并发请求等。在实际应用中,可以根据具体情况进行优化和调整,以满足不同的业务需求。

七、代码实现

  • 引入依赖
<!-- jwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
  • jwt工具类
public class JWTUtil {
    private static final String SECRET_KEY = "xxxxx"; // 定义key
    
    private static final long EXPIRATION_TIME = 864_000_000;// 10 天有效期

    // 生成 JWT
    public static String generateToken(String userId) {
        return Jwts.builder()
            .setSubject(userId)
            .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
            .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
            .compact();
    }

    // 从 JWT 中获取 userId
    public static String getUserIdFromToken(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(SECRET_KEY)
            .parseClaimsJws(token)
            .getBody();
        return claims.getSubject();
    }

    // 验证 JWT 是否过期
    public static boolean isTokenExpired(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(SECRET_KEY)
            .parseClaimsJws(token)
            .getBody();
        return claims.getExpiration().before(new Date());
    }
}
  • 保存token和获取token
@Service
public class AuthService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String REDIS_TOKEN_PREFIX = "user_token:";

    // 保存 JWT 到 Redis
    public void saveToken(String userId, String token) {
        redisTemplate.opsForValue().set(REDIS_TOKEN_PREFIX + userId, token, 30, TimeUnit.DAYS);
    }

    // 从 Redis 中获取 Token
    public String getToken(String userId) {
        return redisTemplate.opsForValue().get(REDIS_TOKEN_PREFIX + userId);
    }

    // 删除 Redis 中的 Token(登出)
    public void deleteToken(String userId) {
        redisTemplate.delete(REDIS_TOKEN_PREFIX + userId);
    }
}
  • 登录Controller
@RestController
@RequestMapping("/sysLogin")
@Api(tags = "用户登录接口")
public class SysLoginController {
    Logger logger = LogManager.getLogger(this.getClass());
    @Autowired
    private SysLoginService loginService;

    @Autowired
    private DefaultKaptcha defaultKaptcha;

    @Autowired
    public StringRedisTemplate redisTemplate;

    // 登录接口
    @PostMapping("/login")
    @ApiOperation(value = "用户登录",tags = "用户登录接口")
    public Result<String> login(@RequestBody LoginBody loginBody, HttpServletRequest request) {
        logger.info("Request URI: {}, Method: {}, Params: {}", request.getRequestURI(), "login", loginBody.toString());
        // 调用serivce登录认证,返回token令牌
        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                loginBody.getUuid());
        logger.info("Login success, token: {}", token);
        return Result.success(Constants.TOKEN, token);
    }

    // 登出接口
    @PostMapping("/logout")
    @ApiOperation(value = "用户登出",tags = "登出接口接口")
    public Result<String> logout(@RequestParam String userId,HttpServletRequest request) {
        logger.info("Request URI: {}, Method: {}, Params: {}", request.getRequestURI(), "logout", userId);
        loginService.logout(userId);
        return Result.success();
    }

    /**
     * 生成验证码
     *
     * @throws Exception
     */
    @RequestMapping(value = "/captcha", method = RequestMethod.GET)
    public Result<Map<String, Object>> getCaptchaImage(HttpServletRequest request) throws Exception {
        logger.info("Request URI: {}, Method: {}", request.getRequestURI(), "getCaptchaImage");
        // 保存验证码信息
        String uuid = UUIDGenerator.generate();
        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
        String capStr =  defaultKaptcha.createText();;
        String code = defaultKaptcha.createText();

        BufferedImage image = defaultKaptcha.createImage(capStr);
        redisTemplate.opsForValue().set(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
        // 转换流信息写出
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        try {
            ImageIO.write(image, "jpg", os);
        } catch (IOException e) {
            return Result.error(e.getMessage());
        }
        Map<String, Object> map = new HashMap<>();
        map.put("uuid", uuid);
        map.put("img", Base64.encode(os.toByteArray()));
        logger.info("Generate captcha success, uuid: {}, code: {}", uuid, code);
        return Result.success(map);
    }
  • service方法
@Component
public class SysLoginService {

    @Autowired
    private AuthService authService;
	//用户表
    @Autowired
    private AccoutInfoService accoutInfoService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    Logger logger = LogManager.getLogger(this.getClass());
    /**
     * 登录验证
     *
     * @param username 用户名
     * @param password 密码
     * @param code 验证码
     * @param uuid 唯一标识
     * @return 结果
     */
    public String login(String username, String password, String code, String uuid){
        // 验证码校验
        validateCaptcha(username, code, uuid);
        // 登录前置校验
        String userId = loginPreCheck(username, password);

        String token = JWTUtil.generateToken(userId);
        authService.saveToken(userId, token);
        return token;
    }

    /**
     * 退出登录
     * @param userId
     */
    public void logout(String userId) {
        authService.deleteToken(userId);
    }

    public void validateCaptcha(String username, String code, String uuid){
        logger.info("{}验证码校验开始",username);
        // 构建验证码的缓存键
        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
        // 获取验证码
        String captcha = redisTemplate.opsForValue().get(verifyKey);
        if (captcha == null || StringUtils.isEmpty(captcha)){
            // 抛出验证码已失效异常
            logger.info("{}验证码已失效",username);
            throw new CaptchaExpireException();
        }
        if (!code.equalsIgnoreCase(captcha) ){
            // 抛出验证码不匹配异常
            logger.info("{}验证码不匹配",username);
            throw new CaptchaException();
        }
    }

    public String loginPreCheck(String username, String password){
        AccoutInfo accoutInfo = accoutInfoService.getAccoutInfoByLoginName(username);
        //效验用户名是否存在
        if (accoutInfo == null) {
            //抛出用户不存在异常
            logger.info("{}用户不存在",username);
            throw new UserDoesNotExistException();
        }
        int status = accoutInfo.getStatus();
        if (status == UserStatusEnum.DISABLED.getCode()) {
            logger.info("登录用户:{} 已被禁用.", username);
            throw new ServiceException("用户已被禁用", HttpStatus.FORBIDDEN.value());
        }
        String userPassword = accoutInfo.getPassword();
        //验证密码,可自行定义密码规则
        if (!userPassword.equals(DigestUtils.md5Hex(password))){
            //密码不正确
            logger.info("{}密码不正确:{}",username,DigestUtils.md5Hex(userPassword));
            throw new UserPasswordNotMatchException("用户密码不匹配");
        }
        return accoutInfo.getId();
    }

}
  • 拦截器实现
@Component
public class JWTInterceptor implements HandlerInterceptor {


    @Autowired
    private AuthService authService;

    @Autowired
    private ObjectMapper objectMapper;

    Logger logger = LogManager.getLogger(this.getClass());


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        long startTime = System.currentTimeMillis();
        // 记录请求开始时间
        request.setAttribute("startTime", startTime);

        // 记录请求信息
        String uri = request.getRequestURI();
        String method = request.getMethod();
        String params = getParams(request);
        logger.debug("****************************************************");
        logger.info("Request URI: {}, Method: {}, Params: {}", uri, method, params);
        logger.debug("****************************************************");
        try {
            // 获取请求头中的 token
            String token = request.getHeader("Authorization");
            if (token == null || token.isEmpty()) {
                logger.error("Missing token");
                sendErrorResponse(response, "Missing token", HttpStatus.UNAUTHORIZED.value());
                return false;
            }

            // 从 token 中解析 userId
            String userId = JWTUtil.getUserIdFromToken(token);

            // 检查 Redis 中的 token 是否有效
            String redisToken = authService.getToken(userId);
            if (redisToken == null || !redisToken.equals(token)) {
                logger.error("Token expired or invalid");
                sendErrorResponse(response, "Token expired or invalid", HttpStatus.UNAUTHORIZED.value());
                return false;
            }

            // 将 userId 存入请求属性,供后续使用
            request.setAttribute("userId", userId);
            return true;
        } catch (Exception e) {
            logger.error("Invalid token:{}",e);
            sendErrorResponse(response, "Invalid token", HttpStatus.UNAUTHORIZED.value());
            return false;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        long startTime = (long) request.getAttribute("startTime");
        long endTime = System.currentTimeMillis();
        long executeTime = endTime - startTime;

        // 记录响应状态和执行时间
        int status = response.getStatus();
        logger.info("Response Status: {}, Execute Time: {} ms", status, executeTime);
    }

    private String getParams(HttpServletRequest request) {
        Map<String, String[]> parameterMap = request.getParameterMap();
        StringBuilder paramsBuilder = new StringBuilder();
        for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
            if (paramsBuilder.length() > 0) {
                paramsBuilder.append(", ");
            }
            paramsBuilder.append(entry.getKey()).append(": ").append(Arrays.toString(entry.getValue()));
        }
        return paramsBuilder.toString();
    }

    private void sendErrorResponse(HttpServletResponse response, String message, int customStatusCode) throws IOException {
        // 设置标准的HTTP状态码
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json;charset=UTF-8");

        Map<String, Object> errorResponse = new HashMap<>();
        errorResponse.put("msg", message);
        errorResponse.put("code", customStatusCode);

        response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
    }

}
  • 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private SysLogInterceptor sysLogInterceptor;

    @Autowired
    private JWTInterceptor jwtInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        // 注册 JWT 拦截器,并指定拦截路径
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/sysLogin/**") // 拦截所有请求
                .excludePathPatterns("/sysLogin/login", "/sysLogin/logout","/sysLogin/captcha"); // 登录和登出接口不拦截
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值