Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5Mobile wallpaper 6
2879 字
14 分钟
4-各司其职的后端-Springboot Vue 教程

Sa-Token的安装#

在进行业务层讲解之前,我们先安装一下Sa-Token,由于文章有时效性,安装之前请参考一下官方文档。

官方文档:在 SpringBoot 环境集成

将依赖输入到pom.xml

<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.43.0</version>
</dependency>

然后打开application.properties,往里添加Sa-Token的配置信息

############## Sa-Token 配置 (文档: <https://sa-token.cc>) ##############
# token 名称(同时也是 cookie 名称)
sa-token.token-name=satoken
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
sa-token.timeout=2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
sa-token.active-timeout=-1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
sa-token.is-concurrent=true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
sa-token.is-share=false
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
sa-token.token-style=uuid
# 是否输出操作日志
sa-token.is-log=true
# token前缀
sa-token.token-prefix=Bearer

更多配置信息请查看Sa-Token的官方文档进行学习Sa-Token文档

什么是Sa-Token?#

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0分布式Session会话微服务网关鉴权 等一系列权限相关问题。

为什么需要Sa-Token?#

为了解决你在博客系统中**“用户是谁”和“用户能做什么”**这两个核心问题。

1. 识别“用户是谁”(身份认证 / Authentication)#

  • 问题:当用户访问你的博客系统时,后端怎么知道这个人是谁?是不是他自己声明的那个用户?
  • Sa-Token 解决:用户通过输入用户名和密码登录后,Sa-Token 会给他发一个唯一的“通行证”(Token)。后续每次用户访问需要登录的接口时,都带着这个“通行证”。Sa-Token 就能识别出:“哦,拿着这张通行证的,就是用户 A。”

2. 决定“用户能做什么”(权限授权 / Authorization)#

  • 问题:后端知道用户 A 是谁了,那用户 A 能不能发布文章?能不能删除别人的评论?管理员能不能看到后台数据?
  • Sa-Token 解决:Sa-Token 不仅能识别用户身份,还能管理用户的角色(如“管理员”、“普通用户”)和权限(如“发布文章”、“删除评论”)。它会根据你定义的规则,判断当前用户是否有权执行某个操作。如果没权限,就直接拒绝。

3. 没有 Sa-Token 会怎样?#

如果你不使用像 Sa-Token 这样的鉴权框架,你就需要:

  • 自己实现登录逻辑:生成 Token、存储 Token、校验 Token。
  • 自己管理会话:Token 的有效期、刷新、用户强制下线等。
  • 自己实现权限判断:在每个需要权限的接口里,手动写代码判断用户有没有这个角色或权限。

这会非常复杂、容易出错,而且会把大量的鉴权逻辑混入你的业务代码中,导致代码难以维护。

Sa-Token 的作用就是把这些复杂、重复且核心的鉴权逻辑帮你封装好,让你只需关注业务逻辑,大大提高开发效率和系统安全性。

实现业务层#

什么是业务层?#

人类的协作有分工,代码也有,不通分工的代码可以让我们更好的理解项目结构和代码。

我们在这里让Controller层只负责接收 HTTP 请求,调用 Service 层处理业务逻辑,返回 HTTP 响应。

具体的业务逻辑交给Service层解决

实现 UserService#

我们新建一个service软件包,和entity,controller,mapper是同级的。

接着新建一个UserService接口

这个 UserService 将包含用户注册、登录、登出以及根据 ID 获取用户信息的业务逻辑。

首先,定义 UserService 接口。它继承了 Mybatis-Plus 提供的 IService<User>,这样你就能自动获得很多基本的 CRUD 方法。

package com.example.demo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.demo.entity.User;
public interface UserService extends IService<User> {
/**
* 用户注册
* @param user 用户实体,包含用户名和密码
* @return 注册成功的用户实体(可能包含自动生成的ID)
*/
User register(User user);
/**
* 用户登录
* @param username 用户名
* @param password 密码 (未加密前)
* @return 登录成功的用户实体,如果登录失败返回null
*/
User login(String username, String password);
/**
* 用户登出
*/
void logout();
}

接着,在service下创建一个Impl软件包,在Impl下创建一个UserServiceImpl类,用于具体实现UserService接口里的的方法。

