news 2026/4/17 9:19:28

基于Redis实现登录功能思路详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Redis实现登录功能思路详解

本文使用的是手机号+验证码的登录方式,其中验证码是通过在控制台输出,并没有真的发送到手机上(太麻烦,主要目的还是学习使用Redis)

重点是看思路,而不是具体的代码实现

UserServiceImpl实现类

整体结构

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

@Slf4j

@Service

publicclassUserServiceImplextendsServiceImpl<UserMapper, User>implementsIUserService {

@Autowired

privateStringRedisTemplate stringRedisTemplate;

@Override

publicResult sendCode(String phone, HttpSession session) {

//...

}

@Override

publicResult login(LoginFormDTO loginForm, HttpSession session) {

//...

}

privateUser createUserWithPhone(String phone) {

//...

}

}

sendCode方法

这个是发送验证码的方法

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

publicResult sendCode(String phone, HttpSession session) {

// 1. 校验手机号

if(RegexUtils.isPhoneInvalid(phone)) {

// 2. 如果不符合,返回错误信息

returnResult.fail("手机号格式错误!");

}

// 3. 如果符合,生成验证码

String code = RandomUtil.randomNumbers(6);

// 4. 保存验证码到redis

stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY +phone,code,RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);

// 5. 发送验证码

log.debug("发送短信验证码成功,验证码:{}", code);

// 6. 返回结果

returnResult.ok();

}

注:这里的RedisConstants是一个用来存放各种常量的类

1

2

3

4

5

6

publicclassRedisConstants {

publicstaticfinalString LOGIN_CODE_KEY ="login:code:";

publicstaticfinalLong LOGIN_CODE_TTL = 2L;

publicstaticfinalString LOGIN_USER_KEY ="login:token:";

publicstaticfinalLong LOGIN_USER_TTL = 30L;

}

login方法

这里使用了MybatisPlus来操作数据库(User user = query().eq("phone", phone).one();),但是这个不是重点

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

publicResult login(LoginFormDTO loginForm, HttpSession session) {

// 1. 校验手机号

String phone = loginForm.getPhone();

if(RegexUtils.isPhoneInvalid(phone)) {

returnResult.fail("手机号格式错误!");

}

// 2. 从redis获取验证码并校验

String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY +phone);

String code = loginForm.getCode();

if(cacheCode ==null|| !cacheCode.equals(code)) {

// 3. 不一致,报错

returnResult.fail("验证码错误!");

}

// 4. 一致,根据手机号查询用户

User user = query().eq("phone", phone).one();

// 5. 判断用户是否存在

if(user ==null) {

// 6. 不存在,创建新用户并保存

user = createUserWithPhone(phone);

}

// 7. 保存用户信息到redis

String token= UUID.randomUUID().toString(true);

UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);

Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,newHashMap<>(),

CopyOptions.create()

.setIgnoreNullValue(true)

.setFieldValueEditor((fieldName, fieldValue)->fieldValue.toString()));

stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap);

stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

returnResult.ok(token);

}

createUserWithPhone方法

在login方法中调用了该方法

这里也使用了MybatisPlus来操作数据库(save(user);)

1

2

3

4

5

6

7

8

9

privateUser createUserWithPhone(String phone) {

// 1. 创建用户

User user =newUser();

user.setPhone(phone);

user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));

// 2. 保存用户

save(user);

returnuser;

}

拦截器

整体框架

其实就是实现了HandlerInterceptor的两个方法

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

@Slf4j

@Component

publicclassLoginInterceptorimplementsHandlerInterceptor {

@Autowired

privateStringRedisTemplate stringRedisTemplate;

@Override

publicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throwsException {

//...

}

@Override

publicvoidafterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throwsException {

// 移除用户

UserHolder.removeUser();

}

}

UserHolder是ThreadLocal 持有类

1

2

3

4

5

6

7

8

9

10

11

12

publicclassUserHolder {

privatestaticfinalThreadLocal<UserDTO> tl =newThreadLocal<>();

publicstaticvoidsaveUser(UserDTO user){

tl.set(user);

}

publicstaticUserDTO getUser(){

returntl.get();

}

publicstaticvoidremoveUser(){

tl.remove();

}

}

preHandle方法

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

publicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throwsException {

// 1.获取请求头中的token

String token = request.getHeader("authorization");

if(StrUtil.isBlank(token)) {

// 不存在,拦截

response.setStatus(401);

returnfalse;

}

// 2.基于token获取redis中的用户

String key = RedisConstants.LOGIN_USER_KEY + token;

Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);

// 3.判断用户是否存在

if(userMap.isEmpty()) {

// 4.不存在,拦截

response.setStatus(401);

returnfalse;

}

// 5.将查询到的Hash数据转换为UserDTO对象

UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap,newUserDTO(),false);

// 6.存在,保存用户信息到ThreadLocal

UserHolder.saveUser(userDTO);

// 7.刷新token有效期

stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

// 8.放行

returntrue;

}

注:authorization 是前端定义的用来传递token的key

配置类

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

@Configuration

publicclassMvcConfigimplementsWebMvcConfigurer {

@Autowired

privateLoginInterceptor loginInterceptor;

@Override

publicvoidaddInterceptors(InterceptorRegistry registry) {

registry.addInterceptor(loginInterceptor)

.addPathPatterns("/**")

.excludePathPatterns(

"/user/code",

"/user/login",

"/blog/hot",

"/shop/**",

"/shop-type/**",

"/upload/**",

"/voucher/**"

);

}

}

整体思路

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

flowchart TD

subgraph A[发送验证码流程]

A1["前端请求 发送验证码"] --> A2["校验手机号格式"]

