当前位置:   article > 正文

前后端分离,使用sa-token作为安全框架快速搭建一个微服务项目_sa-token 实战项目

sa-token 实战项目

目录

1、项目简介

2、新建一个maven的聚合工程,并引入一些相应的版本控制。

2.1、新建一个maven项目,去掉src目录。作为我们整个微服务项目的父模块,并在这个模块中进行依赖的管理

2.2、在父模块下新建三个子模块(sa-gateway、sa-sysuser、sa-user)

2.3、sa-gateway网关的一些编码与配置

2.4、后台服务端sa-sysuser的一些编码与配置

3、总结


之前写过一个单体项目,现在想把它升级为微服务项目。在拆分升级的过程中发现了很多问题,本次就来记录一下遇到的问题和解决方法。(这篇文章只是记录一下拆分项目的基础架构,并使用sa-token做微服务项目的安全框架,快速搭建起一个微服务项目)

sa-token的官网:Sa-Token

1、项目简介

之前的项目是一个基于B2C的单体商城项目。使用到的技术栈有spring boot3.1.5、MySQL8.0.30、redis7.0.10,使用minio作为项目的文件上传,使用spring security作为项目的安全框架;使用vue3+element-plus开发前端,并最终将整个项目部署到nginx上。

本次重新拆分这个单体项目,使之成为一个微服务项目。本次我打算使用spring cloud加spring cloud Alibaba作为微服务项目的支撑点,使用sa-token作为整个项目的安全框架,还是使用vue3开发前端。

业务:本次项目是一个基于B2C的商城项目,项目有两个端。

前台的客户端(user)和后台的管理端(admin)。项目启动之后,默认的页面就是前台客户的首页,类似于京东和淘宝的首页。客户可以在这个首页浏览商品,搜索商品等。这些共性的功能是不需要客户进行登陆的,只有在涉及到一些敏感的操作时才会要求客户进行登录(如:添加商品到购物车,客户购买商品提交订单等等);

后台的管理端(admin)是提供给商城的管理人员使用的。因此,要想进入后台的管理端必需要求员工先进行登录验证身份。登录之后,就可以查看商品的销售情况、查看订单、对商品进行一系列的操作、对优惠卷,积分等进行一些控制。当然要想完成以上的功能还需要拥有一些对应的权限和角色。相比较与前台的客户端,后台管理端对角色和权限的要求会十分严格,本次使用sa-token的也主要会是后台的管理端。

2、新建一个maven的聚合工程,并引入一些相应的版本控制。

我们就是在这个聚合工程中完成我们微服务的拆分与整合。

在此说明,本篇文章中,我只会搭建一些基础的框架,并使用sa-token完成我们对于后台管理端和前台客户的整合。因此,本篇文章中目前就只有:gateway网关模块、sysuser系统的用户模块、user前台客户的用户模块。一些其他的功能如商品模块、订单模块等等,我会在后续的文章中创建并实现其相应的功能。(我也会创建一个git仓库,将项目的源码,整体拷贝上去,有需要的可以自行拉取查看)

