【电商项目实战】MD5登录加密及JSR303自定义注解

🎉🎉欢迎来到我的CSDN主页!🎉🎉

🏅我是Java方文山,一个在CSDN分享笔记的博主。📚📚

🌟推荐给大家我的专栏《电商项目实战》。🎯🎯

👉点击这里,就可以查看我的主页啦!👇👇

Java方文山的个人主页

🎁如果感觉还不错的话请给我点赞吧!🎁🎁

💖期待你的加入,一起学习,一起进步!💖💖

请添加图片描述

目录

一、登录功能实现

1.数据接收类

2.数据响应类

3.功能实现

二、全局异常抓捕

1.自定义异常

2.全局异常抓捕

 三、JSR303验证

 1.JSR303验证

 2.自定义JSR303注解

四、MD5加密登录

​ 1.前端加密

 2.后端加密

五、用户数据存储

 1.用户信息存储Redis

 2.Redis存储代码优化

 3.前端顶部信息显示


一、登录功能实现

1.数据接收类

我们在做登录功能的时候肯定需要接收前端传递到后端的值,但是我们直接用已有的实体会污染这个类(如果还需要做验证之类的话)所以我们先编写一个Vo类进行参数接收。

UserVo

@Data
public class UserVo {

    private String phone;//用户电话
    private String password;//用户密码

}

 2.数据响应类

 我们后端向前端响应数据最好统一格式,所以这里就有两个类响应类和响应枚举类JsonResponseBody

package com.csdn.shop.resp;

import lombok.Data;

@Data
public class JsonResponseBody<T> {

    private Integer code;
    private String msg;
    private T data;
    private Long total;

    private JsonResponseBody(com.star.easyshop.resp.JsonResponseStatus jsonResponseStatus, T data) {
        this.code = jsonResponseStatus.getCode();
        this.msg = jsonResponseStatus.getMsg();
        this.data = data;
    }

    private JsonResponseBody(com.star.easyshop.resp.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>(com.star.easyshop.resp.JsonResponseStatus.OK, null);
    }

    public static <T> JsonResponseBody<T> success(T data) {
        return new JsonResponseBody<T>(com.star.easyshop.resp.JsonResponseStatus.OK, data);
    }

    public static <T> JsonResponseBody<T> success(T data, Long total) {
        return new JsonResponseBody<T>(com.star.easyshop.resp.JsonResponseStatus.OK, data, total);
    }

    public static <T> JsonResponseBody<T> unknown() {
        return new JsonResponseBody<T>(com.star.easyshop.resp.JsonResponseStatus.UN_KNOWN, null);
    }

    public static <T> JsonResponseBody<T> other(com.star.easyshop.resp.JsonResponseStatus jsonResponseStatus) {
        return new JsonResponseBody<T>(jsonResponseStatus, null);
    }

}

JsonResponseStatus

package com.csdn.shop.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();
    }

}

3.功能实现

这里就直接展示controller和servicelmpl的代码,接口的定义我相信大家也都会写。

 controller

@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private IUserService userService;

    @ResponseBody
    @RequestMapping("/login")
    public JsonResponseBody<?> login(UserVo vo){
      return userService.login(vo);
    }

}

UserServiceImpl


@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public JsonResponseBody<?> login(UserVo vo) {
        //手机号为空
        if (vo.getPhone()==null || vo.getPhone().trim().length()==0){
            return  JsonResponseBody.other(JsonResponseStatus.LOGIN_MOBILE_INFO);
        }
        //密码为空
        if (vo.getPassword()==null || vo.getPassword().trim().length()==0){
            return  JsonResponseBody.other(JsonResponseStatus.LOGIN_PASSWORD_INFO);
        }
        //根据手机号和密码查询
        User one = getOne(new QueryWrapper<User>().lambda().eq(User::getId, vo.getPhone()).eq(User::getPassword, vo.getPassword()), false);
        //用户未找到
        if (one==null){
            return  JsonResponseBody.other(JsonResponseStatus.LOGIN_NO_EQUALS);
        }
        return  JsonResponseBody.other(JsonResponseStatus.OK);
    }
}

