The Dark Horse Review Project mainly includes the following functions:
Required projects Private message me with the information
The tables include:
tb_user: user table
tb_user_info: user details table
tb_shop: Merchant information table
tb_shop_type: Merchant type table
tb_blog: User diary table ( Master’s store visit diary)
tb_follow: User follow list
tb_voucher: Coupon list
tb_voucher_order: Coupon order table
Note: Mysql version adopts version 5.7 and above
3.1 Import back-end project into Idea
3.2 Note: Modify the mysql and redis address information in the application.yaml file to your own information
3.3 Start the project After starting the project, visit: http://localhost:8081/shop-type/list in the browser. If you can see the data, it proves that there is no problem with the operation.
4.1 Import the nginx folder. Copy the nginx folder to any directory. Make sure that the directory does not contain Chinese, special characters and spaces, for example:
4.2 Run the front-end project. Open a CMD window in the directory where nginx is located, and enter the command to start nginx:
start nginx.exe
Open the chrome browser and click the mouse on the blank page Right-click and select Inspect to open the developer tools:
Then visit: http://127.0.0.1:8080 and you will see the page:
The backend will save the generated verification code and user information to the session, and return the sessionId to the front end and save it in the cookie
When the user logs in, the cookie will be brought to the backend to initiate a request. When the backend performs verification, it will be obtained from the cookie. sessionId, through sessionId, user information can be obtained from the session and saved in ThreadLocal
Each subsequent thread will have a copy of the user information in ThreadLocal. Different threads can obtain the user information after Implement different operations to achieve thread isolation
Main code:
@Slf4j @RestController @RequestMapping("/user") public class UserController { @Resource private IUserService userService; /** * 发送手机验证码 */ @PostMapping("code") public Result sendCode(@RequestParam("phone") String phone, HttpSession session) { // 发送短信验证码并保存验证码 return userService.sendCode(phone, session); } }
@Service @Slf4j public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { @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 session.setAttribute("code",code); // 5.模拟发送验证码 log.debug("发送短信验证码成功,验证码:{}", code); // 返回ok return Result.ok(); } }
Main code:
UserController
/** * 登录功能 * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码 */ @PostMapping("/login") public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){ // 实现登录功能 return userService.login(loginForm, session); }
UserServiceImpl
@Override public Result login(LoginFormDTO loginForm, HttpSession session) { // 1.校验手机号 String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)) { // 如果不符合,返回错误信息 return Result.fail("手机号格式错误!"); } // 2.校验验证码 Object cacheCode = session.getAttribute("code"); String code = loginForm.getCode(); if (cacheCode == null || !cacheCode.toString().equals(code)) { // 3.验证码不一致,则报错 return Result.fail("验证码错误"); } // 4.验证码一致,根据手机号查询用户 User user = query().eq("phone", phone).one(); // 5.判断用户是否存在 if (user == null) { // 6.用户不存在,则创建用户并保存 user = createUserWithPhone(phone); } // 7.保存用户信息到session中,UserDTO只包含简单的用户信息, // 而不是完整的User,这样可以隐藏用户的敏感信息(例如:密码等),还能减少内存使用 session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class)); // 8.返回ok return Result.ok(); } private User createUserWithPhone(String phone) { // 1.创建用户 User user = new User(); user.setPhone(phone); // 随机设置昵称 user_mrkuw05lok user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)); // 2.保存用户 save(user); return user; }
When the user requests to log in, a cookie will be carried, and the cookie contains JSEESIONID
In order to prevent users from verifying user information every time they request each controller, you can add an interceptor.
The interceptor only needs to verify once and save the user information when the user requests access. to ThreadLocal for use by subsequent threads
Main code:
Write ThreadLocal in the tool class
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(); } }
Write login interceptor in tool class
public class LoginInterceptor implements HandlerInterceptor { /** * 前置拦截 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.获取session HttpSession session = request.getSession(); // 2.获取session中的用户 Object user = session.getAttribute("user"); // 3.判断用户是否存在 if(user == null){ // 4.不存在,拦截,返回401状态码 response.setStatus(401); return false; } // 5.存在,保存用户信息到ThreadLocal UserHolder.saveUser((User)user); // 6.放行 return true; } /** * 后置拦截器 * @param request * @param response * @param handler * @param ex * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 请求结束后移除用户,防止ThreadLocal造成内存泄漏 UserHolder.removeUser(); } }
Add interceptor configuration class in configuration class
@Configuration public class MvcConfig implements WebMvcConfigurer { /** * 添加拦截器 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { // 登录拦截器 registry.addInterceptor(new LoginInterceptor()) // 排除不需要拦截的路径 .excludePathPatterns( "/shop/**", "/voucher/**", "/shop-type/**", "/upload/**", "/blog/hot", "/user/code", "/user/login" ); } }
UserController
@GetMapping("/me") public Result me(){ // 获取当前登录的用户并返回 UserDTO user = UserHolder.getUser(); return Result.ok(user); }
手机号作为key,String类型的验证码作为value
用户登录时正好会提交手机号,方便通过Redis进行校验验证码
token作为key,Hash类型的用户信息作为value
后端校验成功后,会返回token给前端,前端会将token保存到sessionStorage中(这是浏览器的存储方式),以后前端每次请求都会携带token,方便后端通过Redis校验用户信息
前端代码:将后端返回的token保存到sessionStorage中
前端每次请求时,都会通过拦截器将token设置到请求头中,赋值给变量authorization,后端通过authorization获取前端携带的token进行校验
修改之前代码,将验证码存入Redis
@Service @Slf4j 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 // session.setAttribute("code",code); // 4.保存验证码到 redis // "login:code:"是业务前缀,以"login:code:" + 手机号为key,过期时间2分钟 stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES); // 5.模拟发送验证码 log.debug("发送短信验证码成功,验证码:{}", code); // 返回ok return Result.ok(); } }
修改之前代码,从Redis获取验证码并校验
随机生成token,保存用户信息到redis中,返回token
@Override public Result login(LoginFormDTO loginForm, HttpSession session) { // 1.校验手机号 String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)) { // 如果不符合,返回错误信息 return Result.fail("手机号格式错误!"); } // // 2.校验验证码 // Object cacheCode = session.getAttribute("code"); // String code = loginForm.getCode(); // if (cacheCode == null || !cacheCode.toString().equals(code)) { // // 3.验证码不一致,则报错 // return Result.fail("验证码错误"); // } // 2.从Redis获取验证码并校验 String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone); String code = loginForm.getCode(); if (cacheCode == null || !cacheCode.equals(code)) { // 3.验证码不一致,则报错 return Result.fail("验证码错误"); } // 4.验证码一致,根据手机号查询用户 User user = query().eq("phone", phone).one(); // 5.判断用户是否存在 if (user == null) { // 6.用户不存在,则创建用户并保存 user = createUserWithPhone(phone); } // // 7.保存用户信息到session中,UserDTO只包含简单的用户信息,而不是完整的User,这样可以隐藏用户的敏感信息(例如:密码等),还能减少内存使用 // session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class)); // 7.保存用户信息到redis中 // 7.1随机生成token,作为登录令牌 // 使用hutool工具中的UUID,true表示不带“-”符号的UUID String token = UUID.randomUUID().toString(true); // 7.2将User对象转为Hash类型进行存储 UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); // 由于使用的是stringRedisTemplate,所以存入的value中的值必须都是String类型的 // 但是UserDTO中的id是Long类型的,所以进行对象属性拷贝时,需要自定义实现转换规则 Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); // 7.3存入redis, "login:token:"是业务前缀,以 "login:token:" + token作为key stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap); // 7.4设置token有效期,有效期为30分钟 stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES); // 8.返回token return Result.ok(token); } private User createUserWithPhone(String phone) { // 1.创建用户 User user = new User(); user.setPhone(phone); // 随机设置昵称 user_mrkuw05lok user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)); // 2.保存用户 save(user); return user; }
token刷新问题是指,用户长时间不进行界面操作时,到了过期时间,token自动失效;但是,用户一旦进行操作,就需要给token续期,即更新token过期时间
为了解决token刷新问题,需要加2个拦截器
第一个拦截器可以拦截所有请求,只要用户有请求就刷新token,并保存用户信息到ThreadLocal中
第二个拦截器只对登录请求进行拦截,从ThreadLocal中获取用户信息进行校验
刷新token的拦截器代码:
public class RefreshTokenInterceptor implements HandlerInterceptor { // 因为LoginInterceptor不是通过Spring进行管理的Bean,所以不能再LoginInterceptor中进行注入StringRedisTemplate // 可以通过构造方法传入StringRedisTemplate private StringRedisTemplate stringRedisTemplate; public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } /** * 前置拦截 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // // 1.获取session // HttpSession session = request.getSession(); // // 2.获取session中的用户 // Object user = session.getAttribute("user"); // // 3.判断用户是否存在 // if(user == null){ // // 4.不存在,拦截,返回401状态码 // response.setStatus(401); // return false; // } // // 5.存在,保存用户信息到ThreadLocal // UserHolder.saveUser((UserDTO)user); // // 6.放行 // return true; // 1.获取请求头中的token String token = request.getHeader("authorization"); if (StrUtil.isBlank(token)) { // 不存在,则拦截,返回401状态码 response.setStatus(401); return false; } // 2.通过token获取redis中的用户 Map<Object, Object> userMap = stringRedisTemplate.opsForHash() .entries(RedisConstants.LOGIN_USER_KEY + token); // 3.判断用户是否存在 if (userMap.isEmpty()) { // 4.用户不存在,则拦截,返回401状态码 response.setStatus(401); return false; } // 5.将redis中Hash类型数据转换成UserDTO对象 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); // 6.用户存在,保存用户信息到ThreadLocal UserHolder.saveUser(userDTO); // 7.刷新token有效期 stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES); // 8.放行 return true; } /** * 后置拦截器 * @param request * @param response * @param handler * @param ex * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 请求结束后移除用户,防止ThreadLocal造成内存泄漏 UserHolder.removeUser(); } }
登录拦截器的代码:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.判断是否需要拦截(ThreadLocal中是否有用户) if (UserHolder.getUser() == null) { // 没有,需要拦截,设置状态码 response.setStatus(401); // 拦截 return false; } // 有用户,则放行 return true; } }
@Configuration public class MvcConfig implements WebMvcConfigurer { @Resource private StringRedisTemplate stringRedisTemplate; /** * 添加拦截器 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { // 登录拦截器 registry.addInterceptor(new LoginInterceptor()) // 排除不需要拦截的路径 .excludePathPatterns( "/shop/**", "/voucher/**", "/shop-type/**", "/upload/**", "/blog/hot", "/user/code", "/user/login" ).order(1); // token刷新的拦截器,order越小,执行优先级越高,所以token刷新的拦截器先执行 registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**") .excludePathPatterns( // RefreshTokenInterceptor拦截器也需要放行"/user/code","/user/login",不然token过期后再重新登录就会一直被拦截 "/user/code", "/user/login") .order(0); } }
The above is the detailed content of Analysis of enterprise examples of SMS login using Redis. For more information, please follow other related articles on the PHP Chinese website!