SpingBoot的项目实战--模拟电商【2.登录】

🥳🥳Welcome Huihui's Code World ! !🥳🥳

接下来看看由辉辉所写的关于SpringBoot电商项目的相关操作吧 

目录

🥳🥳Welcome Huihui's Code World ! !🥳🥳

一.功能需求

二.代码编写

1.登录功能的完成

2.全局异常的处理

3.登录密码的两次加密

(1)为什么要加密两次

(2)整体的加密流程

(3)具体流程的代码实现

①前端加密

②加密之后传到后端

③后端拿取用户信息

 ④后端加密

⑤登录测试

 4.前后端表单验证

(1)引入依赖

(2)实体类/Vo类加上注解

(3)Controller层加注解

(4)自定义JSR 303注解

①编写自定义注解类

②编写自定义注解的注解解析类

③配置注解解析类

④在实体类中使用自定义注解

5.登录状态的更换


一.功能需求

①完成用户登录功能

②用户的各种错误操作都需要给出相应的错误提示,而不是抛出异常【全局异常处理】

③用户登录的密码的两次加密

        表单数据➡后端【加密】

        后端数据➡数据库【加密】

④用户输入表单时,前端【表单验证】和后端【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类:

  1. 数据传输:VO类可以用于封装客户端和服务器之间的数据传输,例如RESTful API的请求和响应对象。

  2. 数据库实体映射:VO类可以用于将数据库表的记录映射为Java对象,并进行数据的读取和存储操作。

  3. 领域模型中的值对象:在领域驱动设计(DDD)中,VO类可以用于表示领域模型中的值对象,如金额、日期范围等。

总之,VO类主要用于封装和传递数据,以提高代码的可读性、可维护性和可测试性。它们通常是不可变的,并且只包含属性和访问方法

package com.wh.easyshop.vo;

import lombok.Data;


@Data
public class UserVo {
    private String mobile;
    private String password;

}

但是上面的这个登录功能的代码只是很粗浅的,我们还需要将它进行升级

2.全局异常的处理

一个用户在输入自己的信息进行登录的时候,很可能会有一些非常规操作,一般这个时候,它会有一些错误以及异常抛出,我们可以使用全局异常进行处理

全局异常:

  1. 方便错误排查和日志记录:全局异常处理可以捕捉并记录异常信息,方便开发人员进行错误排查和系统故障分析。

  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:/";
    }

}

好啦,今天的分享就到这了,希望能够帮到你呢!😊😊 

  • 26
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是辉辉啦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值