提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


基于session实现用户登录

在这里插入图片描述

1、发送短信验证码

在这里插入图片描述

 @Override
    public Result sendCode(String phone, HttpSession session) {
        //校验手机号
        if (RegexUtils.isPhoneInvalid(phone)){
            //不符合返回
            return Result.fail("手机号格式错误");
        }
        //符合,生成验证码
        String code = RandomUtil.randomNumbers(6);//这个是hutool提供的随机生成6位数的API
        //保存验证码到session
        session.setAttribute("code",code);
        //发送验证码(真实情况下发送验证码,要调用第三方的平台,我们这里只是模拟一下)
        log.debug("发送短信验证码成功,验证码{}", code);
        return Result.ok();
    }

2、短信验证登录

在这里插入图片描述

 @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //校验手机号
        String phone = loginForm.getPhone();//手机号
        if (RegexUtils.isPhoneInvalid(phone)){
            //不符合返回
            return Result.fail("手机号格式错误");
        }
        //校验验证码

        String codeClient = loginForm.getCode();//用户填写的验证码

        String codeServer = (String)session.getAttribute("code");//服务器发送给客户端的验证码
        if (StringUtils.isEmpty(codeClient)){
            //不符合返回
            return Result.fail("请填写验证码");
        }
        if (!codeClient.equals(codeServer)){
            //不符合返回
            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();
    }

    private User createUserWithPhone(String phone){
        //创建用户
        User user =new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
        //保存用户
        save(user);
        return user;
    }

3、使用拦截器解决登录验证

在这里插入图片描述
在这里插入图片描述
因为一个用户的请求就对应一个线程,为了获取user的线程安全问题,我们把user绑定在线程域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();
    }
}

public class LoginIntercepter implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
       //获取session
        HttpSession session = request.getSession();
        UserDTO user = (UserDTO)session.getAttribute("user");
        if (user==null){//判断用户是否存在
            response.setStatus(401);
            return false;
        }
        UserHolder.saveUser(user);
        return true;
    }


    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    UserHolder.removeUser();//一定要remove,避免内存泄漏
    }
}

4、 集群下,使用session解决用户登录的问题

在这里插入图片描述

redis实现用户登录

1、基于redis实现用户登录的业务流程

在这里插入图片描述
1、发送短信验证:不再将验证码保存在session中,而是保存在redis中,使用String类型即可,key为手机号,value为验证码
2、短信登录和注册:同样需要校验手机号和验证码、判断用户是否存在等。如果用户存在,服务器创建token并返回给客户端,用户保存在redis中,这里推荐Hash类型保存,key为token,如果用户不存在就注册用户,并保存在redis中,逻辑同上

token不推荐用手机号,不安全

在这里插入图片描述
在这里插入图片描述

3、校验用户登录状态的时候,请求会携带token过来,如果通过token校验用户是否存在

1、发送短信验证

@Override
    public Result sendCode(String phone, HttpSession session) {
        //校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            //不符合返回
            return Result.fail("手机号格式错误");
        }
        //符合,生成验证码
        String code = RandomUtil.randomNumbers(6);//这个是hutool提供的随机生成6位数的API
        /**
         * 保存验证码到redis中
         *
         * key:login:code:手机号
         * value:验证码
         * 保存两分钟
         */
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //发送验证码(真实情况下发送验证码,要调用第三方的平台,我们这里只是模拟一下)
        log.debug("发送短信验证码成功,验证码{}", code);
        return Result.ok();
    }

2、短信登录和注册

@Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //校验手机号
        String phone = loginForm.getPhone();//手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            //不符合返回
            return Result.fail("手机号格式错误");
        }
        //校验验证码

        String codeClient = loginForm.getCode();//用户填写的验证码
        //todo 从redis获取验证码
        String codeServer = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);

        if (StringUtils.isEmpty(codeClient)) {
            //不符合返回
            return Result.fail("请填写验证码");
        }
        if (!codeClient.equals(codeServer)) {
            //不符合返回
            return Result.fail("验证码填写有误");
        }

        //校验一致,判断用户是否存在
        User user = query().eq("phone", phone).one();

        if (user == null) {
            //创建新用户并保存

            user = createUserWithPhone(phone);
        }

        //todo 保存用户到redis中
        //随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        //将user转化为hashMap存储
        String tokenKey = token+LOGIN_USER_KEY;
        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token, BeanUtil.beanToMap(userDTO,new HashMap<>()
                , CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString())
        ));
        //设置LOGIN_USER_KEY有效期为半小时,注意的是在校验用户登录状态的时候,要更新这个时间,
        /**
         * session,是只要用户超过30分钟不操作,就会删除session中的数据,用户就得重新登录
         * 我们这里就要模仿session的做法, 如果用户长时间不访问操作系统就让用户重新登录
         */
        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
        //返回token
        return Result.ok(token);
    }

3、校验用户登录状态

public class LoginIntercepter implements HandlerInterceptor {
    
   private StringRedisTemplate stringRedisTemplate;
   
   public LoginIntercepter(StringRedisTemplate stringRedisTemplate){
       this.stringRedisTemplate = stringRedisTemplate;
   }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
         // TODO 获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)){
            //不存在,拦截
            response.setStatus(401);
            return false;
        }
        String key = RedisConstants.LOGIN_CODE_KEY + token;
        //TODO 基于token获取redis中的用户(hashMap)
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        if (userMap.isEmpty()){//判断用户是否存在
            response.setStatus(401);
            return false;
        }
        //todo 将查询到的hashMap数据转化为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
       
        UserHolder.saveUser(userDTO);
        //todo 刷新todo的有效期
        stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        //todo 放行
        return true;
    }


    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    UserHolder.removeUser();//一定要remove,避免内存泄漏
    }
}


@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginIntercepter(stringRedisTemplate))
                .excludePathPatterns(
                 "/shop/**",
                 "/voucher/**",
                 "/shop-type/**",
                 "/upload/**",
                 "/blog/hot",
                 "/user/code",
                 "/user/login"
                );
    }
}

优化

之前的拦截器,虽然也能起到刷新token有效期的作用,但是,那个token只有在访问需要登录验证的一些handler时候才会刷新
,这显然是不行的,为了解决这个问题,我们在定义一个拦截器,第一个拦截器先执行,拦截所有请求,只负责刷新token的时间(有就刷新,没有就不刷新,不负责拦截),第二个负责判断用户是不是存在(每次用户登录的时候,我们都把user保存在了ThreadLocal中,只需判单ThreadLocal中是否存在即可)

在这里插入图片描述
在这里插入图片描述

拦截器1——负责刷新token有效期

package com.hmdp.config;

import com.hmdp.utils.LoginIntercepter;
import com.hmdp.utils.RefrshTokenInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginIntercepter())
                .excludePathPatterns(
                 "/shop/**",
                 "/voucher/**",
                 "/shop-type/**",
                 "/upload/**",
                 "/blog/hot",
                 "/user/code",
                 "/user/login"
                ).order(1);
        //order越小,拦截器优先级越高
        registry.addInterceptor(new RefrshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

拦截器2——负责校验用户是否登录

package com.hmdp.utils;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class LoginIntercepter implements HandlerInterceptor {


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断是否需要拦截(ThreadLocal中是否有用户)
        if (UserHolder.getUser()==null){
            //没有,需要拦截
            response.setStatus(401);
            return false;
        }
        //有用户放行
        return true;
    }


    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();//一定要remove,避免内存泄漏
    }
}
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