2.1、新建一个maven项目,去掉src目录。作为我们整个微服务项目的父模块,并在这个模块中进行依赖的管理

  1. <properties>
  2. <maven.compiler.source>17</maven.compiler.source>
  3. <maven.compiler.target>17</maven.compiler.target>
  4. <spring-cloud.version>2022.0.2</spring-cloud.version>
  5. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  6. <alibaba.version>2022.0.0.0-RC2</alibaba.version>
  7. <mysql.verison>8.0.30</mysql.verison>
  8. <fastjson.version>2.0.21</fastjson.version>
  9. <lombok.version>1.18.20</lombok.version>
  10. <mybatisplus.version>3.5.3.1</mybatisplus.version>
  11. <spring.boot.version>3.1.5</spring.boot.version>
  12. <pagehelper.version>1.4.3</pagehelper.version>
  13. <hutool.version>5.8.18</hutool.version>
  14. <knife4j.version>4.3.0</knife4j.version>
  15. <sa-token.version>1.37.0</sa-token.version>
  16. <druid.version>1.2.20</druid.version>
  17. </properties>
  18. <dependencyManagement>
  19. <dependencies>
  20. <dependency>
  21. <groupId>org.springframework.boot</groupId>
  22. <artifactId>spring-boot-starter-parent</artifactId>
  23. <version>${spring.boot.version}</version>
  24. <type>pom</type>
  25. <scope>import</scope>
  26. </dependency>
  27. <dependency>
  28. <groupId>com.alibaba</groupId>
  29. <artifactId>druid-spring-boot-starter</artifactId>
  30. <version>${druid.version}</version>
  31. </dependency>
  32. <!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
  33. <dependency>
  34. <groupId>cn.dev33</groupId>
  35. <artifactId>sa-token-spring-boot3-starter</artifactId>
  36. <version>${sa-token.version}</version>
  37. </dependency>
  38. <!-- Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.cc -->
  39. <dependency>
  40. <groupId>cn.dev33</groupId>
  41. <artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
  42. <version>${sa-token.version}</version>
  43. </dependency>
  44. <dependency>
  45. <groupId>cn.dev33</groupId>
  46. <artifactId>sa-token-redis-jackson</artifactId>
  47. <version>${sa-token.version}</version>
  48. </dependency>
  49. <dependency>
  50. <groupId>com.github.xiaoymin</groupId>
  51. <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
  52. <version>${knife4j.version}</version>
  53. </dependency>
  54. <!-- mybatisplus和spring boot整合的起步依赖 -->
  55. <dependency>
  56. <groupId>com.baomidou</groupId>
  57. <artifactId>mybatis-plus-boot-starter</artifactId>
  58. <version>${mybatisplus.version}</version>
  59. </dependency>
  60. <dependency>
  61. <groupId>cn.hutool</groupId>
  62. <artifactId>hutool-all</artifactId>
  63. <version>${hutool.version}</version>
  64. </dependency>
  65. <!-- mysql驱动 -->
  66. <dependency>
  67. <groupId>mysql</groupId>
  68. <artifactId>mysql-connector-java</artifactId>
  69. <version>${mysql.verison}</version>
  70. </dependency>
  71. <!--fastjson-->
  72. <dependency>
  73. <groupId>com.alibaba</groupId>
  74. <artifactId>fastjson</artifactId>
  75. <version>${fastjson.version}</version>
  76. </dependency>
  77. <!-- lombok依赖 -->
  78. <dependency>
  79. <groupId>org.projectlombok</groupId>
  80. <artifactId>lombok</artifactId>
  81. <version>${lombok.version}</version>
  82. </dependency>
  83. <!--spring cloud-->
  84. <dependency>
  85. <groupId>org.springframework.cloud</groupId>
  86. <artifactId>spring-cloud-dependencies</artifactId>
  87. <version>${spring-cloud.version}</version>
  88. <type>pom</type>
  89. <scope>import</scope>
  90. </dependency>
  91. <!--spring cloud alibaba-->
  92. <dependency>
  93. <groupId>com.alibaba.cloud</groupId>
  94. <artifactId>spring-cloud-alibaba-dependencies</artifactId>
  95. <version>${alibaba.version}</version>
  96. <type>pom</type>
  97. <scope>import</scope>
  98. </dependency>
  99. <!-- 分页插件-->
  100. <dependency>
  101. <groupId>com.github.pagehelper</groupId>
  102. <artifactId>pagehelper-spring-boot-starter</artifactId>
  103. <version>${pagehelper.version}</version>
  104. </dependency>
  105. </dependencies>
  106. </dependencyManagement>

注意:由dependencyManagement的依赖坐标并不会自动下载,它只是用于集中管理项目中依赖项的版本号。如果这些依赖版本你本地的maven仓库没有,那么就会爆红,这并不是错误。你只需要在子模块中使用到了相应的依赖,这时才会去中央仓库进行下载。下载之后,父模块的pom文件中爆红的坐标就会正常了。

