🥳🥳Welcome Huihui's Code World ! !🥳🥳
接下来看看由辉辉所写的关于SpringBoot电商项目的相关操作吧
目录
🥳🥳Welcome Huihui's Code World ! !🥳🥳
一.功能需求
①完成用户登录功能
②用户的各种错误操作都需要给出相应的错误提示,而不是抛出异常【全局异常处理】
③用户登录的密码的两次加密
表单数据➡后端【加密】
后端数据➡数据库【加密】
④用户输入表单时,前端【表单验证】和后端【JSR303】都需要有相对应的验证
⑤用户登录成功之后,需要在首页显示出登录的用户的昵称【Redis+Cookie】
二.代码编写
1.登录功能的完成
package com.wh.easyshop.service.impl; import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper; import com.wh.easyshop.model.User; import com.wh.easyshop.mapper.UserMapper; import com.wh.easyshop.resp.JsonResponseBody; import com.wh.easyshop.resp.JsonResponseStatus; import com.wh.easyshop.service.IUserService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.wh.easyshop.vo.UserVo; import org.springframework.stereotype.Service; import javax.swing.*; /** * <p> * 用户信息表 服务实现类 * </p> * * @author wh * @since 2023-12-27 */ @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { /** * 登录 * @param userVo * @return */ public JsonResponseBody<?> login (UserVo userVo){ //如果传过来的电话号码(登录名)为空,那么提示用户相应的信息 if(userVo.getMobile()==null){ return JsonResponseBody.other(JsonResponseStatus.LOGIN_MOBILE_INFO); } //如果传过来的密码为空,那么提示用户相应的信息 if(userVo.getPassword()==null){ return JsonResponseBody.other(JsonResponseStatus.LOGIN_PASSWORD_INFO); } User user = getOne(new QueryWrapper<User>().lambda() //判断用户名以及密码是否一致 .eq(User::getId, userVo.getMobile()) .eq(User::getPassword, userVo.getPassword())); //如果内容匹配不成功,那么提示用户相应的信息 if(user==null){ return JsonResponseBody.other(JsonResponseStatus.LOGIN_NO_EQUALS); } //如果带来的信息都一致,则提示成功 return JsonResponseBody.success(); } }
其中用到的响应类
package com.wh.easyshop.resp; import lombok.Data; @Data public class JsonResponseBody<T> { private Integer code; private String msg; private T data; private Long total; private JsonResponseBody(JsonResponseStatus jsonResponseStatus, T data) { this.code = jsonResponseStatus.getCode(); this.msg = jsonResponseStatus.getMsg(); this.data = data; } private JsonResponseBody(JsonResponseStatus jsonResponseStatus, T data, Long total) { this.code = jsonResponseStatus.getCode(); this.msg = jsonResponseStatus.getMsg(); this.data = data; this.total = total; } public static <T> JsonResponseBody<T> success() { return new JsonResponseBody<T>(JsonResponseStatus.OK, null); } public static <T> JsonResponseBody<T> success(T data) { return new JsonResponseBody<T>(JsonResponseStatus.OK, data); } public static <T> JsonResponseBody<T> success(T data, Long total) { return new JsonResponseBody<T>(JsonResponseStatus.OK, data, total); } public static <T> JsonResponseBody<T> unknown() { return new JsonResponseBody<T>(JsonResponseStatus.UN_KNOWN, null); } public static <T> JsonResponseBody<T> other(JsonResponseStatus jsonResponseStatus) { return new JsonResponseBody<T>(jsonResponseStatus, null); } }
package com.wh.easyshop.resp; import lombok.Getter; @Getter public enum JsonResponseStatus { OK(200, "OK"), UN_KNOWN(500, "未知错误"), LOGIN_MOBILE_INFO(5001, "未携带手机号或手机号格式有误"), LOGIN_PASSWORD_INFO(5002, "未携带密码或不满足格式"), LOGIN_NO_EQUALS(5003, "登录信息不一致"), LOGIN_MOBILE_NOT_FOUND(5004, "登录手机号未找到"), ; private final Integer code; private final String msg; JsonResponseStatus(Integer code, String msg) { this.code = code; this.msg = msg; } public String getName(){ return this.name(); } }
我这里为了规范,还建了一个vo类,而且也考虑到后面需要做验证,为了不污染实体类与数据库的连接,还是需要建一个vo类的
VO类:
数据传输:VO类可以用于封装客户端和服务器之间的数据传输,例如RESTful API的请求和响应对象。
数据库实体映射:VO类可以用于将数据库表的记录映射为Java对象,并进行数据的读取和存储操作。
领域模型中的值对象:在领域驱动设计(DDD)中,VO类可以用于表示领域模型中的值对象,如金额、日期范围等。
总之,VO类主要用于封装和传递数据,以提高代码的可读性、可维护性和可测试性。它们通常是不可变的,并且只包含属性和访问方法
package com.wh.easyshop.vo; import lombok.Data; @Data public class UserVo { private String mobile; private String password; }
但是上面的这个登录功能的代码只是很粗浅的,我们还需要将它进行升级
2.全局异常的处理
一个用户在输入自己的信息进行登录的时候,很可能会有一些非常规操作,一般这个时候,它会有一些错误以及异常抛出,我们可以使用全局异常进行处理
全局异常:
方便错误排查和日志记录:全局异常处理可以捕捉并记录异常信息,方便开发人员进行错误排查和系统故障分析。
提供友好的用户体验:通过合理的异常处理,可以向用户提供友好的错误提示,帮助他们理解发生的问题,并提供相应的解决方案
我先把原先的代码进行了修改,将前面有错误提示信息的地方,都换成异常【自定义异常】给它抛出
自定义异常
package com.wh.easyshop.exception; import com.wh.easyshop.resp.JsonResponseStatus; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @EqualsAndHashCode(callSuper = true) @AllArgsConstructor @NoArgsConstructor @Data public class BusinessException extends RuntimeException { private JsonResponseStatus jsonResponseStatus; }
但是这个异常抛出了,我们需要一个类来处理它--全局异常处理类,编写全局异常处理类,需要 用到一个注解@
RestControllerAdvice
@RestControllerAdvice:
@RestControllerAdvice
是 Spring 框架中的一个注解,用于定义全局异常处理器(Global Exception Handler)。在 Spring MVC 中,当应用程序抛出异常时,可以使用
@ExceptionHandler
注解来处理该异常。但是,如果在多个控制器中都有相同的异常处理逻辑,那么需要在每个控制器中都编写相同的代码,这样会导致代码冗余和可维护性差。
@RestControllerAdvice
的作用就是解决这个问题,它结合了@ControllerAdvice
和@ResponseBody
两个注解的功能,用于全局处理控制器抛出的异常,并返回相应的错误信息。使用
@RestControllerAdvice
注解的类可以包含多个被@ExceptionHandler
注解修饰的方法,每个方法用于处理不同类型的异常。当应用程序抛出异常时,Spring 框架会根据异常的类型自动调用对应的异常处理方法。
@RestControllerAdvice
类中的异常处理方法可以包含自定义的逻辑,比如记录日志、返回特定的错误信息等。通常,异常处理方法会返回一个包含错误信息的响应实体,供客户端进行处理。总之,
@RestControllerAdvice
注解用于定义全局异常处理器,通过集中处理控制器抛出的异常,提高代码的可维护性和复用性。它可以在一个类中定义多个异常处理方法,根据异常类型自动调用相应的方法,并返回相应的错误信息。【其实简而言之,就是当controller抛出异常的时候,不会往外面抛了,这个注解相当于是@Controller的增强类,把@Controller给包裹起来了】全局异常的编写
package com.wh.easyshop.exception; import com.wh.easyshop.resp.JsonResponseBody; import com.wh.easyshop.resp.JsonResponseStatus; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.BindException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Objects; @RestControllerAdvice // 声明这是一个全局异常处理器类 @Slf4j // 使用log4j进行日志记录 public class GlobalExceptionHandler { // 处理业务异常 @ExceptionHandler(BusinessException.class) public JsonResponseBody<?> exceptionBusinessException(BusinessException e) { JsonResponseStatus status = e.getJsonResponseStatus(); // 获取异常的状态信息 log.info(status.getMsg()); // 记录日志 return JsonResponseBody.other(status); // 返回状态信息 } // 处理其他类型的异常 @ExceptionHandler(Throwable.class) public JsonResponseBody<?> exceptionThrowable(Throwable e) { log.info(e.getMessage()); // 记录日志 return JsonResponseBody.other(JsonResponseStatus.UN_KNOWN); // 返回未知状态信息 } }
3.登录密码的两次加密
(1)为什么要加密两次
第一次加密防止前端传递数据时被截取
第二次加密防止数据库泄露
(2)整体的加密流程
MD5(MD5(pass明文+固定salt)+随机salt)
第一次固定salt写死在前端
第二次加密采用随机的salt 并将每次生成的salt保存在数据库中(3)具体流程的代码实现
①前端加密
对用户输入的密码进行md5加密(固定的salt)
引入md5的js
<script src="http://www.gongjuji.net/Content/files/jquery.md5.js" type="text/javascript"></script>
使用MD5加密
<script> $("#login").click(()=>{ let mobile=$("#mobile").val() let password=$("#password").val() password=$.md5(password) $.post(' ${springMacroRequestContext.contextPath}/user/login',{ mobile,password },resp=>{ },"json") }) </script>
但是我们知道MD5它的加密方式是不可逆的,也很容易被解析,所以我们可以自己加盐进去,在前后都加上字符
②加密之后传到后端
将加密后的密码传递到后端
package com.wh.easyshop.controller; import com.sun.corba.se.spi.orb.ParserImplBase; import com.wh.easyshop.resp.JsonResponseBody; import com.wh.easyshop.service.IUserService; import com.wh.easyshop.vo.UserVo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * <p> * 用户信息表 前端控制器 * </p> * * @author wh * @since 2023-12-27 */ @RestController @RequestMapping("/user") public class UserController { @Autowired private IUserService userService; @RequestMapping("/login") public JsonResponseBody<?> login(UserVo userVo){ return userService.login(userVo); } }
③后端拿取用户信息
使用用户id取出用户信息
④后端加密
后端对前端传过来的加密后的密码在进行md5加密(取出盐),然后与数据库中存储的密码进行对比
package com.wh.easyshop.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper; import com.wh.easyshop.exception.BusinessException; import com.wh.easyshop.model.User; import com.wh.easyshop.mapper.UserMapper; import com.wh.easyshop.resp.JsonResponseBody; import com.wh.easyshop.resp.JsonResponseStatus; import com.wh.easyshop.service.IUserService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.wh.easyshop.util.MD5Utils; import com.wh.easyshop.vo.UserVo; import org.springframework.stereotype.Service; import javax.swing.*; /** * <p> * 用户信息表 服务实现类 * </p> * * @author wh * @since 2023-12-27 */ @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { /** * 登录 * @param userVo * @return */ @Override public JsonResponseBody<?> login (UserVo userVo){ //通过用户名拿到用户的信息 User user = getOne(new QueryWrapper<User>().lambda().eq(User::getId, userVo.getMobile()), false); //如果内容匹配不成功,那么提示用户相应的信息 if(user==null){ throw new BusinessException(JsonResponseStatus.LOGIN_MOBILE_NOT_FOUND); } //把数据库的盐值与前端的密码都拿出来,再加密一次【随机盐值】 String secret = MD5Utils.formPassToDbPass(userVo.getPassword(), user.getSalt()); //将数据库里面的密码与上面二次加密之后的密码进行比较,如果不一致就提示相应信息 if(!user.getPassword().equals(secret)){ throw new BusinessException(JsonResponseStatus.LOGIN_NO_EQUALS); } //如果带来的信息都一致,则提示成功 return JsonResponseBody.success(); } }
⑤登录测试
🔺登录与注册之间的逻辑大概也是差不多的,再这里小编没有做注册了,但是没有做注册,我们的数据库中就没有数据,所以我们要使用debug将数据手动的加到数据库中,不然这个登录就永远都是失败的
把盐值拿到也放到数据库中,这里我用的是固定的盐值,大家也可以用时间戳等一些随机的不会重复的数字
package com.wh.easyshop.util; import org.springframework.stereotype.Component; import org.springframework.util.DigestUtils; import java.nio.charset.StandardCharsets; import java.util.UUID; @Component public class MD5Utils { //加密盐,与前端一致 private static final String salt = "f1g2h3j4"; public static String md5(String src) { return DigestUtils.md5DigestAsHex(src.getBytes(StandardCharsets.UTF_8)); } public static String createSalt() { return UUID.randomUUID().toString().replace("-", ""); } /** * 将前端的明文密码通过MD5加密方式加密成后端服务所需密码,混淆固定盐salt,安全性更可靠 */ public static String inputPassToFormPass(String inputPass) { String str = salt.charAt(1) + String.valueOf(salt.charAt(5)) + inputPass + salt.charAt(0) + salt.charAt(3); return md5(str); } /** * 将后端密文密码+随机salt生成数据库的密码,混淆固定盐salt,安全性更可靠 */ public static String formPassToDbPass(String formPass, String salt) { String str = salt.charAt(7) + String.valueOf(salt.charAt(9)) + formPass + salt.charAt(1) + salt.charAt(5); return md5(str); } public static void main(String[] args) { String formPass = inputPassToFormPass("123456"); System.out.println("前端加密密码:" + formPass); String salt = createSalt(); System.out.println("后端加密随机盐:" + salt); String dbPass = formPassToDbPass(formPass, salt); System.out.println("后端加密密码:" + dbPass); } }
4.前后端表单验证
我们为了规范用户的输入,常常会在前端的表单中进行验证,如果用户输入不规范,会在页面中显示出相应的提示,去引导用户输入规范的数据,但我们也可能会遇到这样的一种情况:有的用户不使用我们的表单进行提交,而是使用API测试工具,那么就有可能输入不规范的数据,如果这个时候我们再后端没有进行验证的话,不规范的数据便会进入的数据库中。为了避免这种情况,我们也需要在后端进行验证。这里我们使用的是JSR 303验证框架。
JSR 303:
全称为 Java Specification Request 303,是Java平台上的一个规范,用于定义面向对象的验证框架,也被称为Bean Validation(Bean验证)。
JSR 303提供了一套标准的注解,开发人员可以通过在Java类的字段、方法或者参数上添加这些注解来定义验证规则。这些注解用于描述字段的数据类型、长度、非空约束、正则表达式等验证条件。在运行时,验证框架会根据这些注解自动进行验证,并将验证结果返回给开发人员。【如果我们不使用这个框架进行验证的话,那么我们就需要在调用业务方法之前,手动的去定义每一个属性的规范】
使用JSR 303验证框架的步骤:
(1)引入依赖
<!--jsr303--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
(2)实体类/Vo类加上注解
关于JSR 303,小编之前也写了一篇文章,有兴趣的可以戳进去看看
package com.wh.easyshop.vo; import lombok.Data; import javax.validation.constraints.NotBlank; @Data public class UserVo { @NotBlank private String mobile; @NotBlank private String password; }
(3)Controller层加注解
package com.wh.easyshop.controller; import com.sun.corba.se.spi.orb.ParserImplBase; import com.wh.easyshop.resp.JsonResponseBody; import com.wh.easyshop.service.IUserService; import com.wh.easyshop.vo.UserVo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; /** * <p> * 用户信息表 前端控制器 * </p> * * @author wh * @since 2023-12-27 */ @RestController @RequestMapping("/user") public class UserController { @Autowired private IUserService userService; @RequestMapping("/login") @ResponseBody public JsonResponseBody<?> login(@Valid UserVo userVo){ return userService.login(userVo); } }
这时候就代表已经将校验开启了,如果用户在表单/API测试工具不输入值,都会返回一个未知错误(全局异常类)
但这样返回的是一个未知错误,对于我们开发人员来说,是不好的,所以我们在全局异常中,再写一个处理绑定异常的方法
package com.wh.easyshop.exception; import com.wh.easyshop.resp.JsonResponseBody; import com.wh.easyshop.resp.JsonResponseStatus; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.BindException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Objects; @RestControllerAdvice // 声明这是一个全局异常处理器类 @Slf4j // 使用log4j进行日志记录 public class GlobalExceptionHandler { // 处理业务异常 @ExceptionHandler(BusinessException.class) public JsonResponseBody<?> exceptionBusinessException(BusinessException e) { JsonResponseStatus status = e.getJsonResponseStatus(); // 获取异常的状态信息 log.info(status.getMsg()); // 记录日志 return JsonResponseBody.other(status); // 返回状态信息 } // 处理其他类型的异常 @ExceptionHandler(Throwable.class) public JsonResponseBody<?> exceptionThrowable(Throwable e) { log.info(e.getMessage()); // 记录日志 return JsonResponseBody.other(JsonResponseStatus.UN_KNOWN); // 返回未知状态信息 } // 处理绑定异常 @ExceptionHandler(BindException.class) public JsonResponseBody<?> exceptionThrowable(BindException e) { log.info(e.getMessage()); // 记录日志 return JsonResponseBody.other(JsonResponseStatus.LOGIN_NO_EQUALS); // 返回未知状态信息 } }
那么这时候,就不再是未知错误了,而是我们写好的一个错误枚举中的错误信息
我们在编写项目的过程中,也可能遇到一些特殊的需求,可能JSR 303中原有的注解不能够完成这些特殊的需求,那么我们便可以自己定义JSR 303的注解
(4)自定义JSR 303注解
①编写自定义注解类
package com.wh.easyshop.util; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author是辉辉啦 * @create 2023-12-31-11:08 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @Constraint(validatedBy = {MatchExprConstraintValidator.class}) public @interface MatchExpr { /** * 自定义的部分 * @return */ boolean require() default true;//是否必填 String expr() default "";//填的内容是什么(正则) /** * 使用jsr 303必须要的部分 * @return */ String message() default "{javax.validation.constraints.NotBlank.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
②编写自定义注解的注解解析类
package com.wh.easyshop.util; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import lombok.Data; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.Map; /** * MatchExpr的注解解析类 */ @Data public class MatchExprConstraintValidator implements ConstraintValidator<MatchExpr, String> { // 是否必须匹配 private boolean require; // 正则表达式 private String expr; @Override public void initialize(MatchExpr matchExpr) { // 初始化时获取注解中的参数值 expr = matchExpr.expr(); require = matchExpr.require(); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { // 如果不需要匹配,直接返回true if (!require) return true; // 如果值为空,根据require的值返回false或true if (StringUtils.isEmpty(value)) return false; // 使用正则表达式进行匹配,返回匹配结果 return value.matches(expr); } }
③配置注解解析类
@Constraint(validatedBy = {MatchExprConstraintValidator.class})
④在实体类中使用自定义注解
package com.wh.easyshop.vo; import com.wh.easyshop.util.MatchExpr; import lombok.Data; import javax.validation.constraints.NotBlank; /** * 用户视图对象 */ @Data public class UserVo { /** * 手机号码,使用正则表达式进行验证 * @param mobile 手机号码 */ @MatchExpr(require = true,expr = "(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}") private String mobile; /** * 密码,不能为空 * @param password 密码 */ @NotBlank private String password; }
如果此时输入的是正确的手机号
那么就不会进入断点,不会抛出那个绑定异常
出现的错误也只是手机号未找到
在项目中,为了方便编码以及后续的优化修复,通常是不会在代码中出现自定义的变量,所以这里我们编写一个常量类,把我们自定义的变量放入其中进行统一管理
package com.wh.easyshop.util; /** * 常量类 */ public abstract class Constants { //手机号正则匹配 public static final String EXPR_MOBILE = "(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}"; //密码规则匹配 public static final String EXPR_PASSWORD = "[a-zA-Z0-9]{32}"; }
修改前的代码
修改后的代码
5.登录状态的更换
用户没有登录时,在首页的登录按钮那里就是登录,如果用户已经登录了,那么便显示当前用户的昵称,这里是将用户的信息存储到redis中,
但又因为这个项目是单体项目,所以我们得想办法让用户能够获取到redis中的用户信息,这里我们又两种方式,可以用session以及cookie。但是session是存在服务器的,如果我们将用户的信息存入到session中,那么会让服务器承受过大的压力,所以这里我选择使用cookie来存放。
但这里还存在一个问题:我们是直接将redis中的所有的信息都直接放大cookie中吗?如果是这样的话,那么我们都不必要使用redis了,我们应该是需要哪个用户的信息,就拿到的特殊的标识去redis中取对应用户的信息。说到特殊标识,我们应该想到的是用户id,但如果使用用户id来拿,又会非常的不安全【id通常为主键,可以通过id做许多其他的操作】,所以我们可以手动生成一个唯一标识,这里我是用的雪花id,通过雪花id去拿到redis中的用户信息。
package com.wh.easyshop.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper; import com.github.yitter.idgen.YitIdHelper; import com.sun.deploy.net.HttpResponse; import com.wh.easyshop.exception.BusinessException; import com.wh.easyshop.model.User; import com.wh.easyshop.mapper.UserMapper; import com.wh.easyshop.resp.JsonResponseBody; import com.wh.easyshop.resp.JsonResponseStatus; import com.wh.easyshop.service.IRedisService; import com.wh.easyshop.service.IUserService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.wh.easyshop.util.Constants; import com.wh.easyshop.util.CookieUtils; import com.wh.easyshop.vo.UserVo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpRequest; import org.springframework.stereotype.Service; import org.springframework.util.DigestUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.swing.*; import java.util.Date; /** * <p> * 用户信息表 服务实现类 * </p> * * @author wh * @since 2023-12-27 */ @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { @Autowired private RedisTemplate redisTemplate; @Autowired IRedisService redisService; /** * 登录 * @param userVo * @return */ @Override public JsonResponseBody<?> login (UserVo userVo, HttpServletRequest request, HttpServletResponse response){ //通过用户名拿到用户的信息 User user = getOne(new QueryWrapper<User>().lambda().eq(User::getId, userVo.getMobile()), false); //如果内容匹配不成功,那么提示用户相应的信息 if(user==null){ throw new BusinessException(JsonResponseStatus.LOGIN_MOBILE_NOT_FOUND); } //把数据库的盐值与前端的密码都拿出来,再加密一次【随机盐值】 // String timestamp = System.currentTimeMillis()+"";//时间戳 String secret =user.getSalt()+userVo.getPassword();//将数据库密码与时间戳拼接起来 String s = DigestUtils.md5DigestAsHex((secret).getBytes()); //将数据库里面的密码与上面二次加密之后的密码进行比较,如果不一致就提示相应信息 if(!user.getPassword().equals(s)){ throw new BusinessException(JsonResponseStatus.LOGIN_NO_EQUALS); } //将个人信息放入到redis中【到时候可以根据这个token去拿到用户的信息】 String token=YitIdHelper.nextId()+""; redisService.setUserToRedis(token,user); //把token中的内容存到cookie中去传给用户 CookieUtils.setCookie(request,response,"UserToken",token,7200);//令牌【redis】 CookieUtils.setCookie(request,response,"nickname",user.getNickname(),7200);//用户昵称 //如果带来的信息都一致,则提示成功 return JsonResponseBody.success(); } }
这里使用redis还需要导入pom依赖
<!-- Redis 相关依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.5.6</version> </dependency>
使用了雪花id也要导入依赖呢
<!--雪花ID--> <dependency> <groupId>com.github.yitter</groupId> <artifactId>yitter-idgenerator</artifactId> <version>1.0.6</version> </dependency>
其中使用了cookie进行存储,我这里也写了一个工具类【方便对cookie进行操作】
package com.wh.easyshop.util; import lombok.extern.slf4j.Slf4j; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; @Slf4j public class CookieUtils { /** * @Description: 得到Cookie的值, 不编码 */ public static String getCookieValue(HttpServletRequest request, String cookieName) { return getCookieValue(request, cookieName, false); } /** * @Description: 得到Cookie的值 */ public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) { Cookie[] cookieList = request.getCookies(); if (cookieList == null || cookieName == null) { return null; } String retValue = null; try { for (int i = 0; i < cookieList.length; i++) { if (cookieList[i].getName().equals(cookieName)) { if (isDecoder) { retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8"); } else { retValue = cookieList[i].getValue(); } break; } } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return retValue; } /** * @Description: 得到Cookie的值 */ public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) { Cookie[] cookieList = request.getCookies(); if (cookieList == null || cookieName == null) { return null; } String retValue = null; try { for (int i = 0; i < cookieList.length; i++) { if (cookieList[i].getName().equals(cookieName)) { retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString); break; } } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return retValue; } /** * @Description: 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码 */ public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue) { setCookie(request, response, cookieName, cookieValue, -1); } /** * @param request * @param response * @param cookieName * @param cookieValue * @param cookieMaxage * @Description: 设置Cookie的值 在指定时间内生效,但不编码 */ public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage) { setCookie(request, response, cookieName, cookieValue, cookieMaxage, false); } /** * @Description: 设置Cookie的值 不设置生效时间,但编码 * 在服务器被创建,返回给客户端,并且保存客户端 * 如果设置了SETMAXAGE(int seconds),会把cookie保存在客户端的硬盘中 * 如果没有设置,会默认把cookie保存在浏览器的内存中 * 一旦设置setPath():只能通过设置的路径才能获取到当前的cookie信息 */ public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, boolean isEncode) { setCookie(request, response, cookieName, cookieValue, -1, isEncode); } /** * @Description: 设置Cookie的值 在指定时间内生效, 编码参数 */ public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) { doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode); } /** * @Description: 设置Cookie的值 在指定时间内生效, 编码参数(指定编码) */ public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, String encodeString) { doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString); } /** * @Description: 删除Cookie带cookie域名 */ public static void deleteCookie(HttpServletRequest request, HttpServletResponse response,String cookieName) { doSetCookie(request, response, cookieName, null, -1, false); } /** * @Description: 设置Cookie的值,并使其在指定时间内生效 */ private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) { try { if (cookieValue == null) { cookieValue = ""; } else if (isEncode) { cookieValue = URLEncoder.encode(cookieValue, "utf-8"); } Cookie cookie = new Cookie(cookieName, cookieValue); if (cookieMaxage > 0) cookie.setMaxAge(cookieMaxage); if (null != request) {// 设置域名的cookie String domainName = getDomainName(request); log.info("========== domainName: {} ==========", domainName); if (!"localhost".equals(domainName)) { cookie.setDomain(domainName); } } cookie.setPath("/"); response.addCookie(cookie); } catch (Exception e) { e.printStackTrace(); } } /** * @Description: 设置Cookie的值,并使其在指定时间内生效 */ private static void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, String encodeString) { try { if (cookieValue == null) { cookieValue = ""; } else { cookieValue = URLEncoder.encode(cookieValue, encodeString); } Cookie cookie = new Cookie(cookieName, cookieValue); if (cookieMaxage > 0) cookie.setMaxAge(cookieMaxage); if (null != request) {// 设置域名的cookie String domainName = getDomainName(request); log.info("========== domainName: {} ==========", domainName); if (!"localhost".equals(domainName)) { cookie.setDomain(domainName); } } cookie.setPath("/"); response.addCookie(cookie); } catch (Exception e) { e.printStackTrace(); } } /** * @Description: 得到cookie的域名 */ private static String getDomainName(HttpServletRequest request) { String domainName = null; String serverName = request.getRequestURL().toString(); if (serverName == null || serverName.equals("")) { domainName = ""; } else { serverName = serverName.toLowerCase(); serverName = serverName.substring(7); final int end = serverName.indexOf("/"); serverName = serverName.substring(0, end); if (serverName.indexOf(":") > 0) { String[] ary = serverName.split("\\:"); serverName = ary[0]; } final String[] domains = serverName.split("\\."); int len = domains.length; if (len > 3 && !isIp(serverName)) { // www.xxx.com.cn domainName = "." + domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1]; } else if (len <= 3 && len > 1) { // xxx.com or xxx.cn domainName = "." + domains[len - 2] + "." + domains[len - 1]; } else { domainName = serverName; } } return domainName; } public static String trimSpaces(String IP) {//去掉IP字符串前后所有的空格 while (IP.startsWith(" ")) { IP = IP.substring(1, IP.length()).trim(); } while (IP.endsWith(" ")) { IP = IP.substring(0, IP.length() - 1).trim(); } return IP; } public static boolean isIp(String IP) {//判断是否是一个IP boolean b = false; IP = trimSpaces(IP); if (IP.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")) { String s[] = IP.split("\\."); if (Integer.parseInt(s[0]) < 255) if (Integer.parseInt(s[1]) < 255) if (Integer.parseInt(s[2]) < 255) if (Integer.parseInt(s[3]) < 255) b = true; } return b; } }
还有我们将数据存入到redis中的时候,通常会带有很多的前缀和后缀,为了便于我们操作,我们可以把其中的前后缀都给去除
package com.wh.easyshop.util; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration // 声明这是一个配置类 public class RedisConfig { @Bean // 声明这是一个Spring Bean,用于创建RedisTemplate实例 public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { // 创建RedisTemplate实例 RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); // 设置键的序列化器为StringRedisSerializer redisTemplate.setKeySerializer(new StringRedisSerializer()); // 设置值的序列化器为GenericJackson2JsonRedisSerializer redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // 设置哈希表键的序列化器为StringRedisSerializer redisTemplate.setHashKeySerializer(new StringRedisSerializer()); // 设置哈希表值的序列化器为GenericJackson2JsonRedisSerializer redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); // 设置连接工厂 redisTemplate.setConnectionFactory(connectionFactory); // 初始化RedisTemplate redisTemplate.afterPropertiesSet(); // 返回RedisTemplate实例 return redisTemplate; } }
在编码的时候,发现那一段把用户的信息保存到redis中的代码可能会在许多地方用到【并且也考虑到后续要将用户信息拿出】,所以将方法都封装起来了,方面以后的调用以及修改
package com.wh.easyshop.service; import com.wh.easyshop.model.User; public interface IRedisService { /** * 将登陆User对象保存到Redis中,并以Token为键 */ void setUserToRedis(String token, User user); /** * 根据token令牌获取redis中存储的user对象 */ User getUserByToken(String token); }
package com.wh.easyshop.service; import com.wh.easyshop.model.User; import com.wh.easyshop.util.Constants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service public class RedisServiceImpl implements IRedisService { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public void setUserToRedis(String token, User user) { redisTemplate.opsForValue().set(Constants.REDIS_USER_PREFIX + token, user, 7200, TimeUnit.SECONDS); } @Override public User getUserByToken(String token) { return (User) redisTemplate.opsForValue().get(Constants.REDIS_USER_PREFIX + token); } }
其中也用到了常量类
接着就是前端的内容显示了
效果
代码
<script type="text/javascript" src="js/jquery-1.12.4.min.js"></script> <script> $(function(){ let nickname=getCookie("nickname"); if(null!=nickname&&''!=nickname&&undefined!=nickname) { //设置昵称 $('#nickname').text("您好,"+nickname); //隐藏登录注册按钮 $('p.fl>span:eq(1)').css("display","none"); //显示昵称和退出按钮 $('p.fl>span:eq(0)').css("display","block"); }else{ //隐藏昵称 $('#nickname').text(""); //显示登录注册按钮 $('p.fl>span:eq(1)').css("display","block"); //隐藏昵称和退出按钮 $('p.fl>span:eq(0)').css("display","none"); } }); function getCookie(cname) { var name = cname + "="; var decodedCookie = decodeURIComponent(document.cookie); var ca = decodedCookie.split(';'); for(var i = 0; i <ca.length; i++) { var c = ca[i]; while (c.charAt(0) == ' ') { c = c.substring(1); } if (c.indexOf(name) == 0) { return c.substring(name.length, c.length); } } return ""; } </script> <div class="head"> <div class="wrapper clearfix"> <div class="clearfix" id="top"> <h1 class="fl"><a href="${ctx}/"><img src="img/logo.png"/></a></h1> <div class="fr clearfix" id="top1"> <p class="fl"> <span> <span id="nickname"></span> <a href="${ctx}/user/userLogout">退出</a> </span> <span style="display: none"> <a href="${ctx}/page/login.html" id="login">登录</a> <a href="${ctx}/page/reg.html" id="reg">注册</a> </span> </p> <form action="#" method="get" class="fl"> <input type="text" placeholder="热门搜索:干花花瓶" /> <input type="button" /> </form> <div class="btn fl clearfix"> <a href="${ctx}/page/mygxin.html"><img src="img/grzx.png"/></a> <a href="#" class="er1"><img src="img/ewm.png"/></a> <a href="${ctx}/shopCar/queryShopCar"><img src="img/gwc.png"/></a> <p><a href="#"><img src="img/smewm.png"/></a></p> </div> </div> </div> <ul class="clearfix" id="bott"> <li><a href="${ctx}/">首页</a></li> <li> <a href="#">所有商品</a> <div class="sList"> <div class="wrapper clearfix"> <a href="${ctx}/page/paint.html"> <dl> <dt><img src="img/nav1.jpg"/></dt> <dd>浓情欧式</dd> </dl> </a> <a href="${ctx}/page/paint.html"> <dl> <dt><img src="img/nav2.jpg"/></dt> <dd>浪漫美式</dd> </dl> </a> <a href="${ctx}/page/paint.html"> <dl> <dt><img src="img/nav3.jpg"/></dt> <dd>雅致中式</dd> </dl> </a> <a href="${ctx}/page/paint.html"> <dl> <dt><img src="img/nav6.jpg"/></dt> <dd>简约现代</dd> </dl> </a> <a href="${ctx}/page/paint.html"> <dl> <dt><img src="img/nav7.jpg"/></dt> <dd>创意装饰</dd> </dl> </a> </div> </div> </li> <li> <a href="${ctx}/page/flowerDer.html">装饰摆件</a> <div class="sList2"> <div class="clearfix"> <a href="${ctx}/page/proList.html">干花花艺</a> <a href="${ctx}/page/vase_proList.html">花瓶花器</a> </div> </div> </li> <li> <a href="${ctx}/page/decoration.html">布艺软饰</a> <div class="sList2"> <div class="clearfix"> <a href="${ctx}/page/zbproList.html">桌布罩件</a> <a href="${ctx}/page/bzproList.html">抱枕靠垫</a> </div> </div> </li> <li><a href="${ctx}/page/paint.html">墙式壁挂</a></li> <li><a href="${ctx}/page/perfume.html">蜡艺香薰</a></li> <li><a href="${ctx}/page/idea.html">创意家居</a></li> </ul> </div> </div>
这里顺便也把退出的功能做一下【将cookie清除就好啦】
package com.wh.easyshop.controller; import com.sun.corba.se.spi.orb.ParserImplBase; import com.sun.deploy.net.HttpResponse; import com.wh.easyshop.resp.JsonResponseBody; import com.wh.easyshop.service.IUserService; import com.wh.easyshop.util.CookieUtils; import com.wh.easyshop.vo.UserVo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpRequest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; /** * <p> * 用户信息表 前端控制器 * </p> * * @author wh * @since 2023-12-27 */ @Controller @RequestMapping("/user") public class UserController { @Autowired private IUserService userService; @RequestMapping("/login") @ResponseBody public JsonResponseBody<?> login(@Valid UserVo userVo, HttpServletRequest request, HttpServletResponse response){ return userService.login(userVo,request,response); } /** * 退出登录 * @param request * @param response * @return */ @RequestMapping("/userLogout") public String login(HttpServletRequest request, HttpServletResponse response){ CookieUtils.deleteCookie(request,response,"UserToken"); CookieUtils.deleteCookie(request,response,"nickname"); return "redirect:/"; } }
好啦,今天的分享就到这了,希望能够帮到你呢!😊😊