微服务架构整合Sa-Token实现网关鉴权及鉴权服务

微服务架构整合Sa-Token实现网关鉴权及鉴权服务

这个项目是自己做的一个练手项目,使用SpringCloudAlibaba微服务架构,在做鉴权模块的时候想起来之前在网上看见的Sa-Token项目,被称为国产鉴权之光故查阅了他们的文档实现了一套鉴权服务 一整套使用下来发现SaToken还是比较迎合国内程序员的编码习惯,与SpringSecurity那一套繁琐的过滤链责任链模式不同,SaToken主要还是依靠调方法类中的方法来实现登入,登出,鉴权等一系列流程

项目架构

代码实现

引入依赖,由于是微服务项目,我们用户信息必须存储在Redis上,SaToken已经为我们提供了SaToken整合Redis的依赖,无需我们手动实现代码 ,注意由于GateWay网关是基于WebFlux实现的与我们平常的MVC项目引入的依赖不同,SaToken的官方文档中也已经提及

xml 复制代码
<!--Sa-token-->
<!-- Sa-Token 权限认证(Reactor响应式集成), 在线文档:https://sa-token.cc -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
    <version>1.37.0</version>
</dependency>

<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-redis-jackson</artifactId>
    <version>1.37.0</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

网关中的鉴权拦截器SaToken也已经为我们留下拓展点,我们只需要实现我们自己的业务逻辑即可 代码如下

java 复制代码
package com.titi.apigateway.config;

import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.titi.titicommon.enums.AppHttpCodeEnum;
import com.titi.titicommon.result.ResponseResult;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;

import java.util.stream.Collectors;

@Configuration
public class SaTokenFilterConfiguration {
    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
                // 拦截地址
                .addInclude("/**")    /* 拦截全部path */
                // 开放地址
                .addExclude("/favicon.ico")
                // 鉴权方法:每次访问进入
                .setAuth(obj -> {
                    // 登录校验 -- 拦截所有路由,并排除/auth/** 用于开放登录
                    SaRouter.match("/**", "/auth/**", r -> StpUtil.checkLogin());
                    // 权限认证 -- 不同模块, 校验不同权限
                    SaRouter.match("/passenger/**", r -> StpUtil.checkPermission("passenger"));
                    SaRouter.match("/driver/**", r -> StpUtil.checkPermission("driver"));
                    SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
//                  SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
// 更多匹配 ...  */
                })
                // 异常处理方法:每次setAuth函数出现异常时进入
                .setError(e -> {
                    return SaResult.error(e.getMessage());
               });
    }

    /**
     * 由于网关没有引入springMVC依赖,所以使用feign的时候需要手动装配messageConverters
     * @param converters
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
        return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
    }
}

SaToken为了兼容不同业务场景,将获取权限与获取当前用户角色的方法也开放给我们使用者,是需要实现对应的接口再注册到Spring中即可 这里的逻辑大家根据自己的系统实现即可,一般思路是从缓存或者数据库中的表获取对应userId下的权限或者角色信息 这里获取到的权限大家Debug源码的时候可以看到请求经过SaToken的鉴权过滤器的时候底层会调用这两个方法来获取用户权限来鉴权

java 复制代码
package com.titi.apigateway.config;

import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollUtil;
import com.titi.feign.client.UserServiceClient;
import com.titi.titicommon.DTO.UserPermissionDto;
import com.titi.titicommon.result.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 自定义权限验证接口扩展 
 */
@Component
public class StpInterfaceImpl implements StpInterface {

    @Resource
    private UserServiceClient userServiceClient;

    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        // 返回此 loginId 拥有的权限列表
        //调用远程服务获取权限列表
        ResponseResult<List<UserPermissionDto>> userPermission = userServiceClient.getUserPermission(Long.parseLong(loginId.toString()));
        List<UserPermissionDto> permissList = userPermission.getData();
        if (CollUtil.isEmpty(permissList)){
            return null;
        }
        return permissList.stream().map(UserPermissionDto::getPermission).collect(Collectors.toList());
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        // 返回此 loginId 拥有的角色列表
        return null;
    }

}

授权登入部分

这里我抽象除了Auth服务用于多种身份下的角色登入,因为我的系统包括乘客端,司机端,管理员端,正常来说为了系统的可拓展性我应该再加一张角色表来做权限校验,但是由于是练手项目就怎么简单怎么来了,我只是用了单权限表来做 为了保证Auth服务尽量轻量级大部分的业务逻辑我都写在User服务中使用Feign进行远程调用 从代码中可以看出来在登入逻辑校验通过后我们只需要将用户的userId交给StpUtil工具类SaToken就会帮我们做例如Token生成,Token时效性设置,Token同步Redis,生成存放Token的Cookie返回给前端,还是非常方便的

