之前写过一个单体项目,现在想把它升级为微服务项目。在拆分升级的过程中发现了很多问题,本次就来记录一下遇到的问题和解决方法。(这篇文章只是记录一下拆分项目的基础架构,并使用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官方封装好的。它默认完成的方法有:
- 检查此账号是否之前已有登录;
- 为账号生成
Token
凭证与Session
会话; - 记录 Token 活跃时间;
- 通知全局侦听器,xx 账号登录成功;
- 将
Token
注入到请求上下文; - 将用户信息存入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的微服务快速开发项目
前端的代码也在这个微服务项目中。