Comentarios de Dark Horse: integración de proyectos git y redis para lograr el inicio de sesión del código de verificación de SMS

Tabla de contenido

IDEA integra git

Problemas con las sesiones tradicionales 

esquema redis

Procesos de negocio

La estructura de datos elegida

proceso de acceso general

Enviar código de verificación por SMS

 Obtener código de verificación

Configurar el interceptor de inicio de sesión

Clase de configuración de registro de interceptor

interceptador

Problema de actualización de estado de usuario

Actualizar solución de problema


IDEA integra git

El almacén remoto utiliza Code Cloud, crea un almacén y copia la URL del almacén

 Haga clic en la idea, aparece la opción git, haga clic en ok

 Luego haga clic derecho en el proyecto y haga clic en controles remotos

 Complete la url para integrar git

Problemas con las sesiones tradicionales 

La autenticación de inicio de sesión tradicional usa la sesión para la autenticación de inicio de sesión y almacena el código de verificación de inicio de sesión y la información del usuario en la sesión. Usamos la sesión para operar los datos. ¿Qué tiene de malo esto?

Cada servidor tomcat tiene su propia sesión, asumiendo que el usuario visita el primer tomcat por primera vez y almacena su información en la sesión del primer servidor, pero el usuario visita el segundo tomcat por segunda vez tomcat, luego en el segundo servidor , no debe haber ninguna sesión almacenada en el primer servidor, es decir, la sesión no se comparte entre los servidores , por lo que habrá problemas con toda la función de interceptación de inicio de sesión en este momento, y los datos de redis en sí se comparten. Sí, puede evitar el problema de compartir sesión

esquema redis

Procesos de negocio

  • Guarde el código de verificación en redis
  • Almacenar datos de usuario en redis

La estructura de datos elegida

El tipo Cadena se usa para almacenar el código de verificación, y la clave usa el código comercial + número de teléfono móvil

Use hash al almacenar datos de usuario, clave use código comercial + tonke aleatorio

Hash puede almacenar cada campo en el objeto de forma independiente, y puede hacer CRUD para un solo campo, y ocupa menos memoria. De hecho, el tipo String también se puede usar, pero el tipo hash consume menos memoria, por lo que se usa el tipo String

proceso de acceso general

 Cuando se complete el registro, el usuario iniciará sesión para verificar si el número de teléfono móvil y el código de verificación enviados por el usuario son consistentes. Si son consistentes, la información del usuario se consultará de acuerdo con el número de teléfono móvil. Si no es así existe, se creará uno nuevo. Finalmente, los datos del usuario se guardarán en redis y el Token generado es la clave de redis. Cuando verifiquemos si el usuario está logueado, llevaremos el token de acceso, sacaremos el valor correspondiente al token de redis, y juzgue si estos datos existen. De lo contrario, intercéptelo. Si existe, guárdelo en In threadLocal, simplifique el código para que las empresas posteriores obtengan la información del usuario. Las empresas posteriores deben iniciar sesión en la información del usuario. , siempre que se obtenga de threadLocal, no es necesario obtenerlo de redis para aumentar la complejidad y, finalmente, publicarlo .

Constantes de código comercial de Redis (continúe agregando más adelante)

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;

   
}

Enviar código de verificación por SMS

Utilice el formulario de impresión de registro para imprimir el código de verificación en la consola

 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();
    }

 Obtener código de verificación

Necesitamos filtrar la información confidencial del usuario y solo devolver los datos de información del usuario necesarios al front-end. Necesitamos encapsular el objeto dto y, al mismo tiempo, generar un token en uuid como clave para que redis obtenga datos. Finalmente, debemos necesita establecer el tiempo de caducidad para evitar que la memoria reids explote Establézcalo en 30 minutos, porque el tiempo de caducidad de la sesión también es de 30 minutos

 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);
    }

Configurar el interceptor de inicio de sesión

Clase de configuración de registro de interceptor

@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"
                );
    }
}

interceptador

El objeto stringRedisTemplate requerido por el interceptor no se puede inyectar directamente a través de @Autowired

Debido a que nosotros creamos manualmente el objeto LoginInterceptor , no está bajo el control de Spring y no está en el contenedor de Spring , por lo que no se puede inyectar en el bean en el contenedor de Spring. Solo se puede inyectar en el objeto stringRedisTemplate. a través del método de construcción en la clase de configuración.

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;
    }
}

Problema de actualización de estado de usuario

Es decir, la autenticación de inicio de sesión y la actualización del estado del usuario están vinculadas en un interceptor, y las rutas que requieren autenticación de inicio de sesión no son todas rutas. Si el usuario no accede a la ruta que requiere autenticación de inicio de sesión, el estado del usuario no se puede actualizar , es decir, después de 30 minutos, el inicio de sesión fallará, se retirará, lo que obviamente no se ajusta a nuestro sentido común

Actualizar solución de problema

Podemos agregar otro interceptor para formar una cadena de interceptores. El primer interceptor intercepta todas las rutas, y su función es actualizar el estado de inicio de sesión, y la autenticación de inicio de sesión se completa con el segundo interceptor.

 

 Interceptor de estado de actualización

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();
    }

Interceptor de autenticación de inicio de sesión

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;
    }
}

Para garantizar el orden de ejecución de los interceptores, la clase de configuración necesita establecer el tamaño de la orden y el orden en que se ejecutan los interceptores.Si no se establece, se ejecutará en el orden de registro.

@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);
    }
}

Supongo que te gusta

Origin blog.csdn.net/weixin_64133130/article/details/132331202
Recomendado
Clasificación