您的当前位置:首页正文

最详细的基于Session实现登录 + Redis的改进

2024-11-29 来源:个人技术集锦

首先实现发送验证码的功能,即用户输入手机号,先判断入参是否正常,然后这个用户是不是新用户,如果是就注册,然后最终发送验证码,同时将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是没有办法共享的。

满足上述三个条件的结构也就呼之欲出了

Redis

 这里使用随机生成一个字符串(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分钟后下线,所以我们要增加一个拦截器。

 将大部分代码移动到新添加的拦截器

原来的拦截器仅仅判断是否存在即可

在配置类处进行注册,并且要保证新加入的拦截器优先执行,即设置优先级

显示全文