kotlin 复制代码
package com.titi.auth.controller;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import com.titi.feign.client.SMSServiceClient;
import com.titi.feign.client.UserServiceClient;
import com.titi.titicommon.DO.TitiUser;
import com.titi.titicommon.DTO.SMSUserVerifyRequest;
import com.titi.titicommon.DTO.TitiUserDto;
import com.titi.titicommon.DTO.UserVerifyRequest;
import com.titi.titicommon.constants.AuthConstants;
import com.titi.titicommon.constants.RegexConstants;
import com.titi.titicommon.enums.AppHttpCodeEnum;
import com.titi.titicommon.exception.BussinessException;
import com.titi.titicommon.result.ResponseResult;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;

@RestController
public class PassengerUserController {

    @Autowired
    private UserServiceClient userServiceClient;

    @Autowired
    private SMSServiceClient smsServiceClient;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 登入注册二合一接口
     * @param titiUserDto
     * @param httpServletRequest
     * @return
     */
    @PostMapping("/doLogin")
    public ResponseResult doLoginOrRegister(@RequestBody TitiUserDto titiUserDto, HttpServletRequest httpServletRequest){
        //调用用户服务来进行登入或者注册登入
        ResponseResult login = userServiceClient.login(titiUserDto);
        if (!login.getCode().equals(200)){
            return ResponseResult.errorResult(login.getCode(),login.getErrorMessage());
        }
        Long userId = (Long) login.getData();
        StpUtil.login(userId);
        return ResponseResult.okResult(StpUtil.getTokenInfo());
    }

    @PostMapping("/logout")
    public ResponseResult doLogout(){
        if (StpUtil.isLogin()) {
            StpUtil.logout(StpUtil.getLoginId());
        }
        return ResponseResult.okResult(null);
    }

    @PostMapping("/sendVerifyCode")
    public ResponseResult sendVerifyCode(@RequestBody @Validated UserVerifyRequest userVerifyRequest, HttpServletRequest httpServletRequest){
        SMSUserVerifyRequest smsUserVerifyRequest = new SMSUserVerifyRequest();
        BeanUtils.copyProperties(userVerifyRequest,smsUserVerifyRequest);
        smsUserVerifyRequest.setIp(httpServletRequest.getRemoteAddr());
        String verifyTypeKey = AuthConstants.verifyMap.get(userVerifyRequest.getVerifyType());
        if (StrUtil.isBlank(verifyTypeKey)){
            throw new BussinessException(AppHttpCodeEnum.PARAM_INVALID);
        }
        smsUserVerifyRequest.setVerifyType(verifyTypeKey);
        //60s内只能接收一次验证码,使用Redis加锁来实现
        if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(verifyTypeKey + userVerifyRequest.getPhone()))) {
            throw new BussinessException(AppHttpCodeEnum.SMS_TIME_LIMIT);
        }
        //调用短信服务来发送信息
        return smsServiceClient.sendVerifyCode(smsUserVerifyRequest);
    }
}

GateWay重写转发请求携带Token转发给服务

有些业务场景中,我们在GateWay网关鉴权完毕分发请求给服务的时候,服务中的某些场景还需要使用用户信息,但是此时由于GateWay转发请求前端传来的Token已经丢失,这个时候我们可以在GateWay中增加一个全局过滤器在请求转发之前重写一次转发请求,在转发请求中携带上用户信息,这边我直接携带的就是userId了因为是内部服务调用也不用担心安全问题 `

java 复制代码
package com.titi.apigateway.filter;

import cn.dev33.satoken.stp.StpUtil;
import com.titi.titicommon.constants.AuthConstants;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class AuthorizeFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest newRequest = exchange
                .getRequest()
                .mutate()
                .header(AuthConstants.HTTP_LOGIN_HEADER, StpUtil.getLoginId(-1L).toString())
                .build();
        ServerWebExchange finalRequest = exchange.mutate().request(newRequest).build();
        return chain.filter(finalRequest);
    }
}

结语

SaToken的使用体验还是非常不错的,不愧是国产项目,简单易上手以及提供了很多拓展点供用户拓展,大家可以试一下呀

相关推荐
追逐时光者1 小时前
推荐 12 款开源美观、简单易用的 WPF UI 控件库,让 WPF 应用界面焕然一新!
后端·.net
Jagger_1 小时前
敏捷开发流程-精简版
前端·后端
苏打水com2 小时前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
间彧3 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧3 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧3 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧3 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧3 小时前
Spring Cloud Gateway详解与应用实战
后端
EnCi Zheng5 小时前
SpringBoot 配置文件完全指南-从入门到精通
java·spring boot·后端
烙印6015 小时前
Spring容器的心脏:深度解析refresh()方法(上)
java·后端·spring