@Autowried
private UserService userService;
@PostMapping("/code")
public Result sendCode(String phone) {
// 发送短信验证码并保存验证码
return userService.sendCode(phone); // 这里的主要业务我们放在实现类去
}
// 首先我们注入Spring提供的Redis
@Autowired
private StringRedisTemplate stringRedisTemplate; // 默认的key和value都是String类型
// 发送验证码的操作
@Override
public Result sendCode(String phone) {
// 首先校验手机号
if(RegexUtils.isPhoneInvalid(phone)) { // 这里其实一堆的正则表达式的工具类,来验证号码的合法性
// 手机号码不合法,返回错误信息
return Result.fail("手机号格式错误");
}
// 符合,生成验证码
String code = RandomUtil.randomNumbers(6); // 这里使用的是hutool提供的工具类,它里面封装了很多实用的工具类,比如生成随机数,对象转换,对象转hash等等(墙裂推荐)
// 将生成的验证码保存在redis里面,并且设置默认到期时间为3分钟 (用电话号码作为键,保证唯一性)
// 这里的这些键可以封装成常量类,使代码看起来更优雅,但这里为了直观我直接写死了,后面会提供常量类
stringRedisTemplate.opsForValue().set("login:code:" + phone,code,3,TimeUnit.MINUTES);
// 这里直接模拟发送短信验证码了,直接控制台看输出,不是这里的重点
log.debug("发送短信验证码成功:验证码:{}" + code);
// 最终返回 ok
return Result.ok();
}
// loginForm:登录需要的参数,包含手机号验证码或者是手机号和密码的组合
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm) {
return userService.login(loginForm);
}
// 实现登录功能
public Result login(LoginFormDTO loginForm) {
// 从请求体中获取手机号
String phone = loginForm.getPhone();
// 校验手机号
if(RegexUtils.isPhoneInvalid(phone)) {
// 不符合,返回错误信息
return Result.fail("手机号格式错误");
}
// 从redis中获取验证码,根据我们前面传入的key
String cacheCode = stringRedisTemplate.opsForValue().get("login:code:" + phone);
// 再获取用户传过来的验证码
String code = loginForm.getCode();
if(cacheCode == null || !cacheCode.equals(code)) {
// 返回错误信息
return Result.fail("验证码错误");
}
// 根据手机号查询用户
User user = query.eq("phone",phone).one();
// 判断用户是否存在
if(user == null) {
// 不存在,创建新用户并保存,createUserWithPhone方法在后面定义
user.createUserWithPhone(phone);
}
// 随机生成token,作为登录令牌传给前端,让前端每次请求都带着这个token
// 因为每次都会带着这个token,这里我们不再使用手机号作为key,防止用户敏感信息泄露
String token = UUID.randomUUID().toString(true); // 这里我使用uuid生成随机字符串
// 将user对象转为hashMap存储在redis里面
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); // 这里也是防止敏感信息泄露,将User对象先转换成UserDTO对象,UserDTO对象里面只封装了一些最基本的信息,比如用户昵称,用户头像
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName,fieldVaue) -> fieldVaue.toString())); // 将所有值都转换为String类型
// 存储在redis
stringRedisTemplate.opsForHash().putAll("login:token:" + token,userMap);
// 仿照session,设置30分钟有效时间,超过30分钟没有进行任何操作,清除数据(涉及token的续期,我会在后面的拦截器里面实现)
stringRedisTemplate.expire("lgoin:token:" + token,30,TimeUnit.MINUTES);
// 返回token
return Result.ok(token);
}
// 保存用户的方法 => createUserWithPhone
private User createUserWithPhone(String phone) {
// 创建用户
User user = new User();
user.setPhone(phone);
user.setNickName("user_" + RandomUtil.randomNumbers(5)); // 为用户默认随机生成用户名,我采用的hutool的生成随机数
// 保存用户
save(user); // 这都是mp里面自带的方法,不多说了
return user;
}
// 刷新token的拦截器:拦截所有的请求,只要用户发了一次请求,就会刷新一次token的过期时间,并对所有请求放行
public class RefreshTokenInterceptor implements HandlerInterceptor {
// 这里不能使用注解注入,因为这是我们自定义的拦截器,并没有交给Spring管理,这里采用构造方法的方式来进行依赖注入
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
// 进入controller之前拦截
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取请求头中的token,封装在authorization的
String token = request.getHeader("authorization");
if(!StrUtil.isBlank(token)) {
return true;
}
// 基于token获取redis中的用户
Map<Object,Object> userMap = stringRedisTemplate.opsForValue().entries("login:token:" + token); // entries()方法会自动判断是否为null,因此下面就不用判断是否为null了
// 判断用户是否存在
if(userMap.isEmpty()) {
return true;
}
// 将查询到的hash数据转为UserDTO对象,依旧使用的hutool提供的工具类
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),fasle); // false表示不忽略错误
// 存在,保存用户信息到ThreadLocal,ThreadLocal是线程私有的,相对于其他线程隔离
UserHolder.saveUser(userDTO); // UserHolder也是封装的操作ThreadLocal的工具类,后文会提供
// 刷新token的有效期 -> 30 min
stringRedisTemplate.expire("login:token:" + token,30,TimeUnit.MINUTES);
// 放行
return true;
}
// 之后
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
// 登录拦截器:拦截部分请求,并且在token拦截器之后执行,判断ThreadLocal里面是否有值来决定是否放行
public class LoginInterceptor implements HandlerInterceptor {
// 进入controller之前进行拦截
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断是否需要拦截 (ThreadLocal中是否有用户)
if (UserHolder.getUser() == null) {
// 没有,需要拦截
response.setStatus(401);
// 拦截
return false;
}
// 有用户,放行
return true;
}
}
此时虽然已经定义好了拦截器,但是还不会生效,不要忘了去注册我们定义好的拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 添加我们自定义的拦截器 => LoginInterceptor
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 刷新token拦截器,拦截所有请求,优先级更高
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0); // order的意义就是设置拦截器执行顺序的优先级,默认都是0,会按照你定义顺序执行,这里只是为了更明确才设置了值,可以省略。
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
).order(1); // 排除不需要拦截的路径,优先级更低
}
}
这部分代码只是为了让代码更加简洁优雅,根据自己的需求选择叭
首先是前面出现次数极多的hutool工具类,直接导入maven坐标就可以使用了
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
public abstract class RegexPatterns {
/**
* 手机号正则
*/
public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";
/**
* 邮箱正则
*/
public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";
/**
* 密码正则。4~32位的字母、数字、下划线
*/
public static final String PASSWORD_REGEX = "^\\w{4,32}$";
/**
* 验证码正则, 6位数字或字母
*/
public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$";
}
public class RegexUtils {
/**
* 是否是无效手机格式
* @param phone 要校验的手机号
* @return true:符合,false:不符合
*/
public static boolean isPhoneInvalid(String phone){
return mismatch(phone, RegexPatterns.PHONE_REGEX);
}
/**
* 是否是无效邮箱格式
* @param email 要校验的邮箱
* @return true:符合,false:不符合
*/
public static boolean isEmailInvalid(String email){
return mismatch(email, RegexPatterns.EMAIL_REGEX);
}
/**
* 是否是无效验证码格式
* @param code 要校验的验证码
* @return true:符合,false:不符合
*/
public static boolean isCodeInvalid(String code){
return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);
}
// 校验是否不符合正则格式
private static boolean mismatch(String str, String regex){
if (StrUtil.isBlank(str)) {
return true;
}
return !str.matches(regex);
}
}
public class RedisConstants {
public static final String LOGIN_CODE_KEY = "login:code:";
public static final Long LOGIN_CODE_TTL = 3L;
public static final String LOGIN_USER_KEY = "login:token:";
public static final Long LOGIN_USER_TTL = 36000L;
public static final Long CACHE_NULL_TTL = 2L;
public static final Long CACHE_SHOP_TTL = 30L;
public static final String CACHE_SHOP_KEY = "cache:shop:";
public static final String LOCK_SHOP_KEY = "lock:shop:";
public static final Long LOCK_SHOP_TTL = 10L;
public static final String SECKILL_STOCK_KEY = "seckill:stock:";
public static final String BLOG_LIKED_KEY = "blog:liked:";
public static final String FEED_KEY = "feed:";
public static final String SHOP_GEO_KEY = "shop:geo:";
public static final String USER_SIGN_KEY = "sign:";
}
spring:
redis:
host: 127.0.0.1
port: 6379
password:
lettuce: # 默认就是lettuce的连接池
pool:
max-active: 10
max-idle: 10
min-idle: 1
time-between-eviction-runs: 10s