登录功能的书写
后端接口的书写
(1)创建配置文件
粘贴这两个文件(E:\project\AllProJect\Shangpin Selection\项目材料素材\资料\资料\03-配置文件)
在spzx-manager服务的src/resources目录下创建application.yml、application-dev.yml文件,文件的内容如下所示:
# application.yml文件内容==================================================================================
spring:
application:
name: service-manager
profiles:
active: dev # 激活的环境文件
# application-dev.yml文件内容=============================================================================
# 配置服务端口号
server:
port: 8501
# 配置数据库连接信息
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_spzx?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
# Redis的相关配置
data:
redis:
host: localhost
port: 6379
# password: 1234
# mybatis的配置
mybatis:
config-location: classpath:/mybatis-config.xml
mapper-locations: classpath:/mapper/*/*.xml
导入课程资料中提供的:mybatis-config.xml和logback-spring.xml配置文件
(2)创建启动类
ManagerApplication
package com.atguigu.spzx;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ManagerApplication {
public static void main(String[] args) {
SpringApplication.run(ManagerApplication.class , args) ;
}
}
(3)创建实体类
SysUser
创建与数据库表对应的实体类:
BaseEntity: 定义一个BaseEntity实体类,在该实体类中定义公共的属性
- 类名: BaseEntity
- 包路径: com.xuan.spzx.model.entity.base
- 用途: 为其他实体类提供通用字段
- 使用@Data注解(Lombok)自动生成getter/setter方法
- 使用@Schema注解为Swagger API文档提供字段说明
- 使用@JsonFormat注解格式化日期时间显示格式
- 实现Serializable接口支持序列化
// com.xuan.spzx.model.entity.base
package com.xuan.spzx.model.entity.base;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class BaseEntity implements Serializable {
@Schema(description = "唯一标识")
private Long id;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "修改时间")
private Date updateTime;
@Schema(description = "是否删除")
private Integer isDeleted;
}
SysUser用户实体类定义:
// com.xuan.spzx.model.entity.system
@Data
public class SysUser extends BaseEntity {
private static final long serialVersionUID = 1L;
private String userName; // 该字段的属性名称和数据表字段不一致
private String password;
private String name;
private String phone;
private String avatar;
private String description;
private Integer status;
}
LoginDto
创建一个LoginDto实体类,封装登录请求参数。
// com.xuan.spzx.model.dto.system
package com.xuan.spzx.model.dto.system;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "用户登录请求参数")
public class LoginDto {
@Schema(description = "用户名")
private String userName ;
@Schema(description = "密码")
private String password ;
@Schema(description = "提交验证码")
private String captcha ;
@Schema(description = "验证码key")
private String codeKey ;
}
LoginVo
创建一个LoginVo实体类,封装登录成以后响应结果数据。
package com.xuan.spzx.model.vo.system;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "登录成功响应结果实体类")
public class LoginVo {
@Schema(description = "令牌")
private String token ;
@Schema(description = "刷新令牌,可以为空")
private String refresh_token ;
}
(4)三层书写
IndexController
表现层代码实现(初始化->引入Service业务代码->用户登录)
package com.atguigu.spzx.controller;
import com.atguigu.spzx.model.dto.system.LoginDto;
import com.atguigu.spzx.model.vo.common.Result;
import com.atguigu.spzx.model.vo.common.ResultCodeEnum;
import com.atguigu.spzx.model.vo.system.LoginVo;
import com.atguigu.spzx.service.SysUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "用户接口")
@RestController
@RequestMapping(value = "/admin/system/index")
public class IndexController {
@Autowired
private SysUserService sysUserService ;
@Operation(summary = "登录接口")
@PostMapping(value = "/login")
public Result<LoginVo> login(@RequestBody LoginDto loginDto) {
LoginVo loginVo = sysUserService.login(loginDto) ;
return Result.build(loginVo , ResultCodeEnum.SUCCESS) ;
}
}
SysUserService
业务层代码实现(初始化->引入Mapper的代码->业务代码)
实现类
impl/SysUserServiceImpl它实现了SysUserService接口。该类被@Service注解标记,表明它是一个Spring框架管理的业务服务层组件,负责处理系统用户相关的业务逻辑
package com.xuan.spzx.manager.service.impl;
import com.alibaba.fastjson.JSON;
import com.xuan.spzx.manager.mapper.SysUserMapper;
import com.xuan.spzx.manager.service.SysUserService;
import com.xuan.spzx.model.dto.system.LoginDto;
import com.xuan.spzx.model.entity.system.SysUser;
import com.xuan.spzx.model.vo.system.LoginVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Service
public class SysUserServiceImpl implements SysUserService {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private RedisTemplate<String,String> redisTemplate;
//用户登录
@Override
public LoginVo login(LoginDto loginDto) {
//1.获取提交用户名,LoginDto获取到
String userName = loginDto.getUserName();
//2.根据用户名查询数据库用户表sys_user
SysUser sysUser =sysUserMapper.selectUserInfoByUserName(userName);
//3.如果用户名查不到对应信息,用户不存在,返回错误信息
if (sysUser == null){
throw new RuntimeException("用户不存在");
}
//4.如果用户名存在,则判断输入密码与数据库密码是否一致
//把输入密码进行md5加密
String database_password = sysUser.getPassword();//数据库密码
String input_password =
DigestUtils.md5DigestAsHex(loginDto.getPassword().getBytes());
//比较
if(!input_password.equals(database_password)){
throw new RuntimeException("密码错误");
}
//5.如果一致,则返回登录成功信息,如果不一致登录失败
//登录成功生成用户唯一标识token
String token = UUID.randomUUID().toString().replaceAll("-","");
//6.把用户信息保存到redis中
//key: token value:用户信息
redisTemplate.opsForValue()
.set("user:login"+token,
JSON.toJSONString(sysUser),
7,
TimeUnit.DAYS
);
//7.返回登录成功信息,loginVo对象返回
LoginVo loginVo = new LoginVo();
loginVo.setToken(token);
return loginVo;
}
}
SysUserMapper
@Mapper注解
- 作用: 标识这是一个MyBatis的Mapper接口
- 功能: MyBatis会自动为该接口生成实现类,用于执行数据库操作
- 持久层代码实现
这个Mapper接口主要用于用户登录验证:
- 当用户输入用户名和密码进行登录时
- 系统通过该接口查询对应用户名的用户信息
- 获取到用户数据后,验证密码是否正确
- 完成用户身份认证
@Mapper
public interface SysUserMapper {
/**
* 根据用户名查询用户数据
* @param userName
* @return
*/
public abstract SysUser selectByUserName(String userName) ;
}
SysUserMapper.xml
目录和配置文件中的sql映射,位置有关系
创建映射文件并且编写sql语句: 文件位置classpath: /mapper/system/SysUserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xuan.spzx.manager.mapper.SysUserMapper">
<sql id="columns">
id,username userName ,password,name,phone,avatar,description,status,create_time,update_time,is_deleted
</sql>
<select id="selectUserInfoByUserName" resultType="com.xuan.spzx.model.entity.system.SysUser">
SELECT <include refid="columns"/> FROM sys_user where userName = #{userName}
</select>
</mapper>
(5)测试启动
启动:我启动时遇到两处报错
1.lombok的版本报错问题
java: java.lang.NoSuchFieldError: Class com.sun.tools.javac.tree.JCTree$JCIm
Lombok在早期版本中使用反射访问com.sun.tools.javac.tree.JCTree$JCImport类的qualid字段,该字段在Java 21中的类型发生了变化,在Java 21及更高版本中,qualid字段的类型从JCTree变更为JCFieldAccess。这导致了Lombok无法正确访问该字段,从而抛出NoSuchFieldError异常。
在项目的依赖项中添加Lombok的最新版本
在修改项目父文件的pom.xml
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</dependency>
2.yml文件缩进错误
#spring:
# application:
# name: service-manager
# profiles:
# active:dev
# application.yml
spring:
profiles:
active: dev # 这里指定默认环境
启动成功!
http://localhost:8501/doc.html#/home
添加中文提示,重启
点击测试
docker restart redis
重启
(6)异常处理
新建包
新建GlobalExceptionHandler类->全局异常
package com.xuan.spzx.common.exception;
import com.xuan.spzx.model.vo.common.Result;
import com.xuan.spzx.model.vo.common.ResultCodeEnum;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
public class GlobalExceptionHandler {
//全局异常处理
@ExceptionHandler(Exception.class)//拦截所有异常,所有异常都用这个方法处理
@ResponseBody
public Result error(){
return Result.build(null, ResultCodeEnum.SYSTEM_ERROR);
}
}
新建GuiguException类->自定义
package com.xuan.spzx.common.exception;
import com.xuan.spzx.model.vo.common.ResultCodeEnum;
import lombok.Data;
@Data
public class GuiguException extends RuntimeException {
private Integer code;
private String message;
private ResultCodeEnum resultCodeEnum;
public GuiguException(ResultCodeEnum resultCodeEnum) {
this.code = resultCodeEnum.getCode();
this.message = resultCodeEnum.getMessage();
this.resultCodeEnum = resultCodeEnum;
}
}
在第一个里面引入自定义异常
//自定义异常处理
@ExceptionHandler(GuiguException.class)
@ResponseBody
public Result error(GuiguException e){
return Result.build(null, e.getResultCodeEnum());
}
package com.xuan.spzx.common.exception;
import com.xuan.spzx.model.vo.common.Result;
import com.xuan.spzx.model.vo.common.ResultCodeEnum;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
public class GlobalExceptionHandler {
//全局异常处理
@ExceptionHandler(Exception.class)//拦截所有异常,所有异常都用这个方法处理
@ResponseBody
public Result error(){
return Result.build(null, ResultCodeEnum.SYSTEM_ERROR);
}
//自定义异常处理
@ExceptionHandler(GuiguException.class)
@ResponseBody
public Result error(GuiguException e){
return Result.build(null, e.getResultCodeEnum());
}
}
修改登录使用异常
if (sysUser == null){
// throw new RuntimeException("用户不存在");
throw new GuiguException(ResultCodeEnum.LOGIN_ERROR);
}
if(!input_password.equals(database_password)){
// throw new RuntimeException("密码错误");
throw new GuiguException(ResultCodeEnum.LOGIN_ERROR);
}
重启成功
前端接入登录
当后端接口开发好了以后就可以让前端去请求该登录接口完成登录操作。
修改前端代码
修改src/utils/request.js更改基础请求路径
const service = axios.create({
baseURL: 'http://localhost:8501', // 后端服务的ip地址和端口号
timeout: 10000,
withCredentials: true,
})
修改src/api/login.js更改登录接口地址
// 登录接口
export const Login = data => {
return request({
url: '/admin/system/index/login',
method: 'post',
data,
})
}
发送登录请求,那么此时会报一个错误:
报错的原因是因为此时的请求是一个跨域的请求。
跨域请求
跨域请求简介
跨域请求:通过一个域的JavaScript脚本和另外一个域的内容进行交互
域的信息:协议、域名、端口号
同域:当两个域的协议、域名、端口号均相同
如下所示:
同源【域】策略:在浏览器中存在一种安全策略就是同源策略,同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。
CORS概述
官网地址:https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
- CORS的全称为Cross-origin Resource Sharing,中文含义是跨域资源共享,
- CORS 是跨域的一种解决方案,CORS 给了web服务器一种权限:服务器可以选择是否允许跨域请求访问到它们的资源。
CORS解决跨域
后端服务器开启跨域支持:
方案一:在IndexController上添加**@CrossOrigin**注解
@RestController
@RequestMapping(value = "/admin/system/index")
@CrossOrigin(allowCredentials = "true" , originPatterns = "*" , allowedHeaders = "*")
public class IndexController {
}
弊端:每一个controller类上都来添加这样的一个接口影响开发效率、维护性较差
方案二:添加一个配置类配置跨域请求(用这个,全局)
// com.atguigu.spzx.manager.config
@Component
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 添加路径规则
.allowCredentials(true) // 是否允许在跨域的情况下传递Cookie
.allowedOriginPatterns("*") // 允许请求来源的域规则
.allowedMethods("*")
.allowedHeaders("*") ; // 允许所有的请求头
}
}
重启后端->成功