二、全局异常抓捕

1.自定义异常

全局异常捕获可以用于收集应用程序中发生的异常信息,并进行监控和分析。通过记录异常日志、统计异常发生的频率和类型等,可以帮助开发人员及时发现潜在的问题,并进行修复和优化。

如果直接在刚刚我们所写的代码中添加日志记录,会比较麻烦,我们将他变成一个异常然后被异常抓捕后进行记录就大大减少了我们的代码量。

BusinessException

@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
@Data
public class BusinessException extends RuntimeException {

    private JsonResponseStatus jsonResponseStatus;

}

自定义异常类并提供了一些常用的方法和构造函数,方便在业务逻辑中抛出和处理异常。异常类中的 JsonResponseStatus 用于封装业务异常的相关状态信息,方便异常处理器进行处理和返回给前端。

2.全局异常抓捕

首先修改UserServiceImpl的代码,全部改为抛异常的方式

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public JsonResponseBody<?> login(UserVo vo) {
        //手机号为空
        if (vo.getPhone()==null || vo.getPhone().trim().length()==0){
           throw new BusinessException(JsonResponseStatus.LOGIN_MOBILE_INFO);

        }
        //密码为空
        if (vo.getPassword()==null || vo.getPassword().trim().length()==0){
            throw new BusinessException(JsonResponseStatus.LOGIN_PASSWORD_INFO);
        }
        //根据手机号和密码查询
        User one = getOne(new QueryWrapper<User>().lambda().eq(User::getId,     vo.getPhone()).eq(User::getPassword, vo.getPassword()), false);
        //用户未找到
        if (one==null){
            throw new BusinessException(JsonResponseStatus.LOGIN_NO_EQUALS);
        }
        return  JsonResponseBody.other(JsonResponseStatus.OK);
    }
}

编写一个全局异常抓捕类,进行异常抓捕,该类是一个增强controller,会将原本的异常拦截在此

@RestControllerAdvice
@Slf4j
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);
    }



}

测试一下,查看效果

 三、JSR303验证

1.JSR303验证

在刚刚的那种方式中太过于简单,而且还没有进行手机号正则判断、密码正则判断等,如果我们需要判断的属性非常多if标签就需要写很多,非常的不方便,所以需要用到JSR303进行验证

JSR303是依赖所以首先需要在pom文件中添加依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

 JSR303基本校验规则

注解作用类型解释
@NotNull任何类型属性不能为null
@NotEmpty集合集合不能为null,且size大于0
@NotBlanck字符串、字符字符类不能为null,且去掉空格之后长度大于0
@AssertTrueBoolean、boolean布尔属性必须是true
@Min数字类型(原子和包装)限定数字的最小值(整型)
@Max同@Min限定数字的最大值(整型)
@DecimalMin同@Min限定数字的最小值(字符串,可以是小数)
@DecimalMax同@Min限定数字的最大值(字符串,可以是小数)
@Range数字类型(原子和包装)限定数字范围(长整型)
@Length字符串限定字符串长度
@Size集合限定集合大小
@Past时间、日期必须是一个过去的时间或日期
@Future时期、时间必须是一个未来的时间或日期
@Email字符串必须是一个邮箱格式
@Pattern字符串、字符正则匹配字符串

在我们的Vo类加上注解

@Data
public class UserVo {

    @NotBlank
    private String phone;//用户电话
    @NotBlank
    private String password;//用户密码

}

在controller的方法上开启检查加上注解@Valid 或者 @Validated 进行参数校验的时候,如果不加BindingResult那么会抛出异常

既然这里会抛出异常,那么我们继续往全局异常抓捕类里面加一个异常抓捕方法

     //绑定异常抓捕
    @ExceptionHandler(BindException.class)
    public JsonResponseBody<?> exceptionThrowable(BindException e) {
        //日志记录
        log.info(e.getMessage());
        return JsonResponseBody.other(JsonResponseStatus.LOGIN_NO_EQUALS);
    }

