java——redis随笔——实战——短信登录
前言:
此章节用到的知识点:mybatisPlus ;参考网址:https://www.bilibili.com/video/BV1Xu411A7tL?p=7&vd_source=79bbd5b76bfd74c2ef1501653cee29d6
正常新建一个接口:
再新建这个接口的实现类:
修改接口:
修改实现类:
然后就可以注入并使用了:
=============================================================
此章节还需要参考:创建一个类来统一结果返回:https://blog.csdn.net/qq_42331202/article/details/115823134
统一的返回结果是json格式,类似下面的数据结构
{ "success":布尔,//代表响应是否成功 "code":数字,//响应码 "message":字符串,//返回消息 "data":HashMap //返回数据 }
首先写一个接口,定义数据返回响应码
package com.example.learn.common; /** * @author : wangbo * @version : 1.0 * @date :Create in 2021/4/18 * @description : */ public interface ResultCode { // 成工时返回的状态码 public static Integer SUCCESS=20000; // 失败是返回的状态码 public static Integer ERROR=20001; }
写统一返回结果的类
package com.example.learn.common; import lombok.Data; import java.util.HashMap; import java.util.Map; /** * @author : wangbo * @version : 1.0 * @date :Create in 2021/4/18 * @description : */ @Data public class Result { private boolean success; private Integer code; private String message; private Map<String,Object> data=new HashMap<>(); private Result(){ } public static Result ok(){ Result result=new Result(); result.setSuccess(true); result.setCode(ResultCode.SUCCESS); result.setMessage("成功"); return result; } public static Result error(){ Result result=new Result(); result.setSuccess(false); result.setCode(ResultCode.ERROR); result.setMessage("失败"); return result; } public Result success(boolean success){ this.setSuccess(success); return this; } public Result code(Integer code){ this.setCode(code); return this; } public Result message(String message) { this.setMessage(message); return this; } public Result data(Map<String, Object> data) { this.setData(data); return this; } }
其他参考:https://blog.csdn.net/qq_51447496/article/details/131414235?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-5-131414235-blog-104906051.235^v38^pc_relevant_anti_vip&spm=1001.2101.3001.4242.4&utm_relevant_index=8
@Data @NoArgsConstructor @AllArgsConstructor public class Result<T> { private int code; private String message; private T data; public Result(T data) { this.code = 200; this.message = "success"; this.data = data; } public Result(T data, boolean success, String message) { if (success) { this.code = 200; this.message = "success"; } else { this.code = 500; this.message = message; } this.data = data; } public Result(int code, String message) { this.code = code; this.message = message; this.data = null; } public static <T> Result<T> success(T data) { return new Result<>(data); } public static <T> Result<T> fail(String message) { return new Result<>(500, message); } public static <T> Result<T> fail(int code, String message) { return new Result<>(code, message); } }
======================================================================
拦截器:https://www.cnblogs.com/xiaobaibailongma/p/17060812.html
======================================================================
hutool工具包快速入门:https://blog.csdn.net/Peaceuai/article/details/127953211
package com.hmdp.utils; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.StrUtil; import com.hmdp.dto.UserDTO; import org.springframework.data.redis.core.StringRedisTemplate; 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; import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY; import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL; public class RefreshTokenInterceptor implements HandlerInterceptor { private StringRedisTemplate stringRedisTemplate; public RefreshTokenInterceptor(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 (StrUtil.isBlank(token)) { return true; } // 2.基于TOKEN获取redis中的用户 String key = LOGIN_USER_KEY + token; Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key); // 3.判断用户是否存在 if (userMap.isEmpty()) { return true; } // 5.将查询到的hash数据转为UserDTO UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); // 6.存在,保存用户信息到 ThreadLocal UserHolder.saveUser(userDTO); // 7.刷新token有效期 stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES); // 8.放行 return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 移除用户 UserHolder.removeUser(); } }
======================================================================
黑马视频地址:https://www.bilibili.com/video/BV1cr4y1671t?p=25&vd_source=79bbd5b76bfd74c2ef1501653cee29d6
参考笔记博客:https://blog.csdn.net/weixin_50523986/article/details/131815165
参考笔记博客:https://cyborg2077.github.io/2022/10/22/RedisPractice/#%E5%86%85%E5%AE%B9%E6%A6%82%E8%BF%B0
======================================================================
整体访问流程
IUserService
package com.hmdp.service; import com.baomidou.mybatisplus.extension.service.IService; import com.hmdp.dto.LoginFormDTO; import com.hmdp.dto.Result; import com.hmdp.entity.User; import javax.servlet.http.HttpSession; /** * <p> * 服务类 * </p> * * @author 虎哥 * @since 2021-12-22 */ public interface IUserService extends IService<User> { Result sendCode(String phone, HttpSession session); Result login(LoginFormDTO loginForm, HttpSession session); Result sign(); Result signCount(); }
UserServiceImpl
package com.hmdp.service.impl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.copier.CopyOptions; import cn.hutool.core.lang.UUID; import cn.hutool.core.util.RandomUtil; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmdp.dto.LoginFormDTO; import com.hmdp.dto.Result; import com.hmdp.dto.UserDTO; import com.hmdp.entity.User; import com.hmdp.mapper.UserMapper; import com.hmdp.service.IUserService; import com.hmdp.utils.RegexUtils; import com.hmdp.utils.UserHolder; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.connection.BitFieldSubCommands; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import javax.annotation.Resource; import javax.servlet.http.HttpSession; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import static com.hmdp.utils.RedisConstants.*; import static com.hmdp.utils.SystemConstants.USER_NICK_NAME_PREFIX; /** * <p> * 服务实现类 * </p> * * @author 虎哥 * @since 2021-12-22 */ @Slf4j @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { @Resource private StringRedisTemplate stringRedisTemplate; @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 stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES); // 5.发送验证码 log.debug("发送短信验证码成功,验证码:{}", code); // 返回ok return Result.ok(); } @Override public Result login(LoginFormDTO loginForm, HttpSession session) { // 1.校验手机号 String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)) { // 2.如果不符合,返回错误信息 return Result.fail("手机号格式错误!"); } // 3.从redis获取验证码并校验 String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone); String code = loginForm.getCode(); if (cacheCode == null || !cacheCode.equals(code)) { // 不一致,报错 return Result.fail("验证码错误"); } // 4.一致,根据手机号查询用户 select * from tb_user where phone = ? User user = query().eq("phone", phone).one(); // 5.判断用户是否存在 if (user == null) { // 6.不存在,创建新用户并保存 user = createUserWithPhone(phone); } // 7.保存用户信息到 redis中 // 7.1.随机生成token,作为登录令牌 String token = UUID.randomUUID().toString(true); // 7.2.将User对象转为HashMap存储 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())); // 7.3.存储 String tokenKey = LOGIN_USER_KEY + token; stringRedisTemplate.opsForHash().putAll(tokenKey, userMap); // 7.4.设置token有效期 stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES); // 8.返回token return Result.ok(token); } @Override public Result sign() { // 1.获取当前登录用户 Long userId = UserHolder.getUser().getId(); // 2.获取日期 LocalDateTime now = LocalDateTime.now(); // 3.拼接key String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM")); String key = USER_SIGN_KEY + userId + keySuffix; // 4.获取今天是本月的第几天 int dayOfMonth = now.getDayOfMonth(); // 5.写入Redis SETBIT key offset 1 stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true); return Result.ok(); } @Override public Result signCount() { // 1.获取当前登录用户 Long userId = UserHolder.getUser().getId(); // 2.获取日期 LocalDateTime now = LocalDateTime.now(); // 3.拼接key String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM")); String key = USER_SIGN_KEY + userId + keySuffix; // 4.获取今天是本月的第几天 int dayOfMonth = now.getDayOfMonth(); // 5.获取本月截止今天为止的所有的签到记录,返回的是一个十进制的数字 BITFIELD sign:5:202203 GET u14 0 List<Long> result = stringRedisTemplate.opsForValue().bitField( key, BitFieldSubCommands.create() .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0) ); if (result == null || result.isEmpty()) { // 没有任何签到结果 return Result.ok(0); } Long num = result.get(0); if (num == null || num == 0) { return Result.ok(0); } // 6.循环遍历 int count = 0; while (true) { // 6.1.让这个数字与1做与运算,得到数字的最后一个bit位 // 判断这个bit位是否为0 if ((num & 1) == 0) { // 如果为0,说明未签到,结束 break; }else { // 如果不为0,说明已签到,计数器+1 count++; } // 把数字右移一位,抛弃最后一个bit位,继续下一个bit位 num >>>= 1; } return Result.ok(count); } private User createUserWithPhone(String phone) { // 1.创建用户 User user = new User(); user.setPhone(phone); user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)); // 2.保存用户 save(user); return user; } }
UserController
package com.hmdp.controller; import cn.hutool.core.bean.BeanUtil; import com.hmdp.dto.LoginFormDTO; import com.hmdp.dto.Result; import com.hmdp.dto.UserDTO; import com.hmdp.entity.User; import com.hmdp.entity.UserInfo; import com.hmdp.service.IUserInfoService; import com.hmdp.service.IUserService; import com.hmdp.utils.UserHolder; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.http.HttpSession; /** * <p> * 前端控制器 * </p> * * @author 虎哥 */ @Slf4j @RestController @RequestMapping("/user") public class UserController { @Resource private IUserService userService; @Resource private IUserInfoService userInfoService; /** * 发送手机验证码 */ @PostMapping("code") public Result sendCode(@RequestParam("phone") String phone, HttpSession session) { // 发送短信验证码并保存验证码 return userService.sendCode(phone, session); } /** * 登录功能 * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码 */ @PostMapping("/login") public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){ // 实现登录功能 return userService.login(loginForm, session); } /** * 登出功能 * @return 无 */ @PostMapping("/logout") public Result logout(){ // TODO 实现登出功能 return Result.fail("功能未完成"); } @GetMapping("/me") public Result me(){ // 获取当前登录的用户并返回 UserDTO user = UserHolder.getUser(); return Result.ok(user); } @GetMapping("/info/{id}") public Result info(@PathVariable("id") Long userId){ // 查询详情 UserInfo info = userInfoService.getById(userId); if (info == null) { // 没有详情,应该是第一次查看详情 return Result.ok(); } info.setCreateTime(null); info.setUpdateTime(null); // 返回 return Result.ok(info); } @GetMapping("/{id}") public Result queryUserById(@PathVariable("id") Long userId){ // 查询详情 User user = userService.getById(userId); if (user == null) { return Result.ok(); } UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); // 返回 return Result.ok(userDTO); } @PostMapping("/sign") public Result sign(){ return userService.sign(); } @GetMapping("/sign/count") public Result signCount(){ return userService.signCount(); } }
Result
package com.hmdp.dto; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; /*** * 参考网址:https://blog.csdn.net/qq_42331202/article/details/115823134 */ @Data @NoArgsConstructor @AllArgsConstructor public class Result { private Boolean success; private String errorMsg; private Object data; private Long total; public static Result ok() { return new Result(true, null, null, null); } public static Result ok(Object data) { return new Result(true, null, data, null); } public static Result ok(List<?> data, Long total) { return new Result(true, null, data, total); } public static Result fail(String errorMsg) { return new Result(false, errorMsg, null, null); } }
userDTO
package com.hmdp.dto; import lombok.Data; @Data public class UserDTO { private Long id; private String nickName; private String icon; }
LoginFormDTO
package com.hmdp.dto; import lombok.Data; @Data public class LoginFormDTO { private String phone; private String code; private String password; }
User
package com.hmdp.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; import java.io.Serializable; import java.time.LocalDateTime; /** * <p> * * </p> * * @author 虎哥 * @since 2021-12-22 */ @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("tb_user") public class User implements Serializable { private static final long serialVersionUID = 1L; /** * 主键 */ @TableId(value = "id", type = IdType.AUTO) private Long id; /** * 手机号码 */ private String phone; /** * 密码,加密存储 */ private String password; /** * 昵称,默认是随机字符 */ private String nickName; /** * 用户头像 */ private String icon = ""; /** * 创建时间 */ private LocalDateTime createTime; /** * 更新时间 */ private LocalDateTime updateTime; }
RedisConstants
package com.hmdp.utils; public class RedisConstants { public static final String LOGIN_CODE_KEY = "login:code:"; public static final Long LOGIN_CODE_TTL = 2L; 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:"; }
RegexUtils
package com.hmdp.utils; import cn.hutool.core.util.StrUtil; /** * @author 虎哥 */ 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); } }
UserHolder
package com.hmdp.utils; import com.hmdp.dto.UserDTO; 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(); } }
- 对于token有效期的设置:每当有一个请求进来,有效期就应该刷新,需要改变拦截器的代码
- com/hmdp/interceptor/LoginInterceptor.java
// 这里是拦截器 不在spring容器中 不能同@Autowired注入stringRedisTemplate // 换个方法 在配置类中注入 // 或者加个注解:@Configuration 就可以自动注入啦 private StringRedisTemplate stringRedisTemplate; // 构造函数 public LoginInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1 获取请求头中的token // 这一步需要前端把token保存到请求头中 String token = request.getHeader("authorization"); if(StrUtil.isBlank(token)){ // 不存在 拦截 返回401状态码 response.setStatus(401); return false; } // 2 基于token获取redis中的用户 // 只用 get 只能取出一个 // 用entrise 取出的是一个map Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY+token); // 3 判断用户是否存在 // 如果userMap是null的话 自动返回一个空值 if (userMap.isEmpty()){ return true; } // 5 将查询到的Map对象转为UserDto // 忽略转换过程的错误:肯定不忽略 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(),false); // 6 存在 保存用户信息到Threadlocal中 UserHolder.saveUser(userDTO); // 7 刷新token有效期 stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL, TimeUnit.MINUTES); // 8 放行 return true; }
- com/hmdp/config/MvcConfig.java
package com.hmdp.config; import com.hmdp.interceptor.LoginInterceptor; import com.hmdp.interceptor.RefreshTokenInterceptor; 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; @Configuration public class MvcConfig implements WebMvcConfigurer { // @Configuration:这里可以注入 @Autowired private StringRedisTemplate stringRedisTemplate; @Override public void addInterceptors(InterceptorRegistry registry) { // 后执行 默认情况下按照添加顺序执行 拦截需要登录的请求 registry.addInterceptor(new LoginInterceptor()) .excludePathPatterns( "/user/code", "/user/login", "/blog/hot", "/shop/**", "/upload/**", "/shop-type/**", "/voucher/**" ) // 确定执行顺序 越小越先执行 .order(1); // 先执行 拦截所有请求 registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)) .addPathPatterns("/**") // 确定执行顺序 越小越先执行 .order(0); } }