Skip to content

登录 ​

请求数据LoginDto ​

因为之前我们在实体类password上添加了@JsonIgnore注解, 使用实体类接收前端请求参数的时候password会被忽略, 所以这里不能使用实体类对象接收登录参数, 可以自定义登录参数的LoginDto

java
package com.cghbh.dto;


import lombok.Data;

/**
 * 登录请求DTO
 * 用于接收前端登录接口提交的账号密码参数
 */
@Data
public class LoginDto {

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;
}

响应数据UserLoginVo ​

通过接口文档的说明,我们可以看到,最终登录完成后,要给前端响应的数据结构如下:

json
{
    "code": 1,
    "msg": "success",
    "data": {
        "id": 2,
        "username": "zhangwuji",
        "name": "张无忌",
        "image": "https://java-ai.oss-cn-beijing.aliyuncs.com/2.png",
        "roleLabel": "user_normal",
        "token": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJ6aGFuZ3d1amkiLCJuYW1lIjoi5byg5peg5b-MIiwiZXhwIjoxNzQ3MzQwNDY1fQ.BVzOz0f7HUzQ-GjO_gZCyfjPQL4vcwJbSgUvZJrWPYo"
    }
}

响应的数据中,包含 id、username、name、image、roleLabel、token 这6个属性。

为了方便数据的封装管理,这里呢,我们可以定义一个实体类来封装这些信息,后面会通过实体类直接封装数据访问层的查询结果 , 登录密码比对的时候会用到password和salt字段的值, 并且这两个字段后期不需要返回到前端

创建实体类,存放在 qk-entity 模块的 com.cghbh.vo 包中,具体的结构如下:

java
package com.cghbh.vo;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;

/**
 * 用户登录返回VO
 * 登录成功后返回给前端的数据
 */
@Data
public class UserLoginVo {

    /**
     * 用户id
     */
    private Long id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 用户姓名
     */
    private String name;

    /**
     * 用户头像图片地址
     */
    private String image;

    /**
     * 角色标识:user_normal普通用户
     */
    private String roleLabel;

    /**
     * jwt令牌
     */
    private String token;

    /**
     * 密码:用于后期密码比对,不返回前端
     */
    @JsonIgnore
    private String password;

    /**
     * 盐:用于后期密码加密比对,不返回前端
     */
    @JsonIgnore
    private String salt;
}

LoginController ​

新建一个LoginController,具体代码如下:

java
package com.cghbh.controller;

import com.cghbh.common.Result;
import com.cghbh.dto.LoginDto;
import com.cghbh.service.UserService;
import com.cghbh.vo.UserLoginVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@Slf4j
public class LoginController {
    private final UserService userService;

    /**
     * 用户登录
     *
     * @param loginDto 登录请求参数
     * @return 登录的信息
     */
    @PostMapping("/login")
    public Result userLogin(@RequestBody LoginDto loginDto) {
        UserLoginVo userLoginVo = userService.userLogin(loginDto);
        return Result.success(userLoginVo);
    }
}

UserService ​

在UserService接口中,增加login方法:

java
/**
 * 用户登录
 * @param loginDto
 * @return
 */
UserLoginVo login(UserLoginDto loginDto); 

UserMapper ​

由于我们的需求是需要查询用户角色的label,因此需要联合用户表、角色表进行查询。而且是在登录的时候根据用户名称查询用户存不存在的情况

java
package com.cghbh.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cghbh.dto.UserPageDto;
import com.cghbh.entity.User;
import com.cghbh.vo.UserLoginVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface UserMapper extends BaseMapper<User> {
    Page<User> getUsersByPage(IPage<User> page, @Param("userPageDto") UserPageDto userPageDto);

    List<User> getUsersByRoleLabel(@Param("label") String getUsersByRoleLabel);

    UserLoginVo getUserInfoByName(@Param("username") String username); 
}

UserMapper.xml ​

在xml文件中编写联表查询的SQL。

xml
<?xml version="1.0" encoding="UTF-8" ?>
<!-- 引入mybatis的xml方式编写sql的约束 -->
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cghbh.mapper.UserMapper">
    <select id="getUserInfoByName" resultType="com.cghbh.vo.UserLoginVo">
        select user.*, role.name role_label from user left join role on user.role_id = role.id
        where user.username = #{username}
    </select>
</mapper>

UserServiceImpl ​

在实现类中编写业务逻辑:

java
@Override
public UserLoginVo userLogin(LoginDto loginDto) {
    if (loginDto == null || StringUtils.isBlank(loginDto.getUsername()) || StringUtils.isBlank(loginDto.getPassword())) {
        throw new BusinessException("用户名或密码不能为空!");
    }
    // 查询用户
    UserLoginVo userLoginVo = userMapper.getUserInfoByName(loginDto.getUsername());
    if (userLoginVo == null) {
        throw new BusinessException("当前用户不存在");
    }
    // 对密码加盐值对比
    String pwd = DigestUtil.md5Hex(loginDto.getPassword() + userLoginVo.getSalt());
    if (!StringUtils.equals(pwd, userLoginVo.getPassword())) {
        throw new BusinessException("密码不正确,请检查后重试");
    }
    // 所有的异常问题都验证完成了之后,对用户发token
    userLoginVo.setToken("");
    return userLoginVo;
}

鉴权 ​

jwt ​

JWT全称JSON Web Token,本质上是一个用于身份识别的字符串,由三个部分组成:

  • 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:
  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:
  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来

实现 ​

在 qk-common模块的pom.xml中引入JWT的依赖,一样的需要在父工程中锁定版本,不再赘述。

xml
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

在qk-common模块下的com.cghbh.util中创建如下工具类

java
package com.cghbh.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

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

public class JwtUtil {

    private JwtUtil() {
    }

    // 秘钥
    private static final String SECRET_KEY = "cWluZ2tl";
    // 令牌有效期(12小时)
    private static final long EXPIRATION_TIME = 12 * 60 * 60 * 1000;

    /**
     * 生成JWT令牌
     *
     * @param claims 自定义声明信息
     * @return 生成的JWT令牌
     */
    public static String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY.getBytes())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .compact();
    }

    /**
     * 解析JWT令牌
     *
     * @param token JWT令牌
     * @return 解析后的Claims对象
     */
    public static Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY.getBytes())
                .parseClaimsJws(token)
                .getBody();
    }
}

登录时下发令牌 ​

该代码在qk-management的UserServiceImpl业务模块中。

java
package com.cghbh.service.impl;

import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cghbh.common.PageResult;
import com.cghbh.dto.LoginDto;
import com.cghbh.dto.UserPageDto;
import com.cghbh.entity.User;
import com.cghbh.exception.BusinessException;
import com.cghbh.exception.NotFoundException;
import com.cghbh.mapper.UserMapper;
import com.cghbh.service.UserService;
import com.cghbh.util.JwtUtil;
import com.cghbh.vo.UserLoginVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;

@Slf4j
@RequiredArgsConstructor
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    private final UserMapper userMapper;

    @Override
    public UserLoginVo userLogin(LoginDto loginDto) {
        if (loginDto == null || StringUtils.isBlank(loginDto.getUsername()) || StringUtils.isBlank(loginDto.getPassword())) {
            throw new BusinessException("用户名或密码不能为空!");
        }
        // 查询用户
        UserLoginVo userLoginVo = userMapper.getUserInfoByName(loginDto.getUsername());
        if (userLoginVo == null) {
            throw new BusinessException("当前用户不存在");
        }
        // 对密码加盐值对比
        String pwd = DigestUtil.md5Hex(loginDto.getPassword() + userLoginVo.getSalt());
        if (!StringUtils.equals(pwd, userLoginVo.getPassword())) {
            throw new BusinessException("密码不正确,请检查后重试");
        }
        // 所有的异常问题都验证完成了之后,对用户发token
        // 载荷
        HashMap<String, Object> payload = new HashMap<>();
        payload.put("id", userLoginVo.getId());
        payload.put("username", userLoginVo.getUsername());
        String token = JwtUtil.generateToken(payload);
        userLoginVo.setToken(token);
        return userLoginVo;
    }
}

拦截器 ​

拦截器是SpringBoot提供的用来动态拦截请求的方式,它会在指定方法调用前后,根据业务需要执行预先设定的代码。

在拦截器当中,我们通常也是做一些通用性的操作,比如:我们可以通过拦截器来拦截前端发起的请求,将登录校验的逻辑全部编写在拦截器当中。

在校验的过程当中,如发现用户登录了(携带JWT令牌且是合法令牌),就可以直接放行,如果校验时发现并没有登录或是非法令牌,就可以直接给前端响应未登录的错误信息。

拦截器的使用分为两步:

  1. 定义拦截器
  2. 注册配置拦截器

