手把手教你入门vue+springboot开发(三)--登录功能后端


前言

前两篇我们用vue+springboot框架实现了一个很简单的查询数据的demo,通过这个demo主要是熟悉IDEAU和VSCode等工具的使用,以及熟悉vue+springboot前后端分离开发的基本流程。接下来两篇我们将在前面的基础上实现登录这个B/S典型应用场景,进一步深入的理解B/S架构、vue的一些基础插件、前后端交互过程。


一、redis安装

1)首先下载redis压缩包Redis-x64-3.0.504.zip,地址如下:
https://github.com/MicrosoftArchive/redis/releases
2)解压到本地路径C:\Program Files,进入解压后Redis-x64-3.0.504目录,执行以下命令:
redis-server.exe redis.windows.conf
如下图所示表示redis启动成功。
在这里插入图片描述
先把数据库中password改成md5加密,如下图,admin密码为:12345678,数据库中改成它的md5码:25d55ad283aa400af464c76d713c07ad
在这里插入图片描述

二、后端代码

1.修改application.yml文件

spring配置字段下增加以下内容连接redis服务器。

data:
    redis:
      host: localhost
      port: 6379

2.增加utils文件

增加utils包,里面增加JwtUtil、Md5Util、ThreadLocalUtil类,增加JWT、MD5和线程局部变量方法。
JwtUtil类代码:

package com.example.demo.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;

import java.util.Date;
import java.util.Map;

public class JwtUtil {

    private static final String KEY = "itheima";

    //接收业务数据,生成token并返回
    public static String genToken(Map<String, Object> claims) {
        return JWT.create()
                .withClaim("claims", claims)
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 ))
                .sign(Algorithm.HMAC256(KEY));
    }

    //接收token,验证token,并返回业务数据
    public static Map<String, Object> parseToken(String token) {
        return JWT.require(Algorithm.HMAC256(KEY))
                .build()
                .verify(token)
                .getClaim("claims")
                .asMap();
    }

}

Md5Util类代码:

package com.example.demo.utils;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Md5Util {
    /**
     * 默认的密码字符串组合,用来将字节转换成 16 进制表示的字符,apache校验下载的文件的正确性用的就是默认的这个组合
     */
    protected static char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    protected static MessageDigest messagedigest = null;

    static {
        try {
            messagedigest = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException nsaex) {
            System.err.println(Md5Util.class.getName() + "初始化失败,MessageDigest不支持MD5Util。");
            nsaex.printStackTrace();
        }
    }

    /**
     * 生成字符串的md5校验值
     *
     * @param s
     * @return
     */
    public static String getMD5String(String s) {
        return getMD5String(s.getBytes());
    }

    /**
     * 判断字符串的md5校验码是否与一个已知的md5码相匹配
     *
     * @param password  要校验的字符串
     * @param md5PwdStr 已知的md5校验码
     * @return
     */
    public static boolean checkPassword(String password, String md5PwdStr) {
        String s = getMD5String(password);
        return s.equals(md5PwdStr);
    }


    public static String getMD5String(byte[] bytes) {
        messagedigest.update(bytes);
        return bufferToHex(messagedigest.digest());
    }

    private static String bufferToHex(byte bytes[]) {
        return bufferToHex(bytes, 0, bytes.length);
    }

    private static String bufferToHex(byte bytes[], int m, int n) {
        StringBuffer stringbuffer = new StringBuffer(2 * n);
        int k = m + n;
        for (int l = m; l < k; l++) {
            appendHexPair(bytes[l], stringbuffer);
        }
        return stringbuffer.toString();
    }

    private static void appendHexPair(byte bt, StringBuffer stringbuffer) {
        char c0 = hexDigits[(bt & 0xf0) >> 4];// 取字节中高 4 位的数字转换, >>>
        // 为逻辑右移,将符号位一起右移,此处未发现两种符号有何不同
        char c1 = hexDigits[bt & 0xf];// 取字节中低 4 位的数字转换
        stringbuffer.append(c0);
        stringbuffer.append(c1);
    }
}

ThreadLocalUtil类代码:

package com.example.demo.utils;

import java.util.HashMap;
import java.util.Map;

/**
 * ThreadLocal 工具类
 */
@SuppressWarnings("all")
public class ThreadLocalUtil {
    //提供ThreadLocal对象,
    private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();

    //根据键获取值
    public static <T> T get(){
        return (T) THREAD_LOCAL.get();
    }

    //存储键值对
    public static void set(Object value){
        THREAD_LOCAL.set(value);
    }


    //清除ThreadLocal 防止内存泄漏
    public static void remove(){
        THREAD_LOCAL.remove();
    }
}

3.增加Result类

bean增加Result类,定义HTTP返回结果格式。

package com.example.demo.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

//统一响应结果
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Result<T> {
    private Integer code;//业务状态码  0-成功  1-失败
    private String message;//提示信息
    private T data;//响应数据

    //快速返回操作成功响应结果(带响应数据)
    public static <E> Result<E> success(E data) {
        return new Result<>(0, "操作成功", data);
    }

    //快速返回操作成功响应结果
    public static Result success() {
        return new Result(0, "操作成功", null);
    }

    public static Result error(String message) {
        return new Result(1, message, null);
    }
}

4.修改UserController类