2.2、在父模块下新建三个子模块(sa-gateway、sa-sysuser、sa-user)

分别在三个模块中引入整合sa-token所需要的依赖。

详细请看sa-token的官网:依赖引入说明 (sa-token.cc)

使用nacos作为注册中心,在三个模块中引入nacos的相应依赖,并配置好yml的连接参数。

如图所示:

可以看到我们的三个子模块都注入到了nacos中。

在创建这三个子模块之后,我又新建了一个common公共模块,用来存放整合微服务项目用到的公共方法和依赖。使项目中的所有子模块都引入common公共模块的依赖,由于这个common公共模块只是用来存放一些公共的方法,所以,这个模块是不需要入驻到nacos中的,它甚至不需要主启动类。

如果注册nacos有不懂或出错的地方,可以参考一下这篇文章:springboot3整合nacos实现注册中心和配置中心(详细入门)_springboot3 nacos-CSDN博客

2.3、sa-gateway网关的一些编码与配置

(注意,我本次微服务项目的数据库采用的是分库,分表的形式。既每一个子模块都有一个相应的数据库与之对应。但是,对于redis,我整个微服务连接的是同一个redis。如果,你的微服务项目架构与我的不同,可能需要修改一下对于sa-token的配置。具体的可以查看一下sa-token的官网)

由于使用vue3作为前端项目,直接访问后端会出现跨域问题,因此需要在gateway网关出配置跨域的参数以及一些服务的映射:

  1. spring:
  2. profiles:
  3. active: dev
  4. cloud:
  5. gateway:
  6. routes:
  7. - id: sa-user #路由规则id,自定义,唯一
  8. uri: lb://sa-user #路由目标的微服务,lb代表负载均衡
  9. predicates: #路由断言,判断请求是否符合规则,符合则路由到目标
  10. - Path= /user/** #以请求路径做判断,以/user开头的符合
  11. - id: sa-sysuser #路由规则id,自定义,唯一
  12. uri: lb://sa-sysuser #路由目标的微服务,lb代表负载均衡
  13. predicates: #路由断言,判断请求是否符合规则,符合则路由到目标
  14. - Path= /sys/** #以请求路径做判断,以/user开头的符合
  15. globalcors:
  16. cors-configurations:
  17. '[/**]':
  18. allowedOrigins: "*"
  19. allowedMethods: "*"
  20. allowedHeaders: "*"

在这里重新理一下我们的思路。后台管理端的所有接口都要拦截,只有登录过后才能访问。而前台客户端大部分接口都可以直接访问,只有一部分接口才需要登录之后访问。

基于这个思想,我们就可以在gateway网关中实现sa-token的过滤器了。

  1. @Configuration
  2. @Slf4j
  3. public class SaTokenConfigure {
  4. /**
  5. * 重写 Sa-Token 框架内部算法策略
  6. */
  7. @Autowired
  8. private JwtUtil jwtUtil;
  9. @Autowired
  10. public void rewriteSaStrategy() {
  11. // 重写 Token 生成策略
  12. SaStrategy.instance.createToken = (loginId, loginType) -> {
  13. // return SaFoxUtil.getRandomString(60); // 随机60位长度字符串
  14. return jwtUtil.generateToken((Integer) loginId); // 使用 JWT 方式生成 Token
  15. };
  16. }
  17. // 排除sys系统中不需要拦截的路径
  18. @Autowired
  19. private ExcludeSysPath excludeSysPath;
  20. // 注册 Sa-Token全局过滤器
  21. @Bean
  22. public SaReactorFilter getSaReactorFilter() {
  23. return new SaReactorFilter()
  24. .addInclude("/**") // 拦截所有请求
  25. // 鉴权方法:每次访问进入
  26. .setAuth(obj -> {
  27. // 登录校验 -- 拦截所有后台管理端的所有路由,并放开一些特定的接口
  28. SaRouter.
  29. match("/sys/**").
  30. notMatch(excludeSysPath.getSyspaths()).
  31. check(r->StpUtil.checkLogin());
  32. })
  33. // 异常处理方法:每次setAuth函数出现异常时进入
  34. .setError(e -> {
  35. log.error("出现登录异常=======>"+e.getMessage());
  36. return Result.errorData(e.getMessage());
  37. })
  38. //当你使用 header 头提交 token 时,会产生跨域问题。解决方法== 前置函数:在每次认证函数之前执行
  39. .setBeforeAuth(obj -> {
  40. SaHolder.getResponse()
  41. // ---------- 设置跨域响应头 ----------
  42. // 允许指定域访问跨域资源
  43. .setHeader("Access-Control-Allow-Origin", "*")
  44. // 允许所有请求方式
  45. .setHeader("Access-Control-Allow-Methods", "*")
  46. // 允许的header参数
  47. .setHeader("Access-Control-Allow-Headers", "*")
  48. // 有效时间
  49. .setHeader("Access-Control-Max-Age", "3600")
  50. ;
  51. // 如果是预检请求,则立即返回到前端
  52. SaRouter.match(SaHttpMethod.OPTIONS)
  53. .free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
  54. .back();
  55. })
  56. ;
  57. }
  58. }

