登录的几种方式

使用Session完成登录

1. 手机号发送验证码

逻辑步骤:
  1. 校验手机号格式是否正确。
  2. 生成验证码(例如使用Hutool工具类)。
  3. 将手机号和验证码存入Session。
  4. 返回验证码发送成功的响应。

2. 用户登录逻辑

逻辑步骤:
  1. 从Session中获取存储的手机号和验证码。
  2. 校验前端传来的手机号和验证码是否与Session中一致。
  3. 如果一致,根据手机号查询用户是否存在。
    • 如果不存在,创建新用户并随机生成用户名。
  4. 将用户信息存入Session。
package com.hmdp.service.impl;import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import javax.servlet.http.HttpSession;import static com.hmdp.utils.SystemConstants.USER_NICK_NAME_PREFIX;/*** <p>* 服务实现类* </p>** @author 虎哥* @since 2021-12-22*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Overridepublic Result sendCode(String phone, HttpSession session) {// 校验手机号if (RegexUtils.isPhoneInvalid(phone)) {// 不符合 返回错误信息return Result.fail("手机号格式错误");}// 生成验证码String code = RandomUtil.randomNumbers(6);// 保存验证码到Sessionsession.setAttribute("code",code);session.setAttribute("phone",phone);//TODO 发送验证码 需要调用第三方log.debug("发送验证码成功,验证码{}",code);// 返回成功return Result.ok();}@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {// 校验手机号和验证码if (!loginForm.getPhone().equals(session.getAttribute("phone"))) {return Result.fail("手机号和之前的不同");}// 校验码不一致,报错if (!session.getAttribute("code").equals(loginForm.getCode())) {return Result.fail("验证码不正确");}// 一致,根据手机号查询用户User user = query().eq("phone", loginForm.getPhone()).one();if (user==null) {// 用户不存在 创建新的用户 保存用户到数据库,保存用户到Sessionuser = createUserWithPhone(loginForm.getPhone());}// 用户存在,直接保存用户到Sessionsession.setAttribute("user",user);return Result.ok();}private User createUserWithPhone(String phone) {User user = new User();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10)); // 设置默认的用户名save(user);return user;}
}

3. 登录校验拦截器

逻辑步骤:
  1. 拦截所有需要登录验证的请求。
  2. 检查Session中是否存在user对象。
  3. 如果不存在,返回未登录的错误信息;否则将user信息转换为只含有id,昵称和头像地址的类之后(保护用户隐私信息)存入TreadLocal当中,并且放行。
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {Object user = request.getSession().getAttribute("user");if (user == null) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getWriter().write("未登录,请先登录");return false;}// 将用户信息存入 ThreadLocal // 为了保护用户的隐私需要专门设置一个类只存储用户的昵id,称和头像地址UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);UserContext.setUser(userDTO );return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 清理 ThreadLocal 防止内存泄漏UserContext.clear();}
}

 UserContext 工具类:

public class UserContext {private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();public static void saveUser(UserDTO user){tl.set(user);}public static UserDTO getUser(){return tl.get();}public static void removeUser(){tl.remove();}
}

还需要在MvcConfig当中配置拦截器

package com.hmdp.config;import com.hmdp.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class MvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 登录拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/voucher/**").order(0);}
}

session 在服务器端有默认的时长(过期时间),这是由服务器配置决定的。默认情况下,Session 的有效期会受到服务器设置的影响,而无需手动设置时长。如果需要自定义时长,可以进行配置。 (默认值

在大多数 Servlet 容器(如 Tomcat、Jetty)中,Session 的默认超时时间是 30分钟

这个时间表示,如果用户在 30 分钟内没有访问服务器,Session 会被销毁。)

基于Redis代替Session登录

问题Session 是存储在服务器内存中的,默认情况下每个服务器实例维护自己的 Session 数据。在分布式系统中,不同的请求可能被分配到不同的服务器实例,从而导致无法访问原始 Session。 

虽然 Session 使用方便,但在分布式、高并发、跨平台场景下,其缺点可能带来较大的限制。因此,很多现代应用倾向于采用 无状态认证(如 JWT) 或集中式存储方案(如 Redis)来代替传统的 Session。选择方案时需要根据业务需求、系统架构和可接受的复杂性权衡决定。

1. 手机号发送验证码

逻辑:
  • 校验手机号是否规范。
  • 使用 Hutool 工具生成验证码。
  • 将验证码存入 Redis,设置过期时间为 2 分钟。

2. 用户登录逻辑

逻辑:
  1. 从 Redis 获取验证码,并与前端提交的验证码比对。
  2. 根据手机号查询用户,不存在则创建新用户。
  3. 生成登录令牌(token),将用户信息转换为 Hash 并存入 Redis,设置有效期为 30 分钟。
  4. 返回 token 给前端。
package com.hmdp.service.impl;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import javax.servlet.http.HttpSession;import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.*;
import static com.hmdp.utils.SystemConstants.USER_NICK_NAME_PREFIX;/*** <p>* 服务实现类* </p>** @author 虎哥* @since 2021-12-22*/
@Service
@Slf4j
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {final private StringRedisTemplate stringRedisTemplate;@Overridepublic Result sendCode(String phone, HttpSession session) {// 校验手机号if (RegexUtils.isPhoneInvalid(phone)) {// 不符合 返回错误信息return Result.fail("手机号格式错误");}// 生成验证码String code = RandomUtil.randomNumbers(6);/*// 保存验证码到Sessionsession.setAttribute("code",code);session.setAttribute("phone",phone);*/// 存储到Redis中stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);//TODO 发送验证码 需要调用第三方log.debug("发送验证码成功,验证码{}",code);// 返回成功return Result.ok();}@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {/*// 校验手机号和验证码if (!loginForm.getPhone().equals(session.getAttribute("phone"))) {return Result.fail("手机号和之前的不同");}// 校验码不一致,报错if (!session.getAttribute("code").equals(loginForm.getCode())) {return Result.fail("验证码不正确");}*///String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + loginForm.getPhone());if(code == null || ! code.equals(loginForm.getCode())){return Result.fail("验证码不正确或者手机号错误");}// 一致,根据手机号查询用户User user = query().eq("phone", loginForm.getPhone()).one();if (user==null) {// 用户不存在 创建新的用户 保存用户到数据库,保存用户到Sessionuser = createUserWithPhone(loginForm.getPhone());}// 用户存在,直接保存用户到Session
//        session.setAttribute("user",user);// 保存到Redis中 以hash模式存储// 随机生成一个token作为登录令牌String token  = UUID.randomUUID().toString(true);// 将User对象转为UserDTO 再以token为key,Hash形式存储UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);// 将userDTO转换为mapMap<String, Object> Usermap = BeanUtil.beanToMap(userDTO,new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)-> fieldValue.toString()));stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY +token,Usermap);// 设置有效期stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES);// 返回tokenreturn Result.ok(token);}private User createUserWithPhone(String phone) {User user = new User();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10)); // 设置默认的用户名save(user);return user;}
}

3. 登录拦截器

逻辑:
  1. 从请求头中获取 token
  2. 使用 token 从 Redis 获取用户信息。
  3. 如果用户信息为空,拦截请求。
  4. 将用户信息保存到 ThreadLocal
  5. 刷新 token 的有效期。

 但是如果我们还是只在登录的拦截器当中刷新token的有效值,那么就只会在局部范围内保证token有效。而不是全局范围内,保证用户的token不会过期。

所以我们需要加一层 加一层拦截器(RefreshTokenInterceptor),虽然说是拦截器,但是他不进行拦截操作,拦截操作还是有LoginInterceptor进行拦截。

什么只在 LoginInterceptor 中刷新 token 不够?

局限性

LoginInterceptor 只对需要登录的接口进行拦截。如果用户只访问公开页面或非登录接口(如 /home/shop 等),这些请求不会经过 LoginInterceptor,导致 token 无法刷新。

如果用户长时间浏览公开页面后访问需要登录的页面,可能因 token 过期被迫重新登录,影响用户体验。

全局活跃性保证

用户访问任何页面都应该被视为活跃状态,无论页面是否需要登录,都需要刷新 token 的有效期。单独依赖 LoginInterceptor 只能保证在局部范围(需要登录的接口)内刷新 token

为什么需要 RefreshTokenInterceptor?

  • RefreshTokenInterceptor 的目的是在全局范围内检测 token 并刷新其有效期。
  • 它负责在所有请求(无论是否需要登录)中检查 token,并且将用户保存到TreadLocal当中,但不进行登录状态的校验。
  • 真正的拦截操作(判断用户是否登录)仍由 LoginInterceptor 执行。(LoginInterceptor 可以查看ThreadLocal中是否存在user就可以判断是否登录了)

RefreshTokenInterceptor的代码: 

package com.hmdp.Interceptor;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
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;import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;@RequiredArgsConstructor
public class RefreshTokenInterceptor implements HandlerInterceptor {final StringRedisTemplate stringRedisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 从请求头当中获取tokenString token = request.getHeader("authorization");//如果 string token =  "" ;  这个 token != null,而是 长度为0。所以不可以直接用 == nullif (StrUtil.isBlankIfStr(token)) {response.setStatus(401);return true;}String key = LOGIN_USER_KEY + token;// 从Redis获取用户Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);// 判断userMapif (userMap.isEmpty()) {response.setStatus(401);return true;}// 将userMap转换为BeanUserDTO userDTO= BeanUtil.fillBeanWithMap(userMap, new UserDTO(),false);
//        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);// 保存到ThreadLocal里面UserHolder.saveUser(userDTO);// 刷新token的有效期stringRedisTemplate.expire(key,LOGIN_USER_TTL, TimeUnit.MINUTES);return  true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}

LoginInterceptor拦截器代码: 

package com.hmdp.Interceptor;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;// 判断是否需要拦截也就是TreadLocal当中是否存在user
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {UserDTO userDTO = UserHolder.getUser();if (userDTO == null) {response.setStatus(401);return  false;}// 有用户放行return  true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}

MvcConfig配置 

package com.hmdp.config;import com.hmdp.Interceptor.LoginInterceptor;
import com.hmdp.Interceptor.RefreshTokenInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class MvcConfig implements WebMvcConfigurer {final StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 拦截所有 执行顺序默认都是0,按照添加顺序执行,指定Order,越小越先执行// token刷新拦截器registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);// 拦截部分请求registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code","/user/login","blog/hot","/shop/**","/shop-type/**","/voucher/**").order(1);}
}

MvcConfig作为Spring管理的Bean,可以通过构造注入或字段注入获取StringRedisTemplate。由于拦截器实例是手动创建的,MvcConfig需要将StringRedisTemplate显式传递给LoginInterceptor的构造方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/501892.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

电子电气架构 --- 中央HPC架构

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 所谓鸡汤,要么蛊惑你认命,要么怂恿你拼命,但都是回避问题的根源,以现象替代逻辑,以情绪代替思考,把消极接受现实的懦弱,伪装成乐观面对不幸的…

SMTP发送邮件的过程

&#xff08;1&#xff09;SMTP客户端首先请求与服务器端的25号端口建立TCP连接(1分)。&#xff08;2&#xff09;连接建立成功后&#xff0c;客户端和服务器通过握手阶段验证双方身份(1分)。&#xff08;3&#xff09;验证成功后&#xff0c;客户端首先向服务器端通告邮件发送…

数据挖掘——神经网络分类

神经网络分类 神经网络分类人工神经网络多层人工神经网络 误差反向传播&#xff08;BP&#xff09;网络后向传播算法 神经网络分类 人工神经网络 人工神经网络主要由大量的神经元以及它们之间的有向连接构成。包含三个方面&#xff1a; 神经元的激活规则 主要是指神经元输入…

MySQL 【多表查询】

一 . 概述 多表关系&#xff1a; 一对多(多对一) &#xff0c; 多对多 &#xff0c;一对一 1&#xff09; 一对一 案例: 用户 与 用户详情的关系 关系: 一对一关系&#xff0c;多用于单表拆分&#xff0c;将一张表的基础字段放在一张表中&#xff0c;其他详情字段放在另 一张表…

联发科MTK6771/MT6771安卓核心板规格参数介绍

MT6771&#xff0c;也被称为Helio P60&#xff0c;是联发科技(MediaTek)推出的一款中央处理器(CPU)芯片&#xff0c;可运行 android9.0 操作系统的 4G AI 安卓智能模块。MT6771芯片采用了12纳米工艺制造&#xff0c;拥有八个ARM Cortex-A73和Cortex-A53核心&#xff0c;主频分别…

dbeaver导入导出数据库(sql文件形式)

目录 前言dbeaver导出数据库dbeaver导入数据库 前言 有时候我们需要复制一份数据库&#xff0c;可以使用dbeaver简单操作&#xff01; dbeaver导出数据库 选中数据库右键->工具->转储数据库 dbeaver导入数据库 选中数据库右键->工具->执行脚本 mysql 默…

vip与haproxy构建nginx高可用集群传递客户端真实ip

问题 系统使用了vip与haproxy实现高可用以及对nginx进行负载均衡&#xff0c;但是发现在上游的应用服务无法拿到客户端的请求ip地址&#xff0c;拿到的是主haproxy机器的ip&#xff0c;以下是nginx与haproxy的缩减配置&#xff1a; location ~* ^/(xx|xx) {proxy_pass http:/…

【C#深度学习之路】如何使用C#实现Yolo5/8/11全尺寸模型的训练和推理

【C#深度学习之路】如何使用C#实现Yolo5/8/11全尺寸模型的训练和推理 项目背景项目实现调用方法项目展望写在最后项目下载链接 本文为原创文章&#xff0c;若需要转载&#xff0c;请注明出处。 原文地址&#xff1a;https://blog.csdn.net/qq_30270773/article/details/1449186…

ARM 汇编基础总结

GNU 汇编语法 编写汇编的过程中&#xff0c;其指令、寄存器名等可以全部使用大写&#xff0c;也可以全部使用小写&#xff0c;但是不能大小写混用。 1. 汇编语句的格式 label: instruction comment label即标号&#xff0c;表示地址位置&#xff0c;有些指令前面可能会有标…

米哈游可切换角色背景动态壁纸

米哈游可切换角色背景动态壁纸 0. 视频 B站演示: 米哈游可切换角色背景动态壁纸-wallpaper 1. 基本信息 作者: 啊是特嗷桃系列: 复刻系列 (衍生 wallpaper壁纸引擎 用)网站: 网页版在线预览 (没有搞大小适配, 建议横屏看; 这个不能切角色, 只能在wallpaper中切)仓库: GitHub…

Mac iTerm2集成DeepSeek AI

1. 去deepseek官网申请api key&#xff0c;DeepSeek 2. 安装iTerm2 AI Plugin插件&#xff0c;https://iterm2.com/ai-plugin.html&#xff0c;插件解压后直接放到和iTerms相同的位置&#xff0c;默认就在/Applications 下 3. 配置iTerm2 4. 重启iTerm2,使用快捷键呼出AI对话…

HTML——66.单选框

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title>单选框</title></head><body><!--input元素的type属性&#xff1a;(必须要有)--> <!--单选框:&#xff08;如所住省会&#xff0c;性别选择&…

深入Android架构(从线程到AIDL)_10 主线程(UI 线程)的角色

目录 4、 细说主线程(UI线程)的角色 近程通信 远程通信 4、 细说主线程(UI线程)的角色 近程通信 在Android里&#xff0c;无论组件在那一个进程里执行&#xff0c;于预设情形下&#xff0c;他们都是由该进程里的主线程来负责执行之。例如下述的范例&#xff0c;由一个Acti…

掌握RabbitMQ:全面知识点汇总与实践指南

前言 RabbitMQ 是基于 AMQP 高级消息队列协议的消息队列技术。 特点&#xff1a;它通过发布/订阅模型&#xff0c;实现了服务间的高度解耦。因为消费者不需要确保提供者的存在。 作用&#xff1a;服务间异步通信&#xff1b;顺序消费&#xff1b;定时任务&#xff1b;请求削…

渗透测试-非寻常漏洞案例

声明 本文章所分享内容仅用于网络安全技术讨论&#xff0c;切勿用于违法途径&#xff0c;所有渗透都需获取授权&#xff0c;违者后果自行承担&#xff0c;与本号及作者无关&#xff0c;请谨记守法. 此文章不允许未经授权转发至除先知社区以外的其它平台&#xff01;&#xff0…

前端-计算机网络篇

一.网络分类 1.按照网络的作用范围进行分类 &#xff08;1&#xff09;广域网WAN(Wide Area Network) 广域网的作用范围通常为几十到几千公里,因而有时也称为远程网&#xff08;long haul network&#xff09;。广域网是互联网的核心部分&#xff0c;其任务是长距离运送主机…

开发培训-慧集通(iPaaS)集成平台脚本开发Groovy基础培训视频

‌Groovy‌是一种基于Java虚拟机&#xff08;JVM&#xff09;的敏捷开发语言&#xff0c;结合了Python、Ruby和Smalltalk的许多强大特性。它旨在提高开发者的生产力&#xff0c;通过简洁、熟悉且易于学习的语法&#xff0c;Groovy能够与Java代码无缝集成&#xff0c;并提供强大…

腾讯云智能结构化 OCR:驱动多行业数字化转型的核心引擎

在当今数字化时代的汹涌浪潮中&#xff0c;数据已跃升为企业发展的关键要素&#xff0c;其高效、精准的处理成为企业在激烈市场竞争中脱颖而出的核心竞争力。腾讯云智能结构化 OCR 技术凭借其前沿的科技架构与卓越的功能特性&#xff0c;宛如一颗璀璨的明星&#xff0c;在交通、…

vulnhub Earth靶机

搭建靶机直接拖进来就行 1.扫描靶机IP arp-scan -l 2.信息收集 nmap -sS -A -T4 192.168.47.132 得到两个DNS; 在443端口处会让我们加https dirb https://earth.local/ dirb https://terratest.earth.local/ #页面下有三行数值 37090b59030f11060b0a1b4e0000000000004312170a…

消息中间件类型都有哪些

在消息中间件的专业术语中&#xff0c;我们可以根据其特性和使用场景将其划分为几种主要的类型。这些类型不仅反映了它们各自的技术特点&#xff0c;还决定了它们在不同应用场景下的适用性。 1. 点对点&#xff08;Point-to-Point&#xff09;消息中间件&#xff1a; • 这类中…