2.自定义JSR303注解

刚刚我们的JSR303虽然拦截非空字段但是没有判断手机号是否正常等,所以我们现在写一个自定义JSR303注解类进行正则判断

package com.csdn.shop.core;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

/**
 * @author Java方文山
 * @compay csdn_Java方文山
 * @create 2023-12-29-19:03
 */
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(
        validatedBy = {MatchExprConstraintValidator.class}
)
public @interface BooleanExpr {
    String message() default "{javax.validation.constraints.NotBlank.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    boolean require () default  false;//判断这个字段是否是必填
    String expr () default "" ;//正则规则

}

 这里的expr也就是正则判断是通过用户定义的,大大提升了复用性。

@Data
public class UserVo {

    @BooleanExpr(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 phone;//用户电话

    @BooleanExpr(require = true ,expr ="[a-zA-Z0-9]{32}")
    private String password;//用户密码

}

我们还需要 自定义的验证器,用于验证字符串是否满足指定的正则表达式规则。

package com.csdn.shop.core;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.Data;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

@Data
class MatchExprConstraintValidator implements ConstraintValidator<BooleanExpr, String> {

    private boolean require;
    private String expr;

    @Override
    public void initialize(BooleanExpr matchExpr) {
        expr = matchExpr.expr();
        require = matchExpr.require();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (!require) return true;
        if (StringUtils.isEmpty(value)) return false;
        return value.matches(expr);
    }

}

在这个类中, ConstraintValidator<BooleanExpr, String> 表示该验证器的泛型参数,其中 BooleanExpr 是一个自定义注解,String 则是要验证的目标类型。

该类实现了 ConstraintValidator 接口,并重写了 initialize isValid 方法。initialize 方法用于初始化验证器,从注解中获取正则表达式和是否必须的信息。isValid 方法用于实际的验证逻辑,根据是否必须和目标字符串是否为空来判断是否满足正则表达式规则。

现在我们登录错误的电话验证一下

四、MD5加密登录

首先看一下登录页面

<!DOCTYPE html>
<html>
	<head>
		<#include "common/head.html">
		<link rel="stylesheet" type="text/css" href="css/public.css"/>
		<link rel="stylesheet" type="text/css" href="css/login.css"/>
		<script type="text/javascript" src="js/jquery-1.12.4.min.js"></script>
	</head>
	<body>
		<!-------------------login-------------------------->
		<div class="login">
			<form action="${ctx}/user/login" method="post">
				<h1><a href="${ctx}/"><img src="img/temp/logo.png"></a></h1>
				<p></p>
				<div class="msg-warn hide"><b></b>公共场所不建议自动登录,以防账号丢失</div>
				<p><input style="font-size:14px;" type="text" id="phone" value="" placeholder="昵称/邮箱/手机号"></p>
				<p><input style="font-size:14px;" type="password" id="password" value="" placeholder="密码"></p>
				<p><input type="button" id="login" value="登  录"></p>
				<p class="txt"><a class="" href="${ctx}/page/reg.html">免费注册</a><a href="${ctx}/page/forget.html">忘记密码?</a></p>
			</form>
		</div>
		
	</body>
</html>

<script>
	//登陆点击事件
	$("#login").click(()=>{
		//获取手机号和密码的值
		let phone=$("#phone").val()
		let password=$("#password").val()
		$.post('${springMacroRequestContext.contextPath}/user/login',{
			phone,password
		},resp=>{

		},"json")
	})
</script>

这么发送登录请求,密码暴露的风险太大了

