Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

整体登陆流程

1、验证码生成与发送机制

用户点击”发送验证码”后,系统首先进行手机号格式校验,校验通过后生成6位数字验证码并存储到Redis中,设置5分钟过期时间。Redis的key采用规范化格式:login:code:{手机号}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Result sendCode(String phone, HttpSession session) {
// 校验手机号格式是否正确
if (RegexUtils.isPhoneInvalid(phone)) {
// 如果手机号格式不正确,返回错误信息
return Result.fail("手机号格式错误!");
}

// 生成6位随机数字验证码
String code = RandomUtil.randomNumbers(6);

// 将验证码保存到Redis,设置5分钟有效期
// key格式:login:code:{手机号}
redisTemplate.opsForValue().set("login:code:" + phone, code, 5, TimeUnit.MINUTES);

// 模拟发送验证码,实际项目中这里应该调用短信服务API
log.debug("发送验证码成功!验证码为:{}", code);

// 返回成功响应
return Result.ok();
}

2、根据验证码登陆。

收到验证码填写后,进行登陆。首先还是先校验手机号。校验失败返回错误提示。校验成功后开始校验验证码,验证码从redis里面通过手机号phone取,校验失败返回错误提示。校验成功后,通过手机号查询user表里是否有用户,如果没有则自动创建新用户。随后生成token,键为uuid,值为用户信息,并存储到redis中,设置过期时间30分钟。并把token返回给前端,后续前端的任何请求都会携带token。

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
55
56
57
58
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();

// 再次校验手机号格式
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误!");
}

String code = loginForm.getCode();
// 从Redis获取之前存储的验证码
String cacheCode = redisTemplate.opsForValue().get("login:code:" + phone);

// 验证码校验:检查是否存在或是否匹配
if (cacheCode == null || !cacheCode.equals(code)) {
return Result.fail("验证码错误!");
}

// 验证通过,根据手机号查询用户
User user = query().eq("phone", phone).one();

// 判断用户是否存在,不存在则自动注册
if (user == null) {
user = createUserWithPhone(phone);
}

// 生成登录令牌token(UUID的简化版,不带连字符)
String token = UUID.randomUUID().toString(true);

// 将User对象转换为UserDTO对象(数据传输对象,通常包含较少敏感字段)
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);

// 将UserDTO转为Map,便于存入Redis的Hash结构
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
// 将Map中的所有值转为String类型,因为Redis要求值为String
userMap.replaceAll((k, v) -> v.toString());

// 将用户信息以Hash结构存入Redis
// key格式:login:token:{token}
redisTemplate.opsForHash().putAll("login:token:" + token, userMap);
// 设置token的有效期为30分钟
redisTemplate.expire("login:token:" + token, 30, TimeUnit.MINUTES);

// 返回token给客户端
return Result.ok(token);
}

private User createUserWithPhone(String phone) {
// 创建用户对象
User user = new User();
// 设置手机号
user.setPhone(phone);
// 设置默认昵称:前缀 + 10位随机字符串
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
// 保存用户到数据库
save(user);
// 返回创建的用户对象
return user;
}

3、登陆状态验证

登陆成功后还没完成。需要通过token来验证是否是登陆中。使用两层拦截器。第一层拦截器拦截所有请求,如果token存在且redis中token的值没有过期,则从redis中取出用户并保存到ThreadLocal,并且刷新token有效期。否则直接放行。第二层拦截器就判断ThreadLocal是否有值,如果没有说明没有登陆,直接拦截。否则就放行。这样就保证了在使用的过程中token会不断刷新有效期,只有没有使用的时候token才会过期。

第一层拦截器

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
55
56
57
58
59
60
61
62
63
package com.hmdp.interceptor;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
* 第一层拦截器
*/
@Component
public class RefreshInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate redisTemplate;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从请求头中获取token
String token = request.getHeader("authorization");
// 判断token是否为空,为空则直接放行
if (StrUtil.isBlank(token)) {
return true;
}

// 基于token从Redis中获取用户信息
// Redis中用户信息以Hash结构存储,key格式为"login:token:{token值}"
Map<Object, Object> userEntries = redisTemplate.opsForHash().entries("login:token:" + token);

// 判断用户信息是否存在,不存在则直接放行
if (userEntries.isEmpty()) {
return true;
}

// 将Redis中的用户数据转换为UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userEntries, new UserDTO(), false);

// 将用户信息存入ThreadLocal,便于当前线程后续使用
UserHolder.saveUser(userDTO);

// 刷新Redis中token的过期时间,延长用户登录状态
// 设置为30分钟
redisTemplate.expire("login:token:" + token, 30 * 60, TimeUnit.SECONDS);

// 放行请求
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
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.hmdp.interceptor;

import com.hmdp.utils.UserHolder;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

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

/**
* 第二层拦截器
*/
@Component
public class TokenInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//第一层拦截器过后,如果threadLocal没有用户,说明没有登录,返回401
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();
}
}

拦截器配置

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
package com.hmdp.config;


import com.hmdp.interceptor.RefreshInterceptor;
import com.hmdp.interceptor.TokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private TokenInterceptor tokenInterceptor;
@Autowired
private RefreshInterceptor refreshInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor).
excludePathPatterns("/user/code","/user/login",
"/shop/**",
"/blog/hot",
"/shop-type/**").order(1);
registry.addInterceptor(refreshInterceptor).addPathPatterns("/**").order(0);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Result queryById(Long id) {
// 1.查询Redis
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回
Shop shop1 = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop1);
}

// 4.不存在,根据id查询数据库
Shop shop = getById(id);
if(shop==null){
return Result.fail("店铺不存在");
}
stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop));

return Result.ok(shop);
}

评论