注意:本文已整合nacos注册中心(最简单模式),在看之前先整合喔。
一、整体介绍
关于springcloud的gateway介绍我就不赘述了,网上的相关文章很多。给大家贴一个:
Gateway网关简介及使用-CSDN博客
1、关于satoken的简介
-
Sa-Token 是一款轻量级的Java权限认证框架,可以用来解决登录认证、权限认证、Session会话、单点登录、OAuth2.0、微服务网关鉴权等一系列权限相关问题。
-
框架集成简单、最简单的可以使用一个依赖即可完成,API设计优雅,通过 Sa-Token,你将以一种极其简单的方式实现系统的权限认证部分。
相信看到这里的同学已经对他有了初步的了解,下面我们直接开始进行整合。
二、整合思路
将satoken和gateway分为两个微服务,satoken模块主要用来做登录、登出等用户相关的东西(也可将用户相关的东西抽象出来一个模块,使用openFeign进行远程调用,看自己的想法)等;gateway模块用来判断用户请求转发前是否是出于登录状态,粗颗粒度的鉴权(本文未做);
接下来开始整合;
三、项目结构与代码
1、整体结构
父级模块引入的具体依赖为:
1.8 UTF-8 UTF-8 2.6.13 2021.0.5.0 2021.0.5 org.springframework.boot spring-boot-starter-jdbccom.mysql mysql-connector-jorg.projectlombok lomboktrue org.springframework.boot spring-boot-starter-testtest com.baomidou mybatis-plus-boot-starter3.5.1 com.github.xiaoymin knife4j-spring-boot-starter3.0.2 com.alibaba fastjson2.0.11 com.alibaba druid-spring-boot-starter1.2.14 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discoveryorg.springframework.boot spring-boot-dependencies${spring-boot.version} pom import com.alibaba.cloud spring-cloud-alibaba-dependencies${spring-cloud-alibaba.version} pom import org.springframework.cloud spring-cloud-dependencies${spring-cloud.version} pom import 有几个依赖可能是多余的但是不影响(尽量不要动他,防止出问题)
2、common模块
1.项目结构
解释:本deom中的common模块主要是用来存放实体类(本应放在pojo模块,偷个懒),自定义异常处理的类,还有自定义返回结果(都是比较基础的东西,不在赘述);
3、uaa模块(satoken模块)
1、项目结构
解释:本模块主要用用来做用户的登录以及登出的,我们一步一步的带大家走;
2、思维讲解和逻辑解析
1、引入模块依赖pom.xml
com.cqie common0.0.1-SNAPSHOT cn.dev33 sa-token-spring-boot-starter1.37.0 cn.dev33 sa-token-jwt1.37.0 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discoverycn.dev33 sa-token-redis-jackson1.37.0 org.apache.commons commons-pool22、编写application.yml与application-dev.yml文件
#application.yml server: port: 3080 spring: application: name: saTokenService #微服务名称 profiles: active: dev #代表是开发环境 cloud: #nacos相关基础配置 nacos: server-addr: xxxxx #自己的nacos端口 #redis相关基础配置 redis: port: 6379 host: 127.0.0.1 # 数据库配置 datasource: druid: driver-class-name: ${factory.datasource.driver-class-name} url: jdbc:mysql://${factory.datasource.host}:${factory.datasource.port}/${factory.datasource.database}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true username: ${factory.datasource.username} password: ${factory.datasource.password} #sa-token相关配置 sa-token: # token 名称(同时也是 cookie 名称) token-name: token # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 72000 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: simple-uuid # # 是否从cookie中读取token is-read-cookie: false # # 是否从head中读取token is-read-header: true #是否输出操作日志 is-log: true #是否开启sa-token的启动动画 is-print: off #关闭控制台启动动画 # jwt秘钥 jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk #mybatis-plus的相关依赖 mybatis-plus: global-config: banner: off #关闭mybatis-plus的启动动画 configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql语句 mapper-locations: classpath:mapper/*.xml
#application-dev.yml factory: datasource: driver-class-name: com.mysql.cj.jdbc.Driver host: localhost port: 3306 database: sa_token_test username: root password: 123456
关于为什么会使用的dev呢,因为我是从上个项目cv过来的哈哈哈。
注意在本模块的启动类上加一个@EnableWebMvc
3、全局异常拦截器
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 全局运行时异常拦截器 * * @param e * @return */ @ExceptionHandler(BaseException.class) public Result ExceptionHandler(BaseException e) { log.info("异常信息为:{}", e.getMessage()); return Result.error(e.getMessage()); } /** * 全局异常拦截器 * * @param e * @return */ @ExceptionHandler(Exception.class) public Result ExceptionHandler(Exception e) { log.info("异常信息为:{}", e.getMessage()); return Result.error(e.getMessage()); } }
这个不赘述;
4、SatokenConfig(uaa模块)
@Configuration @Slf4j public class SaTokenConfig implements WebMvcConfigurer { /** * jwt生成 * @return */ @Bean public StpLogic stpLogicJWT(){ return new StpLogicJwtForSimple(); } /** * 配置所有需要被排除的路径 * * @return */ public List
getList() { List list = new ArrayList<>(); list.add("/favicon.ico"); list.add("/error"); list.add("/swagger-resources/**"); list.add("/webjars/**"); list.add("/v2/**"); list.add("/doc.html"); list.add("**/swagger-ui.html"); return list; } /** * 注册Sa-token拦截器,打开注解鉴权功能 * * @param registry */ public void addInterceptors(InterceptorRegistry registry) { log.info("开始注册Sa-token拦截器............"); // 注册 Sa-Token 拦截器,打开注解式鉴权功能 registry.addInterceptor(new SaInterceptor(handle -> { //还可以分模块进行校验,不同模块需要不同的鉴权 SaRouter.match("/user/**", "/user/doLogin", r -> StpUtil.checkRoleOr("user", "admin")); })).addPathPatterns("/**").excludePathPatterns(getList()); } /** * 跨域配置 * * @param registry */ public void addCorsMappings(CorsRegistry registry) { log.info("开始进行跨域拦截器配置......."); registry.addMapping("/**") .allowedHeaders("*") .allowedMethods("*") .maxAge(1800) .allowedOrigins("*"); } } - 首先关于jwt的生成,那个东西直接在官方文档抄的一模一样,在依赖中我也做了明显的标识,在yml里面我也有写到satoken整合使用到了jwt,关于为什么用呢,我单纯觉得长更安全(狗头);
- 此处使用到这个sa-token的拦截器和官方文档也是一样的,不过排除了登录接口,其他接口进来需要校验是否有这两个角色权限。(比较重要的一点就是,只有访问当前模块下的接口才会走这个拦截器来校验,走其他模块的不会校验,要在其他模块下自己校验);
- 跨域我也不细说了,放行这里排除的是swagger的相关路径。
5、controller代码
@RestController @RequestMapping("/user/") @Api(tags = "用户信息管理") @Slf4j public class UserController { @Resource private UserService userService; /** * 登录 * * @param username * @param password * @return */ @GetMapping("doLogin") public Result doLogin(String username, String password) { //此处仅做模拟,真实情况,要查询数据库 if (userService.doLogin(username, password)) { return Result.success("登陆成功"); } return Result.error("登陆失败"); } /** * 查询登录页状态 * * @return */ //@SaCheckLogin //查看是否已经登录,已经登录才可以访问这个接口 //只要满足了里面的一个文件条件就行 @SaCheckOr( login = @SaCheckLogin, role = @SaCheckRole("admin"), permission = @SaCheckPermission("user.get"), safe = @SaCheckSafe("update.password"), basic = @SaCheckBasic(account = "sa:123456"), disable = @SaCheckDisable("submit-orders") ) @ApiOperation("是否登录") @GetMapping("isLogin") public Result isLogin() { return Result.success("当前的登录状态为", StpUtil.isLogin()); } /** * 查询token信息 * * @return */ @SaCheckRole("user") @GetMapping("tokenInfo") public Result tokenInfo() { return Result.success(StpUtil.getTokenInfo()); } /** * 获取角色权限集合 * @return */ @SaCheckPermission(value = "qwewe",orRole = "admin") //双重or,表示权限为qweqwe或者角色权限为admin @GetMapping("getPermissionList") public Result getPermissionList() { List
booleanList = new ArrayList<>(); //获取该角色权限集合 List permissionList = StpUtil.getPermissionList(); log.info("全部权限信息为:{}",permissionList); //判断当前账号是否含有指定权限,返回true或者false boolean b = StpUtil.hasPermission("user.add"); booleanList.add(b); // 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException StpUtil.checkPermission("user.add"); // 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过] StpUtil.checkPermissionAnd("user.add", "user.get"); // 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可] StpUtil.checkPermissionOr("user.add", "user.delete", "user.get"); boolean hassed = StpUtil.hasPermission("art.add"); booleanList.add(hassed); return Result.success(booleanList); } @GetMapping("logout") public Result logout(Long id){ //强制下线 StpUtil.logout(id); // 强制指定账号注销下线 // StpUtil.logout(10001, "PC"); // 强制指定账号指定端注销下线 // StpUtil.logoutByTokenValue("token"); // 强制指定 Token 注销下线 //踢人下线 // StpUtil.kickout(10001); // 将指定账号踢下线 // StpUtil.kickout(10001, "PC"); // 将指定账号指定端踢下线 // StpUtil.kickoutByTokenValue("token"); // 将指定 Token 踢下线 return Result.success(); } } 上面的使用的StpUtil全是在官方文档有的,不赘述。
6、serviceImpl代码
/** *
* 服务实现类 *
* * @author 小飞 * @since 2024-04-16 */ @Service public class UserServiceImpl extends ServiceImplimplements UserService { @Resource private UserMapper userMapper; /** * 登录 * @param username * @param password * @return */ @Override public boolean doLogin(String username, String password) { User user = userMapper.selectOne(new LambdaQueryWrapper ().eq(User::getUsername, username)); if (user == null || !Objects.equals(user.getPassword(), password)){ return false; } StpUtil.login(user.getId()); return true; } } 基础登录很简单,不多解释
7、然后就是角色权限的代码了
@Component public class StpInterfaceImpl implements StpInterface { /** * 返回权限集合 * @param o * @param s * @return */ public List
getPermissionList(Object o, String s) { List list = new ArrayList<>(); list.add("user.add"); list.add("user.update"); list.add("user.get"); list.add("art.*"); return list; } /** * 返回一个账号所拥有的角色标识集合 * @param o * @param s * @return */ public List getRoleList(Object o, String s) { // 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList (); list.add("admin"); list.add("super-admin"); return list; } } 其他地方的代码就不贴了。。。几乎没写。。
4、gateway模块
1、项目结构
2、思维讲解和逻辑分析
1、引入模块依赖
com.cqie common0.0.1-SNAPSHOT cn.dev33 sa-token-reactor-spring-boot-starter1.37.0 org.springframework.cloud spring-cloud-starter-gatewayorg.springframework.boot spring-boot-starter-freemarkercom.alibaba.cloud spring-cloud-starter-alibaba-nacos-discoverycn.dev33 sa-token-jwt1.37.0 cn.dev33 sa-token-redis-jackson1.37.0 org.apache.commons commons-pool2org.springframework.cloud spring-cloud-loadbalancer2、编写application.yml与application-dev.yml文件
#application.yml server: port: 3070 spring: profiles: active: dev # 开发环境 application: name: gatewayService main: banner-mode: off datasource: druid: driver-class-name: ${factory.datasource.driver-class-name} url: jdbc:mysql://${factory.datasource.host}:${factory.datasource.port}/${factory.datasource.database}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true username: ${factory.datasource.username} password: ${factory.datasource.password} redis: port: 6379 host: 127.0.0.1 cloud: nacos: server-addr: 192.168.87.135:8848 gateway: routes: - id: bookInfo-service #唯一标识,必须唯一 uri: lb://bookInfoService #路由的目标地址 predicates: #路由断言,判断请求是否符合规则 - Path=/bookInfo/** #判断路径是否以这个路径开头 - id: saToken-service #唯一标识,必须唯一 uri: lb://saTokenService #路由的目标地址 predicates: #路由断言,判断请求是否符合规则 - Path=/user/** #判断路径是否以这个路径开头 #sa-token相关配置 sa-token: # token 名称(同时也是 cookie 名称) token-name: token # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 72000 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: simple-uuid # # 是否从cookie中读取token is-read-cookie: false # # 是否从head中读取token is-read-header: true # #能否写入到heard中 # is-write-header: true #是否输出操作日志 is-log: true #是否开启sa-token的启动动画 is-print: off #关闭控制台启动动画 # jwt秘钥 jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk
#application-dev.yml factory: datasource: driver-class-name: com.mysql.cj.jdbc.Driver host: localhost port: 3306 database: sa_token_test username: root password: 123456
3、saTokenConfig(gateway模块)
@Component @RequiredArgsConstructor public class SaTokenConfig { /** * 注册 [Sa-Token全局过滤器] */ @Bean public SaReactorFilter getSaReactorFilter() { return new SaReactorFilter() // 指定 [拦截路由] .addInclude("/**") /* 拦截所有path */ // 指定 [放行路由] .addExclude("/favicon.ico", "/user/doLogin") // 指定[认证函数]: 每次请求执行 .setAuth(obj -> { System.out.println("---------- sa全局认证"); SaRouter.match("/user/**").check(r -> StpUtil.checkLogin()); }) // 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); return SaResult.error(e.getMessage()); }) ; } }
好了到目前位置全部的有效代码都配置完了;
四、运行以及提出问题
服务注册成功!
1、运行测试(我用的apifox)
登录成功,我这里没有把token返回给前端,哈哈,只能复制一下到header了。。。
看看redis里面呢
也生成进去了
稍微提一嘴,在satoken里面集成了satoken自带的redis依赖后,只要登录了就会在底层生成token存入到redis中,不用我们自己写存取的过程
好,那就是登录成功了哈
我们在测试一下UserController里面的isLogin方法:
开始测试:
好!重要的来了!
大家看,这里的登录状态为true,意味着我们是直接由于路由调用到这方法的,可是,如果大家有细心看gteway层的saTokenConfig的代码的话就会发现一个问题
我们在satoken的过滤器里面只是放行了登录的端口,没有放行查询登录信息的端口,这里的我们做了拦截,要对其他端口进行satoken校验登录,那么想得比较多的朋友就有问题了,我们只是在satoken模块进行登录了,怎么到gateway模块也能进行查询登录呢?这里就是最容易让人产生疑惑的地方。接下来我来告诉大家原因:
因为StpUtil.checkLogin()底层会根据生成token的方式去查询redis中是否有我们从header中传过来的token是否相等,并且把里面的角色id拿出来对比,是否一样,如果一样的话就验证通过,如果不一样的话就不行;所以我们在gateway模块也集成了在satoken模块的喜多相同的配置信息,方便直接查找,不用手动;后续如果想要校验其他的或者鉴权也可以直接使用StpUtil的内部的方法,只要token传对了就行;
那么我们接着走:修改gateway的satokenConfig的
把验证登录修改为鉴权为有没有admin这个角色权限,重启gateway小模块测试一下:
失败咯,没有这个角色权限呢。。。又有细心的同学发现了,我们在登陆的时候不是给了权限的吗,怎么不能像登录那样用token查到呢?
漏漏漏,当然不行,你这个给用户权限是在satoken模块,鉴权却是在gateway模块,token只携带了redis里面存的身份验证的信息,没有鉴权的信息,那么这个时候就可以把这个类复制一遍拿到gateway里面的config包下,重新给权限(重新给是有点傻的)可以让satoken暴露出一个接口是查询用户权限信息的然后返回,我们在gateway重写这个类之后,在里面用openFeign远程调用一下这个接口,然后给进去,也不是特别难得方法,大家可以下去试试
大家可以自己试试登出的方法,我这里就不演示了。。。。
五、最后
大家看到这里应该也大差不差的懂了一些了吧,我说一点自己的想法,上面的那个satoken登录之后写入reids,本来我是不想用的,我自己集成reids写的一个生成和存取token的类,但是我发现那个需要自己手动判断登录,而且不能再其他地方使用鉴权,我就放弃了,改用他自己的这个了,用下来感觉也不错。如果大家还有什么问题可以直接评论区问我哟,虽然我也是小白。。一起加油,代码包在下方gitee,大家自取喔。(如有错误请指正,谢谢啦)
sa_token_demo
还没有评论,来说两句吧...