Spring Could 轻松整合GateWay网关拦截 + Sa-Token登录认证

一、Sa-Token

        Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0分布式Session会话微服务网关鉴权 等一系列权限相关问题。

        当你受够 Shiro、SpringSecurity 等框架的三拜九叩之后,你就会明白,相对于这些传统老牌框架,Sa-Token 的 API 设计是多么的简单、优雅!

 

 

二、什么是Gateway?

大家可以这么理解,GateWay网关是我们项目的大门,是所有请求的统一入口处,以本篇文章的项目为例,可能更便于大家理解。

三、项目结构

这套项目采用的技术栈为

springboot 2.4.2 + spring could alibaba 2021.1 + spring could 2020.0.6

oss:对象存储服务

auth:认证服务

gateway:网关服务

subject:业务模块服务

并且所有模块都已交由nacos管理,bootstrap.yml配置文件内容如下:

spring:
  application:
    name: jc-club-gateway-dev
  profiles:
    active: dev
  cloud:
    nacos:
      config:
        server-addr: IP地址 + 端口
        prefix: ${spring.application.name}
        group: DEFAULT_GROUP
        namespace:
        file-extension: yaml
      discovery:
        enabled: true
        server-addr: IP地址 + 端口

四、具体步骤

1、网关层实现拦截 

        首先在新建的gateway网关的pom文件中新增依赖

<dependencies>
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>8.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>

        <!-- 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>

        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.6</version>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

在网关新建application.yml文件,并添加sa-token、以及其他三个模块的nacos,大家根据自己的实际情况对应配置

server:
  port: 5000
spring:
  cloud:
    gateway:
      routes:
        - id: oss
          uri: lb://jc-club-oss-dev
          predicates:
            - Path=/oss/**
          filters:
            - StripPrefix=1
        - id: auth
          uri: lb://jc-club-auth-dev
          predicates:
            - Path=/auth/**
          filters:
            - StripPrefix=1
        - id: subject
          uri: lb://jc-club-subject-dev
          predicates:
            - Path=/subject/**
          filters:
            - StripPrefix=1
sa-token:
  # token 名称(同时也是 cookie 名称)
  token-name: satoken
  # token 有效期(单位:秒) 默认30天,-1 代表永久有效
  timeout: 2592000
  # token 如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
  active-timeout: -1
  # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个 token 
  is-share: true
  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128)
  token-style: random-32
  # 是否输出操作日志
  is-log: true 

 新建SaTokenConfigure,,实现网关拦截


/**
 * 权限验证配置器
 */
@Configuration
public class SaTokenConfigure {
    // 注册 Sa-Token全局过滤器 
    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
            // 拦截地址 
            .addInclude("/**")    /* 拦截全部path */
            // 鉴权方法:每次访问进入 
            .setAuth(obj -> {
                System.out.println("-------- 前端访问path:" + SaHolder.getRequest().getRequestPath());
                // 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录 
                SaRouter.match("/auth/**", "/auth/user/**", r -> StpUtil.checkRole("admin"));
                // 权限认证 -- 不同模块, 校验不同权限 
                SaRouter.match("/oss/**", r -> StpUtil.checkLogin());
                SaRouter.match("/subject/subject/add", r -> StpUtil.checkPermission("subject:add"));
                SaRouter.match("/subject/**", r -> StpUtil.checkLogin());

            });
    }
}

2、认证服务层实现Sa-token登录认证 

首先导入依赖

        <!-- Sa-Token 权限认证 -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot-starter</artifactId>
            <version>1.38.0</version>
        </dependency>
        <!-- sa-token 依赖-->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot-starter</artifactId>
            <version>1.37.0</version>
        </dependency>

然后在application.yml中添加配置

############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
  # token 名称(同时也是 cookie 名称)
  token-name: satoken
  # token 有效期(单位:秒) 默认30天,-1 代表永久有效
  timeout: 2592000
  # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
  active-timeout: -1
  # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
  is-share: true
  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
  token-style: random-32
  # 是否输出操作日志
  is-log: true

 然后我们写一个测试类进行登录的简单测试

