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依赖,内有RandomUtil
String code = RandomUtil.randomNumbers(6);
//3.保存验证码到session
session.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.存入session
session.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 {
@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.判断用户是否存在. 不存在:拦截;存在:放入ThreadLocal,放行(写了ThreadLocal的封装工具类UserHolder)
if(user==null){
response.setStatus(401);
response.getWriter().write("用户未登录!");
return false;
}
UserHolder.saveUser((UserDTO) user);
return true;
}
@Override
public 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 {
@Override
public 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 {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号:利用util下RegexUtils进行正则验证
if(RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手机号格式不正确!");
}
//2.生成验证码:导入hutool依赖,内有RandomUtil
String code = RandomUtil.randomNumbers(6);
//3.保存验证码到Redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY +phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);//有效期2mins
//4.发送验证码
log.info("验证码为: " + code);
log.debug("发送短信验证码成功!");
return Result.ok();
}
@Override
public 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)生成token
String 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)存储到Redis
String 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 {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public 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;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取请求头中的token
String 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.保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
//7.更新token的有效时间,只要用户还在访问我们就需要更新token的存活时间
stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
//8.放行
return true;
}
@Override
public 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 {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public 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;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取请求头中的token
String 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.保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
//7.更新token的有效时间,只要用户还在访问我们就需要更新token的存活时间
stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
//8.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//销毁,以免内存泄漏
UserHolder.removeUser();
}
}
LoginInterceptor
public class LoginInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断是否需要拦截
if(UserHolder.getUser()==null){
response.setStatus(401);
response.getWriter().write("用户未登录!");
return false;
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//销毁,以免内存泄漏
UserHolder.removeUser();
}
}