 1.前端加密

首先引入md5的依赖

<script src="http://www.gongjuji.net/Content/files/jquery.md5.js" type="text/javascript"></script>

对password进行加密

<script>
	//登陆点击事件
	$("#login").click(()=>{
		//获取手机号和密码的值
		let phone=$("#phone").val()
		let password=$("#password").val()
		//加密的同时加salt
		password=$.md5("csdn"+password+"xw"+password)
		$.post('${springMacroRequestContext.contextPath}/user/login',{
			phone,password
		},resp=>{

		},"json")
	})
</script>

这时候同样的密码123显示的就不一样了

 2.后端加密

在后端也进行加密双重保证,这里提供一个MD5工具类方便大家使用

package com.csdn.shop.utils;

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);
    }

}

 首先需要将我们数据库的加密密码和salt修改成我们目前的规则

  @Override
    public JsonResponseBody<?> login(UserVo vo) {
        //将前端的密码进行加密
        String pwd=vo.getPassword();
        long time = System.currentTimeMillis();
        //进行加秘密(salt就是时间戳)
        DigestUtils.md5DigestAsHex((pwd+time).getBytes(StandardCharsets.UTF_8));

        //根据手机号和密码查询
        User one = getOne(new QueryWrapper<User>().lambda().eq(User::getId, vo.getPhone()).eq(User::getPassword, vo.getPassword()), false);
        //用户未找到
        if (one==null){
            throw new BusinessException(JsonResponseStatus.LOGIN_NO_EQUALS);
        }
        return  JsonResponseBody.other(JsonResponseStatus.OK);
    }

登录测试一下

 将我们的salt和加密后的密码赋值给数据库的用户(注意这个操作就类似于注册,如果不将数据库的密码和salt进行替换待会登录的时候,我们的这种加密方式是获取不到真正的密码的)

修改登录的方法

    @Override
    public JsonResponseBody<?> login(UserVo vo) {
        //根据手机号和密码查询
        User one = getOne(new QueryWrapper<User>().lambda().eq(User::getId, vo.getPhone()), false);
        //用户未找到
        if (one==null){
            throw new BusinessException(JsonResponseStatus.LOGIN_NO_EQUALS);
        }
        //前端的密码 + 数据库的盐
        String str=vo.getPassword()+one.getSalt();
        //进行加密
        str= DigestUtils.md5DigestAsHex(str.getBytes(StandardCharsets.UTF_8));
        //将加密后的密码和数据库的原密码进行比较
        if(!str.equals(one.getPassword())){
            
            throw new BusinessException(JsonResponseStatus.LOGIN_PASSWORD_NOT_FOUND);
        }
        return  JsonResponseBody.other(JsonResponseStatus.OK);
    }
}

 现在就是先根据电话号查询用户,将用户的salt取出来后和前端的密码进行加密,如果加密后的密码和数据库的密码一致就说明登录成功,如果salt不一样,或者前端密码不一样都不会和数据库的密码一致。

随后在前端做好返回值的判断即可

五、用户数据存储

我们用户进行登录后,肯定需要将用户信息进行存储的但是存在session会加大服务器的资源,所以我们要将登录成功的用户信息存储在redis缓存中去。

1.用户信息存储Redis

pom依赖引入

<!-- redis:缓存数据库 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.5.6</version>
</dependency>
<!-- commons-pool2:实现对象池化的框架,没有的话启动时redis报错 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.11.1</version>
</dependency>

yml配置文件

#redis
redis:
  # redis数据库索引(默认为0)
  database: 0
  # redis服务器地址(默认为localhost)
  host: 182.92.153.8
  # redis端口(默认为6379)
  port: 6379
  # redis访问密码(默认为空)
  password:
  # redis连接超时时间(单位毫秒):不能设置为0,否则报无法连接
  timeout: 6000ms
  # redis高级客户端
  lettuce:
    pool:
      # 最大可用连接数(默认为8,负数表示无限)
      max-active: 8
      # 最大空闲连接数(默认为8,负数表示无限)
      max-idle: 8
      # 最小空闲连接数(默认为0,该值只有为正数才有用)
      min-idle: 0
      # 从连接池中获取连接最大等待时间(默认为-1,单位为毫秒,负数表示无限)
      max-wait: -1

