个人简介:Java领域优质创作者;阿里云技术博主、星级博主、专家博主;正在Java学习的路上摸爬滚打,记录学习的过程~
个人主页:.29.的博客
学习社区:进去逛一逛~
实现基于session的登录流程:发送验证码、登录注册、校验登陆状态
🚀流程介绍
登录流程
:
-
发送验证码
: -
-
用户输入手机号,点击发送按钮进行手机号提交,程序会校验手机号是否合法,不合法时要求用户重新输入手机号,合法则在后台生成对应的验证码并保存至
session
,之后通过短信方式将验证码发送给用户。 -
-
什么是HttpSession?
-
HttpSession是Java Web中的一个接口,它提供了一种在服务器端存储和检索用户相关信息的机制。当用户第一次访问Web应用程序时,服务器会为该用户创建一个唯一的session ID,并将该ID存储在一个名为JSESSIONID的cookie中,然后将该ID与一个新的HttpSession对象相关联。在用户与Web应用程序交互期间,可以使用HttpSession对象来存储和检索与该用户相关的信息。当用户关闭浏览器或超过session超时时间时,session对象将被销毁。
以下是获取和使用HttpSession对象的常用方法:
1.获取HttpSession对象:
HttpSession session = request.getSession();
2.向session中存储数据:
session.setAttribute("key", value);
3.从session中获取数据:
Object value = session.getAttribute("key");
4.从session中删除数据:
session.removeAttribute("key");
5.使session失效:
session.invalidate();
-
-
注册、登录
:-
- 用户将手机号、验证码输入,后台从session中获取验证码与用户输入的验证码进行比对校验,如果不一致则无法通过校验,提示用户验证码错误,验证码一致则后台根据手机号查询用户,若用户不存在,则为用户创建账号信息并保存至数据库中,最后无论用户是否存在,都将用户的信息保存至session中,方便后续业务获取当前用户信息。
-
校验登陆状态
: -
-
用户在客户端发起请求时,Cookie会携带用户的 JsessionId 后台,后台根据 JsessionId 从session中获取用户信息,如果没有用户信息就表示未登录,会对请求进行拦截,如果有用户信息,将其存入到本地线程 ThreadLocal 中并放行。
-
-
为什么使用ThreadLocal:
-
- 每个用户其实对应都是去找tomcat线程池中的一个线程来完成工作的, 使用完成后再进行回收,既然每个请求都是独立的,所以在每个用户去访问我们的工程时,我们可以使用threadlocal来做到线程隔离,每个线程操作自己的一份数据。
-
什么是 JsessionId ?
-
-
JSessionId是Java Web应用程序中的一个会话标识符,用于跟踪用户与Web应用程序之间的会话。当用户第一次访问Web应用程序时,服务器会为该用户创建一个唯一的JSessionId,并将其存储在cookie中。在随后的请求中,浏览器会将该cookie发送回服务器,以便服务器可以识别用户并维护会话状态。
在Java Web应用程序中,可以使用HttpSession对象来访问和管理会话状态。
-
-
-
🚀代码实现
业务逻辑实现
:
-
统一返回类型 实体类
: -
-
@Data @NoArgsConstructor @AllArgsConstructor public class Result { private Boolean success; private String errorMsg; private Object data; private Long total; public static Result ok(){ return new Result(true, null, null, null); } public static Result ok(Object data){ return new Result(true, null, data, null); } public static Result ok(List<?> data, Long total){ return new Result(true, null, data, total); } public static Result fail(String errorMsg){ return new Result(false, errorMsg, null, null); } }
-
-
校验手机号、邮箱、验证码格式
: -
-
public abstract class RegexPatterns { /** * 手机号正则 */ public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$"; /** * 邮箱正则 */ public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$"; /** * 密码正则。4~32位的字母、数字、下划线 */ public static final String PASSWORD_REGEX = "^\\w{4,32}$"; /** * 验证码正则, 6位数字或字母 */ public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$"; }
-
-
-
public class RegexUtils { /** * 是否是无效手机格式 * @param phone 要校验的手机号 * @return true:符合,false:不符合 */ public static boolean isPhoneInvalid(String phone){ return mismatch(phone, RegexPatterns.PHONE_REGEX); } /** * 是否是无效邮箱格式 * @param email 要校验的邮箱 * @return true:符合,false:不符合 */ public static boolean isEmailInvalid(String email){ return mismatch(email, RegexPatterns.EMAIL_REGEX); } /** * 是否是无效验证码格式 * @param code 要校验的验证码 * @return true:符合,false:不符合 */ public static boolean isCodeInvalid(String code){ return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX); } // 校验是否不符合正则格式 private static boolean mismatch(String str, String regex){ if (StrUtil.isBlank(str)) { //Hutool工具:StrUtil return true; } return !str.matches(regex); } }
-
-
发送短信验证码 业务
: -
-
@Override public Result sendCode(String phone, HttpSession session) { //1. 手机号不合法? if(RegexUtils.isPhoneInvalid(phone)){ //2. 不合法,返回错误信息 return Result.fail("手机号格式错误!"); } //3. 借助工具类,生成验证码(Hutool工具) String code = RandomUtil.randomNumbers(6); //4. 保存验证码至session域 session.setAttribute("code",code); //5. 发送验证码 log.debug("发送短信验证码成功,验证码: " + code); //日志、方便控制台查看 /* 调用验证码服务...(具体逻辑参照具体服务供应商的文档) */ //6. 返回ok return Result.ok(); }
-
-
登录、注册 业务
: -
-
/** * session实现登录功能 * @param loginForm * @param session * @return */ @Override public Result login(LoginFormDTO loginForm, HttpSession session) { //1. 校验手机号 String phone = loginForm.getPhone(); if(RegexUtils.isPhoneInvalid(phone)){ //2. 返回错误信息 return Result.fail("手机号格式错误"); } //3. 校验验证码 Object cacheCode = session.getAttribute("code"); String code = loginForm.getCode(); if(code == null || !code.toString().equals(cacheCode)){ //不一致,返回错误信息 return Result.fail("验证码错误"); } // 一致,根据手机号获取用户 User user = this.query().eq("phone", phone).one(); //(mybatisPlus提供的Service层方法) //5. 判断用户是否存在 if(user == null){ //6. 不存在,创建新用户 user = new User(); user.setPhone(phone); //设置phone user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)); //设置随机昵称 this.save(user); // 存入数据库(mybatisPlus提供的Service层方法) } //7. 用户存在,存入session域 session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class)); //返回ok return Result.ok(); }
-
-
创建并设置ThreadLocal 自定义工具类
: -
-
public class UserHolder { 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(); } }
-
-
校验登陆状态 拦截器
: -
-
/** * TODO 登录 拦截器 * @author .29. * @create 2023-11-26 16:37 */ public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.获取session HttpSession session = request.getSession(); //2. 获取用户 Object user = session.getAttribute("user"); //3. 验证用户是否存在 if(user == null){ //4. 不存在,进行拦截,返回401状态码 response.setStatus(401); return false; } //5. 存在,存入ThreadLocal(自定义工具类UserHolder,作用:创建并设置ThreadLocal) UserHolder.saveUser((UserDTO) user); //6. 放行 return true; } }
-
-
Spring Boot使用Spring MVC拦截器的步骤如下:
-
1.创建一个拦截器类并实现
HandlerInterceptor接口
,该接口包含三个方法:preHandle、postHandle和afterCompletion。preHandle方法在请求处理之前调用,postHandle方法在请求处理之后调用,afterCompletion方法在视图渲染之后调用。 -
2.在拦截器类上使用
@Component
或@Configuration
注解将其声明为Spring组件。 -
3.创建一个配置类并实现
WebMvcConfigurer
接口,该接口包含一个addInterceptors方法,用于注册拦截器。 -
4.在addInterceptors方法中使用addInterceptor方法注册拦截器,并使用addPathPatterns方法指定要拦截的请求路径。
-
-
-
-
使拦截器生效 SpringMvc配置类
: -
-
/** * TODO SpringMVC配置类,使拦截器生效 * @author .29. * @create 2023-11-26 16:49 */ @Configuration public class MvcConfig implements WebMvcConfigurer { @Resource private StringRedisTemplate stringRedisTemplate; @Override public void addInterceptors(InterceptorRegistry registry) { //1. 添加登录拦截器、同时设置无需拦截的路径 registry.addInterceptor(new LoginInterceptor()).excludePathPatterns( "/shop/**", "/voucher/**", "/shop-type/**", "/upload/**", "/blog/hot", "/user/code", "/user/login" ).order(0); //order默认0,order值越大拦截器越后执行 } }
-
🚀存在共享问题
每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时 整个登录拦截功能就会出现问题,我们能如何解决这个问题呢?早期的方案是session拷贝,就是说虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session,这样的话,就可以实现session的共享了
但是这种方案具有两个大问题
1、每台服务器中都有完整的一份session数据,服务器压力过大。
2、session拷贝数据时,可能会出现延迟。