在这个类中,我注册了sa-token的全局过滤器,并且自定义了token的生成风格(使用jwt根据签名来生成token,并指定过期时间。这个jwt的工具类我放在common公共模块了),而不是使用sa-token官方的uuid方式。

在拦截路径时,只拦截了与后台管理端(admin)相关的请求。并且,放行了一些路径。如(后台管理的登录接口,验证码的生成接口等等),前台客户端我暂时还没有拦截,这个要拦截的话,牵扯到的接口就比较细分了,并且还牵扯到订单、购物车的接口开发,所以在这个sa-token的全局过滤器中没有拦截。在使用sa-token的全局过滤器时出现了跨域问题。我在前端使用header头提交token时,会出现跨域问题,不过sa-token的官方已经为我们提供好了解决方法。

使用 Sa-Token 的全局过滤器解决跨域问题(三种方式全版) - 掘金 (juejin.cn)

设置内部服务的外网隔离,并且在用户登录成功之后,对token的过期时间进行刷新(如果你使用sa官方的token就不需要手动刷新。只需要在yml配置文件中指定刷新时间即可。但是我这里是使用jwt生成的token,还是需要手动刷新一下的),我们可以使用网关的全局过滤器来实现

  1. @Component
  2. @Order(100)
  3. @Slf4j
  4. public class ForwardAuthFilter implements GlobalFilter {
  5. @Autowired
  6. private JwtUtil jwtUtil;
  7. @Override
  8. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  9. // 在请求头中添加凭证
  10. ServerHttpRequest newRequest = exchange
  11. .getRequest()
  12. .mutate()
  13. .header("sa-gateway","zhangqiao")
  14. .build();
  15. ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
  16. //后台管理端,用户登录成功,刷新token
  17. List<String> list = exchange.getRequest().getHeaders().get("token");
  18. if (list != null && list.size() > 0) {
  19. String token = list.get(0);
  20. // 刷新token
  21. String refreshToken = jwtUtil.refreshToken(token);
  22. log.info("刷新后的token为:" + refreshToken);
  23. }
  24. //前台用户端,用户登录成功,刷新token
  25. List<String> userList = exchange.getRequest().getHeaders().get("usertoken");
  26. if (userList != null && userList.size() > 0) {
  27. String token = userList.get(0);
  28. // 刷新token
  29. String refreshToken = jwtUtil.refreshToken(token);
  30. log.info("刷新后的usertoken为:" + refreshToken);
  31. }
  32. // 继续过滤链
  33. return chain.filter(newExchange);
  34. }
  35. }

既然已经在网关出添加了特殊的请求头,那么,在各个子模块(除网关)中就要去判断请求的合法性。我们可以把这个判断方法写在common公共模块中,供其他模块的使用。(注意要排除网关服务)

