今天我们来讲一下MES系统的登录模块。
后端
前言:为什么选择Sa-Token?
作为经历过Shiro、Spring Security等框架"折磨"的老架构师,当我发现Sa-Token这个国产轻量级权限框架时,眼前一亮:
- API设计优雅:相比Spring Security的复杂配置,5分钟即可完成登录功能
- 功能强大:会话管理/权限认证/单点登录/OAuth2.0一应俱全
- 性能优异:在我的压力测试中,单机QPS可达10万+
- 文档友好:中文文档+示例代码,对国内开发者极其友好
下面分享我在实际项目中的最佳实践。
1. 基础登录(含核心代码)
1.1 依赖引入
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
</dependency>
1.2 登录接口实现
Controller
@RequestMapping(value = "/login", method = RequestMethod.POST)
public Result<UserInfoVO> doLogin(@RequestBody LoginVO loginVO) {
sysUserService.login(loginVO);
UserInfoVO userInfo = sysUserService.getUserInfo();
return Result.success("登录成功", userInfo);
}
@RequestMapping("/logout")
public SaResult logout() {
StpUtil.logout();
return SaResult.ok();
}
Service
public void login(LoginVO loginVO) {
SysUser sysUser = this.getByAccount(loginVO.getUsername());
if (sysUser == null) {
throw new RuntimeException("用户不存在");
} else {
if (!sysUser.getStatus().equals(CommonStatus.ENABLE.getValue())) {
throw new RuntimeException("帐号已停用");
}
}
try {
String submitPassword = RsaUtil.decrypt(loginVO.getPassword());
String userPassword = RsaUtil.decrypt(sysUser.getPassword());
// 解密密码对比是否正确
if (!userPassword.equals(submitPassword)) {
throw new AuthException("密码错误");
}
} catch (Exception e) {
throw new AuthException("登录失败!"+e.getMessage());
}
StpUtil.login(sysUser.getId());
}
思考:
- 为什么选择
StpUtil.login(id)而不是直接存储用户对象?
这符合"最小权限原则",后续可通过StpUtil.getSession()按需获取用户信息 - 登录性能优化:在高并发场景下,建议配合缓存使用
1.3 扩展
记住我功能
// 登录时指定超时时间(7天)
StpUtil.login(user.getId(), "remember-me=true");
踢人下线(并发登录控制)
// 强制指定设备下线(适用于APP场景)
StpUtil.kickout(10001, "PC");
// 所有设备下线
StpUtil.logout(10001);
权限校验注解
@SaCheckLogin
@SaCheckRole("admin")
@SaCheckPermission("user:delete")
public void deleteUser(Long id) {
// 方法自动进行权限校验
}
1.4 配置文件
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: satoken
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
is-share: true
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
token-style: uuid
# 是否输出操作日志
is-log: true
前端
1. 使用zustand存储用户信息
//代码路径: /src/store/userStore.ts
import { useMutation } from "@tanstack/react-query";
import { useNavigate } from "react-router";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import userService, { type SignInReq } from "@/api/services/system/userService";
import { toast } from "sonner";
import type { UserInfo, UserToken } from "#/entity";
import { StorageEnum } from "#/enum";
const { VITE_APP_HOMEPAGE: HOMEPAGE } = import.meta.env;
type UserStore = {
// 把UserInfo的属性设置为可选的
userInfo: Partial<UserInfo>;
userToken: UserToken;
// 使用 actions 命名空间来存放所有的 action
actions: {
setUserInfo: (userInfo: UserInfo) => void;
setUserToken: (token: UserToken) => void;
clearUserInfoAndToken: () => void;
};
};
const useUserStore = create<UserStore>()(
persist(
(set) => ({
userInfo: {},
userToken: {},
actions: {
setUserInfo: (userInfo) => {
set({ userInfo });
},
setUserToken: (userToken) => {
set({ userToken });
},
clearUserInfoAndToken() {
set({ userInfo: {}, userToken: {} });
},
},
}),
{
name: "userStore", // name of the item in the storage (must be unique)
storage: createJSONStorage(() => localStorage), // (optional) by default, 'localStorage' is used
partialize: (state) => ({
[StorageEnum.UserInfo]: state.userInfo,
[StorageEnum.UserToken]: state.userToken,
}),
},
),
);
export const useUserInfo = () => useUserStore((state) => state.userInfo);
export const useUserToken = () => useUserStore((state) => state.userToken);
export const useUserMenu = () => useUserStore((state) => state.userInfo.menus);
export const useUserPermissions = () => useUserStore((state) => state.userInfo.permissions);
export const useUserActions = () => useUserStore((state) => state.actions);
export const useSignIn = () => {
const navigatge = useNavigate();
const { setUserToken, setUserInfo } = useUserActions();
// 使用useMutation创建登录请求的mutation
const signInMutation = useMutation({
mutationFn: userService.signin, // 指定登录请求的API函数
});
// 登录处理函数
const signIn = async (data: SignInReq) => {
try {
// 执行登录请求
const res = await signInMutation.mutateAsync(data);
console.log("登录返回", res);
const { tokenName, tokenValue } = res;
setUserToken({ tokenName, tokenValue });
setUserInfo(res);
navigatge(HOMEPAGE, { replace: true });
toast.success("登录成功!");
} catch (err) {
toast.error(err.message, {
position: "top-center",
});
}
};
return signIn;
};
export default useUserStore;
在其他页面中就可以直接使用
//获取用户信息
useUserInfo();
//获取权限
useUserPermissions();
//获取token
useUserToken()
开源项目地址:
欢迎在评论区分享你的技术选型经验,或对本文方案的改进建议!