A2 -- 不合法 --> A3["返回错误 手机号格式错误"]

A2 -- 合法 --> A4["生成6位验证码"]

A4 --> A5["保存验证码到Redis"]

A5 --> A6["返回成功"]

end

subgraph B[登录流程]

B1["前端请求 登录"] --> B2["校验手机号格式"]

B2 -- 不合法 --> B3["返回错误"]

B2 -- 合法 --> B4["从Redis获取验证码"]

B4 --> B5{"验证码是否正确"}

B5 -- 否 --> B6["返回验证码错误"]

B5 -- 是 --> B7["根据手机号查询用户"]

B7 --> B8{"用户是否存在"}

B8 -- 否 --> B9["创建新用户"]

B8 -- 是 --> B10["使用已有用户"]

B9 --> B11["生成Token"]

B10 --> B11

B11 --> B12["用户信息写入Redis"]

B12 --> B13["返回Token"]

end

subgraph C[请求拦截流程]

C1["请求到达拦截器"] --> C2["从请求头获取Token"]

C2 --> C3{"Token是否存在"}

C3 -- 否 --> C4["返回401"]

C3 -- 是 --> C5["从Redis获取用户信息"]

C5 --> C6{"用户是否存在"}

C6 -- 否 --> C4

C6 -- 是 --> C7["保存用户到ThreadLocal"]

C7 --> C8["刷新Token有效期"]

C8 --> C9["放行请求"]

end

subgraph D[请求结束]

D1["请求完成"] --> D2["清理ThreadLocal"]

end

B13 --> C1

C9 --> D1

复制到未命名绘图 - draw.io中用mermaid格式文件创建流程图

优化

目前之后访问被拦截的页面才会刷新有效期,所以这里我们需要优化一下

方式是采用拦截器链,即再加一个拦截器来拦截全部页面,以此来更新有效期

RefreshTokenInterceptor

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

@Slf4j

@Component

publicclassRefreshTokenInterceptorimplementsHandlerInterceptor {

@Autowired

privateStringRedisTemplate stringRedisTemplate;

@Override

publicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throwsException {

// 1.获取请求头中的token

String token = request.getHeader("authorization");

if(StrUtil.isBlank(token)) {

returntrue;

}

// 2.基于token获取redis中的用户

String key = RedisConstants.LOGIN_USER_KEY + token;

Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);

// 3.判断用户是否存在

if(userMap.isEmpty()) {

returntrue;

}

// 5.将查询到的Hash数据转换为UserDTO对象

UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap,newUserDTO(),false);

// 6.存在,保存用户信息到ThreadLocal

UserHolder.saveUser(userDTO);

// 7.刷新token有效期

stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

// 8.放行

returntrue;

}

@Override

publicvoidafterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throwsException {

// 移除用户

UserHolder.removeUser();

}

}

LoginInterceptor

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

@Slf4j

@Component

publicclassLoginInterceptorimplementsHandlerInterceptor {

@Override

publicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throwsException {

// 判断是否需要拦截(ThreadLocal中是否有用户)

if(UserHolder.getUser() ==null) {

response.setStatus(401);

returnfalse;

}

// 有用户,则放行

returntrue;

}

@Override

publicvoidafterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throwsException {

// 移除用户

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

@Configuration

publicclassMvcConfigimplementsWebMvcConfigurer {

@Autowired

privateLoginInterceptor loginInterceptor;

@Autowired

privateRefreshTokenInterceptor refreshTokenInterceptor;

@Override

publicvoidaddInterceptors(InterceptorRegistry registry) {

// 登录拦截器

registry.addInterceptor(loginInterceptor)

.addPathPatterns("/**")

.excludePathPatterns(

"/user/code",

"/user/login",

"/blog/hot",

"/shop/**",

"/shop-type/**",

"/upload/**",

"/voucher/**"

).order(1);

// 刷新token拦截器

registry.addInterceptor(refreshTokenInterceptor)

.addPathPatterns("/**").order(0);

}

}

注:order方法是用来设置哪一个拦截器在前,哪一个在后;规则:数字小的在前,数字大的在后


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/17 9:14:25

Sverchok实体建模指南:从基础几何到复杂结构的完整流程

Sverchok实体建模指南&#xff1a;从基础几何到复杂结构的完整流程 【免费下载链接】sverchok Sverchok 项目地址: https://gitcode.com/gh_mirrors/sv/sverchok Sverchok是一款功能强大的Blender插件&#xff0c;为用户提供了基于节点的可视化编程环境&#xff0c;用于…

作者头像 李华
网站建设 2026/4/17 9:13:24

如何在phpMyAdmin中执行多条SQL语句_分号分隔与批量执行解析

phpMyAdmin默认禁用多语句执行&#xff0c;需在设置中启用“Allow executing multiple statements”选项&#xff08;≥4.7.0版本&#xff09;&#xff0c;但仅支持简单SELECT/INSERT/UPDATE/DELETE组合&#xff1b;含变量、事务、DELIMITER或LOAD DATA的语句仍会失败&#xff…

作者头像 李华
网站建设 2026/4/17 9:10:07

给立创天空星STM32F407VET6移植FreeRTOS,我用STM32CubeMX只花了10分钟

立创天空星STM32F407VET6开发板10分钟快速部署FreeRTOS实战指南 拿到一块新开发板最令人头疼的莫过于环境搭建。作为嵌入式开发者&#xff0c;我们都经历过反复查阅手册、调试时钟配置、解决编译错误的痛苦过程。而今天&#xff0c;借助STM32CubeMX这款神器&#xff0c;即使是初…

作者头像 李华