在common模块中:

  1. @Configuration
  2. //排除网关服务
  3. @ConditionalOnClass(DispatcherServlet.class)
  4. @Slf4j
  5. public class SaTokenFilter implements WebMvcConfigurer {
  6. // 注册 Sa-Token 全局过滤器
  7. @Bean
  8. public SaServletFilter getSaServletFilter() {
  9. return new SaServletFilter()
  10. .addInclude("/**")
  11. .addExclude("/favicon.ico")
  12. .setAuth(obj -> {
  13. // 校验是否由网关发出的请求 身份凭证
  14. String header = SaHolder.getRequest().getHeader("sa-gateway");
  15. if(header==null){
  16. throw new ResultException(555,"没有经过网关");
  17. }
  18. if (!"zhangqiao".equals(header)){
  19. throw new ResultException(555,"网关发布的凭证不对");
  20. }
  21. })
  22. .setError(e -> Result.errorData(e.getMessage()))
  23. ;
  24. }
  25. }

这样,除gateway网关模块外,所有的子服务模块在接到请求时,都会验证请求的合法性。如果是从网关转发的请求,请求头上都会携带相应的数据。如果是直接访问子模块的请求,那么会抛出异常。

项目写到这里,我们可以先进行一些测试,以保证我们代码的正确性;

我在sa-sysuser模块中,写了一个生成验证码的方法,并进行了放行。写了一个测试方法,没有放行。现在我们来验证一下我们之前写的代码正确性;

通过网关访问验证码方法,运行结果为:

可以看到,返回了我们想要的数据。

我们直接访问sa-sysuser模块的验证码方法,运行结果为:

可以看到出现了异常,这是正确的,因为它没有从网关访问。并且报错结果就是我们自定义抛出的异常。

访问我在sa-sysuser写的没有放行的方法,运行结果为:

可以看到爆出了一个异常,这是正确的。因为我们设置后台管理端的所有接口都要先登录才能访问,此时用户没有登录,所以访问不到相应的方法。

2.4、后台服务端sa-sysuser的一些编码与配置

首先,设置与gateway网关想同的token生成策略;

  1. @Configuration
  2. public class SaTokenConfigure {
  3. /**
  4. * 重写 Sa-Token 框架内部算法策略
  5. */
  6. @Autowired
  7. private JwtUtil jwtUtil;
  8. @Autowired
  9. public void rewriteSaStrategy() {
  10. // 重写 Token 生成策略
  11. SaStrategy.instance.createToken = (loginId, loginType) -> {
  12. // return SaFoxUtil.getRandomString(60); // 随机60位长度字符串
  13. return jwtUtil.generateToken((Integer) loginId); // 使用 JWT 方式生成 Token
  14. };
  15. }
  16. }

接下来,就是我们的重点,设计登录方法

相应的controller层:
 

  1. //登录
  2. @PostMapping("/login")
  3. public Result<String> login(@RequestBody LoginDto loginDto){
  4. String token = userService.login(loginDto);
  5. return Result.successData(token);
  6. }

对应实现的service层:
 

  1. //用户登录方法
  2. @Override
  3. public String login(LoginDto loginDto) {
  4. // 先检验验证码
  5. String codeRedis = redisTemplate.opsForValue().get(loginDto.getCodeKey());
  6. if (codeRedis==null){
  7. throw new ResultException(555,"验证码不存在");
  8. }
  9. if (!codeRedis.equals(loginDto.getCodeValue().toLowerCase())) {
  10. throw new ResultException(555, "验证码错误");
  11. }
  12. // 验证码正确,删除redis中的验证码
  13. redisTemplate.delete(loginDto.getCodeKey());
  14. // 用户登录
  15. User sysUser = this.getOne(new LambdaQueryWrapper<User>().eq(User::getUsername, loginDto.getUsername()));
  16. if(sysUser==null){
  17. throw new ResultException(555,"用户不存在");
  18. }
  19. String md5 = SaSecureUtil.md5(loginDto.getPassword());
  20. if (!md5.equals(sysUser.getPassword())){
  21. throw new ResultException(555,"密码错误");
  22. }
  23. //根据用户id登录,第1步,先登录上
  24. StpUtil.login(sysUser.getId());
  25. // 第2步,获取 Token 相关参数
  26. SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
  27. // 第3步,返回给前端
  28. return tokenInfo.getTokenValue();
  29. }

