Welcome Huihui's Code World ! !
接下来看看由辉辉所写的关于SpringBoot电商项目的相关操作吧
目录
Welcome Huihui's Code World ! !
一.功能需求
二.代码编写
1.登录功能的完成
2.全局异常的处理
3.登录密码的两次加密
(1)为什么要加密两次
(2)整体的加密流程
(3)具体流程的代码实现
①前端加密
②加密之后传到后端
③后端拿取用户信息
⑤登录测试
①完成用户登录功能
②用户的各种错误操作都需要给出相应的错误提示,而不是抛出异常【全局异常处理】
③用户登录的密码的两次加密
表单数据➡后端【加密】
后端数据➡数据库【加密】
④用户登录成功之后,需要在首页显示出登录的用户的昵称【Redis+Cookie】
⑤用户输入表单时,前端【表单验证】和后端【JSR303】都需要有相对应的验证
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.*; /** *
* 用户信息表 服务实现类 *
* * @author wh * @since 2023-12-27 */ @Service public class UserServiceImpl extends ServiceImplimplements 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 ().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
{ 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 JsonResponseBody success() { return new JsonResponseBody (JsonResponseStatus.OK, null); } public static JsonResponseBody success(T data) { return new JsonResponseBody (JsonResponseStatus.OK, data); } public static JsonResponseBody success(T data, Long total) { return new JsonResponseBody (JsonResponseStatus.OK, data, total); } public static JsonResponseBody unknown() { return new JsonResponseBody (JsonResponseStatus.UN_KNOWN, null); } public static JsonResponseBody other(JsonResponseStatus jsonResponseStatus) { return new JsonResponseBody (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
使用MD5加密
但是我们知道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; /** *
* 用户信息表 前端控制器 *
* * @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.*; /** *
* 用户信息表 服务实现类 *
* * @author wh * @since 2023-12-27 */ @Service public class UserServiceImpl extends ServiceImplimplements IUserService { /** * 登录 * @param userVo * @return */ @Override public JsonResponseBody> login (UserVo userVo){ //通过用户名拿到用户的信息 User user = getOne(new QueryWrapper ().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); } }
好啦,今天的分享就到这了,希望能够帮到你呢!