赞
踩
目录
2、新建一个maven的聚合工程,并引入一些相应的版本控制。
2.1、新建一个maven项目,去掉src目录。作为我们整个微服务项目的父模块,并在这个模块中进行依赖的管理
2.2、在父模块下新建三个子模块(sa-gateway、sa-sysuser、sa-user)
之前写过一个单体项目,现在想把它升级为微服务项目。在拆分升级的过程中发现了很多问题,本次就来记录一下遇到的问题和解决方法。(这篇文章只是记录一下拆分项目的基础架构,并使用sa-token做微服务项目的安全框架,快速搭建起一个微服务项目)
sa-token的官网:Sa-Token
之前的项目是一个基于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的也主要会是后台的管理端。
我们就是在这个聚合工程中完成我们微服务的拆分与整合。
在此说明,本篇文章中,我只会搭建一些基础的框架,并使用sa-token完成我们对于后台管理端和前台客户的整合。因此,本篇文章中目前就只有:gateway网关模块、sysuser系统的用户模块、user前台客户的用户模块。一些其他的功能如商品模块、订单模块等等,我会在后续的文章中创建并实现其相应的功能。(我也会创建一个git仓库,将项目的源码,整体拷贝上去,有需要的可以自行拉取查看)
- <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文件中爆红的坐标就会正常了。
分别在三个模块中引入整合sa-token所需要的依赖。
详细请看sa-token的官网:依赖引入说明 (sa-token.cc)
使用nacos作为注册中心,在三个模块中引入nacos的相应依赖,并配置好yml的连接参数。
如图所示:
可以看到我们的三个子模块都注入到了nacos中。
在创建这三个子模块之后,我又新建了一个common公共模块,用来存放整合微服务项目用到的公共方法和依赖。使项目中的所有子模块都引入common公共模块的依赖,由于这个common公共模块只是用来存放一些公共的方法,所以,这个模块是不需要入驻到nacos中的,它甚至不需要主启动类。
如果注册nacos有不懂或出错的地方,可以参考一下这篇文章:springboot3整合nacos实现注册中心和配置中心(详细入门)_springboot3 nacos-CSDN博客
(注意,我本次微服务项目的数据库采用的是分库,分表的形式。既每一个子模块都有一个相应的数据库与之对应。但是,对于redis,我整个微服务连接的是同一个redis。如果,你的微服务项目架构与我的不同,可能需要修改一下对于sa-token的配置。具体的可以查看一下sa-token的官网)
由于使用vue3作为前端项目,直接访问后端会出现跨域问题,因此需要在gateway网关出配置跨域的参数以及一些服务的映射:
- spring:
- profiles:
- active: dev
- cloud:
- 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 框架内部算法策略
- */
- @Autowired
- private JwtUtil jwtUtil;
- @Autowired
- public void rewriteSaStrategy() {
- // 重写 Token 生成策略
- SaStrategy.instance.createToken = (loginId, loginType) -> {
- // return SaFoxUtil.getRandomString(60); // 随机60位长度字符串
- return jwtUtil.generateToken((Integer) loginId); // 使用 JWT 方式生成 Token
- };
- }
-
-
- // 排除sys系统中不需要拦截的路径
- @Autowired
- private ExcludeSysPath excludeSysPath;
-
- // 注册 Sa-Token全局过滤器
- @Bean
- public 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 {
- @Autowired
- private JwtUtil jwtUtil;
-
- @Override
- public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
- // 在请求头中添加凭证
- ServerHttpRequest newRequest = exchange
- .getRequest()
- .mutate()
- .header("sa-gateway","zhangqiao")
- .build();
- ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
- //后台管理端,用户登录成功,刷新token
- List<String> list = exchange.getRequest().getHeaders().get("token");
- if (list != null && list.size() > 0) {
- String token = list.get(0);
- // 刷新token
- String refreshToken = jwtUtil.refreshToken(token);
- log.info("刷新后的token为:" + refreshToken);
- }
- //前台用户端,用户登录成功,刷新token
- List<String> userList = exchange.getRequest().getHeaders().get("usertoken");
- if (userList != null && userList.size() > 0) {
- String token = userList.get(0);
- // 刷新token
- String 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 全局过滤器
- @Bean
- public 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写的没有放行的方法,运行结果为:
可以看到爆出了一个异常,这是正确的。因为我们设置后台管理端的所有接口都要先登录才能访问,此时用户没有登录,所以访问不到相应的方法。
首先,设置与gateway网关想同的token生成策略;
- @Configuration
- public class SaTokenConfigure {
- /**
- * 重写 Sa-Token 框架内部算法策略
- */
- @Autowired
- private JwtUtil jwtUtil;
- @Autowired
- public 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层:
- //用户登录方法
- @Override
- public 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
注入到请求上下文;然后获取相应的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层的具体实行:
- @Autowired
- private StpInterfaceImpl stpInterface;
-
- //获取用户信息
- @Override
- public 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 {
- // 用户角色表
- @Autowired
- private IUserRoleService sysUserRoleService;
- //角色权限表
- @Autowired
- private IRolePermissionService sysRoleMenuService;
- // 权限表
- @Autowired
- private IPermissionService sysMenuService;
- // 角色表
- @Autowired
- private IRoleService sysRoleService;
-
- @Autowired
- private RedisTemplate<String,String> redisTemplate;
-
- @Override
- public 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查权限id
- roleIds.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;
- }
-
- @Override
- public 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层实现:
- //修改用户的信息
- @Override
- public 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层实现:
- // 退出登录
- @Override
- public 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模块的日志输出:
至此,我们所有的操作就完成了。理一下思路,我们在gateway网关中注册了sa-token的全局过滤器,并对后台管理端的所有接口都进行了拦截,只放行了一些特定的接口。然后在sa-sysuser模块中对后台用户进行了一些操作。主要是登录、获取用户信息、用户登出。在这个过程中,我们针对sa-token的封装主要有:在gateway模块和sa-sysuser模块都引入sa整合redis的依赖,并配置了相同的redis缓存。在网关中解决了跨域和携带请求头的跨域问题,重新指定了token的生成策略。
我在这里只实现了后台管理端的用户生成,而没有实现前台客户端的用户生成。这篇文章写的挺多了,在下一篇文章中会实现前台客户端的用户一系列操作。这里同样使用到了sa-token。只不过我们一个项目里面有两套账号体系。具体的实现逻辑可以参考sa官方的
git的地址:sa-cloudDemo: 用来构建使用sa-token的微服务快速开发项目
前端的代码也在这个微服务项目中。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。