在这个登录方法中,我们先判断验证码,如果验证码正确。再根据用户名从数据库中查询用户数据(用户名是唯一的)。再将查询出来的用户数据的密码与前端传入的密码进行比较。注意比较的时候要进行相应的加密和解密,我使用的是md5加密方式,所以相对比较简单。你应该根据你的加密方式对密码进行相应的处理。

认证用户完成之后,我们就可以调用.login方法进行登陆了。这个login方法是sa-token官方封装好的。它默认完成的方法有:

  1. 检查此账号是否之前已有登录;
  2. 为账号生成 Token 凭证与 Session 会话;
  3. 记录 Token 活跃时间;
  4. 通知全局侦听器,xx 账号登录成功;
  5. 将 Token 注入到请求上下文;
  6. 将用户信息存入redis,并设置过期时间

然后获取相应的token,并传入到前端。前端会根据后端的返回结果判断用户登录是否成功。如果成功,那么将token存入pinia中,并在接下来每次请求时都会将token数据放在请求头中。

获取用户信息:

在vue3前端项目中,用户登录成功之后,会跳转到首页。这时会挂载一个onMounted方法,这时会先根据token获取用户的信息。由于我们这个是后台管理端(admin),非常看重权限。所以,我们在返回用户信息时要携带上用户拥有的权限和角色。(在返回用户信息时,要不要携带上用户的权限是根据项目的实际情况而定。如果,项目比较看重权限就返回。如果不是很看重权限就可以选择不返回。比如我们设计的前台客户端user就不需要返回相应的权限)

controller层:
 

  1. //获取用户信息
  2. @GetMapping("/getUserInfo")
  3. public Result<SysUserInfo> getSysUSerInfo(@RequestHeader("token") String token){
  4. // System.out.println("token=================>"+token);
  5. SysUserInfo sysUser=userService.getSysUSerInfo(token);
  6. return Result.successData(sysUser);
  7. }

service层的具体实行:
 

  1. @Autowired
  2. private StpInterfaceImpl stpInterface;
  3. //获取用户信息
  4. @Override
  5. public SysUserInfo getSysUSerInfo(String token) {
  6. Integer userId = jwtUtil.getUsernameFromToken(token);
  7. User user = this.getById(userId);
  8. if(user==null){
  9. throw new ResultException(555,"用户不存在");
  10. }
  11. //获取用户权限
  12. List<String> permissionList = stpInterface.getPermissionList(userId, "sys");
  13. //获取用户角色
  14. List<String> roleList = stpInterface.getRoleList(userId, "sys");
  15. return new SysUserInfo(permissionList,roleList,user);
  16. }

注意,我调用了权限接口的方法,用来获取相应的用户的权限和角色。

