前后端分离,使用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目录。作为我们整个微服务项目的父模块,并在这个模块中进行依赖的管理

<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: 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写的没有放行的方法,运行结果为:

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

2.4、后台服务端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官方封装好的。它默认完成的方法有:

  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层的具体实行:
 

  @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模块的日志输出:

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的微服务快速开发项目

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

  • 31
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
前后端分离是一种开发架构,将前端和后端的开发过程分开进行,前端主要负责用户界面展示和交互逻辑,后端负责数据处理和业务逻辑。而sa-token是一种单点登录的解决方案。 sa-token是基于Token一个轻量级权限认证和申请令牌的工具,可以实现前后端分离项目的单点登录功能。它通过生成和验证Token令牌来实现用户身份认证。 在前后端分离架构中,前端发送登录请求到后端,后端通过验证用户的账号和密码,如果验证成功,则生成一个Token令牌,并将该Token返回给前端。前端保存该Token,每次向后端发送请求时,需要在请求的Header中添加Token。后端通过获取请求Header中的Token,并进行验证,如果验证通过,则表示用户已登录。 sa-token提供了一些便捷的API,用于生成Token、验证Token、获取用户信息等操作。通过这些API,我们可以简便地实现单点登录的功能。sa-token还提供了一些定制化的配置选项,可以根据实际需求进行调整。 前后端分离sa-token单点登录方案具有以下优点:简单易用、安全性高、扩展性强等。同时,由于前后端分离,使得前端和后端可以独立开发和部署,提高了开发效率和项目的可维护性。 总之,前后端分离sa-token单点登录是一种解决方案,通过Token令牌验证实现用户身份认证,适用于前后端分离项目,具有诸多优点,可轻松实现单点登录功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张乔24

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值