前言
Sa-Token 是一款 Java 语言的权限认证框架,提供了灵活、高效、易用的权限认证和会话管理功能。它是 SpringBoot、Spring MVC、Servlet 等 Java 技术体系下的轻量级权限认证组件,可以帮助开发者快速实现用户认证、授权和会话管理等功能。
功能结构图
认证流程图
框架特性
一、创建工程项目🎍
1.1 创建后端工程
引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.13</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.38.0</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-fastjson2</artifactId>
<version>1.37.0</version>
</dependency>
<!--无论使用哪种序列化方式,你都必须为项目提供一个 Redis 实例化方案-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter
修改为 sa-token-spring-boot3-starter
即可。
基础配置
server:
port: 8081
spring:
redis:
database: 1
host: 127.0.0.1
port: 6379
password:
timeout: 5000
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
#Sa-token相关配置(与官网一致)
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.2 创建前端工程
本项目使用的vue3,创建一个vue项目,引入下方依赖即可
"dependencies": {
"axios": "^1.6.8",
"element-plus": "^2.7.3",
"pinia": "^2.1.7",
"qs": "^6.12.1",
"vue": "^3.2.37",
"vue-router": "^4.2.5"
},
二、业务代码🎊
后端代码
User.java
@Data
public class User{
//id
@TableId(type = IdType.ASSIGN_ID)
private Long id;
//用户名
private String username;
//密码
private String password;
//账户是否锁住(1被锁0未被锁)
private Integer isLocked;
//账户是否被删除(1删除0未被删除)
@TableLogic
private Integer isDelete;
//创建时间
@TableField(fill = FieldFill.INSERT)
private Date createTime;
//更新时间
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}
UserDTO.java
@EqualsAndHashCode(callSuper = true)
@Data
public class UserDTO extends User {
//记住我
private boolean rememberMe;
//角色列表
private List<Role> roleList;
//登录的设备
private String device;
//图形验证码
private String code;
//图形验证码的key
private String codeKey;
}
UserController.java
注意:代码中定义了一些常量,可以自行替代。
@RestController
@RequestMapping("/user")
@CrossOrigin
public class UserController {
@Autowired
UserService userService;
@Autowired
StringRedisTemplate stringRedisTemplate;
@PostMapping("/doLogin")
public SaResult doLogin(@RequestBody UserDto userDto) {
//从redis中取生成的验证码
String validateCode = stringRedisTemplate.opsForValue().get("code:validate:" + userDto.getCodeKey());
if (userDto.getCode().equals(validateCode)) {
//验证码正确才进行用户验证,先验证了用户存不存在,再验证了密码是否正确
if (userService.login(userDto.getUsername(), userDto.getPassword())) {
StpUtil.login(userDto.getUsername(), new SaLoginModel()
//实现‘记住我’功能
.setIsLastingCookie(userDto.isRememberMe())
//设置登录设备,主要用于实现同端互斥登录,此处没有实现该功能,可以不用管
.setDevice("PC"));
//验证成功就以json的形式返回token
HashMap<String, Object> resultMap = new HashMap<>();
resultMap.put("token", StpUtil.getTokenValue());
return SaResult
.ok(ResponseCode.LOGIN_SUCCESS.getMessage())
.setCode(ResponseCode.LOGIN_SUCCESS.getCode())
.setData(resultMap);
} else {
return SaResult
.error(ResponseCode.USERNAME_PASSWORD_ERROR.getMessage())
.setCode(ResponseCode.USERNAME_PASSWORD_ERROR.getCode());
}
} else {
return SaResult
.error(ResponseCode.VALIDATE_CODE_ERROR.getMessage())
.setCode(ResponseCode.VALIDATE_CODE_ERROR.getCode());
}
}
}
StpUtils.login:
- 检查此账号是否之前已有登录;
- 为账号生成
Token
凭证与Session
会话; - 记录 Token 活跃时间;
- 通知全局侦听器,xx 账号登录成功;
- 将
Token
注入到请求上下文; - 等等其它工作……
SaResult:这个也是一个由Satoken封装的结果响应类,还是挺好用的。
UserService.java
public interface UserService {
boolean login(String username, String password);
}
UserServiceImpl.java
@Service
public class UserServiceImpl implements UserService{
@Autowired
UserDao userDao;
@Override
public boolean login(String username, String password) {
User user = userDao.selectOne(new QueryWrapper<User>().eq("username", username));
//数据库中的密码进行了加密BCrypt也是Satoken提供的一个工具类
return user != null && BCrypt.checkpw(password, user.getPassword());
}
}
UserDao.java
/**
* (User)表数据库访问层
*
* @author yzk
* @since 2024-05-15 16:44:29
*/
public interface UserDao extends BaseMapper<User> {
}
SaAuthenticationConfigure.java
@Configuration
public class SaAuthenticationConfigure implements WebMvcConfigurer {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
//所有接口都会检查是否登录了
.addPathPatterns("/**")
//以下接口不检查,直接放行
.excludePathPatterns("/user/doLogin")
}
}
SaTokenFilter.java
@Configuration
public class SaTokenFilter implements WebMvcConfigurer {
/**
* 注册 [Sa-Token 全局过滤器]
*/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
// 指定 [拦截路由] 与 [放行路由]
.addInclude("/**").addExclude("/favicon.ico")
// 认证函数: 每次请求执行
.setAuth(obj -> {
SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());
// ...
})
// 异常处理函数:每次认证函数发生异常时执行此函数
.setError(e -> {
return SaResult.error(e.getMessage());
})
// 前置函数:在每次认证函数之前执行
.setBeforeAuth(obj -> {
SaHolder.getResponse()
// ---------- 设置跨域响应头 ----------
// 允许指定域访问跨域资源
.setHeader("Access-Control-Allow-Origin", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "*")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*")
// 有效时间
.setHeader("Access-Control-Max-Age", "3600")
;
// 如果是预检请求,则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
.free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
.back();
})
;
}
}
注意这个过滤器!!!!!!!!!!务必配置这个过滤器,用SaToken时,后端使用@CrossOrigin进行了跨域的配置,但是!前端发起请求还是会报跨域问题,而且后端会报'未读取到有效token',是因为前端发起请求时,先发起了预检请求,SaToken拦截了预检请求,预检请求的headers中没有token,所以一直报错。
前端代码
此处删减了部分代码,只保留了登录登录相关的代码
login.vue
<template>
<div class="login-container">
<h2>登录</h2>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="username">用户名:</label>
<el-input type="text" id="username" v-model="user.username" placeholder="请输入用户名" required/>
</div>
<div class="form-group">
<label for="password">密码:</label>
<el-input type="password" id="password" v-model="user.password" placeholder="请输入密码" required/>
</div>
<div class="form-group">
<label>验证码</label>
<el-row :gutter="15">
<el-col :span="14">
<el-input v-model="user.code" style="margin-top: 7px" class="codeInput"
placeholder="请输入验证码"></el-input>
</el-col>
<el-col :span="10">
<el-image @click="changeImage" style="width: 159px; height: 39px" :src="imageUrl"/>
</el-col>
</el-row>
</div>
<div class="form-group">
<el-checkbox v-model="user.rememberMe" label="记住我" size="large"/>
</div>
<button type="submit">登录</button>
<button type="submit" @click="toRegister">注册</button>
</form>
<div class="social-login">
<button @click="toGitee">
<span class="social-icon"></span>使用Gitee登录
</button>
</div>
</div>
</template>
<script setup>
import {h, onMounted, ref} from 'vue';
import {generatorLoginCode, toGiteeOauth, userLogin} from "@/api/userApi";
import {useRouter} from "vue-router";
import {ElCheckbox, ElCol, ElImage, ElInput, ElNotification, ElRow} from "element-plus";
const router = useRouter()
const user = ref({
username: "",
password: "",
rememberMe: false,
code: "",
codeKey: ""
})
const handleSubmit = () => {
userLogin(JSON.stringify(user.value))
.then((resp) => {
if (resp.code === 2000) {
//取出token并存入localStorage
localStorage.setItem("token", resp.data.token)
ElNotification({
title: '提示',
message: h('info', {style: 'color: teal'}, resp.msg),
duration: 3000
})
router.push("/main")
} else {
if (resp.code === 5005) {
ElNotification({
title: '提示',
message: h('error', {style: 'color: red'}, resp.msg),
duration: 3000
})
changeImage()
}
}
})
};
const userLogin = (data) => {
return requests({
url: "/user/doLogin",
data: data,
method: "POST",
})
}
</script>
<style scoped>
</style>
前端点击登录按钮时,发后端接口发起请求,请求成功后,取出响应结果中的token存入localStorage。
requests.js
import axios from 'axios';
import {router} from "@/router";
axios.defaults.crossDomain = true
export const requests = axios.create({
baseURL: 'http://localhost:8081',
timeout: 10000, // 请求超时时间
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
// 请求拦截器
requests.interceptors.request.use(
(config) => {
// 在发送请求之前做些什么
const token = localStorage.getItem('token'); // 假设token存储在localStorage中
if (token) {
config.headers.Token = `Bearer ${token}`; // 添加token到请求头
}
return config;
},
(error) => {
// 对请求错误做些什么
return Promise.reject(error);
}
);
// 响应拦截器
requests.interceptors.response.use(
(response) => {
console.log(response.data.code)
const code=response.data.code
if (code === 5006 ||code===5007|| code===5008) {
//返回登录界面
router.push('/').then(r =>{
location.reload()
} )
// router.go(0)
//删除当前localStorage中的token
localStorage.removeItem("token")
}
return response.data;
},
(error) => {
// 对响应错误做点什么
return Promise.reject(error);
}
);
注意请求拦截器,每次发起请求时,都会从localStorage中取出token,并将其放入headers中
三、测试
集成了redis后,记得先开启redis服务,在登录时,框架会自动保存数据
-
开启前后端服务
本文的代码是从完整项目中抽出的,上述代码只有登录功能
输入用户名和密码后,进入主界面
此时,登录的信息已经自动的被框架自动保存
进redis
IDEA控制台打印出相应的信息