目录

1.RBAC权限控制模型

2.数据表设计

3.权限控制微服务

4.springcloud gateway filter

5.实现效果


1.RBAC权限控制模型

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

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

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

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_spring cloud

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

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

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

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

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

2.数据表设计

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

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_数据_02

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

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_gateway_03

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

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_数据_04

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

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

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_数据_05

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

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_分布式_06

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

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

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_List_07

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_数据_08

 

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_数据_09

至此,一个简单的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());

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_gateway_10

(如上图,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.实现效果

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

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_分布式_11

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_分布式_12

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_List_13

 

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

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

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_gateway_14

判断用户没有权限。

访问查询接口:

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_spring cloud_15

 拥有权限,查询成功。

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

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_数据_16

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

Spring cloud gateway 鉴权Filter springcloudgateway权限验证_List_17

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

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