@RestController
@RequestMapping("/user/")
public class TestController {

    // 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456
    @RequestMapping("doLogin")
    public SaResult  doLogin(String username, String password) {
        // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
        if("zhang".equals(username) && "123456".equals(password)) {
            StpUtil.login(10001);
            SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
            // 第3步,返回给前端
            return SaResult.data(tokenInfo);
        }
        return SaResult.error("登录失败");
    }

    // 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin
    @RequestMapping("isLogin")
    public String isLogin() {
        return "当前会话是否登录:" + StpUtil.isLogin();
    }
}

这里登录账号为:zhang,密码为123456,点击登录将会生成一串token

我们再访问另一个接口,验证是否登录成功

这里可以看到我们是登录成功了的,那么一个简单的登录案例就成功啦,接下来我们i可以对网关的拦截做一个升级改造,我们这里加一个全局异常拦截器以及结合redis进行权限的验证处理

首先我们需要创建一个Result类

 private Boolean success;

    private Integer code;

    private String message;

    
    public static Result fail( Integer code, String message){
        Result result = new Result<>();
        result.setSuccess(false);
        result.setCode(code);
        result.setMessage(message);
        return result;
    }

以及一个resoult枚举类

@Getter
public enum ResultCodeEnum {

    SUCCESS(200,"拿下拿下"),
    FAIL(500,"请求失败咯");

    private int code;
    private String desc;

    ResultCodeEnum(int code , String desc){
        this.code = code;
        this.desc = desc;
    }

    public static ResultCodeEnum getByCode(int codeVal){
        for (ResultCodeEnum resultCodeEnum : ResultCodeEnum.values()) {
            if (resultCodeEnum.code == codeVal){
                return resultCodeEnum;
            }
        }
        return null;
    }
}

 接下来我们添加一个全局异常处理器,只要是satoken的异常我们全部捕捉并返回401错误

/**
 * 全局异常处理
 */
@Component
public class GatewayExceptionHandler implements ErrorWebExceptionHandler {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public Mono<Void> handle(ServerWebExchange serverWebExchange, Throwable throwable) {
        ServerHttpRequest request = serverWebExchange.getRequest();
        ServerHttpResponse response = serverWebExchange.getResponse();
        Integer code = 200;
        String message = "";
        if (throwable instanceof SaTokenException) {
            code = 401;
            message = "用户无权限";
        } else {
            code = 500;
            message = "系统繁忙";
        }
        Result result = Result.fail(code, message);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        return response.writeWith(Mono.fromSupplier(() -> {
            DataBufferFactory dataBufferFactory = response.bufferFactory();
            byte[] bytes = null;
            try {
                bytes = objectMapper.writeValueAsBytes(result);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
            return dataBufferFactory.wrap(bytes);
        }));
    }
}

然后我们利用redis对satoken进行一个权限的校验

@Component
public class StpInterfaceImpl implements StpInterface {

    @Resource
    private RedisUtil redisUtil;

    private final String authPermissionPrefix = "auto.permission";

    private final String authRolePrefix = "auth.role";

    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        return gerAuth(loginId.toString(), authPermissionPrefix);
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        return gerAuth(loginId.toString(), authRolePrefix);
    }

    public List<String> gerAuth(Object loginId, String prefix) {
        String authKey = redisUtil.buildKey(prefix, loginId.toString());
        String authValue = redisUtil.get(authKey);
        if (StringUtils.isBlank(authValue)) {
            return Collections.emptyList();
        }
        List<String> authList = new Gson().fromJson(authValue, List.class);
        return authList;
    }
}

这里时重写了两个类,分别是RedisConfig、RedisUtil的方法 ,详见配置如下