将用户信息存储于redis,并且将redis的键保存到前端cookie,我们首先需要在controller接收 HttpServletRequest , HttpServletResponse两个对象

   @ResponseBody
    @RequestMapping("/login")
    public JsonResponseBody<?> login(@Validated UserVo vo, HttpServletRequest request, HttpServletResponse response){
      return userService.login(vo,request,response);
    }

 serviceimpl代码

这里我们的键最好是唯一的,什么是唯一的?雪花ID,所以将雪花ID的依赖加入进来生成我们的token即可,在redis中的键就是"user:+token"

         <dependency>
            <groupId>com.github.yitter</groupId>
            <artifactId>yitter-idgenerator</artifactId>
            <version>1.0.6</version>
        </dependency>
        <!-- 雪花ID -->

我们的前端也需要保存用户的token方便前端向后端发送请求,这里用到cookie的一个工具类

package com.csdn.shop.utils;

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;
    }

}

 修改service中的代码

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public JsonResponseBody<?> login(UserVo vo, HttpServletRequest request, HttpServletResponse response) {
        //将前端的密码进行加密
        String pwd=vo.getPassword();

        //根据手机号和密码查询
        User one = getOne(new QueryWrapper<User>().lambda().eq(User::getId, vo.getPhone()), false);
        //用户未找到
        if (one==null){
            throw new BusinessException(JsonResponseStatus.LOGIN_NO_EQUALS);
        }
        //前端的密码 + 数据库的盐
        String str=vo.getPassword()+one.getSalt();
        //进行加密
        str= DigestUtils.md5DigestAsHex(str.getBytes(StandardCharsets.UTF_8));
        //将加密后的密码和数据库的原密码进行比较
        if(!str.equals(one.getPassword())){
            throw new BusinessException(JsonResponseStatus.LOGIN_PASSWORD_NOT_FOUND);
        }
        //因为每个用户都是唯一的所以用到唯一表示——雪花ID
        String token = YitIdHelper.nextId()+"";
        //将用户信息保存到Redis
        redisTemplate.opsForValue().set("user:"+token,one);
        //将token保存在cookie方便前端向后端取值
        CookieUtils.setCookie(request,response,"userToken",token,7200);
        return  JsonResponseBody.other(JsonResponseStatus.OK);
    }
}

这里还需要注意两个地方,我们的实体中的时间类型是LocalDateTime我们需要改成Date

 第二个就是我们的数据存储在redis的时候格式不美观所以借助一个工具类进行格式转换避免乱码

package com.csdn.shop.config;

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
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

}

这时候进行登录测试一下

可以看到我们的token存储在了浏览器,用户信息也存储在了redis,下次前端想要获取用户信息就可以带着这个token去到redis进行查找。 

2.Redis存储代码优化

在我们刚刚的代码中用到的键都是手写的"user:"+token,但是这样的代码不好维护,所以我们将这个自变量变为常量(我们也可将上面的正则设置为常量)

public class Constants {

    public static final  String REDIS_USER_PREFIX="user:";

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

}

其次,我们操作Redis的代码可能在别的地方也需要用到,所以我们将其进行封装

Service

public interface IRedisService {

    // 添加用户数据
    void saveUser(String token,User user);

    // 根据token查询用户信息
    User loadUser(String token);

}

ServiceImpl

@Service
public class IRedisServiceImpl implements IRedisService {

    @Autowired
    private RedisTemplate redisTemplate;
    @Override
    public void saveUser(String token,User user) {
        //将用户信息保存到Redis
        redisTemplate.opsForValue().set(Constants.REDIS_USER_PREFIX +token,user);
    }

    @Override
    public User loadUser(String token) {
        return (User) redisTemplate.opsForValue().get(Constants.REDIS_USER_PREFIX +token);
    }
}

 随后修改我们UserServiceImpl的代码即可,调用IRedisServiceImpl的方法

 3.前端顶部信息显示

  $(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");
       }

