基于SpringBoot+Redis实现模拟短信登录/注册功能

前言

本篇为SpringBoot+Redis项目实战笔记。主要是简单记录一下,基于SpringBoot+Redis实现模拟短信登录/注册,刷新令牌拦截器、登录拦截器的功能。如此一来也就实现了单点登录(SSO)的功能,一次登录,即可授权访问所有系统。

一、实战案例

1.添加依赖

(1)pom.xml

<!-- redis 客户端 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Hutool -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.17</version>
</dependency>

2.添加Redis配置

(1)application.yml

spring:
  #---- ^ redis 配置 ----#
  cache:
    type: redis # 缓存类型
  redis:
    host: localhost # Redis数据库主机
    port: 6379 # Redis数据库端口
    password: 123456 # Redis数据库密码
    database: 15 # Redis数据库索引
    timeout: 5000 # Redis数据库连接超时时间,尽量不要零
    jedis:
      pool:
        max-active: 8 # Redis数据库连接池最大连接数
        max-idle: 8 # Redis数据库连接池最大空闲连接数
        min-idle: 0 # Redis数据库连接池最小空闲连接数
        max-wait: -1 # Redis数据库连接池最大阻塞等待时间
  #---- / redis 配置 ----#

3.控制层

(1)UserController.java

package org.example.controller;

import org.example.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;

@Controller
@RequestMapping(value = "api")
public class UserController {

    @Autowired
    private UserServiceImpl userService;

    /**
     * 发送验证码
     * {
     *     "phone": "13800138000"
     * }
     */
    @PostMapping(value = "sendCode")
    @ResponseBody
    @CrossOrigin
    public <T> T sendCode (@RequestBody HashMap<String, Object> data) {
        return userService.sendCode(data);
    }

    /**
     * 登录和注册
     * {
     *     "phone": "13800138000",
     *     "code": "123456"
     * }
     */
    @PostMapping(value = "login")
    @ResponseBody
    @CrossOrigin
    public <T> T login (@RequestBody HashMap<String, Object> data) {
        return userService.login(data);
    }
}

4.接口层

(1)IUserService.java

package org.example.service;

import java.util.HashMap;

public interface IUserService {
    <T> T sendCode(HashMap<String, Object> data);

    <T> T login(HashMap<String, Object> data);
}

5.实现层

(1)UserServiceImpl.java

package org.example.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import org.example.pojo.dto.UserDTO;
import org.example.pojo.entity.User;
import org.example.service.IUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Service
public class UserServiceImpl implements IUserService {

    private static final String LOGIN_CODE_KEY = "Login-Code-";
    private static final int LOGIN_CODE_TTL = 300;
    private static final String LOGIN_USER_KEY = "Login-User-";
    private static final int LOGIN_USER_TTL = 60;

    private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public <T> T sendCode(HashMap<String, Object> data) {
        HashMap<String, Object> responseObj = new HashMap<>();
        try {
            String phone = String.valueOf(data.get("phone"));
            // 校验手机号
            String regex = "^1[3|4|5|6|7|8|9][0-9]\\d{8}$";
            if (!phone.matches(regex)) {
                responseObj.put("code", 500);
                responseObj.put("success", false);
                responseObj.put("msg", "手机号格式错误");
                return (T) responseObj;
            }
            // 生成验证码
            Random random = new Random();
            String code = Integer.toString(100000 + random.nextInt(900000));
            // 保存验证码到Redis缓存(set key value ex 300)
            stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.SECONDS);
            // 模拟发送验证码
            log.info("对手机号{}发送验证码成功,验证码为{}", phone, code);

            responseObj.put("code", 200);
            responseObj.put("success", true);
            responseObj.put("data", code);
            responseObj.put("msg", "发送成功,验证码为" + code + ",验证码在5分钟内有效。");
        } catch (Exception e) {
            responseObj.put("code", 500);
            responseObj.put("success", false);
            responseObj.put("msg", e.getMessage());
        }
        return (T) responseObj;
    }

    @Override
    public <T> T login(HashMap<String, Object> data) {
        HashMap<String, Object> responseObj = new HashMap<>();
        try {
            String phone = String.valueOf(data.get("phone"));
            String code = String.valueOf(data.get("code"));
            // 校验手机号
            String regex = "^1[3|4|5|6|7|8|9][0-9]\\d{8}$";
            if (!phone.matches(regex)) {
                responseObj.put("code", 500);
                responseObj.put("success", false);
                responseObj.put("msg", "手机号格式错误");
                return (T) responseObj;
            }
            // 从Redis缓存获取验证码并且检验验证码
            String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
            if (cacheCode == null || !cacheCode.equals(code)){
                responseObj.put("code", 500);
                responseObj.put("success", false);
                responseObj.put("msg", "验证码错误");
                return (T) responseObj;
            }
            // 模拟数据库查询
            User user = new User(10001L, phone, "全王", "123456");
            if (user == null) {
                // 用户不存在,保存且返回用户信息
                // user = saveUserWithPhone();
            }
            // 创建一个键为随机生成的UUID,值为用户信息的登录令牌,并存储到Redis缓存,并设置60分钟生存时间
            String uuidStr = UUID.randomUUID().toString();
            String uuid = uuidStr.substring(0, 8) + uuidStr.substring(9, 13) + uuidStr.substring(14, 18) + uuidStr.substring(19, 23) + uuidStr.substring(24);
            String tokenKey = LOGIN_USER_KEY + uuid;
            UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
            Map<String, Object> tokenVal = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
            stringRedisTemplate.opsForHash().putAll(tokenKey, tokenVal);
            stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
            // 返回token
            responseObj.put("code", 200);
            responseObj.put("success", true);
            responseObj.put("data", tokenKey);
            responseObj.put("msg", "登录成功");
        } catch (Exception e) {
            responseObj.put("code", 500);
            responseObj.put("success", false);
            responseObj.put("msg", "系统出错");
            e.printStackTrace();
        }
        return (T) responseObj;
    }
}