package com.example.demo.service.Impl;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import com.example.demo.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
// ServiceImpl 已经自动注入了 baseMapper (即 UserMapper),无需再次 @Autowired UserMapper
// -----------------------------------------------------------------------------------------------------------------
// 业务逻辑实现
// -----------------------------------------------------------------------------------------------------------------
/**
* 用户注册业务逻辑
* 注意:这里简化了密码加密过程,实际项目中应使用 BCryptPasswordEncoder 等加密工具
*
* @param user 用户实体,包含用户名和密码
* @return 注册成功的用户实体
*/
@Override
@Transactional // 开启事务,确保数据库操作的原子性
public User register(User user) {
// 1. 校验用户名是否已存在
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", user.getUsername());
User existingUser = baseMapper.selectOne(queryWrapper); // 使用 baseMapper 进行数据库查询
if (existingUser != null) {
throw new RuntimeException("用户名已存在,请更换其他用户名!"); // 抛出运行时异常,全局异常处理器会捕获
}
// 2. 密码加密 (这里只是占位,实际项目中务必使用强加密算法,如 BCrypt)
// 例如:user.setPassword(passwordEncoder.encode(user.getPassword()));
user.setPassword(user.getPassword()); // 简化处理,直接存储明文,切勿在生产环境使用!
// 可以设置默认角色等
// 4. 保存用户到数据库
// baseMapper.insert(user) 是 Mybatis-Plus 提供的插入方法
int rows = baseMapper.insert(user);
if (rows <= 0) {
throw new RuntimeException("用户注册失败!");
}
return user; // 返回注册成功的用户,此时 user 对象会包含数据库生成的ID
}
/**
* 用户登录业务逻辑
*
* @param username 用户名
* @param password 密码 (未加密前)
* @return 登录成功的用户实体,如果登录失败返回null
*/
@Override
public User login(String username, String password) {
// 1. 根据用户名查询用户
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
User user = baseMapper.selectOne(queryWrapper);
// 2. 校验用户是否存在及密码是否匹配
// 实际项目中,password 应该与加密后的存储密码进行比对
if (user == null || !user.getPassword().equals(password)) { // 简化处理,直接比较明文
return null; // 登录失败
}
return user;
}
/**
* 用户登出业务逻辑
*/
@Override
public void logout() {
StpUtil.logout(); // 调用 Sa-Token 的登出方法
}
}

实现统一响应格式#

  • 职责:让前端和后端之间的数据交换有一个统一的“标准格式”。
  • 记住:无论操作成功还是失败,都用同一种“信封”来包装数据,信封上写明了“状态码”、“消息”和“实际数据”。
  • 结构:通常包含 code(状态码)、msg(消息提示)、data(实际返回的数据)。

由于Sa-Token有现成的SaResult了,我们可以直接用SaResult而不用自己手动编写Result

实现LoginResponseDto#

什么是dto#

DTO 的全称是 Data Transfer Object,翻译过来就是“数据传输对象”。 它是一个简单的 Java(或其他语言),主要作用是封装数据。它通常只包含数据字段(属性)以及这些字段的 getter/setter 方法,不包含任何业务逻辑

想象一下,你有一个快递包裹,DTO 就是这个包裹。它把需要发送给别人(或者从别人那里接收)的东西都装在一起。

为什么需要dto#

使用 DTO 的主要目的是为了优化数据传输解耦(降低依赖)

  1. 精简数据,避免过度暴露:
    • 你的数据库实体类(例如 User 实体)可能包含很多字段,比如用户密码、创建时间、修改时间等。但前端可能只需要用户的 idusername
    • 如果直接把完整的 User 实体发送给前端,就可能暴露不必要的敏感信息,或者传输了多余的数据,浪费带宽。
    • DTO 允许你只选择性地包含前端或调用方需要的数据,就像快递包裹里只放收件人需要的东西。
  2. 解耦前后端:
    • 当后端数据库实体类发生变化时(比如增加了一个字段),如果前端直接依赖实体类,那前端也可能需要跟着改。
    • 使用 DTO 后,后端实体类与前端 DTO 之间多了一层转化。只要 DTO 的结构不变,即使实体类变了,前端代码也不受影响。
  3. 适配不同业务场景:
    • 登录时可能需要 usertoken 信息。
    • 显示文章列表时可能只需要文章的 id标题作者名
    • 创建文章时可能需要 标题内容分类
    • 每个场景需要的字段组合都不同,用 DTO 可以灵活地定义。

让我们来新建一个dto包,同样与entitymappercontroller是同一层级的。

在dto包下新建一个LoginResponseDto类。

package com.example.demo.dto;
import cn.dev33.satoken.stp.SaTokenInfo;
import com.example.demo.entity.User;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class LoginResponseDto implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private User user;
private SaTokenInfo SaToken;
}