在前端做好有无用户登录的显示,并在后端做好相应数据的回显

登录前:

登录后:

请添加图片描述

到这里我的分享就结束了,欢迎到评论区探讨交流!!

💖如果觉得有用的话还请点个赞吧 💖

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/278863.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

mac安装k8s环境

安装kubectl brew install kubectl 确认一下安装的版本 kubectl version --client 如果想在本地运行kubernetes 需要安装minikube brew install minikube 需要注意安装minikube需要本地的docker服务是启动的 启动 默认连接的是google的仓库 minikube start 指定阿…

下载和安装AD14 - Altium Designer 14.3.20.54863

这个版本应该还支持XP 系统[doge]&#xff0c;总之就是想安装一下&#xff0c;没什么特别的意义。 下载 资源来自毛子网站&#xff1a;https://rutracker.net/forum/viewtopic.php?t5140739&#xff0c;带上个网页翻译插件就行。要用磁力链接下载&#xff0c;推荐用qbittorr…

远程网络唤醒家庭主机(openwrt设置)

远程网络唤醒家庭主机&#xff08;openwrt设置&#xff09; 前提&#xff1a; 1.配置好主板bios的网络唤醒功能(网络教程自己百度一下找) 2.电脑开启网络唤醒功能(网络教程自己百度一下找) 3.路由器通过ddns实现域名和动态IP绑定内网穿透方法汇总_不修改光猫进行内网穿透-C…

最新AI系统ChatGPT网站H5系统源码,支持AI绘画,GPT语音对话+ChatFile文档对话总结+DALL-E3文生图

一、前言 SparkAi创作系统是基于ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI创作Ch…

代码质量评价及设计原则

1.评价代码质量的标准 1.1 可维护性 可维护性强的代码指的是: 在不去破坏原有的代码设计以及不引入新的BUG的前提下,能够快速的修改或者新增代码. 不易维护的代码指的是: 在添加或者修改一些功能逻辑的时候,存在极大的引入新的BUG的风险,并且需要花费的时间也很长. 代码可…

如何让python在手机上运行,python程序在手机上运行

大家好&#xff0c;给大家分享一下python怎么在手机上运行爱心代码&#xff0c;很多人还不知道这一点。下面详细解释一下。现在让我们来看看&#xff01; 1. 写在前面的话 天天都在PC端运行Python代码的我&#xff0c;今天突然灵光一现&#xff0c;想着是不是能够在移动端运行P…

C++继承与派生——(6)派生类的析构函数

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd; 站在巨人的肩上&#xff0c;是为了超过…

初识智慧城市

文章目录 智慧家居 智慧社区 智慧交通 智慧医疗 智慧教育 智慧旅游 智慧农业 智慧安防 智慧家居 利用智能语音、智能交互等技术,实现用户对家居系统各设备的远程操控和能控制如开关窗帘(窗户)、操控家用电器和照明系统、打扫卫生等操作。利用计算机视觉等技术,对被照看…

Spring boot:3.X + Security OAuth2 自定义登录页面、登出后跳转到登录页

本文描述了基于 Spring Oauth2 的 code 模式&#xff0c;实现登陆同时授权、自定义登录界面、登出页面的功能。 1.Maven依赖&#xff1a; <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactI…

SAP缓存 表缓存( Table Buffering)

本文主要介绍SAP中的表缓存在查询数据&#xff0c;更新数据时的工作情况以及对应概念。 SAP表缓存的工作 查询数据 更新数据 删除数据 表缓存的概念 表缓存技术设置属性 不允许缓冲&#xff1a; 允许缓冲&#xff0c;但已关闭&#xff1a; 缓冲已激活&#xff1a; 已…

笔记1:基于锚框(先验框)的目标检测