6.请求持有者

(1)RequestHolder.java

package org.example.holder;

import org.example.pojo.dto.UserDTO;

/**
 * 请求持有者
 */
public class RequestHolder {

    private static final ThreadLocal<UserDTO> userDTOThreadLocal = new ThreadLocal<>();

    public static void setUser(UserDTO userDTO) {
        userDTOThreadLocal.set(userDTO);
    }

    public static UserDTO getUser() {
        return userDTOThreadLocal.get();
    }

    public static void remove() {
        userDTOThreadLocal.remove();
    }
}

7.简单对象

(1)User.java

package org.example.pojo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long id;
    private String phone;
    private String username;
    private String password;
}

(2)UserDTO.java

package org.example.pojo.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class UserDTO {
    private String phone;
    private String username;
}

8.拦截器

(1)LoginInterceptor.java

package org.example.interceptor;

import cn.hutool.json.JSONObject;
import org.example.holder.RequestHolder;
import org.example.pojo.dto.UserDTO;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;

/**
 * 用户登录拦截器
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json");

        HashMap<String, Object> responseObj = new HashMap<>();
        UserDTO userDTO = RequestHolder.getUser();
        System.out.println("LoginInterceptor :: userDTO -> " + userDTO);
        if (userDTO == null) {
            responseObj.put("code", 401);
            responseObj.put("success", false);
            responseObj.put("msg", "无权限");
            JSONObject json = new JSONObject(responseObj);
            response.getWriter().println(json);
            return false;
        }
        return true;
    }
}

(2)RefreshTokenInterceptor.java

package org.example.interceptor;

import cn.hutool.core.bean.BeanUtil;
import io.netty.util.internal.StringUtil;
import org.example.holder.RequestHolder;
import org.example.pojo.dto.UserDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 刷新Token拦截器
 */
@Component
public class RefreshTokenInterceptor implements HandlerInterceptor {

    private static final int LOGIN_USER_TTL = 60;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String tokenKey = request.getHeader("Authorization");
        System.out.println("RefreshTokenInterceptor :: tokenKey -> " + tokenKey);

        if (StringUtil.isNullOrEmpty(tokenKey)) {
            // 无令牌的键,先不进行拦截
            return true;
        } else {
            // 有令牌的键
            Map<Object, Object> tokenVal = stringRedisTemplate.opsForHash().entries(tokenKey);
            System.out.println("RefreshTokenInterceptor :: tokenVal -> " + tokenVal);
            if (tokenVal.isEmpty()) {
                // 无令牌的值,先不进行拦截
                return true;
            } else {
                // 有令牌的值,Hash数据转UserDTO对象,全局存储用户信息,刷新60分钟生存时间
                UserDTO userDTO = BeanUtil.fillBeanWithMap(tokenVal, new UserDTO(), false);
                System.out.println("RefreshTokenInterceptor :: userDTO -> " + userDTO);
                RequestHolder.setUser(userDTO);
                stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
                return true;
            }
        }
    }

    /**
     * 若preHandle方法返回false,则会调用完此方法后再返回
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        // RequestHolder.remove();
    }
}

9.资源配置

(1)ResourceConfig.java

package org.example.config;

import org.example.interceptor.LoginInterceptor;
import org.example.interceptor.RefreshTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class ResourceConfig extends WebMvcConfigurationSupport {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Autowired
    private RefreshTokenInterceptor refreshTokenInterceptor;

    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // Todo
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(refreshTokenInterceptor).addPathPatterns("/**").excludePathPatterns(
                "/api/sendCode",
                "/api/login"
        );
        registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns(
                "/api/sendCode",
                "/api/login"
        );
    }
}

二、运行效果

1.发送验证码

// ...

2.模拟登录

// ...

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值