这里的user就是user实体类,SaToken是token信息,在后面会用上。

同样也是采用了Lombok的Data注解来自动帮我们实现了Getter和Setter方法。

重构Controller#

打开UserController,重构代码

package com.example.demo.controller;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.example.demo.dto.LoginResponseDto;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
/**
* 用户注册接口
* POST /api/users/register
*
* @param user 用户实体(包含username和password)
* @return 统一响应格式 Result
*/
@PostMapping("/register")
public SaResult register(@RequestBody User user) {
log.info("用户注册请求:{}", user.getUsername());
try {
userService.register(user);
return SaResult.ok("注册成功");
} catch (RuntimeException e) {
log.error("用户注册失败:{}", e.getMessage());
return SaResult.error(e.getMessage());
}
}
/**
* 用户登录接口
* POST /api/users/login
*
* @param user 用户实体(包含username和password)
* @return 统一响应格式 Result,包含登录成功的用户实体(带Token)
*/
@PostMapping("/login")
public SaResult login(@RequestBody User user) {
log.info("用户登录请求:{}", user.getUsername());
User logineduser = userService.login(user.getUsername(), user.getPassword());
if (logineduser != null) {
StpUtil.login(logineduser.getId());
logineduser.setPassword(null);
SaTokenInfo token = StpUtil.getTokenInfo();
LoginResponseDto responseDto = new LoginResponseDto();
responseDto.setUser(logineduser);
responseDto.setSaToken(token);
return SaResult.ok("登录成功")
.setData(responseDto);
} else {
return SaResult.error("用户名或密码错误");
}
}
}

可以很明显得看到,在登录方法里,我们new了一个LoginResponseDto类,并将token和user信息放了进去,统一将这两信息返回到了前端。

重构后的流程图#

graph TD subgraph Frontend UserRequests[用户操作/请求] -->|HTTP/REST 请求| APIEndpoints(API接口) end subgraph Backend direction LR subgraph Controller_Layer APIEndpoints -->|发送前端DTO (例如:LoginRequestDto)| UserController UserController -->|接收后端DTO (例如:LoginResponseDto)| SaResult(统一响应 SaResult) UserController -->|调用业务方法,通常传递实体或业务参数| UserService end subgraph Service_Layer UserService -->|执行业务逻辑,操作实体| UserServiceImpl UserServiceImpl -->|数据操作| UserMapper UserServiceImpl --管理会话/登录状态--> SaToken(Sa-Token 框架) end subgraph Data_Access_Layer UserMapper -->|CRUD操作| MySQLDB(MySQL 数据库) UserMapper -->|CRUD操作| MybatisPlus(Mybatis-Plus 框架) end SaToken --持久化会话信息--> RedisDB(Redis 内存数据库) GlobalExceptionHandler(全局异常处理器) --捕获并封装异常--> SaResult end style Frontend fill:#e0f7fa,stroke:#00bcd4,stroke-width:2px,stroke-dasharray: 5 5; style Backend fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,stroke-dasharray: 5 5; style Controller_Layer fill:#fffde7,stroke:#ffeb3b,stroke-width:1px; style Service_Layer fill:#e3f2fd,stroke:#2196f3,stroke-width:1px; style Data_Access_Layer fill:#fce4ec,stroke:#e91e63,stroke-width:1px; style APIEndpoints fill:#f5f5f5,stroke:#9e9e9e,stroke-width:1px; style SaResult fill:#e0f2f7,stroke:#007bff,stroke-width:1px; style SaToken fill:#fff3e0,stroke:#ff9800,stroke-width:1px; style MybatisPlus fill:#ffebee,stroke:#f44336,stroke-width:1px; style MySQLDB fill:#f0f4c3,stroke:#cddc39,stroke-width:2px; style RedisDB fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px; style GlobalExceptionHandler fill:#fbe9e7,stroke:#ff5722,stroke-width:1px; style UserController fill:#fff9c4,stroke:#ffc107,stroke-width:1px; style UserService fill:#bbdefb,stroke:#2196f3,stroke-width:1px; style UserServiceImpl fill:#90caf9,stroke:#2196f3,stroke-width:1px; style UserMapper fill:#f8bbd0,stroke:#e91e63,stroke-width:1px;
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

4-各司其职的后端-Springboot Vue 教程
https://blog.yumui.top/posts/4-springboot-vue/
作者
Yu Felix
发布于
2025-05-09
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时