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;
@Servicepublic 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 的主要目的是为了优化数据传输和解耦(降低依赖):
- 精简数据,避免过度暴露:
- 你的数据库实体类(例如
User实体)可能包含很多字段,比如用户密码、创建时间、修改时间等。但前端可能只需要用户的id和username。 - 如果直接把完整的
User实体发送给前端,就可能暴露不必要的敏感信息,或者传输了多余的数据,浪费带宽。 - DTO 允许你只选择性地包含前端或调用方需要的数据,就像快递包裹里只放收件人需要的东西。
- 你的数据库实体类(例如
- 解耦前后端:
- 当后端数据库实体类发生变化时(比如增加了一个字段),如果前端直接依赖实体类,那前端也可能需要跟着改。
- 使用 DTO 后,后端实体类与前端 DTO 之间多了一层转化。只要 DTO 的结构不变,即使实体类变了,前端代码也不受影响。
- 适配不同业务场景:
- 登录时可能需要
user和token信息。 - 显示文章列表时可能只需要文章的
id、标题和作者名。 - 创建文章时可能需要
标题、内容和分类。 - 每个场景需要的字段组合都不同,用 DTO 可以灵活地定义。
- 登录时可能需要
让我们来新建一个dto包,同样与entity,mapper,controller是同一层级的。
在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;
@Datapublic 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")@Slf4jpublic 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信息放了进去,统一将这两信息返回到了前端。
重构后的流程图
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时









