登录
请求数据LoginDto
因为之前我们在实体类password上添加了@JsonIgnore注解, 使用实体类接收前端请求参数的时候password会被忽略, 所以这里不能使用实体类对象接收登录参数, 可以自定义登录参数的LoginDto
package com.cghbh.dto;
import lombok.Data;
/**
* 登录请求DTO
* 用于接收前端登录接口提交的账号密码参数
*/
@Data
public class LoginDto {
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
}响应数据UserLoginVo
通过接口文档的说明,我们可以看到,最终登录完成后,要给前端响应的数据结构如下:
{
"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 包中,具体的结构如下:
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,具体代码如下:
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方法:
/**
* 用户登录
* @param loginDto
* @return
*/
UserLoginVo login(UserLoginDto loginDto); UserMapper
由于我们的需求是需要查询用户角色的label,因此需要联合用户表、角色表进行查询。而且是在登录的时候根据用户名称查询用户存不存在的情况
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 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
在实现类中编写业务逻辑:
@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的依赖,一样的需要在父工程中锁定版本,不再赘述。
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>在qk-common模块下的com.cghbh.util中创建如下工具类
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业务模块中。
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令牌且是合法令牌),就可以直接放行,如果校验时发现并没有登录或是非法令牌,就可以直接给前端响应未登录的错误信息。
拦截器的使用分为两步:
- 定义拦截器
- 注册配置拦截器
定义拦截器
拦截器的功能都是和业务直接相关的,因为需要定义在业务模块中。
在qk-management业务模块中的com.cghbh.interceptor包中定义一个LoginInterceptor类,实现 HandlerInterceptor 接口,并重写其所有方法
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 方法。
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中完成用户登录校验的功能,具体增加的实现功能代码如下:
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这样的请求路径接口是不需要进行拦截的,所以需要在配置类中直接放行,需要修改的配置如下:
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 |