之前写项目一致都使用的是 SpringSecurity ,但总是感觉SpringSecurity用起来比较繁琐,所以就打算使用sa-token 进行权限校验以及 登录验证,角色校验,集成比较简单.
整个项目 使用SpringCloud GateWay 进行网关,统一将服务注册到nacos 上去
即 mall_auth : 提供用户登录的功能
mall-mbg: 提供对数据库的查询操作
mall_common: 公共的方法
mall_product: 提供产品查询方法
gateway_test : 充当网关功能,对请求做权限校验然后对请求进行分发
```java 4.0.0
org.example
test_sa_token
pom
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-parent
2.7.0
3.5.9
8.0.29
2021.0.3
2021.0.1.0
1.4.1
1.4.2
gateway_test
mall_auth
mall_common
mall_product
org.projectlombok
lombok
org.springframework.boot
spring-boot-configuration-processor
true
io.github.openfeign
feign-okhttp
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.cloud
spring-cloud-starter-bootstrap
3.1.0
org.springframework.boot
spring-boot-configuration-processor
true
org.springframework.boot
spring-boot-starter-data-redis
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
${spring-cloud-alibaba.version}
pom
import
org.mybatis
mybatis
${mybatis.version}
org.mybatis.generator
mybatis-generator-core
${mybatis-generator.version}
com.github.pagehelper
pagehelper-spring-boot-starter
${pagehelper-starter.version}
mysql
mysql-connector-java
${mysql-connector.version}
```
pom.xml
```java 4.0.0 org.example test satoken 1.0-SNAPSHOT com.example gateway test 0.0.1-SNAPSHOT gatewaytest 用于测试sa_token 1.8 org.springframework.cloud spring-cloud-starter-gateway
org.example
1.0-SNAPSHOT
mall_common
org.springframework.cloud
spring-cloud-starter-openfeign
cn.dev33
sa-token-dao-redis-jackson
1.34.0
org.springframework.cloud
spring-cloud-starter-loadbalancer
cn.dev33
sa-token-reactor-spring-boot-starter
1.34.0
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
org.projectlombok
lombok
```
application.yml 配置
```java server: port: 8201
spring: # redis 相关配置 redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) # password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0
mvc: pathmatch: matching-strategy: antpathmatcher cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true # 使用小写的service-id routes: # 配置路由路径 - id: mall-auth # 认证中心 uri: lb://mall-auth predicates: - Path=mall-auth/* filters: - StripPrefix=1 - id: mall-product # 产品模块 uri: lb://mall-product predicates: - Path=/mall-product/* filters: - StripPrefix=1
sa-token: # token 的名称 同时也是cookie 名称 token-name: satoken # token 的有效期 单位s 默认为30天, -1代表永不过期 timeout: 2592000 # token 临时有效期(指定时间内无操作或者视为token过期) 单位 秒 activity-timeout: -1 # 是否允许同一账号并发登录(为true 时允许一起登录, 为false 时新登录时挤掉旧登录) is-concurrent: true # 在多人登录同一账号时, 是否共用同一个token(为true 的时候所有登录通用同一个token,为false 时每次登录新建一个 token) is-share: false # token 风格 token-style: uuid
```
bootstrap.yml 文件
```java spring: profiles: active: dev application: name: mall-gateway
```
增加SaTokenConfigure 类用于sa-token 的配置
```java package com.example.gateway_test.config;
import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.reactor.filter.SaReactorFilter; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
/* * [Sa-Token 权限认证] 全局配置类 */ @Configuration public class SaTokenConfigure { // 注册 Sa-Token全局过滤器 @Bean public SaReactorFilter getSaReactorFilter() { return new SaReactorFilter() // 拦截地址 .addInclude("/") / 拦截全部path */ // 开放地址 .addExclude("/favicon.ico") // 鉴权方法:每次访问进入 .setAuth(obj -> { // 登录校验 -- 拦截所有路由,并排除/account/user/doLogin用于开放登录 SaRouter.match("/", "/mall-auth/auth/token", r -> StpUtil.checkLogin());
// // 权限认证 -- 不同模块, 校验不同权限
/* SaRouter.match("/mall-product/**", r -> StpUtil.checkRole("normal_role"));
SaRouter.match("/admin/**", r -> StpUtil.checkRole("system_role"));
SaRouter.match("/account/**", r -> StpUtil.checkRole("user"));
SaRouter.match("/admin/**", r -> StpUtil.checkRole("admin"));
SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));*/
// 更多匹配 ... */
})
.setBeforeAuth(obj -> {
// ------设置跨域响应头
SaHolder.getResponse()
// 允许指定域访问跨域资源
.setHeader("Access-Control-Allow-Origin", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
// 有效时间
.setHeader("Access-Control-Max-Age", "3600")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*");
})
// 异常处理方法:每次setAuth函数出现异常时进入
.setError(e -> {
return SaResult.error(e.getMessage());
})
;
}
}
```
增加 StpInterfaceImpl 实现了sa-token 对角色与权限的查询
```java package com.example.gateway_test.config;
import cn.dev33.satoken.stp.StpInterface; import com.example.common.api.RedisKey; import com.example.common.util.RedisUtil; import org.springframework.stereotype.Component;
import javax.annotation.Resource; import java.util.ArrayList; import java.util.List;
/** * @author zhangyang * @version 1.0 * @Date 2023/5/17 14:09 * @Description 对权限进行校验 */ @Component public class StpInterfaceImpl implements StpInterface {
@Resource
private RedisUtil redisUtil;
@Override
public List getPermissionList(Object loginId, String loginType) {
// 根据用户的id 来查询出对应的
Integer userId = Integer.valueOf(String.valueOf(loginId));
List permissionList = redisUtil.getCacheList(RedisKey.getUserPermissionList(userId));
return permissionList;
}
@Override
public List getRoleList(Object loginId, String loginType) {
// 获取缓存
Integer userId = Integer.valueOf(String.valueOf(loginId));
List roleList = redisUtil.getCacheList(RedisKey.getUserRoleList(userId));
return roleList;
}
}
```
增加GlobalException 用于捕获全局异常类
```java package com.example.gateway_test.exception;
import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.exception.NotRoleException; import com.example.common.api.CommonResult; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody;
/** * @author zhangyang * @version 1.0 * @Date 2023/5/17 13:40 * @Description 拦截全局异常类 */ @Slf4j public class GlobalException {
@ResponseBody
@ExceptionHandler
public CommonResult
}
```
其项目主类
```java package com.example.gateway_test;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Import;
@EnableDiscoveryClient @SpringBootApplication @Import(value = { com.example.common.util.RedisUtil.class }) public class GatewayTestApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayTestApplication.class, args);
}
}
```
这个模块处理用户登录请求
增加LoginController 类用于处理登录请求 ```java package com.example.mall_auth.controller;
import com.example.common.api.CommonResult; import com.example.demo.pojo.dto.LoginUserDto; import com.example.demo.pojo.model.User; import com.example.mall_auth.service.LoginService; import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/** * @author zhangyang * @version 1.0 * @Date 2023/5/16 19:55 * @Description */
@RequestMapping("/auth") @RestController public class LoginController {
@Resource
private LoginService loginService;
@PostMapping("/token")
public CommonResult login(@RequestBody User user) {
return CommonResult.success(loginService.login(user.getUsername(), user.getPassword()));
}
}
```
LoginService 接口
```java package com.example.mall_auth.service;
import com.example.demo.pojo.dto.LoginUserDto;
/** * @author zhangyang * @version 1.0 * @Date 2023/5/16 20:03 * @Description */ public interface LoginService {
/**
* 使用sa-token 进行登录
* @param username
* @param password
* @return
*/
LoginUserDto login(String username, String password);
}
```
LoginServiceImpl 接口实现类
```java package com.example.mall_auth.service.impl;
import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.stp.StpUtil; import com.example.demo.mapper.UserMapper; import com.example.demo.pojo.dto.LoginUserDto; import com.example.demo.pojo.model.User; import com.example.mall_auth.service.LoginService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/** * @author zhangyang * @version 1.0 * @Date 2023/5/16 20:03 * @Description 登录接口 */ @Slf4j @Service public class LoginServiceImpl implements LoginService {
@Resource
private UserMapper userMapper;
@Override
public LoginUserDto login(String username, String password) {
User user = userMapper.selectUmsByUserName(username);
if(user!=null && user.getPassword().compareTo(password) ==0) {
// 登录
// 用用户id 做标识,然后将生成的token 写入到缓存中
StpUtil.login(user.getId());
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
LoginUserDto loginUserDto = new LoginUserDto();
loginUserDto.setUser(user);
loginUserDto.setToken(tokenInfo);
return loginUserDto;
}
log.error("该用户---{}---登录失败",username);
return null;
}
}
```
对应的pom 依赖
```java
server: port: 8202
spring: datasource: url: jdbc:mysql://localhost:3306/sa?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 123456 cloud: nacos: config: file-extension: yaml server-addr: http://localhost:8848 discovery: server-addr: http://localhost:8848 redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) # password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 sa-token: # token 的名称 同时也是cookie 名称 token-name: satoken # token 的有效期 单位s 默认为30天, -1代表永不过期 timeout: 100 # token 临时有效期(指定时间内无操作或者视为token过期) 单位 秒 activity-timeout: -1 # 是否允许同一账号并发登录(为true 时允许一起登录, 为false 时新登录时挤掉旧登录) is-concurrent: true # 在多人登录同一账号时, 是否共用同一个token(为true 的时候所有登录通用同一个token,为false 时每次登录新建一个 token) is-share: false # token 风格 token-style: uuid mybatis: mapper-locations: - classpath:dao/.xml - classpath:mapper/*.xml
feign: okhttp: enabled: true client: config: default: connectTimeout: 5000 readTimeout: 5000 loggerLevel: basic
```
pom.xml 文件内容
```java 4.0.0 org.example test satoken 1.0-SNAPSHOT
com.example
mall_auth
0.0.1-SNAPSHOT
mall_auth
mall_auth实现用户登录接口
1.8
org.springframework.boot
spring-boot-starter
com.example
mall-mbg
0.0.1-SNAPSHOT
cn.dev33
sa-token-spring-boot-starter
1.34.0
org.springframework.boot
spring-boot-starter-web
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
cn.dev33
sa-token-dao-redis-jackson
1.34.0
org.springframework.boot
spring-boot-starter-test
test
org.example
mall_common
1.0-SNAPSHOT
compile
org.springframework.boot
spring-boot-maven-plugin
```
使用postman 发送 查询product 的信息
得到的结果为token 无效,说明经过网关的时候被sa-token 给拦截住了。
此时我们应该访问 mall_auth 的登录服务
此时我们会发现本地的redis服务中
那么此时将token信息写入到header 中,我们再次访问product 服务的时候
就可以访问到对应的结果了。
上面实现了对登录的拦截。我们还可以利用sa-token 在网关上做权限与角色的校验。使用RABC 对登录的用户做权限与角色的判断。
我们可以在 mallauth 启动的时候将用户信息注入到redis 中,然后在 gatewaytest 服务中的StpInterfaceImpl 中实现通过userId 到缓存中查询对应的权限以及角色集合的代码。
mall_auth 中的 PermissionInit 代码
```java package com.example.mall_auth.init;
import com.alibaba.nacos.common.utils.MapUtils; import com.example.common.api.RedisKey; import com.example.common.util.RedisUtil; import com.example.mall_auth.service.impl.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils;
import javax.annotation.Resource; import java.util.List; import java.util.Map;
/** * @author zhangyang * @version 1.0 * @Date 2023/5/17 14:41 * @Description */ @Slf4j @Component public class PermissionInit implements ApplicationRunner {
@Resource
private UserService userService;
@Resource
private RedisUtil redisUtil;
@Override
public void run(ApplicationArguments args) {
log.warn("初始化数据补齐--");
// 注入对应的角色信息
Map> userRoleMap = userService.findAllUserRoleList();
if (MapUtils.isNotEmpty(userRoleMap)) {
// 查询
for (Map.Entry> entry : userRoleMap.entrySet()) {
Integer userId = entry.getKey();
List roleList = entry.getValue();
if (!CollectionUtils.isEmpty(roleList)) {
redisUtil.setCacheList(RedisKey.getUserRoleList(userId), roleList);
}
}
}
// 注入对应的权限信息
Map> userPermissionMap = userService.findAllUserPermission();
if(MapUtils.isNotEmpty(userPermissionMap)) {
// 查询出所有的权限
for(Map.Entry> entry : userPermissionMap.entrySet()) {
Integer userId = entry.getKey();
List permissionList = entry.getValue();
if(!CollectionUtils.isEmpty(permissionList)) {
redisUtil.setCacheList(RedisKey.getUserPermissionList(userId),permissionList);
}
}
}
}
}
```
然后gateway_test 中的 StpInterfaceImpl 代码
```java package com.example.gateway_test.config;
import cn.dev33.satoken.stp.StpInterface; import com.example.common.api.RedisKey; import com.example.common.util.RedisUtil; import org.springframework.stereotype.Component;
import javax.annotation.Resource; import java.util.ArrayList; import java.util.List;
/** * @author zhangyang * @version 1.0 * @Date 2023/5/17 14:09 * @Description 对权限进行校验 */ @Component public class StpInterfaceImpl implements StpInterface {
@Resource
private RedisUtil redisUtil;
@Override
public List getPermissionList(Object loginId, String loginType) {
// 根据用户的id 来查询出对应的
Integer userId = Integer.valueOf(String.valueOf(loginId));
List permissionList = redisUtil.getCacheList(RedisKey.getUserPermissionList(userId));
return permissionList;
}
@Override
public List getRoleList(Object loginId, String loginType) {
// 获取缓存
Integer userId = Integer.valueOf(String.valueOf(loginId));
List roleList = redisUtil.getCacheList(RedisKey.getUserRoleList(userId));
return roleList;
}
}
```
那么我们就可以修改SaTokenConfigure 中的 权限认证部分的代码
java SaRouter.match("/mall-product/**", r -> StpUtil.checkRole("normal_role"));
表示 当前登录的用户如果需要访问 mall-product 的服务则需要拥有normal_role 角色
java SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
表示 当前登录的用户如果需要访问 goods 服务则需要拥有goods 的权限
https://github.com/to1233/springcloudlearn/tree/main/testsatoken