毕业设计和各种考试耽搁了快两个月了,突然意识到再不继续学跟我的想法就会越走越远了,遂开始黑马点评的学习

基于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) {
// 1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);

// 4.保存验证码到 session
session.setAttribute("code",code);
// 5.发送验证码
log.debug("发送短信验证码成功,验证码:{}", code);
// 返回ok
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) {
// 1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.校验验证码
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
if(cacheCode == null || !cacheCode.toString().equals(code)){
//3.不一致,报错
return Result.fail("验证码错误");
}
//一致,根据手机号查询用户
User user = query().eq("phone", phone).one();

//5.判断用户是否存在
if(user == null){
//不存在,则创建
user = createUserWithPhone(phone);
}
//7.保存用户信息到session中
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 {
//1.获取session
HttpSession session = request.getSession();
//2.获取session中的用户
Object user = session.getAttribute("user");
//3.判断用户是否存在
if(user == null){
//4.不存在,拦截,返回401状态码
response.setStatus(401);
return false;
}
//5.存在,保存用户信息到Threadlocal
UserHolder.saveUser((User)user);
//6.放行
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);
// token刷新的拦截器
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);
//这里用Redis完成
//session.setAttribute("code",code);
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("手机号格式错误");
}
//这里改用Redis
//String sessionCode = session.getAttribute("code").toString();
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();
//用json存储
//String jsonStr = JSONUtil.toJsonStr(user);
//用hash存储
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 {
//1.检验是否有token
String token = request.getHeader("authorization");
if (token == null || token.isEmpty()){
//1.1 如果没有token直接放行给下一个拦截器,这样肯定会被拦截
return true;
}
//2.检验是否在redis中
//entries得到的是一个map,get查具体的,所以有两个参数key和fieldName
Map<Object, Object> objectMap = stringRedisTemplate.opsForHash().entries(redisConstants.LOGINUSER + token);
if (objectMap.isEmpty()){
return true;
}
UserDTO dto = BeanUtil.mapToBean(objectMap, UserDTO.class, CopyOptions.create());
//3.保存在threadLocal中

//UserDTO userDTO = BeanUtil.fillBeanWithMap(objectMap, new UserDTO(), false);
log.debug(dto.toString());
UserHolder.saveUser(dto);
//4.刷新时间
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;
}
}