Redis实现短信登入功能(一)传统的Session登入
基于Session实现登入流程
分步实现发送短信登入
(1)发送短信验证码
UserController控制层
控制层调用service层的接口
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
return userService.sendCode(phone,session);
}
IUserService接口
接口定义sendCode()方法
public interface IUserService extends IService<User> {
Result sendCode(String phone, HttpSession session);
}
UserServiceImpl实现接口方法
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService{
@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);
//返回ok
return Result.ok();
}
}
注意:
上述代码中使用到了许多工具类,各个工具类的定义如下:
RegexUtils (校验手机号)
public class RegexUtils {
/**
* 是否是无效手机格式
* @param phone 要校验的手机号
* @return true:符合,false:不符合
*/
public static boolean isPhoneInvalid(String phone){
return mismatch(phone, RegexPatterns.PHONE_REGEX);
}
}
RegexUtils 中调用了 RegexPatterns(正则校验)
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}$";
}
我们重启服务,在前端页面输入手机号,点击获取验证码,在idea中的日志:
说明该功能实现完成!
(2)短信验证码的登入
UserController控制层
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 实现登录功能
return userService.login(loginForm, session);
}
IUserService接口
接口定义login()方法
public interface IUserService extends IService<User> {
Result login(LoginFormDTO loginForm, HttpSession session);
}
UserServiceImpl实现接口方法
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1. 校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误");
}
//2. 校验验证码
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",BeanUtil.copyProperties(user,UserDTO.class));
return Result.ok();
}
在login()方法中调用createUserWithPhone()方法创建用户
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;
}
(3)登入校验
拦截器 LoginInterceptor
此处使用拦截器校验用户是否存在,存在就登入;反之return false
public class LoginInterceptor implements HandlerInterceptor {
@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. 不存在,拦截
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();
}
}
配置拦截器 MvcConfig
使拦截器LoginInterceptor 生效!并定义拦截的对象。
在SpringBoot中可以使用addPathPatterns配置需要拦截的路径、excludePathPatterns配置不要拦截的路径。
此处,表示放行的资源(即无需登入,就可以访问的页面)
@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"
);
}
}
注意:
上述的一系列代码,我们还做了安全性考虑,我们使用了UserDTO,并没有直接使用User,一来减少了数据传输的成本,二来避免了User中的敏感数据泄露!
UserDTO
@Data
public class UserDTO {
private Long id;
private String nickName;
private String icon;
}
集群Session共享问题(Redis登入的提出)
再来看看这一张图,当程序到达性能瓶颈的时候,我们需要将原先的一台Tomcat,扩展Tomcat集群,但是每一台Tomcat都会有一个自己的Session空间(不同Tomcat的Session是相互独立的)。如果在登入方面我们不做调整就会出现登入失败的问题。
早期Tomcat是有做过Session拷贝的!但是这种方案会导致内存浪费;在拷贝过程中会产生时延,如果在这个延迟之内有人进行访问那么一样会出现问题!
所以我们急需一种可以代替Session的东西,此物需要满足:
- 数据共享
- 内存存储
- key、value结构
显然用Redis来实现短信登入功能有着天然的优势。