Le projet Dark Horse Review comprend principalement les fonctions suivantes :
Si vous avez besoin d'informations sur le projet, veuillez m'envoyer un message privé
Les tables sont. :
tb_user : table utilisateur
tb_user_info : table de détails de l'utilisateur
tb_shop : table d'informations sur le marchand
tb_shop_type : table de type de marchand
tb_blog : Tableau journal utilisateur (journal de visite expert en magasin) )
tb_follow : Tableau de suivi des utilisateurs
tb_voucher : Tableau des coupons
tb_voucher_order : Tableau de commande des coupons
Remarque : La version MySQL adopte la version 5.7 et supérieure
3.1 Importez le projet backend dans Idea
3.2 Remarque : modifiez les informations d'adresse mysql et redis dans le fichier application.yaml dans votre propres informations
3.3 Après avoir démarré le projet, visitez : http://localhost:8081/shop-type/list dans le navigateur Si vous pouvez voir les données, cela prouve qu'il n'y a pas de problème avec le fonctionnement
.4.1 Importez le dossier nginx et copiez le dossier nginx dans n'importe quel répertoire. Assurez-vous que le répertoire ne contient pas de caractères chinois, spéciaux ni d'espaces, par exemple :
. 4.2 Exécutez le projet front-end et ouvrez une fenêtre CMD dans le répertoire où se trouve nginx, entrez la commande pour démarrer nginx :
start nginx.exe
Ouvrez le navigateur Chrome, faites un clic droit sur la page vierge et sélectionnez Inspecter pour. ouvrez les outils de développement :
Ensuite, visitez : http://127.0.0.1 : 8080, vous pouvez voir la page :
Le backend enregistre le code de vérification généré et les informations utilisateur dans la session, renvoie l'ID de session au frontend et l'enregistre dans le cookie
Lorsque l'utilisateur se connecte, le cookie sera transporté pour faire une demande au backend . Lorsque le backend effectue la vérification, le sessionId est obtenu à partir du cookie. Les informations utilisateur peuvent être obtenues à partir de la session via le sessionId et enregistrées dans ThreadLocal
Chaque fil suivant a une copie des informations de l'utilisateur dans ThreadLocal. peut effectuer différentes opérations après avoir obtenu les informations de l'utilisateur, jouant ainsi un rôle dans l'isolation des threads
Code principal :
@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 ServiceImplimplements 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(); } }
Code principal :
UserController/** * 登录功能 * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码 */ @PostMapping("/login") public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){ // 实现登录功能 return userService.login(loginForm, session); }
@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; }
.
Dans l'ordre pour empêcher l'utilisateur de vérifier les informations utilisateur à chaque fois lors de la demande de chaque contrôleur, vous pouvez ajouter un intercepteur L'intercepteur n'a besoin de vérifier qu'une seule fois lorsque l'utilisateur demande l'accès, puis d'envoyer à l'utilisateur Les informations sont enregistrées dans ThreadLocal pour les threads suivants utiliserCode principal :
Écrire ThreadLocal dans la classe d'outilspublic class UserHolder { private static final ThreadLocaltl = 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 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(); } }
@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" ); } }
@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 ServiceImplimplements 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类型的,所以进行对象属性拷贝时,需要自定义实现转换规则 MapuserMap = 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
登录拦截器的代码:
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); } }
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!