定义拦截器 ​

拦截器的功能都是和业务直接相关的,因为需要定义在业务模块中。

在qk-management业务模块中的com.cghbh.interceptor包中定义一个LoginInterceptor类,实现 HandlerInterceptor 接口,并重写其所有方法

java
package com.cghbh.interceptor;

import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpStatus;
import com.cghbh.util.JwtUtil;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 前置拦截 : 在控制层接口方法(目标方法)执行之前执行
     *
     * @param request  本次的HTTP请求对象
     * @param response 本次的HTTP响应对象
     * @param handler  拦截的目标方法对象
     * @return true: 放行到目标资源  false : 不放行
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }

    /**
     * 后置拦截 : 在目标方法执行之后执行
     *
     * @param request      本次的HTTP请求对象
     * @param response     本次的HTTP响应对象
     * @param handler      拦截的目标方法对象
     * @param modelAndView the {@code ModelAndView} that the handler returned
     *                     (can also be {@code null})
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
        log.info("LoginInterceptor中的postHandle方法执行了.....");
    }

    /**
     * 完成时拦截 : 在服务器响应完毕之后执行
     *
     * @param request  本次的HTTP请求对象
     * @param response 本次的HTTP响应对象
     * @param handler  拦截的目标方法对象
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        log.info("LoginInterceptor中的afterCompletion方法执行了.....");
    }
}

注册配置拦截器 ​

在qk-management业务模块中的com.cghbh.config包中创建一个配置类 WebConfig, 实现 WebMvcConfigurer 接口,并重写 addInterceptors 方法。

java
package com.cghbh.config;

import com.cghbh.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    // 自定义的拦截器对象
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器 , 设置拦截器的拦截路径
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**") // 设置拦截路径
    }
}

令牌拦截器 ​

接下来我们需要在preHandle中完成用户登录校验的功能,具体增加的实现功能代码如下:

java
package com.cghbh.interceptor;

import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpStatus;
import com.cghbh.util.JwtUtil;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 前置拦截 : 在控制层接口方法(目标方法)执行之前执行
     *
     * @param request  本次的HTTP请求对象
     * @param response 本次的HTTP响应对象
     * @param handler  拦截的目标方法对象
     * @return true: 放行到目标资源  false : 不放行
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 获取请求中的token
        String token = request.getHeader("token");
        // 2. 判断请求头中的token令牌是否存在,如果不存在返回 401 状态码
        if (StrUtil.isBlank(token)) {
            response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
            return false;
        }
        // 3. 校验token是否有效 , 校验失败返回 401 状态码
        try {
            Claims claims = JwtUtil.parseToken(token);
        } catch (Exception e) {
            log.error("令牌校验失败,原始令牌:{},失败原因:{}", token, e.getMessage());
            response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
            return false;
        }

        // 4. 放行,访问目标资源
        return true;
    }

    /**
     * 后置拦截 : 在目标方法执行之后执行
     *
     * @param request      本次的HTTP请求对象
     * @param response     本次的HTTP响应对象
     * @param handler      拦截的目标方法对象
     * @param modelAndView the {@code ModelAndView} that the handler returned
     *                     (can also be {@code null})
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
        log.info("LoginInterceptor中的postHandle方法执行了.....");
    }

    /**
     * 完成时拦截 : 在服务器响应完毕之后执行
     *
     * @param request  本次的HTTP请求对象
     * @param response 本次的HTTP响应对象
     * @param handler  拦截的目标方法对象
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        log.info("LoginInterceptor中的afterCompletion方法执行了.....");
    }
}

配置拦截器 ​

注意:像/login这样的请求路径接口是不需要进行拦截的,所以需要在配置类中直接放行,需要修改的配置如下:

java
package com.cghbh.config;

import com.cghbh.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    // 自定义的拦截器对象
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器 , 设置拦截器的拦截路径
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**") // 设置拦截路径
                .excludePathPatterns("/login"); // 设置不拦截路径
    }
}

配置说明 ​

在拦截器中除了可以设置/**拦截所有资源外,还有一些常见拦截路径设置:

拦截路径含义举例
/*一级路径能匹配/depts,/emps,/login,不能匹配 /depts/1
/**任意级路径能匹配/depts,/depts/1,/depts/1/2
/depts/*/depts下的一级路径能匹配/depts/1,不能匹配/depts/1/2,/depts
/depts/**/depts下的任意级路径能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1