Dark Horse Comments - project integration git and redis to achieve SMS verification code login

Table of contents

IDEA integrates git

Problems with traditional sessions 

redis scheme

Business Process

The chosen data structure

overall access process

Send SMS verification code

 Get verification code

Configure login interceptor

Interceptor registration configuration class

interceptor

User Status Refresh Issue

Refresh problem solution


IDEA integrates git

The remote warehouse uses Code Cloud, create a warehouse, and copy the url of the warehouse

 Click in the idea, the git option appears, click ok

 Then right click on the project and click on remotes

 Fill in the url to integrate git

Problems with traditional sessions 

Traditional login authentication uses session for login authentication, and stores the login verification code and user information in the session. We use the session to operate data. What's wrong with this?

Each tomcat server has its own session, assuming that the user visits the first tomcat for the first time and stores his information in the session of the first server, but the user visits the second tomcat for the second time tomcat, then on the second server, there must be no session stored in the first server, that is, the session is not shared among the servers , so there will be problems with the entire login interception function at this time, and the redis data itself is shared Yes, you can avoid the problem of session sharing

redis scheme

Business Process

  • Store the verification code in redis
  • Store user data in redis

The chosen data structure

The String type is used to store the verification code, and the key uses the business code + mobile phone number

Use hash when storing user data, key use business code + random tonke

Hash can store each field in the object independently, and can do CRUD for a single field, and takes up less memory. In fact, the String type can also be used, but the hash type consumes less memory, so the String type is used

overall access process

 When the registration is complete, the user will log in to verify whether the mobile phone number and verification code submitted by the user are consistent. If they are consistent, the user information will be queried according to the mobile phone number. If it does not exist, a new one will be created. Finally, the user data will be saved to redis and generated Token is the key of redis. When we verify whether the user is logged in, we will carry the token for access, take out the value corresponding to the token from redis, and judge whether this data exists. If not, intercept it. If it exists, save it to In threadLocal, simplify the code for subsequent business to obtain user information. Subsequent business needs to log in the user's information, as long as it is fetched from threadLocal, it does not need to be fetched from redis to increase complexity, and finally released .

Redis business code constants (continue to add later)

public class RedisConstants {
    //发送验证码业务标识
    public static final String LOGIN_CODE_KEY = "login:code:";
    public static final Long LOGIN_CODE_TTL = 2L;
    //用户登录业务标识
    public static final String LOGIN_USER_KEY = "login:token:";
    public static final Long LOGIN_USER_TTL = 30L;

   
}

Send SMS verification code

Use the form of log printing to print the verification code on the console

 public Result sendCode(String phone) {
        //校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号码格式不正确");
        }
        //手机号格式正确生成验证码
        String code = RandomUtil.randomNumbers(6);
        //保存到redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //打印日志
        log.debug("手机验证码为:" + code);
        return Result.ok();
    }

 Get verification code

We need to filter user sensitive information, and only return necessary user information data to the front end. We need to encapsulate the dto object , and at the same time generate a token in uuid as the key for redis to fetch data. Finally, we need to set the expiration time to prevent reids memory from exploding. Set it to 30 minutes, because the session expiration time is also 30 minutes

 public Result login(LoginFormDTO loginForm) {
        String phone = loginForm.getPhone();
        String code = loginForm.getCode();
        //校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号码格式不正确");
        }
        //从redis取验证码,进行比对
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        if (cacheCode == null || !cacheCode.equals(code)) {
            return Result.fail("验证码错误");
        }
        //根据电话号码从数据库查询用户是否存在
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getPhone, phone);
        User user = userMapper.selectOne(queryWrapper);
        //不存在则创建
        if (user == null) {
            user = createUserWithPhone(phone);
        }
        String token = UUID.randomUUID().toString(true);
        //只返回不敏感的信息,使用dto进行封装
        UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties(user, userDTO);
        //将dto对象转化为map结构
        Map<String, String> userMap = new HashMap<>();
        userMap.put("id", userDTO.getId().toString());
        userMap.put("icon", userDTO.getIcon());
        userMap.put("nickName", userDTO.getNickName());
//业务代码+token形成redis中的key
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

        return Result.ok(token);
    }

Configure login interceptor

Interceptor registration configuration class

@Configuration
public class MvcConfig implements WebMvcConfigurer {
  @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}

interceptor

The stringRedisTemplate object required by the interceptor cannot be directly injected through @Autowired

Because the LoginInterceptor object is manually created by us , it is not under the control of spring, and it is not in the spring container , so it cannot be injected into the bean in the spring container. It can only be injected into the stringRedisTemplate object through the construction method in the configuration class.

public class LoginInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;

    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            response.setStatus(401);
            return false;
        }
        String tokenKey = LOGIN_USER_KEY + token;

        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
        //3.判断用户是否存在
        if (userMap.isEmpty()) {
            //4.不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        //5.存在,保存用户信息到Threadlocal
        UserHolder.saveUser(userDTO);
        //刷新登录状态
        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
        //6.放行
        return true;
    }
}

User Status Refresh Issue

That is, login authentication and refresh user status are bound in an interceptor, and the paths that require login authentication are not all paths. If the user does not access the path that requires login authentication, the user status cannot be refreshed , that is, after 30 minutes, the login will fail. will withdraw, which obviously does not conform to our common sense

Refresh problem solution

We can add another interceptor to form an interceptor chain. The first interceptor intercepts all paths, and its function is to refresh the login status, and the login authentication is completed by the second interceptor.

 

 Refresh state interceptor

public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            return true;
        }
        // 2.基于TOKEN获取redis中的用户
        String key  = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        // 5.将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }

Login Authentication Interceptor

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.判断是否需要拦截(ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            // 没有,需要拦截,设置状态码
            response.setStatus(401);
            // 拦截
            return false;
        }
        // 有用户,则放行
        return true;
    }
}

In order to ensure the execution order of the interceptors, the configuration class needs to set the size of the order and the order in which the interceptors are executed. If not set, it will be executed in the order of registration

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        // token刷新的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

Guess you like

Origin blog.csdn.net/weixin_64133130/article/details/132331202