springcloud gateway集成RBAC模型——实现用户权限判断

目录

1.RBAC权限控制模型

2.数据表设计

3.权限控制微服务

4.springcloud gateway filter

5.实现效果


1.RBAC权限控制模型

RBAC(Role Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联,而不是直接将权限赋予用户。

一个用户拥有若干个角色,每个角色拥有若干个权限,这样就构成了“用户-角色-权限”的授权模型。这种授权模型的好处在于,不必每次创建用户时都进行权限分配的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,减少频繁设置。

RBAC模型中,用户与角色之间、角色与权限之间,一般是多对多的关系。比如用户a可以同时拥有admin角色和user角色,user角色也可以同时被用户a及用户b拥有。

RBAC通过定义角色的权限,并对用户授予某个角色从而来控制用户的权限,实现了用户和权限的逻辑分离。简单来说就是,权限是属于角色的,想要获得某个权限,则用户必须先获得某个角色,通过角色操纵对应权限下的数据资源。

RBAC支持三个著名的安全原则:

  • 最小权限原则:RBAC可以将角色配置成其完成任务所需的最小权限集合;
  • 责任分离原则:可以通过调用相互独立互斥的角色来共同完成敏感的任务;
  • 数据抽象原则:可以通过权限的抽象来体现。

RBAC模型的缺点是没有提供操作顺序的控制机制,这以缺陷使得RBAC模型很难 适应那些对操作顺序有严格要求的系统。、

下面对RBAC-0模型进行简单实现。

2.数据表设计

权限管理顾名思义,就是表与表之间的关系。所以必须要有三张表(用户表)(角色表)(数据资源表

 用户表:存储用户账号相关信息

 角色表:存储角色相关信息

 数据资源表:存储访问的api路由相关信息

其中用户表和角色表之间应建立映射关系:

表中存储用户账号和角色之间的关系,比如 :

此处只是简单应用,固一个账号只存放了一个角色,实践上一个账户应可存放多个角色,写成list形式以varchar格式存入数据库。

同时角色表和资源表之间也应建立映射关系:

 

至此,一个简单的RBAC权限模型下的数据库设计完毕。

3.权限控制微服务

然后,我们需要编写权限控制微服务,为角色分配权限和根据权限查询其拥有的权限:

@Service
@Slf4j
public class PermissionServiceImpl extends ServiceImpl<PermissionDao, RolesPermission> implements PermissionService {
    @Resource
    private PermissionDao permissionDao;
    @Override
    public boolean permissionAdd(String roles, List<String> codeList) {
        RolesPermission rolesPermission = new RolesPermission();
        rolesPermission.setRole(roles);
        int result = 0;
        RolesPermission role = permissionDao.selectOne(new QueryWrapper<RolesPermission>().eq("role",roles));

        if(role!=null){
            //去掉头尾括号,并转为列表
            List<String> list = new java.util.ArrayList<>(Collections.singletonList(
                    StringUtils.strip(role.getPermissionCode(), "[]")));
            //将新数据添加至列表
            list.addAll(codeList);

            rolesPermission.setPermissionCode(list.toString());
            rolesPermission.setId(role.getId());
            result = permissionDao.updateById(rolesPermission);
        }else {
            rolesPermission.setPermissionCode(codeList.toString());
            result = permissionDao.insert(rolesPermission);
        }
        return result > 0;
    }
}

上述代码以mybatis-plus框架实现,具体不进行展开。上述代码为为角色添加权限方法,方法中,会首先根据角色名称查询数据库是否有权限记录:有,则查出已拥有的权限列表,并将新添加的数据资源code列表存入,更新数据库;无,则添加一条新纪录。

同时,在该微服务控制类(controller)中编写获取路由api相关接口:

    @GetMapping("/get")
    public List<String> pathGet(@RequestParam("roles") String roles) {
        RolesPermission permission = permissionService.getOne(new QueryWrapper<RolesPermission>().eq("role",roles));
        String codes =  StringUtils.strip(permission.getPermissionCode(), "[]");
        List<String> list = Arrays.asList(codes.split(","));
        List<String> pathList = new ArrayList<>();
        for(String code:list){
            String api = pathService.getOne(new QueryWrapper<PathPermission>().eq("permission_code",code.trim())).getPath();
            pathList.add(api);
        }
        return pathList;
    }

通过角色名称,即可返回该角色下所有的可访问api路由。

4.springcloud gateway filter

此处springcloud gateway需是集成了JWT token下的统一网关,可参考上一篇文章

同时,token中需存储了用户角色相关信息,相关token生成工具类参考如下:

@Component
public class JwtTokenUtil {

    private static final String JWT_CACHE_KEY = "jwt:userId:";
    private static final String USER_ID = "userId";
    private static final String USER_NAME = "username";
    private static final String ACCESS_TOKEN = "access_token";
    private static final String REFRESH_TOKEN = "refresh_token";
    private static final String EXPIRE_IN = "expire_in";

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private AuthJwtProperties jwtProperties;

    /**
     * 生成 token 令牌主方法
     * @param userId 用户Id或用户名
     * @return 令token牌
     */

    public Map<String, Object> generateTokenAndRefreshToken(String userId, String username, String roles) {
        //生成令牌及刷新令牌
        Map<String, Object> tokenMap = buildToken(userId, username, roles);
        //redis缓存结果
        cacheToken(userId, tokenMap);
        return tokenMap;
    }

    //将token缓存进redis
    private void cacheToken(String userId, Map<String, Object> tokenMap) {
        stringRedisTemplate.opsForHash().put(JWT_CACHE_KEY + userId, ACCESS_TOKEN, tokenMap.get(ACCESS_TOKEN));
        stringRedisTemplate.opsForHash().put(JWT_CACHE_KEY + userId, REFRESH_TOKEN, tokenMap.get(REFRESH_TOKEN));
        stringRedisTemplate.expire(userId, jwtProperties.getExpiration() * 2, TimeUnit.MILLISECONDS);
    }
    //生成令牌
    private Map<String, Object> buildToken(String userId, String username, String roles) {
        Map<String, String> map = new HashMap<>();
        map.put("roles",roles);
        //生成token令牌
        String accessToken = generateToken(userId, username, map);
        //生成刷新令牌
        String refreshToken = generateRefreshToken(userId, username, null);
        //存储两个令牌及过期时间,返回结果
        HashMap<String, Object> tokenMap = new HashMap<>(2);
        tokenMap.put(ACCESS_TOKEN, accessToken);
        tokenMap.put(REFRESH_TOKEN, refreshToken);
        tokenMap.put(EXPIRE_IN, jwtProperties.getExpiration());
        return tokenMap;
    }
    /**
     * 生成 token 令牌 及 refresh token 令牌
     * @param payloads 令牌中携带的附加信息
     * @return 令牌
     */
    public String generateToken(String userId, String username,
                                Map<String,String> payloads) {
        Map<String, Object> claims = buildClaims(userId, username, payloads);;

        return generateToken(claims);
    }
    public String generateRefreshToken(String userId, String username, Map<String,String> payloads) {
        Map<String, Object> claims = buildClaims(userId, username, payloads);

        return generateRefreshToken(claims);
    }
    //构建map存储令牌需携带的信息
    private Map<String, Object> buildClaims(String userId, String username, Map<String, String> payloads) {
        int payloadSizes = payloads == null? 0 : payloads.size();

        Map<String, Object> claims = new HashMap<>(payloadSizes + 2);
        if(payloadSizes!=0){
            claims.put("roles", payloads.get("roles"));
        }
        claims.put("sub", userId);
        claims.put("username", username);
        claims.put("created", new Date());


        if(payloadSizes > 0){
            claims.putAll(payloads);
        }

        return claims;
    }


    /**
     * 刷新令牌并生成新令牌
     * 并将新结果缓存进redis
     */
    public Map<String, Object> refreshTokenAndGenerateToken(String userId, String username,String roles) {
        Map<String, Object> tokenMap = buildToken(userId, username,roles);
        stringRedisTemplate.delete(JWT_CACHE_KEY + userId);
        cacheToken(userId, tokenMap);
        return tokenMap;
    }


    //缓存中删除token
    public boolean removeToken(String userId) {
        return Boolean.TRUE.equals(stringRedisTemplate.delete(JWT_CACHE_KEY + userId));
    }



    /**
     * 判断令牌是否不存在 redis 中
     *
     * @param token 刷新令牌
     * @return true=不存在,false=存在
     */
    public Boolean isRefreshTokenNotExistCache(String token) {
        String userId = getUserIdFromToken(token);
        String refreshToken = (String)stringRedisTemplate.opsForHash().get(JWT_CACHE_KEY + userId, REFRESH_TOKEN);
        return refreshToken == null || !refreshToken.equals(token);
    }

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return true=已过期,false=未过期
     */
    public Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            //验证 JWT 签名失败等同于令牌过期
            return true;
        }
    }

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public String refreshToken(String token) {
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put("created", new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 验证令牌
     *
     * @param token       令牌
     * @param userId  用户Id用户名
     * @return 是否有效
     */
    public Boolean validateToken(String token, String userId) {

        String username = getUserIdFromToken(token);
        return (username.equals(userId) && !isTokenExpired(token));
    }


    /**
     * 生成令牌
     * @param claims 数据声明
     * @return 令牌
     */
    private String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis()
                + jwtProperties.getExpiration());
        return Jwts.builder().setClaims(claims)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512,
                        jwtProperties.getSecret())
                .compact();
    }
    /**
     * 生成刷新令牌 refreshToken,有效期是令牌的 2 倍
     * @param claims 数据声明
     * @return 令牌
     */
    private String generateRefreshToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + jwtProperties.getExpiration() * 2);
        return Jwts.builder().setClaims(claims)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, jwtProperties.getSecret())
                .compact();
    }

    /**
     * 从令牌中获取数据声明,验证 JWT 签名
     * @param token 令牌
     * @return 数据声明
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(jwtProperties.getSecret()).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }
}

登录验证通过后,使用用户名查询用户的角色,并将角色信息存入token中:

Map<String, Object> tokenMap = jwtTokenUtil
                .generateTokenAndRefreshToken(String.valueOf(account.getId()), username,
                        //用户角色映射表中中查询用户角色
                        rolesService.getOne(new QueryWrapper<AccountRoles>().eq("username",username)).getRoles());

(如上图,token中携带roles信息)

然后,编写gateway过滤器:

import com.alibaba.fastjson.JSON;
import com.seven.springcloud.config.AuthJwtProperties;
import com.seven.springcloud.config.WhiteListProperties;
import com.seven.springcloud.constants.TokenConstants;
import com.seven.springcloud.entities.CommonResult;
import com.seven.springcloud.entities.enums.ResponseCodeEnum;
import com.seven.springcloud.service.PermissionService;
import com.seven.springcloud.util.JwtTokenUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;

@Slf4j
@Configuration
public class JwtAuthCheckFilter {

    private static final String AUTH_TOKEN_URL = "/auth/login";
    private static final String REFRESH_TOKEN_URL = "/auth/token/refresh";
    public static final String USER_ID = "userId";
    public static final String USER_NAME = "username";
    public static final String FROM_SOURCE = "from-source";

    @Resource
    private WhiteListProperties whiteListProperties;
    @Resource
    private AuthJwtProperties authJwtProperties;
    @Resource
    private PermissionService permissionService;
    @Resource
    private JwtTokenUtil jwtTokenUtil;

    @Bean
    @Order(-101)
    public GlobalFilter jwtAuthGlobalFilter() {

        return (exchange, chain) -> {
            log.info("登录判断");
            ServerHttpRequest serverHttpRequest = exchange.getRequest();
            ServerHttpResponse serverHttpResponse = exchange.getResponse();
            ServerHttpRequest.Builder mutate = serverHttpRequest.mutate();
            String requestUrl = serverHttpRequest.getURI().getPath();

            // 跳过对登录请求的 token 检查。因为登录请求是没有 token 的,是来申请 token 的。
            if(AUTH_TOKEN_URL.equals(requestUrl)) {
                log.info("登录url,放行");
                return chain.filter(exchange);
            }

            // 从 HTTP 请求头中获取 JWT 令牌
            String token = getToken(serverHttpRequest);
            if (StringUtils.isEmpty(token)) {
                return unauthorizedResponse(exchange, serverHttpResponse, ResponseCodeEnum.TOKEN_MISSION);
            }

            // 对Token解签名,并验证Token是否过期
            boolean isJwtNotValid = jwtTokenUtil.isTokenExpired(token);
            if(isJwtNotValid){
                return unauthorizedResponse(exchange, serverHttpResponse, ResponseCodeEnum.TOKEN_INVALID);
            }
            // 验证 token 里面的 userId 是否为空
            String userId = jwtTokenUtil.getUserIdFromToken(token);
            String username = jwtTokenUtil.getUserNameFromToken(token);
            if (StringUtils.isEmpty(userId)) {
                return unauthorizedResponse(exchange, serverHttpResponse, ResponseCodeEnum.TOKEN_INVALID);
            }

            // 设置用户信息到请求
            addHeader(mutate, USER_ID, userId);
            addHeader(mutate, USER_NAME, username);
            // 内部请求来源参数清除
            removeHeader(mutate, FROM_SOURCE);
            return chain.filter(exchange.mutate().request(mutate.build()).build());
        };
    }
    //添加头部信息
    private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) {
        if (value == null) {
            return;
        }
        String valueStr = value.toString();
        String valueEncode = urlEncode(valueStr);
        mutate.header(name, valueEncode);
    }
    //移除头部信息
    private void removeHeader(ServerHttpRequest.Builder mutate, String name) {
        mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
    }
    //内容编码,配置为UTF-8
    static String urlEncode(String str) {
        try {
            return URLEncoder.encode(str, "UTF-8");
        }
        catch (UnsupportedEncodingException e)
        {
            return StringUtils.EMPTY;
        }
    }

    @Bean
    @Order(-100)
    public GlobalFilter permissionGlobalFilter() {

        return (exchange, chain) -> {
            log.info("权限判断");
            // 从 HTTP 请求头中获取 JWT 令牌
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            ServerHttpRequest.Builder mutate = request.mutate();
            String path = request.getURI().getPath();


            //对白名单中的地址放行
            List<String> whiteList = whiteListProperties.getWhites();
            for(String str : whiteList){
                if(path.contains(str)){
                    log.info("白名单,放行{}",request.getURI().getPath());
                    return chain.filter(exchange);
                }
            }

            //String headerToken = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION);

            //判断用户权限
            String token = getToken(request);
            boolean permission = hasPermission(token,path);
            if (!permission){
                log.info("用户没有权限");
                return unauthorizedResponse(exchange, response, ResponseCodeEnum.PERMISSION_DENIED);
            }
            return chain.filter(exchange.mutate().request(mutate.build()).build());
        };
    }
    private boolean hasPermission(String token, String path){
        //解码jwt token
        Claims claims = Jwts.parser().setSigningKey(authJwtProperties.getSecret()).parseClaimsJws(token).getBody();
        //获取token中的权限值
        String roles = (String) claims.get("roles");
        if(roles!=null){
            List<String> pathList = permissionService.pathGet(roles);
            path = path.replaceFirst("/api",StringUtils.EMPTY);
            for (String api:pathList){
                if(api.equals(path)){
                    return true;
                }
            }
        }
        return false;
    }


    //请求token
    private String getToken(ServerHttpRequest request) {
        String token = request.getHeaders().getFirst(authJwtProperties.getHeader());
        // 如果前端设置了令牌前缀,则裁剪掉前缀
        if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX))
        {
            token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
        }
        return token;
    }

    //jwt鉴权失败处理类
    private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, ServerHttpResponse serverHttpResponse, ResponseCodeEnum responseCodeEnum) {
        log.warn("token异常处理,请求路径:{}", exchange.getRequest().getPath());
        serverHttpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
        serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        CommonResult<Object> responseResult = new CommonResult<>(responseCodeEnum.getCode(),responseCodeEnum.getMessage());
        DataBuffer dataBuffer = serverHttpResponse.bufferFactory()
                .wrap(JSON.toJSONStringWithDateFormat(responseResult, JSON.DEFFAULT_DATE_FORMAT)
                        .getBytes(StandardCharsets.UTF_8));
        return serverHttpResponse.writeWith(Flux.just(dataBuffer));
    }
}

此处进行了两次拦截过滤:先进行登录拦截(查看是否携带token),再进行权限拦截(查看是否用于权限)。(登录拦截此处不进行展开,具体查看springcloud gateway集成jwt实现登录鉴权

权限判断代码中,首先需要在配置文件进行配置,设置拦截白名单:

auth:
  jwt:
    enabled: true   # 是否开启JWT登录认证功能
    secret: passjava  # JWT 私钥,用于校验JWT令牌的合法性
    expiration: 1800000 # JWT 令牌的有效期,用于校验JWT令牌的合法性,一个小时
    header: Authorization # HTTP 请求的 Header 名称,该 Header作为参数传递 JWT 令牌
    userParamName: userId  # 用户登录认证用户名参数名称
    pwdParamName: password  # 用户登录认证密码参数名称
    useDefaultController: true # 是否使用默认的JwtAuthController
    skipValidUrl: /auth/login
  ignore:
    whites: # 自定义白名单
      - /auth/login
      - /auth/token/refresh
@Data
@Component
@ConfigurationProperties(prefix = "auth.ignore")
public class WhiteListProperties {
    private List<String> whites;
}

对登录登出等不需要权限的路由放行。

其次,需要在gateway微服务中导入feign依赖,编写feign service类调用权限管理的微服务:

@FeignClient(value = "cloud-roles-manage")
public interface PermissionService {
    @GetMapping("/path/get")
    List<String> pathGet(@RequestParam("roles") String roles);
}

通过导入该service,获得token中对应角色信息下的所有可访问路由。然后对当前路由进行配对,若配对成功,则拥有权限,否则则无权限。

若以后业务增长,可访问api较多,不适合一个个进行遍历,可直接进行在角色-数据映射表中存储api名称,然后对该字段进行模糊查询,具体应看具体业务需求进行设计。

上述代码中,jwt token中的一些相关属性请参考上一篇文章:springcloud gateway集成jwt实现登录鉴权

需注意的是:spring-boot2.2.x版本HttpMessageConvertersAutoConfiguration有所改动,加了个@Conditional(NotReactiveWebApplicationCondition.class) , 因为gateway是ReactiveWeb,所以针对HttpMessageConverters的自动配置就不生效了,故需要手动注入HttpMessageConverters,否则feign调用时会报错:

@Configuration
public class FeignConfig {
    @Bean
    @ConditionalOnMissingBean
    public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
        return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
    }
}

5.实现效果

然后,我们对其进行实验,对应权限如下:

 

user角色拥有查询的权限,admin角色拥有查询和添加的权限。

首先登录拥有user角色的账号,获取对应的token。获取token后,携带token访问code:101下的api,进行添加地址:

判断用户没有权限。

访问查询接口:

 拥有权限,查询成功。

切换拥有admin角色账号,获取token,访问添加接口:

 访问成功,控制台打印如下;

先进行判断是否跳过鉴权;然后进行登录判断,是否携带token ,再进行权限判断,是否拥有权限。

以上即为用户权限判断全过程,RBAC模型的简单实现。

### 一、微服务架构 [...此处内容保持不变...] 二、多租户支持 1.2.1.2 多租户支持模块是SaaS(Software as a Service,软件即服务)应用中的关键组成部分,它允许单个实例的应用服务多个客户或租户,同时确保各租户之间的数据和配置相互隔离。以下是多租户支持模块的具体内容: 租户隔离‌: 数据隔离‌:每个租户拥有独立的数据存储,或者通过数据库中的租户ID字段实现逻辑隔离,确保租户数据互不干扰。 配置隔离‌:每个租户可以拥有独立的配置信息,如应用设置、权限规则等,这些配置信息在租户间相互独立。 租户管理‌: 提供租户创建、修改、删除和查询等管理功能,允许管理员或具有相应权限用户对租户信息进行管理。 支持租户状态监控,如活跃租户数量、资源使用情况等,以便进行资源规划和优化。 计费与计量‌: 根据租户的使用情况(如用户数、存储量、API调用次数等)进行计费,支持多种计费模式(如按使用量计费、包月/包年等)。 提供计量和统计功能,以便准确记录和分析租户的使用情况,为计费提供依据。 安全与合规‌: 确保多租户应用符合相关法规和行业标准,如数据隐私保护、安全审计等。 提供数据加密、访问控制等安全措施,防止数据泄露和非法访问。 多租户性能优化‌: 针对多租户环境进行性能优化,如数据库索引优化、缓存策略调整等,以提高系统响应速度和吞吐量。 支持资源动态扩展,根据租户需求自动调整资源分配,确保系统稳定运行。 三、API网关 1.2.1.3 API网关模块作为微服务架构中的关键组件,负责处理外部请求,提供统一的入口、路由、身份验证、限流等功能。以下是API网关模块的具体内容: 请求路由‌: 根据请求的URL、请求方法等信息,将请求路由到相应的微服务实例。 支持动态路由配置,允许管理员根据业务需求灵活调整路由规则。 身份验证与授权‌: 对外部请求进行身份验证,确保只有合法用户才能访问系统资源。 根据用户权限和角色进行授权,控制用户对系统资源的访问权限。 限流与熔断‌: 对外部请求进行限流,防止系统因过载而崩溃。 提供熔断机制,当某个微服务出现故障时,API网关能够自动切断与该微服务的连接,防止故障扩散。 协议转换与数据格式处理‌: 支持多种协议(如HTTP、HTTPS、WebSocket等)和数据格式(如JSON、XML等)的转换和处理。 提供统一的数据格式处理规则,确保外部请求和系统内部微服务之间的数据交互一致性和兼容性。 日志记录与监控‌: 记录所有经过API网关的请求和响应日志,以便进行问题追踪和性能分析。 提供实时监控功能,实时展示API网关的运行状态和性能指标(如请求量、响应时间、错误率等)。 类似于这样的,但是应该比这个更加的完善和全面,不需要夹杂具体的代码在里面
最新发布
07-25
<think>我们正在完善微服务架构文档中的两个模块:1.2.1.2 多租户支持和1.2.1.3 API网关。要求内容详尽、结构清晰、符合SaaS架构设计规范,不包含具体代码。 参考站内引用: [^1]: SaaS是一种软件应用程序交付方式,软件提供商集中化托管一个或多个软件应用程序,并通过互联网向租户提供这些软件应用程序。 [^2]: 微服务架构下,技术选型是去中心化的。每个团队可以根据自身服务的需求和行业发展的现状,自由选择最适合的技术栈。 [^3]: 多租户(multi-tenancy)允许在一个微服务架构中允许多系统共存,利用微服务稳定性以及模块化,租户可以是测试,金丝雀发布,影子系统等,使用租户能够保证代码的隔离性并且能够基于流量租户做路由决策。 [^4]: 一款基于Spring Cloud Alibaba的微服务架构——Saas开发平台,集成了多个功能强大的服务套件,如Spring Cloud GatewaySpring Security Oauth2等。 [^5]: 用户身份验证等子系统可能不属于核心,因为它们对许多SaaS产品很常见并且可以重用。 根据用户上一次的问题和回答,我们已经有了多租户支持和API网关的初步内容,现在需要进一步完善。 1.2.1.2 多租户支持 在SaaS系统中,多租户架构是核心设计,它允许多个租户共享相同的应用程序实例,同时保持各自数据的隔离性。多租户支持主要从以下几个方面进行设计: 1. 租户识别机制:系统需要能够识别每个请求所属的租户。常见的识别方式包括: - 子域名:每个租户使用独立的子域名(如tenant1.app.com) - URL路径:在URL中包含租户标识(如app.com/tenant1/resource) - 请求头:通过自定义HTTP头(如X-Tenant-ID)传递租户标识 识别到的租户标识将被用于后续的数据隔离和资源分配。 2. 数据隔离策略:根据业务需求和安全要求,可以选择不同级别的数据隔离方案: a. 独立数据库(Database per Tenant):每个租户拥有独立的数据库实例,提供最高级别的数据隔离,但成本较高。 b. 共享数据库独立表空间(Shared Database, Separate Schema):所有租户共享同一个数据库,但每个租户拥有独立的表空间(Schema),实现数据逻辑隔离。 c. 共享表空间(Shared Schema):所有租户共享相同的数据库表,通过租户标识字段(如tenant_id)进行数据隔离。这种方式资源利用率最高,但需要确保所有SQL操作都包含租户ID条件,避免数据泄露[^3]。 在实际应用中,可根据租户规模和安全要求混合使用上述策略。例如,对于大型企业租户采用独立数据库,中小型租户采用共享表空间模式。 3. 资源分配与隔离:为了避免“嘈杂邻居”问题,系统需要为每个租户分配合理的资源配额,包括: - 计算资源:CPU、内存配额限制 - 数据库资源:连接池大小、查询时间限制 - 网络资源:带宽限制 这些配额可通过配置中心(如Nacos)动态管理,并配合限流组件(如Sentinel)实现租户级别的资源控制[^4]。 4. 统一身份认证:多租户系统需要统一的身份认证体系,支持OAuth2.0、SAML等协议。每个租户可以配置独立的身份提供商(IdP),系统根据租户上下文选择相应的认证方式。同时,权限管理应基于RBAC模型,确保租户内的用户只能访问授权资源[^5]。 5. 租户生命周期管理:系统应提供租户注册、配置、暂停、删除等全生命周期管理功能。新租户注册时,系统根据预设策略自动分配资源(如创建数据库表空间、初始化配置等)。 1.2.1.3 API网关 API网关作为微服务架构的入口,承担着请求路由、安全控制、流量管理等关键职责。在多租户SaaS系统中,API网关还需支持租户维度的策略管理。 1. 核心功能: a. 动态路由:根据请求路径、租户标识等条件将请求路由至对应的后端服务实例。支持基于服务注册中心(如Nacos)的服务发现,实现负载均衡。 b. 统一认证鉴权:集成OAuth2.0、JWT等认证机制,验证请求的合法性。网关负责验证访问令牌,并将解析后的用户信息(包括租户ID)传递给下游服务。 c. 限流熔断:支持租户级别的限流策略,例如: - 固定窗口限流:限制每个租户单位时间内的请求数 - 令牌桶算法:控制突发流量 当服务出现故障时,网关可触发熔断机制,避免故障扩散[^4]。 d. 日志与监控:记录所有请求的访问日志,包括租户ID、请求时间、响应状态等关键信息。支持将日志发送至ELK等分析平台,实现租户维度的性能监控。 2. 租户策略管理: - 租户路由策略:根据租户ID将请求路由至特定的服务集群(如金丝雀环境或专用集群)。 - 租户限流策略:为不同租户设置独立的QPS阈值,高优先级租户可分配更多资源。 - 租户缓存策略:支持基于租户ID的缓存隔离,避免跨租户缓存污染。 3. 安全增强: - 防止跨租户攻击:在网关层校验请求中的租户ID与用户所属租户是否匹配。 - 敏感操作审计:记录关键操作(如数据导出)的审计日志。 通过以上设计,API网关成为保障多租户SaaS系统安全性和可扩展性的关键组件。 相关问题: 1. 如何选择合适的多租户数据隔离方案? 2. 在API网关中如何实现租户级别的限流策略? 3. 多租户系统中如何进行租户生命周期管理? 4. 如何设计多租户系统的统一身份认证体系? 5. API网关如何与微服务架构中的服务注册中心协同工作?</think>### 1.2.1.2 多租户支持 多租户架构是SaaS系统的核心特征,通过共享基础设施实现租户间的资源隔离与数据安全。设计需满足以下关键维度: #### **租户识别机制** - **标识提取**:通过子域名(如 `tenant1.app.com`)、URL路径(`/tenant1/api`)或请求头(`X-Tenant-ID`)动态获取租户标识[^3]。 - **上下文传递**:租户标识通过线程局部变量(ThreadLocal)或请求上下文(如Spring Cloud Sleuth)透传至下游服务,确保全链路租户隔离。 #### **数据隔离策略** | **方案** | **适用场景** | **优势** | **挑战** | |-------------------------|----------------------------------|-----------------------------------|------------------------------| | **独立数据库** | 高安全需求租户(如金融、医疗) | 物理隔离最强,符合GDPR等合规要求 | 运维成本高,资源利用率低[^1] | | **共享库独立Schema** | 中等规模租户集群 | 逻辑隔离,兼顾安全与资源效率 | 跨租户统计复杂 | | **共享表租户ID隔离** | 海量小微租户 | 资源利用率最高,扩展灵活 | 需严格SQL过滤防泄露[^3] | > 实践中采用混合策略:核心模块(如财务)用独立库,通用模块(如CRM)用共享表+租户ID字段。所有SQL需强制包含租户ID过滤条件(如 `WHERE tenant_id = ?`)[^3]。 #### **资源隔离与配额管理** - **计算资源**:通过Kubernetes Namespace或容器组实现租户级CPU/内存配额限制。 - **数据库连接**:动态数据源路由(如Spring AbstractRoutingDataSource)按租户ID分配独立连接池[^4]。 - **限流熔断**:集成Sentinel为不同租户配置独立QPS阈值,避免“嘈杂邻居”问题[^4]。 #### **统一身份认证** - **分层鉴权**: 1. **租户级**:OAuth2.0 Client Credentials流程验证租户合法性; 2. **用户级**:JWT令牌携带用户角色及租户ID,RBAC模型控制数据权限[^5]。 - **多租户SSO**:支持SAML协议对接企业IDP,实现租户自有身份源集成。 #### **生命周期管理** - **自动化供给**:租户注册时自动触发资源分配(如创建Schema、初始化配置); - **弹性伸缩**:基于租户活跃度动态启停服务实例,降低冷租户资源占用[^1]。 --- ### 1.2.1.3 API网关 作为微服务统一入口,API网关在多租户环境中承担流量治理与安全中枢角色。 #### **核心能力设计** | **功能** | **实现要点** | **多租户增强** | |--------------------|-----------------------------------------------------------------------------|-----------------------------------------------| | **动态路由** | 基于Nacos服务发现,支持URI/Header条件路由 | 按租户ID路由至专属集群(如大客户独享实例组)[^4] | | **认证鉴权** | 集成Spring Security OAuth2,验证JWT有效性 | 令牌绑定租户ID,拒绝跨租户访问请求[^5] | | **限流熔断** | Sentinel租户级规则:<br> - 计数器限流(QPS)<br> - 慢调用比例熔断 | 租户配额动态调整(如VIP租户更高吞吐)[^4] | | **请求转换** | 修改请求头/路径,支持协议转换(HTTP→Dubbo) | 自动注入租户ID至下游服务请求头 | #### **租户策略管理** - **流量染色**:通过请求头标记流量来源(如 `tenant_type=free_trial`),实现金丝雀发布与影子测试[^3]。 - **缓存隔离**:Redis键名拼接租户ID(如 `cache_${tenantId}_user:1`),防止跨租户缓存穿透。 - **审计日志**:记录租户ID、操作时间、资源路径,支持Kibana按租户维度分析。 #### **高可用架构** - **分层部署**: ```mermaid graph LR A[客户端] --> B[CDN边缘节点] B --> C[API Gateway集群] C --> D[微服务集群] ``` - **故障隔离**:租户级服务分组(Service Group),单租户故障不影响全局可用性; - **性能扩展**:基于OpenResty实现无状态网关水平扩容,支撑百万级租户并发[^4]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值