毕业设计和各种考试耽搁了快两个月了,突然意识到再不继续学跟我的想法就会越走越远了,遂开始黑马点评的学习
基于session实现登录
发送验证码
首先校验手机号,通过正则表达式,然后再随机生成一个长度为6的字符串,保存在session中
登录注册
如果输入的验证码和存在session中的是一样的,那么就通过校验找对应的user实体类,user为空就直接注册(也就是insert),然后将user放在session中
检验登录状态
用户在请求时候,会从cookie中携带者JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并且放行

发送验证码
Todo:后续可以根据Ruoyi那个图片验证码改进,但是逻辑都差不多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Override public Result sendCode(String phone, HttpSession session) { if (RegexUtils.isPhoneInvalid(phone)) { return Result.fail("手机号格式错误!"); } String code = RandomUtil.randomNumbers(6);
session.setAttribute("code",code); log.debug("发送短信验证码成功,验证码:{}", code); return Result.ok(); }
|
登录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Override public Result login(LoginFormDTO loginForm, HttpSession session) { String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)) { return Result.fail("手机号格式错误!"); } Object cacheCode = session.getAttribute("code"); String code = loginForm.getCode(); if(cacheCode == null || !cacheCode.toString().equals(code)){ return Result.fail("验证码错误"); } User user = query().eq("phone", phone).one();
if(user == null){ user = createUserWithPhone(phone); } session.setAttribute("user",user);
return Result.ok(); }
|
拦截
初始版本看session中是否有user对象,如果有就把他放到ThreadLocal中,方便后续调用,为什么不每次都在session中取呢?我猜是因为http的request不是随时随地哪个方法都要写的,threadlocal可以比较方便
先写登录的拦截器,由于要进行处理,就跟aop一样,有一个pre有一个after,实现的是HandlerInterceptor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class LoginInterceptor implements HandlerInterceptor {
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); Object user = session.getAttribute("user"); if(user == null){ response.setStatus(401); return false; } UserHolder.saveUser((User)user); return true; } }
|
有了这个拦截器但是还要让他生效,被springmvc管理,需要一个配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| package com.hmdp.config;
import com.hmdp.interceptor.LoginInterceptor; import com.hmdp.interceptor.RefreshInterceptor; import org.springframework.beans.factory.annotation.Autowired; 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 { @Autowired private StringRedisTemplate stringRedisTemplate; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .excludePathPatterns( "/shop/**", "/voucher/**", "/shop-type/**", "/upload/**", "/blog/hot", "/user/code", "/user/login" ).order(1); registry.addInterceptor(new RefreshInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0); } }
|
WebMvcConfigurer配置类其实是Spring内部的一种配置方式,采用JavaBean的形式来代替传统的xml配置文件形式进行针对框架个性化定制,可以自定义一些Handler,Interceptor,ViewResolver,MessageConverter。基于java-based方式的spring mvc配置,需要创建一个配置类并实现WebMvcConfigurer 接口;
常用的方法:
addInterceptors:拦截器
- addInterceptor:需要一个实现HandlerInterceptor接口的拦截器实例
- addPathPatterns:用于设置拦截器的过滤路径规则;addPathPatterns(“/**”)对所有请求都拦截
- excludePathPatterns:用于设置不需要拦截的过滤规则
- 拦截器主要用途:进行用户登录状态的拦截,日志的拦截等。
addViewControllers:页面跳转
拦截到一个路径就跳转到对应的页面
1 2 3 4
| @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/toLogin").setViewName("login"); }
|
这个方法意思估计就是当/toLogin”路径的时候跳转到login页面
Todo:以后慢慢补充
Redis代替session的业务
code和user都存在session中,而session是本地的,在集群模式下会失效,所以需要一个全局的解决方案。
基本的解决思路就是把验证码和user都存在redis里面,设定过期时间,拦截器变成续期即可。有一个问题就是以什么数据结构来存。code可以以string类型来存储。
user其实也可以,我这里原本想的是用JSON存,但是不够直观,而且存储效率也没有hash好,所以还是跟着他用了hash。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| @Service @Slf4j public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { @Autowired private StringRedisTemplate redisTemplate;
@Override public Result sendCode(String phone, HttpSession session) { if (RegexUtils.isPhoneInvalid(phone)){ return Result.fail("手机号格式错误"); } String code = RandomUtil.randomNumbers(6); redisTemplate.opsForValue().set(redisConstants.LOGINREDISCODE + phone,code, 60,TimeUnit.SECONDS); log.debug("验证码为:"+code); return Result.ok(); }
@Override public Result login(LoginFormDTO loginForm, HttpSession session) { if (RegexUtils.isPhoneInvalid(loginForm.getPhone())||RegexUtils.isCodeInvalid(loginForm.getCode())){ return Result.fail("手机号格式错误"); } String code = redisTemplate.opsForValue().get(redisConstants.LOGINREDISCODE + loginForm.getPhone()); if (code == null || !loginForm.getCode().equals(code)){ return Result.fail("验证码有误"); } LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.eq(User::getPhone,loginForm.getPhone()); User user = getOne(lambdaQueryWrapper); if (user == null){ User temp = new User(); temp.setPhone(loginForm.getPhone()); save(temp); user = temp; } String token = UUID.randomUUID(true).toString(); UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create() .setIgnoreNullValue(true) .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
redisTemplate.opsForHash().putAll(redisConstants.LOGINUSER+token,userMap); redisTemplate.expire(redisConstants.LOGINUSER,30,TimeUnit.MINUTES); return Result.ok(token); } }
|
主要就是用了redisTemplate的两种方法,string类型的就用opsForValue,hash用opsForHash,值得注意的是这里putAll是将一个map全部存进去,只有两个参数,而put可能指的是在这个key下的map中的其中一行,也就是说有三个参数,key,name和value。
这里的beanToMap主要记住setFieldValueEditor是编辑域的,那当然有两个参数,修改对应fieldName下的fieldValue
还有一个是setFieldNameEditor编辑name的,比如你想让name变成大写,就用UpperCase
解决登录刷新问题
我们之前的拦截器会排除一些路径进行刷新,但是我们要这些路径被访问的时候也要刷新,所以选择了连个拦截器的方案,其中第一个完成所有redis的续期,后面一个延续拦截指定路径校验登录状态。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @Slf4j public class RefreshInterceptor implements HandlerInterceptor { private StringRedisTemplate stringRedisTemplate; public RefreshInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; }
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("authorization"); if (token == null || token.isEmpty()){ return true; } Map<Object, Object> objectMap = stringRedisTemplate.opsForHash().entries(redisConstants.LOGINUSER + token); if (objectMap.isEmpty()){ return true; } UserDTO dto = BeanUtil.mapToBean(objectMap, UserDTO.class, CopyOptions.create());
log.debug(dto.toString()); UserHolder.saveUser(dto); stringRedisTemplate.expire(redisConstants.LOGINUSER + token,30, TimeUnit.MINUTES); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Slf4j public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { UserDTO user = UserHolder.getUser(); if (user == null){ response.setStatus(401); return false; } log.debug("当前用户:"+user.getNickName()); return true; } }
|