一、登录流程
1、发送验证码
用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号
如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用户
2、短信验证码登录、注册
用户将验证码和手机号进行输入,
后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,
如果不一致,则无法通过校验,
如果一致,则后台根据手机号查询用户,
如果用户不存在,则为用户创建账号信息,保存到数据库。
无论是否存在,都会将用户信息保存到session中,方便后续获得当前登录信息
3、校验登录状态
用户在请求时候,会从cookie中携带者JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并且放行,
校验手机号是否合法
二、发送短信验证码
页面流程:
1、controller层
UserController
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// TODO 发送短信验证码并保存验证码
return userService.sendCode(phone,session);
}
2、格式校验工具类
1️⃣正则表达式
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}$";
}
2️⃣校验手机号格式
使用了hutool
工具库
官网:简介 | Hutool📚简介
import cn.hutool.core.util.StrUtil;
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)) {
return true;
}
return !str.matches(regex);
}
}
3、service层
1️⃣实现sendCode方法
//service层接口
public interface IUserService extends IService<User> {
Result sendCode(String phone, HttpSession session);
}
实现短信验证功能
校验手机号:需要用到正则表达式,这里已经提供好了一个工具类
RegexPatterns
和RegexUtils
校验成功后,使用工具类
RandomUtils
随机生成一个验证码,这里涉及到的发送手机验证码的步骤,并未讲到,可转springboot开发实战篇——阿里云手机短信验证
保存验证码
发送验证码
2️⃣serviceImpl实现类
UserServiceImpl类
@Override
public Result sendCode(String phone, HttpSession session) {
///1、校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
//2、如果不符合,返回错误信息
return Result.fail("手机号格式错误!!!");
}
//3、符合,生成验证码
String code = RandomUtil.randomNumbers(6);
//4、保存验证码到session
session.setAttribute("code",code);
//5、模拟发送验证码
log.debug("发送短信验证码成功,验证码:{}",code);
//6、返回ok
return Result.ok();
}
输入手机号发送验证码,后端随机生成验证码进行校验
优化:阿里云短信手机验证(可以看看我的上一篇文章《手机短信验证登录》)
三、短信验证码登录、注册
1、controller层
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 实现登录功能
return userService.login(loginForm,session);
}
2、service层
IUserService接口实现login方法
Result login(LoginFormDTO loginForm, HttpSession session);
UserServiceImpl实现类
短信验证登录注册逻辑
校验手机号
校验验证码
取出session中保存的验证码与表单中的输入的验证码吗进行比较
不一致:报错
一致:根据手机号查询用户
判断用户是否存在
不存在,根据手机号创建新用户并保存
用户是凭空创建的,所以密码可以没有,
用户呢称和头像都是随机默认的
保存用户信息到session中
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1、校验手机号
String phone= loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
//2、如果不符合,返回错误信息
return Result.fail("手机号格式错误!!!");
}
//2、校验验证码
//取出session中保存的验证码与表单中的输入的验证码吗进行比较
Object cachecode = session.getAttribute("code");
String code=loginForm.getCode();
if(cachecode==null || !cachecode.toString().equals(code)){
//3、不一致:报错
return Result.fail("验证码错误!!");
}
//4、一致:根据手机号数据库查询用户
User user = query().eq("phone", phone).one();
//5、判断用户是否存在
if(user==null){
//6、不存在,创建新用户并保存
user=createUserWithPhone(phone);
}
//7、保存用户信息到session中
session.setAttribute("user",user);
return null;
}
实现创建新用户方法
private User createUserWithPhone(String phone) {
//1、创建用户
User user=new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX
+RandomUtil.randomString(10));
//2、保存用户
save(user);
return user;
}
后端控制台
前端浏览器
这里登录成功了,但是还未做登录校验,所以它又返回了原来的页面
这里会出现页面一闪而过
四、实现登录校验拦截器
1、简介
访问
`Controller
之前都要先校验用户的登录状态,用户的请求直接访问controller
实现这一段逻辑,那随着业务的增加,每个controller都来执行这段逻辑太过麻烦,所以,这里定义了一个拦截器,
这样用户请求就不是直接访问controller,所有的请求都必须先经过这个拦截器,再由拦截器放行,将请求发送给controller,拦截到的信息需要受到保护,然后发送给controller
我们可以得知 每个用户其实对应都是去找tomcat线程池中的一个线程来完成工作的, 使用完成后再进行回收,既然每个请求都是独立的,所以在每个用户去访问我们的工程时,我们可以使用threadlocal来做到线程隔离,每个线程操作自己的一份数据
温馨小贴士:关于threadlocal
如果小伙伴们看过threadLocal的源码,你会发现在threadLocal中,无论是他的put方法和他的get方法, 都是先从获得当前用户的线程,然后从线程中取出线程的成员变量map,只要线程不一样,map就不一样,所以可以通过这种方式来做到线程隔离
2、实现登录校验拦截器
拦截器的作用是将拦截的页面进行校验,校验通过才可放行
这里实现两个方法
前置拦截器preHandle:用户登陆之前需要做校验,根据返回值表示是否拦截,true是放行,false表示拦截
afterCompletion:用户登录视图渲染执行完毕需要销毁,避免内存泄漏
1️⃣ 新建登录拦截器
//方法快捷键:ctrl+i
public class LoginInterceptor implements HandlerInterceptor {
/**
* 前置拦截器
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1、获取session
HttpSession session=request.getSession();
//2、获取session中的用户
Object user = session.getAttribute("user");
//3、判断用户是否存在
if (user == null) {
//4、不存在,拦截:返回401状态码
response.setStatus(401);
return false;
}
//5、存在,保存用户信息到ThreadLocal
UserHolder.saveUser((UserDTO) user);
//6、放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户
UserHolder.removeUser();
}
}
实现拦截器还需要做一些配置,使其拦截生效
2️⃣ 新建SpringMVC配置类
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
//排除不需要拦截的路径——验证码和登录····
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"user/login"
);
}
}
3️⃣ controller层
UserController
@GetMapping("/me")
public Result me(){
// 获取当前登录的用户并返回
User user = UserHolder.getUser();
return Result.ok(user);
}
4️⃣ 修改前端login页面
前端页面中设置登陆后是跳转到个人首页,这里需要进行修改
五、用户信息脱敏
上面用户登录校验信息返回的信息太多了,一些敏感信息也被返回了,这是不安全的,而这里只需要返回用户基本信息——id、呢陈、头像
1、定义一个信息返回类DTO
@Data
public class UserDTO {
private Long id;
private String nickName;
private String icon;
}
2、修改userserviceimpl实现类
调用的是包cn.hutool.core.bean下hutool方法中的工具类BeanUtils
//修改login方法
/**
* copyProperties:属性拷贝——把user中的属性字动拷贝到UserDTO中
* BeanUtils:使用的是包cn.hutool.core.bean下的工具类
*/
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
session.setAttribute("user", userDTO);
3、修改user
将user都修改成userDTO
登录拦截器——LoginInterceptor
修改userHolder
/**
* 线程隔离
*/
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
/**
* 保存用户
* @param user
*/
public static void saveUser(UserDTO user){
tl.set(user);
}
/**
* 获取用户
* @return
*/
public static UserDTO getUser(){
return tl.get();
}
userController中的me方法
@GetMapping("/me")
public Result me(){
// 获取当前登录的用户并返回
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
登录成功
这样也就实现了用户信息的隐藏,只返回了设定的用户信息
如果用户已存在,前端控制台是不会将信息返回的
六、集群的session共享问题
问题分析:
核心思路分析:
每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时 整个登录拦截功能就会出现问题,我们能如何解决这个问题呢?早期的方案是session拷贝,就是说虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session,这样的话,就可以实现session的共享了
但是这种方案具有两个大问题
1、每台服务器中都有完整的一份session数据,服务器压力过大。
2、session拷贝数据时,可能会出现延迟
所以咱们后来采用的方案都是基于redis来完成,我们把session换成redis,redis数据本身就是共享的,就可以避免session共享的问题了
下一篇与大家分享用redis代替session实现短信登录