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

之前写过一个单体项目,现在想把它升级为微服务项目。在拆分升级的过程中发现了很多问题,本次就来记录一下遇到的问题和解决方法。(这篇文章只是记录一下拆分项目的基础架构,并使用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目录。作为我们整个微服务项目的父模块,并在这个模块中进行依赖的管理

<properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><spring-cloud.version>2022.0.2</spring-cloud.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><alibaba.version>2022.0.0.0-RC2</alibaba.version><mysql.verison>8.0.30</mysql.verison><fastjson.version>2.0.21</fastjson.version><lombok.version>1.18.20</lombok.version><mybatisplus.version>3.5.3.1</mybatisplus.version><spring.boot.version>3.1.5</spring.boot.version><pagehelper.version>1.4.3</pagehelper.version><hutool.version>5.8.18</hutool.version><knife4j.version>4.3.0</knife4j.version><sa-token.version>1.37.0</sa-token.version><druid.version>1.2.20</druid.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>${spring.boot.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>${druid.version}</version></dependency><!-- Sa-Token 权限认证,在线文档:https://sa-token.cc --><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot3-starter</artifactId><version>${sa-token.version}</version></dependency><!-- Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.cc --><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-reactor-spring-boot3-starter</artifactId><version>${sa-token.version}</version></dependency><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-redis-jackson</artifactId><version>${sa-token.version}</version></dependency><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId><version>${knife4j.version}</version></dependency><!-- mybatisplus和spring boot整合的起步依赖 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatisplus.version}</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>${hutool.version}</version></dependency><!-- mysql驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql.verison}</version></dependency><!--fastjson--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>${fastjson.version}</version></dependency><!-- lombok依赖 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version></dependency><!--spring cloud--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency><!--spring cloud alibaba--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>${alibaba.version}</version><type>pom</type><scope>import</scope></dependency><!--            分页插件--><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>${pagehelper.version}</version></dependency></dependencies></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网关出配置跨域的参数以及一些服务的映射:

spring:profiles:active: devcloud:gateway:routes:- id: sa-user  #路由规则id,自定义,唯一uri: lb://sa-user  #路由目标的微服务,lb代表负载均衡predicates: #路由断言,判断请求是否符合规则,符合则路由到目标- Path= /user/**    #以请求路径做判断,以/user开头的符合- id: sa-sysuser       #路由规则id,自定义,唯一uri: lb://sa-sysuser    #路由目标的微服务,lb代表负载均衡predicates: #路由断言,判断请求是否符合规则,符合则路由到目标- Path= /sys/** #以请求路径做判断,以/user开头的符合globalcors:cors-configurations:'[/**]':allowedOrigins: "*"allowedMethods: "*"allowedHeaders: "*"

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

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

@Configuration
@Slf4j
public class SaTokenConfigure {/*** 重写 Sa-Token 框架内部算法策略*/@Autowiredprivate JwtUtil jwtUtil;@Autowiredpublic void rewriteSaStrategy() {// 重写 Token 生成策略SaStrategy.instance.createToken = (loginId, loginType) -> {
//            return SaFoxUtil.getRandomString(60);    // 随机60位长度字符串return jwtUtil.generateToken((Integer) loginId);    // 使用 JWT 方式生成 Token};}//    排除sys系统中不需要拦截的路径@Autowiredprivate ExcludeSysPath excludeSysPath;// 注册 Sa-Token全局过滤器@Beanpublic SaReactorFilter getSaReactorFilter() {return new SaReactorFilter().addInclude("/**") // 拦截所有请求// 鉴权方法:每次访问进入.setAuth(obj -> {// 登录校验 -- 拦截所有后台管理端的所有路由,并放开一些特定的接口SaRouter.match("/sys/**").notMatch(excludeSysPath.getSyspaths()).check(r->StpUtil.checkLogin());})// 异常处理方法:每次setAuth函数出现异常时进入.setError(e -> {log.error("出现登录异常=======>"+e.getMessage());return Result.errorData(e.getMessage());})//当你使用 header 头提交 token 时,会产生跨域问题。解决方法== 前置函数:在每次认证函数之前执行.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();});}}

在这个类中,我注册了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,还是需要手动刷新一下的),我们可以使用网关的全局过滤器来实现

@Component
@Order(100)
@Slf4j
public class ForwardAuthFilter implements GlobalFilter {@Autowiredprivate JwtUtil jwtUtil;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//        在请求头中添加凭证ServerHttpRequest newRequest = exchange.getRequest().mutate().header("sa-gateway","zhangqiao").build();ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
//后台管理端,用户登录成功,刷新tokenList<String> list = exchange.getRequest().getHeaders().get("token");if (list != null && list.size() > 0) {String token = list.get(0);
//            刷新tokenString refreshToken = jwtUtil.refreshToken(token);log.info("刷新后的token为:" + refreshToken);}//前台用户端,用户登录成功,刷新tokenList<String> userList = exchange.getRequest().getHeaders().get("usertoken");if (userList != null && userList.size() > 0) {String token = userList.get(0);
//            刷新tokenString refreshToken = jwtUtil.refreshToken(token);log.info("刷新后的usertoken为:" + refreshToken);}// 继续过滤链return chain.filter(newExchange);}
}

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

在common模块中:

@Configuration
//排除网关服务
@ConditionalOnClass(DispatcherServlet.class)
@Slf4j
public class SaTokenFilter implements WebMvcConfigurer {// 注册 Sa-Token 全局过滤器@Beanpublic SaServletFilter getSaServletFilter() {return new SaServletFilter().addInclude("/**").addExclude("/favicon.ico").setAuth(obj -> {// 校验是否由网关发出的请求 身份凭证String header = SaHolder.getRequest().getHeader("sa-gateway");if(header==null){throw new ResultException(555,"没有经过网关");}if (!"zhangqiao".equals(header)){throw new ResultException(555,"网关发布的凭证不对");}}).setError(e -> Result.errorData(e.getMessage()));}
}

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

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

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

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

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

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

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

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

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

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

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

@Configuration
public class SaTokenConfigure {/*** 重写 Sa-Token 框架内部算法策略 */@Autowiredprivate JwtUtil jwtUtil;@Autowiredpublic void rewriteSaStrategy() {// 重写 Token 生成策略 SaStrategy.instance.createToken = (loginId, loginType) -> {
//            return SaFoxUtil.getRandomString(60);    // 随机60位长度字符串return jwtUtil.generateToken((Integer) loginId);    // 使用 JWT 方式生成 Token};}
}

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

相应的controller层:
 

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

对应实现的service层:
 

//用户登录方法@Overridepublic String login(LoginDto loginDto) {
//        先检验验证码String codeRedis = redisTemplate.opsForValue().get(loginDto.getCodeKey());if (codeRedis==null){throw new ResultException(555,"验证码不存在");}if (!codeRedis.equals(loginDto.getCodeValue().toLowerCase())) {throw new ResultException(555, "验证码错误");}
//        验证码正确,删除redis中的验证码redisTemplate.delete(loginDto.getCodeKey());
//        用户登录User sysUser = this.getOne(new LambdaQueryWrapper<User>().eq(User::getUsername, loginDto.getUsername()));if(sysUser==null){throw new ResultException(555,"用户不存在");}String md5 = SaSecureUtil.md5(loginDto.getPassword());if (!md5.equals(sysUser.getPassword())){throw new ResultException(555,"密码错误");}//根据用户id登录,第1步,先登录上StpUtil.login(sysUser.getId());// 第2步,获取 Token  相关参数SaTokenInfo tokenInfo = StpUtil.getTokenInfo();// 第3步,返回给前端return tokenInfo.getTokenValue();}

在这个登录方法中,我们先判断验证码,如果验证码正确。再根据用户名从数据库中查询用户数据(用户名是唯一的)。再将查询出来的用户数据的密码与前端传入的密码进行比较。注意比较的时候要进行相应的加密和解密,我使用的是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层:
 

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

service层的具体实行:
 

  @Autowiredprivate StpInterfaceImpl stpInterface;//获取用户信息@Overridepublic SysUserInfo getSysUSerInfo(String token) {Integer userId = jwtUtil.getUsernameFromToken(token);User user = this.getById(userId);if(user==null){throw new ResultException(555,"用户不存在");}//获取用户权限List<String> permissionList = stpInterface.getPermissionList(userId, "sys");//获取用户角色List<String> roleList = stpInterface.getRoleList(userId, "sys");return new SysUserInfo(permissionList,roleList,user);}

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

实现的权限接口如下:

@Service
public class StpInterfaceImpl implements StpInterface {//    用户角色表@Autowiredprivate IUserRoleService sysUserRoleService;//角色权限表@Autowiredprivate IRolePermissionService sysRoleMenuService;//    权限表@Autowiredprivate IPermissionService sysMenuService;
//    角色表@Autowiredprivate IRoleService sysRoleService;@Autowired
private RedisTemplate<String,String> redisTemplate;@Overridepublic List<String> getPermissionList(Object loginId, String loginType) {
//        先从redis中查询用户权限,查不到再从数据库查String redisPermissionList = redisTemplate.opsForValue().get("permissionList" + loginId);if (redisPermissionList!=null){// 返回此 loginId 拥有的权限列表return JSON.parseArray(redisPermissionList, String.class);}// 返回此 loginId 拥有的权限列表List<UserRole> sysUserRoles = sysUserRoleService.list(new LambdaQueryWrapper<UserRole>().eq(UserRole::getUserId, Integer.parseInt(loginId.toString())));
//        得到角色id集合List<Integer> roleIds = sysUserRoles.stream().map(UserRole::getRoleId).toList();if (roleIds.size()== 0){return null;}List<String> list=new ArrayList<>();
//    根据角色id查权限idroleIds.forEach(roleId -> {List<RolePermission> roleMenus = sysRoleMenuService.list(new LambdaQueryWrapper<RolePermission>().eq(RolePermission::getRoleId, roleId));List<Integer> mends = roleMenus.stream().map(RolePermission::getPermissionId).toList();mends.forEach(menuId ->{Permission sysMenu = sysMenuService.getById(menuId);list.add(sysMenu.getName());});});
//     将查询到的权限放到redis中,便于下次的直接使用redisTemplate.opsForValue().set("permissionList:"+loginId,list.toString(),30, TimeUnit.MINUTES);// 返回此 loginId 拥有的权限列表return list;}@Overridepublic List<String> getRoleList(Object loginId, String loginType) {//        先从redis中查询用户权限,查不到再从数据库查String redisPermissionList = redisTemplate.opsForValue().get("roleList" + loginId);if (redisPermissionList!=null){// 返回此 loginId 拥有的权限列表return JSON.parseArray(redisPermissionList, String.class);}// 返回此 loginId 拥有的角色列表List<UserRole> sysUserRoles = sysUserRoleService.list(new LambdaQueryWrapper<UserRole>().eq(UserRole::getUserId, Integer.parseInt(loginId.toString())));
//        得到角色id集合List<Integer> roleIds = sysUserRoles.stream().map(UserRole::getRoleId).toList();if (roleIds.size()== 0){return null;}List<String> list=new ArrayList<>();roleIds.forEach(roleId->{Role role = sysRoleService.getById(roleId);list.add(role.getName());});redisTemplate.opsForValue().set("roleList:"+loginId,list.toString(),30, TimeUnit.MINUTES);// 返回此 loginId 拥有的角色列表return list;}
}

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

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

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

用户信息的修改:

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

controller层:
 

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

service层实现:

//修改用户的信息@Overridepublic void updateUserInfo(User user, String token) {Integer userId = jwtUtil.getUsernameFromToken(token);
//        保证业务的健壮性,如果用户不存在,则抛出异常if(userId==null){throw new ResultException(555,"用户不存在");}User redisUser = this.getById(userId);if (redisUser==null){throw new ResultException(555,"用户不存在");}
//        用户修改了它的用户名,传入的用户名和redis中的用户名不一致。if (!user.getUsername().equals(redisUser.getUsername())){User user2 = this.getOne(new LambdaQueryWrapper<User>().eq(User::getUsername,user.getUsername()));
//            如果数据库中已经存在了该用户名,则抛出异常if (user2!=null){throw new ResultException(555,"用户名已存在,请你再换一个");}
//            如果用户修改了密码,则对密码进行加密if(user.getPassword()!=null){String md5 = SaSecureUtil.md5(user.getPassword());user.setPassword(md5);}this.updateById(user);log.info("修改用户信息成功");}
//        用户没有修改它的用户名
//            如果用户修改了密码,则对密码进行加密if(user.getPassword()!=null){String md5 = SaSecureUtil.md5(user.getPassword());user.setPassword(md5);}this.updateById(user);log.info("修改用户信息成功");}

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

退出接口:

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

controller层方法:
 

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

service层实现:

//    退出登录@Overridepublic void logout(String token) {Integer userId = jwtUtil.getUsernameFromToken(token);if(userId==null){throw new ResultException(555,"用户不存在");}//退出登陆时,sa-token会自动删除redis中的数据StpUtil.logout(userId);}

现在,我们就完成了所有的基础操作。(具体的前端代码我就不在这个进行叙述了,我放在了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的微服务快速开发项目

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/4699.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

upload-labs后续(14-)

图片马 在图片中加入木马&#xff0c;然后上传让浏览器解析&#xff0c;达到上传木马的目的 制作图片马&#xff1a;可以在notepad或者winhex中打开文件&#xff0c;在文件头中加入图片的文件头&#xff0c;再在文件末尾加入木马即可。 图片文件头如下&#xff1a; 1.Png图片…

学生党学习亚马逊云科技AWS、求职上岸就申AWS Cloud Club队长!

毕业了怎么找工作&#xff1f;该怎么学AWS&#xff1f;这是同学们最关心的问题。最近AWS推出的Cloud Club校园社区计划就可以完美解决这些问题&#xff01; &#x1f3eb;AWS校园社区计划是在学校构建校园社团(全球学校)&#xff0c;带着大家学最热的开发、AI/ML技术&#xff0…

IDEA主题美化【保姆级】

前言 一款好的 IDEA 主题虽然不能提高我们的开发效率&#xff0c;但一个舒适简单的主题可以使开发人员更舒适的开发&#xff0c;时常换一换主题可以带来不一样的体验&#xff0c;程序员的快乐就这么简单。话不多说&#xff0c;先上我自己认为好看的主题设置。 最终效果图: 原…

《机器学习by周志华》学习笔记-线性模型-03

1、多分类学习 1.1、背景 我们在上一节介绍了「线性判别分析(LDA)」,LDA的从二分类任务可以推广到多分类任务中。 而现实中常遇到的多分类学习任务。有些二分类的学习方法可以直接推广到多分类,但是更多情况下是基于一些策略,利用二分类学习器来解决多分类的问题。 1.…

OpenVINO安装教程 Docker版

从 Docker 映像安装IntelDistribution OpenVINO™ 工具套件 本指南介绍了如何使用预构建的 Docker 镜像/手动创建镜像来安装 OpenVINO™ Runtime。 Docker Base 映像支持的主机操作系统&#xff1a; Linux操作系统 Windows (WSL2) macOS(仅限 CPU exectuion) 您可以使用预…

机器学习作业3____决策树(CART算法)

目录 一、简介 二、具体步骤 样例&#xff1a; 三、代码 四、结果 五、问题与解决 一、简介 CART&#xff08;Classification and Regression Trees&#xff09;是一种常用的决策树算法&#xff0c;可用于分类和回归任务。这个算法由Breiman等人于1984年提出&#xff0c;它…

如何让Ubuntu上的MySQL开发更便捷

前言 作为一款开源的数据库开发与数据库管理协同工具&#xff0c;&#xff08;OceanBase Developer Center&#xff0c;简称ODC&#xff09;&#xff0c;针对MySQL数据源&#xff0c;已提供了涵盖SQL开发、变更风险管控、数据安全合规等多个方面的功能&#xff0c;从而为MySQL…

新媒体运营-----短视频运营-----PR视频剪辑----视频调色

新媒体运营-----短视频运营-----PR视频剪辑-----持续更新(进不去说明我没写完)&#xff1a;https://blog.csdn.net/grd_java/article/details/138079659 文章目录 1. Lumetri调色&#xff0c;明暗对比度2. Lumetri调色&#xff0c;创意与矢量示波器2.1 创意2.2 矢量示波器 3. L…

视频美颜SDK与主播美颜工具的技术原理与应用场景分析

在直播视频领域中&#xff0c;视频美颜SDK和主播美颜工具发挥着至关重要的作用。本文将探讨这些工具的技术原理及其在不同应用场景中的应用。 一、视频美颜SDK的技术原理 1.1 图像处理技术 视频美颜SDK的核心技术之一是图像处理技术。根据用户设定的美颜参数进行相应的调整。…

Meta Llama 3 性能提升与推理服务部署

利用 NVIDIA TensorRT-LLM 和 NVIDIA Triton 推理服务器提升 Meta Llama 3 性能 我们很高兴地宣布 NVIDIA TensorRT-LLM 支持 Meta Llama 3 系列模型&#xff0c;从而加速和优化您的 LLM 推理性能。 您可以通过浏览器用户界面立即试用 Llama 3 8B 和 Llama 3 70B&#xff08;该…

SpringBoot 快速开始 Dubbo RPC

文章目录 SpringBoot 快速开始 Dubbo RPC下载 Nacos项目启动项目的创建创建主项目接口定义服务的创建Dubbo 服务提供者的创建服务的消费者创建 添加依赖给 Provider、Consumer 添加依赖 开始写代码定义接口在 Provider 中实现在 Consumer 里面使用创建启动类 注册中心配置启动 …

YOKOGAWA横河手操器维修hart通讯器YHC5150X-01

横河手操器设置注意事项&#xff1a;内藏指示计显示选择与单位设置 有如下 5 种显示模式及单位设置百分比显示、用户设置显示、用户设置和百分比交替显示、输入压力显示、输入压力和百分比交替显示。即应用在当没有输入时操作要求输出为20mA引压方向设置右/左侧高压&#xff0c…

Docker容器:数据管理与镜像的创建(主要基于Dockerfile)

目录 一、Docker 数据管理 1、数据卷&#xff08;Data Volumes&#xff09; 2、数据卷容器&#xff08;DataVolumes Containers&#xff09; 二、容器互联&#xff08;使用centos镜像&#xff09; 三、Docker 镜像的创建 1、基于现有镜像创建 2、基于本地模板创建 3、基…

QT Windows 实现调用Windows API获取ARP 表

简介 使用ping方式获取网络可访问或者存在的设备发现部分会无法ping通但实际网络上存在此设备&#xff0c; 但使用arp -a却可以显示出来&#xff0c; 所以现在使用windows API的方式获取arp 表。 实现 参考Windows提供的示例转化成Qt Qt .pro LIBS -liphlpapiLIBS -lws2_32…

R-Tree: 原理及实现代码

文章目录 R-Tree: 原理及实现代码1. R-Tree 原理1.1 R-Tree 概述1.2 R-Tree 结构1.3 R-Tree 插入与查询 2. R-Tree 实现代码示例&#xff08;Python&#xff09;结语 R-Tree: 原理及实现代码 R-Tree 是一种用于管理多维空间数据的数据结构&#xff0c;常用于数据库系统和地理信…

【CANoe示例分析】TCP Chat(CAPL) with TLS encription

1、工程路径 C:\Users\Public\Documents\Vector\CANoe\Sample Configurations 15.3.89\Ethernet\Simulation\TLSSimChat 在CANoe软件上也可以打开此工程:File|Help|Sample Configurations|Ethernet - Simulation of Ethernet ECUs|Basic AUTOSAR Adaptive(SOA) 2、示例目…

面试题:斐波那契数列

题目描述&#xff1a; 写一个函数,输入n,求斐波那契数列的第n项.斐波那契数列定义如下: F(0) 0 F(1) 1 F(N) F(N - 1) F(N - 2), 其中 N > 1. 解题方法&#xff1a; 算法1: 利用递归实现,这个方法效率有严重问题,时间复杂度为O(2^n) long long Fibon(int n) {if (…

微软如何打造数字零售力航母系列科普03 - Mendix是谁?作为致力于企业低代码服务平台的领头羊,它解决了哪些问题?

一、Mendix 成立的背景 Mendix的成立是为了解决软件开发中最大的问题&#xff1a;业务和IT之间的脱节。这一挑战在各个行业和地区都很普遍&#xff0c;很简单&#xff1a;业务需求通常被描述为IT无法正确解释并转化为软件。业务和IT之间缺乏协作的原因是传统的代码将开发过程限…

WPF —— MVVM 指令执行不同的任务实例

标签页 设置两个按钮&#xff0c; <Button Content"修改状态" Width"100" Height"40" Background"red"Click"Button_Click"></Button><Button Content"测试"Width"100"Height"40&…

如何让用户听话?

​福格教授&#xff08;斯坦福大学行为设计实验室创始人&#xff09;通过深入研究人类行为20年&#xff0c;2007年用自己的名子命名&#xff0c;提出了一个行为模型&#xff1a;福格行为模型。 模型表明&#xff1a;人的行为发生&#xff0c;要有做出行为的动机和完成行为的能…