首先实现发送验证码的功能,即用户输入手机号,先判断入参是否正常,然后这个用户是不是新用户,如果是就注册,然后最终发送验证码,同时将user存入session中,但是user中存有password等私密信息,因此创建一个UserDTO,用于session的传输,最后将UserDTO存入session中
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误");
}
String code = loginForm.getCode();
String sessionCode = (String) session.getAttribute("code");
if (sessionCode == null || !sessionCode.equals(code)) {
return Result.fail("验证码错误");
}
User user = query().eq("phone", phone).one();
if (user == null) {
user = createUserWithPhone(phone);
}
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
return Result.ok();
}
然后实现验证码登录注册功能,检测手机号是正确的之后,直接调用其他模块的服务。
之后,每次的操作都要加入登录验证,即查询session中是否存有这个用户信息,如果存有用户信息,就将这个用户信息从session中取出来,存入ThreadLoacl中(每个线程都有一个独有的ThreadLocalMap),我们就可以通过这个ThreadLoacl将对应的用户信息取出来。
因为几乎每个方法都要加入这类操作,所以考虑使用aop,拦截器进行拦截
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
UserDTO user = (UserDTO)session.getAttribute("user");
if (user == null) {
// 401 未授权
response.setStatus(401);
return false;
}
// ThreadLocal
UserHolder.saveUser(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
threadLocal内存泄漏问题
使用完ThreadLocal,都必须都调用它的remove()方法。所以在拦截器中要在方法返回后对ThreadLocal进行清除。
UserHolder类
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();
}
}
同时也要对拦截器进行注册
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/blog/hot",
"shop-type/**",
"/upload/**",
"/user/code",
"/user/login"
);
}
}
此后,后续获取用户的信息,就可以通过ThreadLocal获取,这样就完成了用户的登录与验证。
但是session是保存在tomcat服务器中的,如果我们采用集群的架构,session是没有办法共享的。
满足上述三个条件的结构也就呼之欲出了
这里使用随机生成一个字符串(token)作为用户信息在redis里的key,以前采用存储在session里的方式,tomcat会自动的将session id写入浏览器的cookie,每次请求cookie就带着session来了,即登录凭证就是这个session id,但是现在我们已经不用这个session登录了,所以要使用这个随机生成的token作为登录凭证,所以用户在访问我们的时候就应该带着token访问,但是tomcat不会自动的将token写入浏览器,就需要我们手动的返回给客户端,客户端将信息保存下来,每次请求携带token。
在js中
下面对代码进行更改
首先修改验证手机号模块,此处生成验证码后将验证码存储于session中,改为存储在redis中,并且设置2分钟的超时时间。
再修改登录方法
此处遇到一个问题,如果我们这样设置过期时间,即30分钟以后需要重新登录,这样是不符合常理的,因为如果这样操作的话,用户就仅仅有30分钟的登录时长,体验明显不好,应该在用户30分钟未操作才进行清除,而不是固定的30分钟删除。
因此需要修改我们的拦截器代码,加入刷新时间的功能
这样代码的改进就基本完成了,但是还是有一些问题,拦截器是拦截的指定路径的,但是如果用户恰恰就访问了没有拦截的路径的信息,他还是会30分钟后下线,所以我们要增加一个拦截器。
将大部分代码移动到新添加的拦截器
原来的拦截器仅仅判断是否存在即可
在配置类处进行注册,并且要保证新加入的拦截器优先执行,即设置优先级