前言
本篇为SpringBoot+Redis项目实战笔记。主要是简单记录一下,基于SpringBoot+Redis实现模拟短信登录/注册,刷新令牌拦截器、登录拦截器的功能。如此一来也就实现了单点登录(SSO)的功能,一次登录,即可授权访问所有系统。
一、实战案例
1.添加依赖
(1)pom.xml
<!-- redis 客户端 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
2.添加Redis配置
(1)application.yml
spring:
#---- ^ redis 配置 ----#
cache:
type: redis # 缓存类型
redis:
host: localhost # Redis数据库主机
port: 6379 # Redis数据库端口
password: 123456 # Redis数据库密码
database: 15 # Redis数据库索引
timeout: 5000 # Redis数据库连接超时时间,尽量不要零
jedis:
pool:
max-active: 8 # Redis数据库连接池最大连接数
max-idle: 8 # Redis数据库连接池最大空闲连接数
min-idle: 0 # Redis数据库连接池最小空闲连接数
max-wait: -1 # Redis数据库连接池最大阻塞等待时间
#---- / redis 配置 ----#
3.控制层
(1)UserController.java
package org.example.controller;
import org.example.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
@Controller
@RequestMapping(value = "api")
public class UserController {
@Autowired
private UserServiceImpl userService;
/**
* 发送验证码
* {
* "phone": "13800138000"
* }
*/
@PostMapping(value = "sendCode")
@ResponseBody
@CrossOrigin
public <T> T sendCode (@RequestBody HashMap<String, Object> data) {
return userService.sendCode(data);
}
/**
* 登录和注册
* {
* "phone": "13800138000",
* "code": "123456"
* }
*/
@PostMapping(value = "login")
@ResponseBody
@CrossOrigin
public <T> T login (@RequestBody HashMap<String, Object> data) {
return userService.login(data);
}
}
4.接口层
(1)IUserService.java
package org.example.service;
import java.util.HashMap;
public interface IUserService {
<T> T sendCode(HashMap<String, Object> data);
<T> T login(HashMap<String, Object> data);
}
5.实现层
(1)UserServiceImpl.java
package org.example.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import org.example.pojo.dto.UserDTO;
import org.example.pojo.entity.User;
import org.example.service.IUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Service
public class UserServiceImpl implements IUserService {
private static final String LOGIN_CODE_KEY = "Login-Code-";
private static final int LOGIN_CODE_TTL = 300;
private static final String LOGIN_USER_KEY = "Login-User-";
private static final int LOGIN_USER_TTL = 60;
private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public <T> T sendCode(HashMap<String, Object> data) {
HashMap<String, Object> responseObj = new HashMap<>();
try {
String phone = String.valueOf(data.get("phone"));
// 校验手机号
String regex = "^1[3|4|5|6|7|8|9][0-9]\\d{8}$";
if (!phone.matches(regex)) {
responseObj.put("code", 500);
responseObj.put("success", false);
responseObj.put("msg", "手机号格式错误");
return (T) responseObj;
}
// 生成验证码
Random random = new Random();
String code = Integer.toString(100000 + random.nextInt(900000));
// 保存验证码到Redis缓存(set key value ex 300)
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.SECONDS);
// 模拟发送验证码
log.info("对手机号{}发送验证码成功,验证码为{}", phone, code);
responseObj.put("code", 200);
responseObj.put("success", true);
responseObj.put("data", code);
responseObj.put("msg", "发送成功,验证码为" + code + ",验证码在5分钟内有效。");
} catch (Exception e) {
responseObj.put("code", 500);
responseObj.put("success", false);
responseObj.put("msg", e.getMessage());
}
return (T) responseObj;
}
@Override
public <T> T login(HashMap<String, Object> data) {
HashMap<String, Object> responseObj = new HashMap<>();
try {
String phone = String.valueOf(data.get("phone"));
String code = String.valueOf(data.get("code"));
// 校验手机号
String regex = "^1[3|4|5|6|7|8|9][0-9]\\d{8}$";
if (!phone.matches(regex)) {
responseObj.put("code", 500);
responseObj.put("success", false);
responseObj.put("msg", "手机号格式错误");
return (T) responseObj;
}
// 从Redis缓存获取验证码并且检验验证码
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
if (cacheCode == null || !cacheCode.equals(code)){
responseObj.put("code", 500);
responseObj.put("success", false);
responseObj.put("msg", "验证码错误");
return (T) responseObj;
}
// 模拟数据库查询
User user = new User(10001L, phone, "全王", "123456");
if (user == null) {
// 用户不存在,保存且返回用户信息
// user = saveUserWithPhone();
}
// 创建一个键为随机生成的UUID,值为用户信息的登录令牌,并存储到Redis缓存,并设置60分钟生存时间
String uuidStr = UUID.randomUUID().toString();
String uuid = uuidStr.substring(0, 8) + uuidStr.substring(9, 13) + uuidStr.substring(14, 18) + uuidStr.substring(19, 23) + uuidStr.substring(24);
String tokenKey = LOGIN_USER_KEY + uuid;
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> tokenVal = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
stringRedisTemplate.opsForHash().putAll(tokenKey, tokenVal);
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 返回token
responseObj.put("code", 200);
responseObj.put("success", true);
responseObj.put("data", tokenKey);
responseObj.put("msg", "登录成功");
} catch (Exception e) {
responseObj.put("code", 500);
responseObj.put("success", false);
responseObj.put("msg", "系统出错");
e.printStackTrace();
}
return (T) responseObj;
}
}
6.请求持有者
(1)RequestHolder.java
package org.example.holder;
import org.example.pojo.dto.UserDTO;
/**
* 请求持有者
*/
public class RequestHolder {
private static final ThreadLocal<UserDTO> userDTOThreadLocal = new ThreadLocal<>();
public static void setUser(UserDTO userDTO) {
userDTOThreadLocal.set(userDTO);
}
public static UserDTO getUser() {
return userDTOThreadLocal.get();
}
public static void remove() {
userDTOThreadLocal.remove();
}
}
7.简单对象
(1)User.java
package org.example.pojo.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id;
private String phone;
private String username;
private String password;
}
(2)UserDTO.java
package org.example.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class UserDTO {
private String phone;
private String username;
}
8.拦截器
(1)LoginInterceptor.java
package org.example.interceptor;
import cn.hutool.json.JSONObject;
import org.example.holder.RequestHolder;
import org.example.pojo.dto.UserDTO;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
/**
* 用户登录拦截器
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
HashMap<String, Object> responseObj = new HashMap<>();
UserDTO userDTO = RequestHolder.getUser();
System.out.println("LoginInterceptor :: userDTO -> " + userDTO);
if (userDTO == null) {
responseObj.put("code", 401);
responseObj.put("success", false);
responseObj.put("msg", "无权限");
JSONObject json = new JSONObject(responseObj);
response.getWriter().println(json);
return false;
}
return true;
}
}
(2)RefreshTokenInterceptor.java
package org.example.interceptor;
import cn.hutool.core.bean.BeanUtil;
import io.netty.util.internal.StringUtil;
import org.example.holder.RequestHolder;
import org.example.pojo.dto.UserDTO;
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;
/**
* 刷新Token拦截器
*/
@Component
public class RefreshTokenInterceptor implements HandlerInterceptor {
private static final int LOGIN_USER_TTL = 60;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String tokenKey = request.getHeader("Authorization");
System.out.println("RefreshTokenInterceptor :: tokenKey -> " + tokenKey);
if (StringUtil.isNullOrEmpty(tokenKey)) {
// 无令牌的键,先不进行拦截
return true;
} else {
// 有令牌的键
Map<Object, Object> tokenVal = stringRedisTemplate.opsForHash().entries(tokenKey);
System.out.println("RefreshTokenInterceptor :: tokenVal -> " + tokenVal);
if (tokenVal.isEmpty()) {
// 无令牌的值,先不进行拦截
return true;
} else {
// 有令牌的值,Hash数据转UserDTO对象,全局存储用户信息,刷新60分钟生存时间
UserDTO userDTO = BeanUtil.fillBeanWithMap(tokenVal, new UserDTO(), false);
System.out.println("RefreshTokenInterceptor :: userDTO -> " + userDTO);
RequestHolder.setUser(userDTO);
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
}
}
/**
* 若preHandle方法返回false,则会调用完此方法后再返回
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
// RequestHolder.remove();
}
}
9.资源配置
(1)ResourceConfig.java
package org.example.config;
import org.example.interceptor.LoginInterceptor;
import org.example.interceptor.RefreshTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Configuration
public class ResourceConfig extends WebMvcConfigurationSupport {
@Autowired
private LoginInterceptor loginInterceptor;
@Autowired
private RefreshTokenInterceptor refreshTokenInterceptor;
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Todo
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(refreshTokenInterceptor).addPathPatterns("/**").excludePathPatterns(
"/api/sendCode",
"/api/login"
);
registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns(
"/api/sendCode",
"/api/login"
);
}
}
二、运行效果
1.发送验证码
// ...
2.模拟登录
// ...