实现的权限接口如下:

  1. @Service
  2. public class StpInterfaceImpl implements StpInterface {
  3. // 用户角色表
  4. @Autowired
  5. private IUserRoleService sysUserRoleService;
  6. //角色权限表
  7. @Autowired
  8. private IRolePermissionService sysRoleMenuService;
  9. // 权限表
  10. @Autowired
  11. private IPermissionService sysMenuService;
  12. // 角色表
  13. @Autowired
  14. private IRoleService sysRoleService;
  15. @Autowired
  16. private RedisTemplate<String,String> redisTemplate;
  17. @Override
  18. public List<String> getPermissionList(Object loginId, String loginType) {
  19. // 先从redis中查询用户权限,查不到再从数据库查
  20. String redisPermissionList = redisTemplate.opsForValue().get("permissionList" + loginId);
  21. if (redisPermissionList!=null){
  22. // 返回此 loginId 拥有的权限列表
  23. return JSON.parseArray(redisPermissionList, String.class);
  24. }
  25. // 返回此 loginId 拥有的权限列表
  26. List<UserRole> sysUserRoles = sysUserRoleService.list(new LambdaQueryWrapper<UserRole>().
  27. eq(UserRole::getUserId, Integer.parseInt(loginId.toString())));
  28. // 得到角色id集合
  29. List<Integer> roleIds = sysUserRoles.stream().map(UserRole::getRoleId).toList();
  30. if (roleIds.size()== 0){
  31. return null;
  32. }
  33. List<String> list=new ArrayList<>();
  34. // 根据角色id查权限id
  35. roleIds.forEach(roleId -> {
  36. List<RolePermission> roleMenus = sysRoleMenuService.list(new LambdaQueryWrapper<RolePermission>().
  37. eq(RolePermission::getRoleId, roleId));
  38. List<Integer> mends = roleMenus.stream().map(RolePermission::getPermissionId).toList();
  39. mends.forEach(menuId ->{
  40. Permission sysMenu = sysMenuService.getById(menuId);
  41. list.add(sysMenu.getName());
  42. });
  43. });
  44. // 将查询到的权限放到redis中,便于下次的直接使用
  45. redisTemplate.opsForValue().set("permissionList:"+loginId,list.toString(),30, TimeUnit.MINUTES);
  46. // 返回此 loginId 拥有的权限列表
  47. return list;
  48. }
  49. @Override
  50. public List<String> getRoleList(Object loginId, String loginType) {
  51. // 先从redis中查询用户权限,查不到再从数据库查
  52. String redisPermissionList = redisTemplate.opsForValue().get("roleList" + loginId);
  53. if (redisPermissionList!=null){
  54. // 返回此 loginId 拥有的权限列表
  55. return JSON.parseArray(redisPermissionList, String.class);
  56. }
  57. // 返回此 loginId 拥有的角色列表
  58. List<UserRole> sysUserRoles = sysUserRoleService.list(new LambdaQueryWrapper<UserRole>().
  59. eq(UserRole::getUserId, Integer.parseInt(loginId.toString())));
  60. // 得到角色id集合
  61. List<Integer> roleIds = sysUserRoles.stream().map(UserRole::getRoleId).toList();
  62. if (roleIds.size()== 0){
  63. return null;
  64. }
  65. List<String> list=new ArrayList<>();
  66. roleIds.forEach(roleId->{
  67. Role role = sysRoleService.getById(roleId);
  68. list.add(role.getName());
  69. });
  70. redisTemplate.opsForValue().set("roleList:"+loginId,list.toString(),30, TimeUnit.MINUTES);
  71. // 返回此 loginId 拥有的角色列表
  72. return list;
  73. }
  74. }

我所实现的是标准的RBAC(基于用户、角色、权限的访问控制模型)。所以,在得到用户id的情况下、先根据用户角色表查出角色id、在根据角色权限表查询权限id,在根据权限表查出具体权限名称。

上面使用了Mybatis-plus的条件构造器和stream流的形式进行查询。

至此,就完成了用户信息的查询和返回。

用户信息的修改:

用户登录成功之后,是有权对自己的信息进行一些修改的。

controller层:
 

  1. //修改用户的信息
  2. @PostMapping("/updateUserInfo")
  3. public Result<String> updateUserInfo(@RequestBody User user, @RequestHeader("token") String token){
  4. userService.updateUserInfo(user,token);
  5. return Result.success();
  6. }

