https://blog.csdn.net/qq_33888850/article/details/129770077
https://blog.csdn.net/weixin_51515308/article/details/128010464
https://www.bilibili.com/video/BV1cr4y1671t?p=24
导入数据库
https://github.com/MagicToDo/hm-dianping
sql文件在 hm-dianping-init\src\main\resources\db目录下
可以先使用Navicat新建hmdp数据库,再使用goland的数据库模块执行sql query(直接导入Navicat会报错)
项目架构
导入项目
idea导入hm-dianping-init项目
修改application.yaml为自己的配置
访问一下 http://localhost:8081/shop-type/list
导入前端项目,放入html目录下,使用Nginx启动
start nginx.exe
控制台切换手机模式,访问localhost:8080
基于Session实现登录流程
1、发送验证码
用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号
如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用户
2、短信验证码登录、注册
用户将验证码和手机号进行输入,后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到session中,方便后续获得当前登录信息
3、校验登录状态
用户在请求的时候,会从cookie中携带JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并放行
session共享问题
多台Tomcat不共享session存储空间,当请求切换到不同的tomcat服务时导致数据丢失的问题
所以我们把数据存入Redis,集群的Redis可以替代session
基于Session实现代码
发送验证码功能
userService创建sendCode接口,并实现该接口
@Override
public Result sendCode(String phone, HttpSession session) {//1.校验手机号:利用util下RegexUtils进行正则验证if(RegexUtils.isPhoneInvalid(phone)){return Result.fail("手机号格式不正确!");}//2.生成验证码:导入hutool依赖,内有RandomUtilString code = RandomUtil.randomNumbers(6);//3.保存验证码到sessionsession.setAttribute("code",code);//4.发送验证码log.info("验证码为: " + code);log.debug("发送短信验证码成功!");return Result.ok();
}
login功能
@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(code==null||!cacheCode.toString().equals(code)){//3.不一致,报错return Result.fail("验证码错误!");}//4.一致,根据手机号查询用户(需要写对应的单表查询方法:select * from tb_user where phone = #{phone})User user = query().eq("phone", phone).one();if(user==null){//5.注册用户user.setPhone(phone);user.setNickName("user_"+RandomUtil.randomString(10));//保存用户save(user);}//6.存入sessionsession.setAttribute("user",user);return Result.ok();
}
登录验证功能
这里用的是cookie,然后用拦截器进行拦截然后验证,但是一般用jwt令牌放入localstroagecookie
上面不能直接把user存入,而是要把保留一些不隐私的信息(UserDto)然后传入Session
//6.存入session
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
拦截器
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.获取sessionHttpSession session = request.getSession();//2.获取session中的用户Object user = session.getAttribute("user");//3.判断用户是否存在. 不存在:拦截;存在:放入ThreadLocal,放行(写了ThreadLocal的封装工具类UserHolder)if(user==null){response.setStatus(401);response.getWriter().write("用户未登录!");return false;}UserHolder.saveUser((UserDTO) user);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//移除用户UserHolder.removeUser();}
}
其中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();}
}
在MvcConfig内添加上我们的拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/upload/**","/voucher/**");}
}
基于Redis实现共享session登录
选择String类型存验证码即可,value:验证码,但是key要区分开来
选择Hash存储用户信息,因为每个字段独立,比较好去CRUD,内存占用少,key用token即可(随机字符串)
之前的session的话,tomcat会自动把session的Id存入Cookie,每次请求都会携带Cookie,所以我们需要手动把token返回给客户端,每次请求客户端都会携带着token
public class RedisConstants {public static final String LOGIN_CODE_KEY = "login:code:";public static final Long LOGIN_CODE_TTL = 2L;public static final String LOGIN_USER_KEY = "login:token:";public static final Long LOGIN_USER_TTL = 36000L;public static final Long CACHE_NULL_TTL = 2L;public static final Long CACHE_SHOP_TTL = 30L;public static final String CACHE_SHOP_KEY = "cache:shop:";public static final String LOCK_SHOP_KEY = "lock:shop:";public static final Long LOCK_SHOP_TTL = 10L;public static final String SECKILL_STOCK_KEY = "seckill:stock:";public static final String BLOG_LIKED_KEY = "blog:liked:";public static final String FEED_KEY = "feed:";public static final String SHOP_GEO_KEY = "shop:geo:";public static final String USER_SIGN_KEY = "sign:";
}
sendCode修改
//3.保存验证码到Redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY +phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);//有效期2mins
UserServiceImpl
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result sendCode(String phone, HttpSession session) {//1.校验手机号:利用util下RegexUtils进行正则验证if(RegexUtils.isPhoneInvalid(phone)){return Result.fail("手机号格式不正确!");}//2.生成验证码:导入hutool依赖,内有RandomUtilString code = RandomUtil.randomNumbers(6);//3.保存验证码到RedisstringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY +phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);//有效期2mins//4.发送验证码log.info("验证码为: " + code);log.debug("发送短信验证码成功!");return Result.ok();}@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {//1.校验手机号String phone = loginForm.getPhone();if(RegexUtils.isPhoneInvalid(phone)){return Result.fail("手机号格式错误!");}//2.从Redis中获取验证码String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);String code = loginForm.getCode();if(cacheCode==null||!cacheCode.equals(code)){//3.不一致,报错return Result.fail("验证码错误!");}//4.一致,根据手机号查询用户(需要写对应的单表查询方法:select * from tb_user where phone = #{phone})User user = query().eq("phone", phone).one();if(user==null){//5.注册用户User newUser = new User();newUser.setPhone(phone);newUser.setNickName("user_"+RandomUtil.randomString(10));save(newUser);user = newUser;}//6.保存用户到Redis//(1)生成tokenString token = UUID.randomUUID().toString(true);//hutools//(2)User转为HashMap存储UserDTO userDTO = BeanUtil.copyProperties(user,UserDTO.class);HashMap<Object, Object> userMap = new HashMap<>();userMap.put("id", userDTO.getId().toString());userMap.put("nickName", userDTO.getNickName());userMap.put("icon", userDTO.getIcon());//(3)存储到RedisString tokenKey = LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);//(4) 设置有效期 stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);return Result.ok(token);}
}
MvcConfig注入stringRedisTemplate,然后传给LoginInterceptor,因为LoginInterceptor不是bean不能用spring注入其他bean
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/upload/**","/voucher/**");}
}
LoginInterceptor
public class LoginInterceptor implements HandlerInterceptor{private final StringRedisTemplate stringRedisTemplate;public LoginInterceptor(StringRedisTemplate stringRedisTemplate){this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)){//不存在,拦截 设置响应状态吗为401(未授权)response.setStatus(401);return false;}//2.基于token获取redis中用户String key=RedisConstants.LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);//3.判断用户是否存在if (userMap.isEmpty()){//4.不存在则拦截,设置响应状态吗为401(未授权)response.setStatus(401);return false;}//5.将查询到的Hash数据转化为UserDTO对象UserDTO userDTO=new UserDTO();BeanUtil.fillBeanWithMap(userMap,userDTO, false);//6.保存用户信息到ThreadLocalUserHolder.saveUser(userDTO);//7.更新token的有效时间,只要用户还在访问我们就需要更新token的存活时间stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);//8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//销毁,以免内存泄漏UserHolder.removeUser();}
}
模拟session的有效期,30min不访问session会过期。不能直接在redis中设置token的有效期为30min,这两种是不一样的。可以通过拦截器达到这一个效果。并且保证了,访问 不需要登陆可以访问的页面 的时候也会刷新token有效期
MvcConfig
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/upload/**","/voucher/**").order(1);//RefreshTokenInterceptor 先于 LoginInterceptor 执行registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);//默认拦截所有请求}}
RefreshTokenInterceptor
public class RefreshTokenInterceptor implements HandlerInterceptor{private final StringRedisTemplate stringRedisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)){return true;}//2.基于token获取redis中用户String key=RedisConstants.LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);//3.判断用户是否存在if (userMap.isEmpty()){return true;}//5.将查询到的Hash数据转化为UserDTO对象UserDTO userDTO=new UserDTO();BeanUtil.fillBeanWithMap(userMap,userDTO, false);//6.保存用户信息到ThreadLocalUserHolder.saveUser(userDTO);//7.更新token的有效时间,只要用户还在访问我们就需要更新token的存活时间stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);//8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//销毁,以免内存泄漏UserHolder.removeUser();}
}
LoginInterceptor
public class LoginInterceptor implements HandlerInterceptor{@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//判断是否需要拦截if(UserHolder.getUser()==null){response.setStatus(401);response.getWriter().write("用户未登录!");return false;}return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//销毁,以免内存泄漏UserHolder.removeUser();}
}