selectAll方法的返回值需要使用Result定义格式,参照下面代码改一下,然后增加login方法处理"/user/login"请求,校验用户名、密码后生成JWT令牌,并保存在redis中,当其它请求过来时需要验证JWT令牌,同时令牌中还保存了用户名和密码等信息,当需要这些信息时就不用再查询mysql数据库。

 @Autowired
    private StringRedisTemplate stringRedisTemplate;

   @PostMapping("/login")
    public Result<String> login(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) {
        //根据用户名查询用户
        User loginUser = userService.findByUserName(username);
        //判断该用户是否存在
        if (loginUser == null) {
            return Result.error("用户名错误");
        }

        //判断密码是否正确  loginUser对象中的password是密文
        if (Md5Util.getMD5String(password).equals(loginUser.getPassword())) {
            //登录成功
            Map<String, Object> claims = new HashMap<>();
            claims.put("id", loginUser.getId());
            claims.put("username", loginUser.getUserName());
            String token = JwtUtil.genToken(claims);
            //把token存储到redis中
            ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
            operations.set(token,token,1, TimeUnit.HOURS);
            return Result.success(token);
        }
        return Result.error("密码错误");
    }

增加后有些字段会是红色,工具支持自动导入相关的类,鼠标放在字段上会出现下图提示,选择“Import class”即可。
在这里插入图片描述
注意Pattern字段工具自动导入的类是
import org.intellij.lang.annotations.Pattern;
这里regexp可能识别不到,那就换成下面的类,但需要增加spring-boot-starter-validation依赖
import jakarta.validation.constraints.Pattern;

5.修改UserMapper类

增加findByUserName方法。

    //根据用户名查询用户
    @Select("select * from users where userName=#{username}")
    User findByUserName(String username);

6.修改UserService和UserServiceImpl类

UserService类增加:

    //根据用户名查询用户
    User findByUserName(String username);

UserServiceImpl类增加:

    @Override
    public User findByUserName(String username) {
        User u = userMapper.findByUserName(username);
        return u;
    }

7.增加LoginInterceptor类

增加interceptors包,里面增加LoginInterceptor类,这里主要通过拦截器处理校验令牌的过程。

@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //处理OPTIONS请求时,返回正确的CORS头部信息
        if (request.getMethod().equals("OPTIONS")) {
            response.setHeader("Access-Control-Allow-Origin", "*");//*表示放行所有的源
            response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, HEAD, OPTIONS");
            response.setHeader("Access-Control-Allow-Headers", "*");
            response.setHeader("Access-Control-Allow-Credentials", "true");
            response.setStatus(HttpServletResponse.SC_OK);
            return false;
        }
        else {
            //令牌验证
            String token = request.getHeader("Authorization");
            //验证token
            try {
                //从redis中获取相同的token
                ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
                String redisToken = operations.get(token);
                if (redisToken == null) {
                    //token已经失效了
                    throw new RuntimeException();
                }
                Map<String, Object> claims = JwtUtil.parseToken(token);

                //把业务数据存储到ThreadLocal中
                ThreadLocalUtil.set(claims);
                //放行
                return true;
            } catch (Exception e) {
                //http响应状态码为401
                response.setStatus(401);
                //不放行
                return false;
            }
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //清空ThreadLocal中的数据
        ThreadLocalUtil.remove();
    }
}

这里调试的时候出现跨域访问不了后端/user/selectAll的情况,所以需要加上OPTIONS这段代码。

8.增加WebConfig类

增加config包,里面增加WebConfig类,如果不需要拦截可以在这里设置。

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //登录接口不拦截
        registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login");
    }

    //解决跨域问题
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedOriginPatterns("*")
                .allowedMethods("GET", "HEAD", "POST","PUT", "DELETE", "OPTIONS")
                .allowCredentials(true).maxAge(3600);
    }
}

解决跨域问题可以直接在这里增加addCorsMappings方法,这样就不需要在每个controller类中增加@CrossOrigin。

9.修改pom.xml文件

增加上面代码需要的依赖。

<!--redis坐标-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
<!--lombok依赖-->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    </dependency>
<!--java-jwt坐标-->
    <dependency>
      <groupId>com.auth0</groupId>
      <artifactId>java-jwt</artifactId>
      <version>4.4.0</version>
    </dependency>
<!--validation依赖-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

下一篇我们继续实现前端代码

最近更新

  1. TCP协议是安全的吗?

    2024-06-15 09:14:03       10 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-15 09:14:03       12 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-15 09:14:03       11 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-15 09:14:03       14 阅读

热门阅读

  1. Web后端开发(分层解耦)

    2024-06-15 09:14:03       5 阅读
  2. 2023-2024山东大学软件学院web数据管理期末

    2024-06-15 09:14:03       8 阅读
  3. 7号楼地面东照西和南照北

    2024-06-15 09:14:03       7 阅读
  4. 嵌入式linux中GPIO和Pinctrl子系统分享

    2024-06-15 09:14:03       7 阅读
  5. 双指针练习:三数之和

    2024-06-15 09:14:03       8 阅读
  6. C++ 字符串分割

    2024-06-15 09:14:03       6 阅读
  7. 2024最新前端技术趋势

    2024-06-15 09:14:03       7 阅读
  8. RichSinkFunction 在 Flink IoT 项目中的应用实战

    2024-06-15 09:14:03       4 阅读
  9. 6.2 文件的缓存位置

    2024-06-15 09:14:03       5 阅读