/**
 * Redis的config处理
 *
 * @author: ChickenWing
 * @date: 2023/10/28
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(redisSerializer);
        redisTemplate.setHashKeySerializer(redisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer());
        return redisTemplate;
    }

    private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
        Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jsonRedisSerializer.setObjectMapper(objectMapper);
        return jsonRedisSerializer;
    }
}
//redis工具类
@Component
@Slf4j
public class RedisUtil {

    @Resource
    private RedisTemplate redisTemplate;

    private static final String CACHE_KEY_SEPARATOR = ".";

    /**
     * 构建缓存key
     */
    public String buildKey(String... strObjs) {
        return Stream.of(strObjs).collect(Collectors.joining(CACHE_KEY_SEPARATOR));
    }

    /**
     * 是否存在key
     */
    public boolean exist(String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 删除key
     */
    public boolean del(String key) {
        return redisTemplate.delete(key);
    }

    /**
     * set(不带过期)
     */
    public void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * set(带过期)
     */
    public boolean setNx(String key, String value, Long time, TimeUnit timeUnit) {
        return redisTemplate.opsForValue().setIfAbsent(key, value, time, timeUnit);
    }

    /**
     * 获取string类型缓存
     */
    public String get(String key) {
        return (String) redisTemplate.opsForValue().get(key);
    }

    public Boolean zAdd(String key, String value, Long score) {
        return redisTemplate.opsForZSet().add(key, value, Double.valueOf(String.valueOf(score)));
    }

    public Long countZset(String key) {
        return redisTemplate.opsForZSet().size(key);
    }

    public Set<String> rangeZset(String key, long start, long end) {
        return redisTemplate.opsForZSet().range(key, start, end);
    }

    public Long removeZset(String key, Object value) {
        return redisTemplate.opsForZSet().remove(key, value);
    }

    public void removeZsetList(String key, Set<String> value) {
        value.stream().forEach((val) -> redisTemplate.opsForZSet().remove(key, val));
    }

    public Double score(String key, Object value) {
        return redisTemplate.opsForZSet().score(key, value);
    }

    public Set<String> rangeByScore(String key, long start, long end) {
        return redisTemplate.opsForZSet().rangeByScore(key, Double.valueOf(String.valueOf(start)), Double.valueOf(String.valueOf(end)));
    }

    public Object addScore(String key, Object obj, double score) {
        return redisTemplate.opsForZSet().incrementScore(key, obj, score);
    }

    public Object rank(String key, Object obj) {
        return redisTemplate.opsForZSet().rank(key, obj);
    }
    
}

这个时候我们访问需要权限的端口时就会抛出无权限异常,到此我们微服务整合网关+Sa-Token简单应用就完成啦!

  • 15
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SpringCloud是基于Spring框架的分布式系统开发框架,它提供了丰富的分布式系统解决方案。而Sa-Token是一个轻量级的Java权限认证与授权框架,提供了简单易用的权限控制功能。 在SpringCloud整合Sa-Token可以为我们的分布式系统提供更加安全可靠的权限认证与授权机制。具体步骤如下: 1. 在SpringCloud的微服务架构中,将Sa-Token配置为一个独立的授权认证中心,可以独立部署,也方便对其进行管理和维护。 2. 在每个微服务中引入Sa-Token的依赖,通过配置Sa-Token的相关属性,实现微服务的用户登录认证。同时,可以使用Sa-Token提供的注解进行权限控制,例如@RequiresPermissions注解可以对接口或方法进行权限校验。 3. 在微服务之间进行访问时,可以通过Sa-TokenToken验证机制,对调用方进行身份认证。因此,每个微服务需要验证并解析Token,以确保请求方的合法性。 4. Sa-Token提供了灵活的权限授权机制,可以根据业务需求配置不同的角色和权限管理。在SpringCloud中,我们可以根据微服务的不同职能划分角色,为每个角色分配相应的权限。通过角色与权限的配置,实现对不同微服务接口的精确控制。 通过将Sa-Token整合SpringCloud中,我们可以实现整个系统的统一权限管理。无论是对用户的认证与授权,还是对接口的权限控制,都可以通过Sa-Token轻松实现。这不仅提高了系统的安全性,同时也简化了系统的开发与维护工作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值