项目实战:仿若依权限管理系统——登录功能
第一步:前端发送ajax请求
前端:用户在登录页面输入账号、密码和验证码,然后点击登录按钮
发送POST形式的Ajax请求到Controller层
export function login(username, password, code, uuid) {
const data = {
username,
password,
code,
uuid
}
return request({
url: '/login',
headers: {
isToken: false
},
method: 'post',
data: data
})
}
第二步:Controller层
SysLoginController接受到了来自前端的POST请求,执行对应的AjaxResult方法
package com.fs.system.web.controller.common;
import java.util.Objects;
import com.fs.common.constant.Constants;
import com.fs.common.core.vo.AjaxResult;
import com.fs.common.core.vo.LoginBody;
import com.fs.common.core.vo.LoginUser;
import com.fs.common.util.ServletUtils;
import com.fs.system.service.impl.SysLoginService;
import com.fs.system.service.impl.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
// 2.3 @RestController 注解
/*
* @RestController注解是@Controller和@ResponseBody的结合
* @Controller:标识这个类是控制器类,负责处理来自客户端的HTTP请求
* @ResponseBody:把数据以JSON字符串的形式返回给客户端
* */
/**
* @author: JackX
* @createTime: * @param: null * @return: null
* @description: 登录验证的Controller
2024/1/4
9:07
*/
@RestController
public class SysLoginController {
@Autowired
private SysLoginService loginService;
// 2.1 @PostMapping 注解
/*
* 代表该方法接受Post请求
* 这个注解会把Http请求映射到Controller上
* */
@PostMapping("/login")
// 2.2 AjaxResult 返回值类型
/*
* AjaxResult是一种常用的返回值类型
* 主要用于把JSON格式的数据返回给浏览器
* */
public AjaxResult login(@RequestBody LoginBody loginBody) throws Exception {
// 2.4 @RequestBody 注解
/*
* @RequestBody:接受前端传递给后端的JSON字符串中的数据
* 表明这个数据是从前端发来的
* 这里前端发来了一个data
const data = {
username,
password,
code,
uuid
}
* 包含 用户名,密码,验证码,唯一标识
* LoginBody(username=admin, password=123456, code=3, uuid=c6729bc11c7a404993d8a701c986bc3d)
* 你问我怎么看到的?debug啊
* */
// 调用.success()方法
AjaxResult ajax = AjaxResult.success();
/*
* 既然成功接受到了人家的ajax请求,那肯定要返回成功
* 所以调用.success()方法
* 该方法返回2个值
* 第一个是msg="操作成功"
* 第二个是code=200
* 200是HTTP常见的状态码之一,成功的状态码,HTTP 200
* */
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), loginBody.getUuid());
/*
* Controller层连接前端和Service层
* 接受到请求后就要去调用Service层的方法了
* 这里调用了SysLoginService类的login方法
* 传入了前端发过来的四个参数 用户名,密码,验证码,唯一标识
* */
/*
* 这个LoginBody也是我们创建的一个数据类
* 用来创建一个专门储存数据的java对象
* ctrl+B快捷键点击LoginBody即可跳转过去
* */
// 添加token
ajax.put(Constants.TOKEN, token);
/*
* .put()方法
* 将键值对添加到一个对象中
* 这里是设置ajax的token值
* */
/*
* Constans是专门的一个常量类
* 里面有一个名为TOKEN的常量
* 默认值为"token"
* */
// 把AjaxResult返回给浏览器
return ajax;
}
}
2.1 @PostMapping 注解
@GetMapping和@PostMapping就是@RequestMapping已经指定请求方式的简写版(源码中可以看出来),GetMapping就是Get请求,PostMapping就是Post请求
@RequestMapping是一个用来处理请求地址到处理器controller功能方法映射规则的注解,这个注解会将 HTTP 请求映射到 MVC 和 REST 控制器的处理方法controller上,可用于类或方法上。注解在类上,表示类中的所有响应请求的方法都是以该地址作为父路径(模块路径)。
2.2 AjaxResult 返回值类型
AjaxResult是一个常用的返回值类型,用于在前后端分离的Web应用中进行数据交互。它主要用于返回浏览器需要的JSON格式数据,以便于浏览器端进行异步处理。它通常包含以下属性:
public class AjaxResult {
// 状态码,0表示请求成功,其他表示请求失败
private int code;
// 消息提示
private String message;
// 返回数据
private Object data;
}
2.3 @RestController 注解
@RestController 是@Controller 和@ResponseBody 的结合
@Controller 将当前修饰的类注入SpringBoot IOC容器,使得从该类所在的项目跑起来的过程中,这个类就被实例化。
@Controller 用于标识一个类是控制器组件。在 Spring MVC(Model-View-Controller)架构中扮演重要角色,负责处理来自客户端的HTTP请求,协调业务逻辑的处理,并根据请求返回适当的视图或数据。
@ResponseBody 它的作用简短截说就是指该类中所有的API接口返回的数据,都会以JSON字符串的形式返回给客户端
2.4 @RequestBody 注解
@RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的);而最常用的使用请求体传参的无疑是POST请求了,所以使用@RequestBody接收数据时,一般都用POST方式进行提交。在后端的同一个接收方法里,@RequestBody与@RequestParam()可以同时使用,@RequestBody最多只能有一个,而@RequestParam()可以有多个。
2.5 @Data
@Data 是一个 Lombok 提供的注解,它可以自动为类生成常用的方法,包括 getter、setter、equals、hashCode 和 toString 等。使用 @Data 注解可以简化代码,使代码更加简洁易读。
debug到这里之后,在FliterChainProxy过滤器链里面走了很久很久很久很久很久
第三步:Service层
Service层主要用于处理业务逻辑部分
也就是全部和登录相关的验证等
SysLoginService实现类:
package com.fs.system.service.impl;
import cn.hutool.core.util.StrUtil;
import com.fs.common.constant.CacheConstants;
import com.fs.common.constant.UserConstants;
import com.fs.common.core.pojo.SysUser;
import com.fs.common.core.vo.LoginUser;
import com.fs.common.enums.UserStatus;
import com.fs.common.exception.ServiceException;
import com.fs.common.exception.user.*;
import com.fs.common.util.DateUtils;
import com.fs.common.util.RedisCache;
import com.fs.common.util.ip.IpUtils;
import com.fs.system.service.ISysConfigService;
import com.fs.system.service.ISysLoginService;
import com.fs.system.service.ISysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import sun.misc.MessageUtils;
import java.util.Objects;
/**
* @author: JackX
* @createTime: * @param: null * @return: null
* @description:
2024/1/4
14:51
*/
/*
登录业务流程:
1.验证码校验
2.后台对前台传递参数的校验
3.根据用户名查询数据库SysUser
4.进行密码输入次数校验
5.记录成功登录的用户信息日志
6.创建token,保存到redis,并返回JWT
* */
@Service
@Slf4j
public class SysLoginService implements ISysLoginService {
@Autowired
private TokenService tokenService;
@Autowired
private RedisCache redisCache;
@Autowired
private ISysUserService userService;
@Autowired
private ISysConfigService configService;
@Autowired
private SysPasswordService passwordService;
/**
* @author: JackX
* @param: username 用户名
* @param: password 密码
* @param: code 验证码
* @param: uuid 唯一标识
* @return: java.lang.String 结果
* @description: 登录
* @createTime:
2024/1/4
15:17
*/
@Override
/*
* 3.1 @Override 注解
* 表明该方法是重写方法
* */
public String login(String username, String password, String code, String uuid) {
// 1.验证码校验
/*
* 调用validateCaptcha方法
* 传入 用户名 验证码 唯一标识
* 判断是否为空,判断验证码是否匹配
* */
validateCaptcha(username, code, uuid);
// 2.登录前置校验
/*
* 传入 用户名 密码
* 判断是否为空,判断长度是否匹配,判断IP黑名单
* */
loginPreCheck(username, password);
// 3.用户验证(这里去mapper层)
/*
* 调用ISysUserService中的selectUserByUserName方法
* 传入 用户名
* 目的是从数据库查询对应用户名的用户
* */
SysUser user = userService.selectUserByUserName(username);
// 4.判断数据库查询结果
/*
* 如果为空,抛异常
* UserStatus 用户状态码类
* 0正常 1停用 2删除
* 如果用户状态码为1停用,抛异常
* 如果用户状态码为2删除,抛异常
* */
if (Objects.isNull(user)) {
log.info("登录用户:{} 不存在.", username);
throw new ServiceException("用户不存在/密码错误");
}
else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
log.info("登录用户:{} 已被删除.", username);
throw new ServiceException("对不起,您的账号已被删除");
}
else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new ServiceException("用户已封禁,请联系管理员");
}
// 5.验证账号密码错误次数
/*
* 如果次数超出,抛异常
* */
passwordService.validate(user,password);
// 能走到这一步,说明之前的5个验证都通过了
// 保存用户信息
LoginUser loginUser = new LoginUser();
loginUser.setUserId(user.getUserId());
loginUser.setUser(user);
// 数据库记录该用户信息(和数据库有关的都去mapper层)
recordLoginInfo(user.getUserId());
// 生成token
return tokenService.createToken(loginUser);
}
/**
* @author: JackX
* @param: username 用户名
* @param: code 验证码
* @param: uuid 唯一表示
* @description: 验证码校验
* 如果验证码为空,抛异常。
* 如果验证码不匹配,抛异常
* @createTime:
2024/1/4
15:12
*/
@Override
public void validateCaptcha(String username, String code, String uuid) {
boolean captchaEnabled = configService.selectCaptchaEnabled();
if (captchaEnabled) {
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
String captcha = redisCache.getCacheObject(verifyKey);
if (captcha == null) {
throw new CaptchaExpireException();
}
if (!code.equalsIgnoreCase(captcha)) {
throw new CaptchaException();
}
}
}
/**
* @author: JackX
* @param: username 用户名
* @param: password 用户密码
* @description: 登录前置校验
* 如果用户名或者密码为空,抛异常。
* 如果用户名或密码长度不在范围内,抛异常。
* 如果IP在黑名单中,抛异常。
* @createTime:
2024/1/4
15:16
*/
public void loginPreCheck(String username, String password) {
// 用户名或密码为空 错误
if (StrUtil.isEmpty(username) || StrUtil.isEmpty(password)) {
throw new UserNotExistsException();
}
// 密码如果不在指定范围内 错误
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH || password.length() > UserConstants.PASSWORD_MAX_LENGTH) {
throw new UserPasswordNotMatchException();
}
// 用户名不在指定范围内 错误
if (username.length() < UserConstants.USERNAME_MIN_LENGTH || username.length() > UserConstants.USERNAME_MAX_LENGTH) {
throw new UserPasswordNotMatchException();
}
// IP黑名单校验
String blackStr = configService.selectConfigByKey("sys.login.blackIPList");
if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) {
throw new BlackListException();
}
}
/**
* @author: JackX
* @param: userId 用户id
* @description: 记录登录信息
* @createTime:
2024/1/4
15:16
*/
@Override
public void recordLoginInfo(Long userId) {
SysUser sysUser = new SysUser();
sysUser.setUserId(userId);
sysUser.setLoginIp(IpUtils.getIpAddr());
sysUser.setLoginDate(DateUtils.getNowDate());
userService.updateUserProfile(sysUser);
}
}
3.1 @Override
表明该方法是重写方法,保证正确重写父类的方法
第四步:Mapper层
Mapper层主要用于和数据库进行交互
会自动调用resources目录下的同名的Mapper.xml文件
并执行里面的SQL语句
调用mapper层的service类
SysUserServiceImpl实现类:
package com.fs.system.service.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.fs.common.constant.UserConstants;
import com.fs.common.core.pojo.*;
import com.fs.common.exception.ServiceException;
import com.fs.system.mapper.*;
import com.fs.system.service.ISysConfigService;
import com.fs.system.service.ISysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
/*
* 用户 业务层处理
* */
@Service
@Slf4j
public class SysUserServiceImpl implements ISysUserService{
@Autowired
private SysUserMapper userMapper;
/*
* 通过用户名查询用户
* 输入 用户名
* 返回 用户
* */
@Override
public SysUser selectUserByUserName(String userName) {
return userMapper.selectUserByUserName(userName);
}
/*
* 修改用户基本信息
* 输入 用户
* 返回 int
* */
@Override
public int updateUserProfile(SysUser user) {
return userMapper.updateUser(user);
}
}
mapper层接口和Mapper.xml文件配合食用
SysUserMapper接口:
/**
* 通过用户名查询用户
*
* @param userName 用户名
* @return 用户对象信息
*/
public SysUser selectUserByUserName(String userName);
Mapper.xml配置文件
SysUserMapper.xml
查询用户名相同、未被删除的用户
<select id="selectUserByUserName" parameterType="String" resultMap="SysUserResult">
<include refid="selectUserVo"/>
where u.user_name = #{userName} and u.del_flag = '0'
</select>