一、边缘框&#xff08;bounding box&#xff09; 1.1 定义 边缘框&#xff1a;真实标注的物体位置 2.1 表示方式 1、&#xff08;x1,y1)和(x2,y2) 2、&#xff08;x1,y1)和w,h 二、锚框(anchor box)/先验框&#xff08;prior bounding box&#xff09; 2.1 定义 对边缘…

webpack打包批量替换路径(string-replace-webpack-plugin插件)

string-replace-webpack-plugin 是一个用于在 webpack 打包后的文件中替换字符串的插件。它可以用于将特定字符串替换为其他字符串&#xff0c;例如将敏感信息从源代码中移除或对特定文本进行本地化处理。比如文件的html、css、js中的路径地址想批量更改一下 http://localhost:…

软件安装(详细讲解VMware虚拟机的安装及VMvare安装winServer2012)实施必用!!!

文章目录 前言&#xff1a;一、VMware虚拟机的安装1.1VMware虚拟机的安装步骤(一直下一步)&#xff1a;1.2Vmware配置虚拟机&#xff1a; 二、VMware安装winServer20122.1激活windows: 思维导图&#xff1a; 前言&#xff1a; 本编给大家详细的介绍VMware虚拟机的安装和安装wi…

自定义View绘制基础之Canvas

画布Canvas Canvas&#xff08;画布&#xff09;是一个用于绘制图形的重要类&#xff0c;它提供了一组绘图操作的方法&#xff0c;允许你在屏幕上绘制各种形状、图像和文本。下面是Canvas类的主要函数&#xff1a; 目录 一、绘制图形和图像 1.drawColor 2.drawCircle 3.dr…

Gitlab 定时备份

要求 1.为了能够备份和恢复&#xff0c;请确保你的系统上安装了Rsync #Debian/Ubauntu sudo apt-get install rsync # RHEL/Centos sudo yum install rsync 配置与备份目标机器之间的免密认证 修改gitlab配置文件 vim /etc/gitlab/gitlab.rb gitlab_rails[manage_backup_…

服务器运行状况监控工具

服务器运行状况监视提供了每个服务器状态和性能的广泛概述&#xff0c;通过监控服务器指标&#xff0c;如 CPU 使用率、内存消耗、I/O、磁盘使用率、进程等&#xff0c;服务器运行状况监控可以避免服务器停机。 服务器性能监控指标 服务器是网络中最重要的组件之一&#xff0…

AI大模型时代下运维开发探索第二篇:基于大模型(LLM)的数据仓库

在SREWorks社区聚集了很多进行运维数仓建设的同学&#xff0c;大家都会遇到类似的挑战和问题&#xff1a; 数仓中存储大量数据消耗成本&#xff0c;但很多存储的数据却并没有消费。进数仓的ETL学习成本高、管理成本高&#xff0c;相关同学配合度低&#xff0c;以及上游结构改动…

OpenAPI,已支持表单数据格式校验

OpenAPI 路径 开放平台 功能简介 「OpenAPI」- 支持表单数据格式校验。 通过「OpenAPI-新增表单数据」接口&#xff0c;新增数据时&#xff0c;若数据格式不匹配&#xff0c;会导致无法新增。 例如&#xff0c;数字不能新增到日期格式的表单字段。 请参考数据格式传参&a…

3d光学轮廓仪测微光学器件应用及其重要意义

微光学器件是光学器件的重要分支&#xff0c;为光学通信、光传感、光计算等领域的发展提供重要支撑。微光学器件具有尺寸小、功耗低、低成本等优势&#xff0c;可以于电子器件集成&#xff0c;实现更高效的数据传输和信号处理。未来&#xff0c;随着微纳加工技术的进一步发展&a…

【办公软件】Excel双坐标轴图表

在工作中整理测试数据&#xff0c;往往需要一个图表展示两个差异较大的指标。比如共有三个数据&#xff0c;其中两个是要进行对比的温度值&#xff0c;另一个指标是两个温度的差值&#xff0c;这个差值可能很小。 举个实际的例子&#xff1a;数据如下所示&#xff0c;NTC检测温…