service层实现:

  1. //修改用户的信息
  2. @Override
  3. public void updateUserInfo(User user, String token) {
  4. Integer userId = jwtUtil.getUsernameFromToken(token);
  5. // 保证业务的健壮性,如果用户不存在,则抛出异常
  6. if(userId==null){
  7. throw new ResultException(555,"用户不存在");
  8. }
  9. User redisUser = this.getById(userId);
  10. if (redisUser==null){
  11. throw new ResultException(555,"用户不存在");
  12. }
  13. // 用户修改了它的用户名,传入的用户名和redis中的用户名不一致。
  14. if (!user.getUsername().equals(redisUser.getUsername())){
  15. User user2 = this.getOne(new LambdaQueryWrapper<User>().
  16. eq(User::getUsername,user.getUsername()));
  17. // 如果数据库中已经存在了该用户名,则抛出异常
  18. if (user2!=null){
  19. throw new ResultException(555,"用户名已存在,请你再换一个");
  20. }
  21. // 如果用户修改了密码,则对密码进行加密
  22. if(user.getPassword()!=null){
  23. String md5 = SaSecureUtil.md5(user.getPassword());
  24. user.setPassword(md5);
  25. }
  26. this.updateById(user);
  27. log.info("修改用户信息成功");
  28. }
  29. // 用户没有修改它的用户名
  30. // 如果用户修改了密码,则对密码进行加密
  31. if(user.getPassword()!=null){
  32. String md5 = SaSecureUtil.md5(user.getPassword());
  33. user.setPassword(md5);
  34. }
  35. this.updateById(user);
  36. log.info("修改用户信息成功");
  37. }

注意修改用户信息时,要特别关注用户名。因为在我们设计中,用户名称是唯一的,用来标识用户的身份信息。所以在向数据库中添加时,一定要保证用户的名称唯一。还有如果用户修改了密码,要先将密码进行加密之后才能存进数据库中。总之,要保证逻辑的严谨性和代码的健壮性。多使用一些if else没事的,反正不会影响到时间复杂度的。

退出接口:

根据token进行退出,由于我们生成的token是jwt形式的。所以,我们自需要删除后端redis中的token信息和前端的pinia中的token数据,就算是完成用户退出了。

controller层方法:
 

  1. //退出登录
  2. @GetMapping("/logout")
  3. public Result<String> logout(@RequestHeader("token")String token){
  4. userService.logout(token);
  5. return Result.success();
  6. }

service层实现:

  1. // 退出登录
  2. @Override
  3. public void logout(String token) {
  4. Integer userId = jwtUtil.getUsernameFromToken(token);
  5. if(userId==null){
  6. throw new ResultException(555,"用户不存在");
  7. }
  8. //退出登陆时,sa-token会自动删除redis中的数据
  9. StpUtil.logout(userId);
  10. }

现在,我们就完成了所有的基础操作。(具体的前端代码我就不在这个进行叙述了,我放在了git仓库中,有需要的可以自行查看)现在,我们将前后端的项目都运行起来,进行相应的测试;

用户的登录操作:

用户登录成功之后,跳转到相应的首页:

后端,sa-sysuser模块的输出日志:

在前端,登录成功之后,自动跳转到首页,并获取登录用户的所有权限,我在前端打印在控制台中了:

接下来,测试一下。用户退出操作:

用户退出成功之后,跳转到登录页面

后端的sa-sysuser模块的日志输出:

3、总结

至此,我们所有的操作就完成了。理一下思路,我们在gateway网关中注册了sa-token的全局过滤器,并对后台管理端的所有接口都进行了拦截,只放行了一些特定的接口。然后在sa-sysuser模块中对后台用户进行了一些操作。主要是登录、获取用户信息、用户登出。在这个过程中,我们针对sa-token的封装主要有:在gateway模块和sa-sysuser模块都引入sa整合redis的依赖,并配置了相同的redis缓存。在网关中解决了跨域和携带请求头的跨域问题,重新指定了token的生成策略。

我在这里只实现了后台管理端的用户生成,而没有实现前台客户端的用户生成。这篇文章写的挺多了,在下一篇文章中会实现前台客户端的用户一系列操作。这里同样使用到了sa-token。只不过我们一个项目里面有两套账号体系。具体的实现逻辑可以参考sa官方的

多账号认证 (sa-token.cc)

git的地址:sa-cloudDemo: 用来构建使用sa-token的微服务快速开发项目

前端的代码也在这个微服务项目中。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/927277
推荐阅读
相关标签
  

闽ICP备14008679号