当前位置:   article > 正文

springcloud gateway集成RBAC模型——实现用户权限判断_rbac模型在spring cloud种

rbac模型在spring cloud种

目录

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.权限控制微服务

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

  1. @Service
  2. @Slf4j
  3. public class PermissionServiceImpl extends ServiceImpl<PermissionDao, RolesPermission> implements PermissionService {
  4. @Resource
  5. private PermissionDao permissionDao;
  6. @Override
  7. public boolean permissionAdd(String roles, List<String> codeList) {
  8. RolesPermission rolesPermission = new RolesPermission();
  9. rolesPermission.setRole(roles);
  10. int result = 0;
  11. RolesPermission role = permissionDao.selectOne(new QueryWrapper<RolesPermission>().eq("role",roles));
  12. if(role!=null){
  13. //去掉头尾括号,并转为列表
  14. List<String> list = new java.util.ArrayList<>(Collections.singletonList(
  15. StringUtils.strip(role.getPermissionCode(), "[]")));
  16. //将新数据添加至列表
  17. list.addAll(codeList);
  18. rolesPermission.setPermissionCode(list.toString());
  19. rolesPermission.setId(role.getId());
  20. result = permissionDao.updateById(rolesPermission);
  21. }else {
  22. rolesPermission.setPermissionCode(codeList.toString());
  23. result = permissionDao.insert(rolesPermission);
  24. }
  25. return result > 0;
  26. }
  27. }

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

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

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

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

4.springcloud gateway filter

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

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

  1. @Component
  2. public class JwtTokenUtil {
  3. private static final String JWT_CACHE_KEY = "jwt:userId:";
  4. private static final String USER_ID = "userId";
  5. private static final String USER_NAME = "username";
  6. private static final String ACCESS_TOKEN = "access_token";
  7. private static final String REFRESH_TOKEN = "refresh_token";
  8. private static final String EXPIRE_IN = "expire_in";
  9. @Resource
  10. private StringRedisTemplate stringRedisTemplate;
  11. @Resource
  12. private AuthJwtProperties jwtProperties;
  13. /**
  14. * 生成 token 令牌主方法
  15. * @param userId 用户Id或用户名
  16. * @return 令token牌
  17. */
  18. public Map<String, Object> generateTokenAndRefreshToken(String userId, String username, String roles) {
  19. //生成令牌及刷新令牌
  20. Map<String, Object> tokenMap = buildToken(userId, username, roles);
  21. //redis缓存结果
  22. cacheToken(userId, tokenMap);
  23. return tokenMap;
  24. }
  25. //将token缓存进redis
  26. private void cacheToken(String userId, Map<String, Object> tokenMap) {
  27. stringRedisTemplate.opsForHash().put(JWT_CACHE_KEY + userId, ACCESS_TOKEN, tokenMap.get(ACCESS_TOKEN));
  28. stringRedisTemplate.opsForHash().put(JWT_CACHE_KEY + userId, REFRESH_TOKEN, tokenMap.get(REFRESH_TOKEN));
  29. stringRedisTemplate.expire(userId, jwtProperties.getExpiration() * 2, TimeUnit.MILLISECONDS);
  30. }
  31. //生成令牌
  32. private Map<String, Object> buildToken(String userId, String username, String roles) {
  33. Map<String, String> map = new HashMap<>();
  34. map.put("roles",roles);
  35. //生成token令牌
  36. String accessToken = generateToken(userId, username, map);
  37. //生成刷新令牌
  38. String refreshToken = generateRefreshToken(userId, username, null);
  39. //存储两个令牌及过期时间,返回结果
  40. HashMap<String, Object> tokenMap = new HashMap<>(2);
  41. tokenMap.put(ACCESS_TOKEN, accessToken);
  42. tokenMap.put(REFRESH_TOKEN, refreshToken);
  43. tokenMap.put(EXPIRE_IN, jwtProperties.getExpiration());
  44. return tokenMap;
  45. }
  46. /**
  47. * 生成 token 令牌 及 refresh token 令牌
  48. * @param payloads 令牌中携带的附加信息
  49. * @return 令牌
  50. */
  51. public String generateToken(String userId, String username,
  52. Map<String,String> payloads) {
  53. Map<String, Object> claims = buildClaims(userId, username, payloads);;
  54. return generateToken(claims);
  55. }
  56. public String generateRefreshToken(String userId, String username, Map<String,String> payloads) {
  57. Map<String, Object> claims = buildClaims(userId, username, payloads);
  58. return generateRefreshToken(claims);
  59. }
  60. //构建map存储令牌需携带的信息
  61. private Map<String, Object> buildClaims(String userId, String username, Map<String, String> payloads) {
  62. int payloadSizes = payloads == null? 0 : payloads.size();
  63. Map<String, Object> claims = new HashMap<>(payloadSizes + 2);
  64. if(payloadSizes!=0){
  65. claims.put("roles", payloads.get("roles"));
  66. }
  67. claims.put("sub", userId);
  68. claims.put("username", username);
  69. claims.put("created", new Date());
  70. if(payloadSizes > 0){
  71. claims.putAll(payloads);
  72. }
  73. return claims;
  74. }
  75. /**
  76. * 刷新令牌并生成新令牌
  77. * 并将新结果缓存进redis
  78. */
  79. public Map<String, Object> refreshTokenAndGenerateToken(String userId, String username,String roles) {
  80. Map<String, Object> tokenMap = buildToken(userId, username,roles);
  81. stringRedisTemplate.delete(JWT_CACHE_KEY + userId);
  82. cacheToken(userId, tokenMap);
  83. return tokenMap;
  84. }
  85. //缓存中删除token
  86. public boolean removeToken(String userId) {
  87. return Boolean.TRUE.equals(stringRedisTemplate.delete(JWT_CACHE_KEY + userId));
  88. }
  89. /**
  90. * 判断令牌是否不存在 redis 中
  91. *
  92. * @param token 刷新令牌
  93. * @return true=不存在,false=存在
  94. */
  95. public Boolean isRefreshTokenNotExistCache(String token) {
  96. String userId = getUserIdFromToken(token);
  97. String refreshToken = (String)stringRedisTemplate.opsForHash().get(JWT_CACHE_KEY + userId, REFRESH_TOKEN);
  98. return refreshToken == null || !refreshToken.equals(token);
  99. }
  100. /**
  101. * 判断令牌是否过期
  102. *
  103. * @param token 令牌
  104. * @return true=已过期,false=未过期
  105. */
  106. public Boolean isTokenExpired(String token) {
  107. try {
  108. Claims claims = getClaimsFromToken(token);
  109. Date expiration = claims.getExpiration();
  110. return expiration.before(new Date());
  111. } catch (Exception e) {
  112. //验证 JWT 签名失败等同于令牌过期
  113. return true;
  114. }
  115. }
  116. /**
  117. * 刷新令牌
  118. *
  119. * @param token 原令牌
  120. * @return 新令牌
  121. */
  122. public String refreshToken(String token) {
  123. String refreshedToken;
  124. try {
  125. Claims claims = getClaimsFromToken(token);
  126. claims.put("created", new Date());
  127. refreshedToken = generateToken(claims);
  128. } catch (Exception e) {
  129. refreshedToken = null;
  130. }
  131. return refreshedToken;
  132. }
  133. /**
  134. * 验证令牌
  135. *
  136. * @param token 令牌
  137. * @param userId 用户Id用户名
  138. * @return 是否有效
  139. */
  140. public Boolean validateToken(String token, String userId) {
  141. String username = getUserIdFromToken(token);
  142. return (username.equals(userId) && !isTokenExpired(token));
  143. }
  144. /**
  145. * 生成令牌
  146. * @param claims 数据声明
  147. * @return 令牌
  148. */
  149. private String generateToken(Map<String, Object> claims) {
  150. Date expirationDate = new Date(System.currentTimeMillis()
  151. + jwtProperties.getExpiration());
  152. return Jwts.builder().setClaims(claims)
  153. .setExpiration(expirationDate)
  154. .signWith(SignatureAlgorithm.HS512,
  155. jwtProperties.getSecret())
  156. .compact();
  157. }
  158. /**
  159. * 生成刷新令牌 refreshToken,有效期是令牌的 2 倍
  160. * @param claims 数据声明
  161. * @return 令牌
  162. */
  163. private String generateRefreshToken(Map<String, Object> claims) {
  164. Date expirationDate = new Date(System.currentTimeMillis() + jwtProperties.getExpiration() * 2);
  165. return Jwts.builder().setClaims(claims)
  166. .setExpiration(expirationDate)
  167. .signWith(SignatureAlgorithm.HS512, jwtProperties.getSecret())
  168. .compact();
  169. }
  170. /**
  171. * 从令牌中获取数据声明,验证 JWT 签名
  172. * @param token 令牌
  173. * @return 数据声明
  174. */
  175. private Claims getClaimsFromToken(String token) {
  176. Claims claims;
  177. try {
  178. claims = Jwts.parser().setSigningKey(jwtProperties.getSecret()).parseClaimsJws(token).getBody();
  179. } catch (Exception e) {
  180. claims = null;
  181. }
  182. return claims;
  183. }
  184. }

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

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

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

然后,编写gateway过滤器:

  1. import com.alibaba.fastjson.JSON;
  2. import com.seven.springcloud.config.AuthJwtProperties;
  3. import com.seven.springcloud.config.WhiteListProperties;
  4. import com.seven.springcloud.constants.TokenConstants;
  5. import com.seven.springcloud.entities.CommonResult;
  6. import com.seven.springcloud.entities.enums.ResponseCodeEnum;
  7. import com.seven.springcloud.service.PermissionService;
  8. import com.seven.springcloud.util.JwtTokenUtil;
  9. import io.jsonwebtoken.Claims;
  10. import io.jsonwebtoken.Jwts;
  11. import lombok.extern.slf4j.Slf4j;
  12. import org.apache.commons.lang3.StringUtils;
  13. import org.springframework.cloud.gateway.filter.GlobalFilter;
  14. import org.springframework.context.annotation.Bean;
  15. import org.springframework.context.annotation.Configuration;
  16. import org.springframework.core.annotation.Order;
  17. import org.springframework.core.io.buffer.DataBuffer;
  18. import org.springframework.http.HttpStatus;
  19. import org.springframework.http.server.reactive.ServerHttpRequest;
  20. import org.springframework.http.server.reactive.ServerHttpResponse;
  21. import org.springframework.web.server.ServerWebExchange;
  22. import reactor.core.publisher.Flux;
  23. import reactor.core.publisher.Mono;
  24. import javax.annotation.Resource;
  25. import java.io.UnsupportedEncodingException;
  26. import java.net.URLEncoder;
  27. import java.nio.charset.StandardCharsets;
  28. import java.util.List;
  29. @Slf4j
  30. @Configuration
  31. public class JwtAuthCheckFilter {
  32. private static final String AUTH_TOKEN_URL = "/auth/login";
  33. private static final String REFRESH_TOKEN_URL = "/auth/token/refresh";
  34. public static final String USER_ID = "userId";
  35. public static final String USER_NAME = "username";
  36. public static final String FROM_SOURCE = "from-source";
  37. @Resource
  38. private WhiteListProperties whiteListProperties;
  39. @Resource
  40. private AuthJwtProperties authJwtProperties;
  41. @Resource
  42. private PermissionService permissionService;
  43. @Resource
  44. private JwtTokenUtil jwtTokenUtil;
  45. @Bean
  46. @Order(-101)
  47. public GlobalFilter jwtAuthGlobalFilter() {
  48. return (exchange, chain) -> {
  49. log.info("登录判断");
  50. ServerHttpRequest serverHttpRequest = exchange.getRequest();
  51. ServerHttpResponse serverHttpResponse = exchange.getResponse();
  52. ServerHttpRequest.Builder mutate = serverHttpRequest.mutate();
  53. String requestUrl = serverHttpRequest.getURI().getPath();
  54. // 跳过对登录请求的 token 检查。因为登录请求是没有 token 的,是来申请 token 的。
  55. if(AUTH_TOKEN_URL.equals(requestUrl)) {
  56. log.info("登录url,放行");
  57. return chain.filter(exchange);
  58. }
  59. // 从 HTTP 请求头中获取 JWT 令牌
  60. String token = getToken(serverHttpRequest);
  61. if (StringUtils.isEmpty(token)) {
  62. return unauthorizedResponse(exchange, serverHttpResponse, ResponseCodeEnum.TOKEN_MISSION);
  63. }
  64. // 对Token解签名,并验证Token是否过期
  65. boolean isJwtNotValid = jwtTokenUtil.isTokenExpired(token);
  66. if(isJwtNotValid){
  67. return unauthorizedResponse(exchange, serverHttpResponse, ResponseCodeEnum.TOKEN_INVALID);
  68. }
  69. // 验证 token 里面的 userId 是否为空
  70. String userId = jwtTokenUtil.getUserIdFromToken(token);
  71. String username = jwtTokenUtil.getUserNameFromToken(token);
  72. if (StringUtils.isEmpty(userId)) {
  73. return unauthorizedResponse(exchange, serverHttpResponse, ResponseCodeEnum.TOKEN_INVALID);
  74. }
  75. // 设置用户信息到请求
  76. addHeader(mutate, USER_ID, userId);
  77. addHeader(mutate, USER_NAME, username);
  78. // 内部请求来源参数清除
  79. removeHeader(mutate, FROM_SOURCE);
  80. return chain.filter(exchange.mutate().request(mutate.build()).build());
  81. };
  82. }
  83. //添加头部信息
  84. private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) {
  85. if (value == null) {
  86. return;
  87. }
  88. String valueStr = value.toString();
  89. String valueEncode = urlEncode(valueStr);
  90. mutate.header(name, valueEncode);
  91. }
  92. //移除头部信息
  93. private void removeHeader(ServerHttpRequest.Builder mutate, String name) {
  94. mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
  95. }
  96. //内容编码,配置为UTF-8
  97. static String urlEncode(String str) {
  98. try {
  99. return URLEncoder.encode(str, "UTF-8");
  100. }
  101. catch (UnsupportedEncodingException e)
  102. {
  103. return StringUtils.EMPTY;
  104. }
  105. }
  106. @Bean
  107. @Order(-100)
  108. public GlobalFilter permissionGlobalFilter() {
  109. return (exchange, chain) -> {
  110. log.info("权限判断");
  111. // 从 HTTP 请求头中获取 JWT 令牌
  112. ServerHttpRequest request = exchange.getRequest();
  113. ServerHttpResponse response = exchange.getResponse();
  114. ServerHttpRequest.Builder mutate = request.mutate();
  115. String path = request.getURI().getPath();
  116. //对白名单中的地址放行
  117. List<String> whiteList = whiteListProperties.getWhites();
  118. for(String str : whiteList){
  119. if(path.contains(str)){
  120. log.info("白名单,放行{}",request.getURI().getPath());
  121. return chain.filter(exchange);
  122. }
  123. }
  124. //String headerToken = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION);
  125. //判断用户权限
  126. String token = getToken(request);
  127. boolean permission = hasPermission(token,path);
  128. if (!permission){
  129. log.info("用户没有权限");
  130. return unauthorizedResponse(exchange, response, ResponseCodeEnum.PERMISSION_DENIED);
  131. }
  132. return chain.filter(exchange.mutate().request(mutate.build()).build());
  133. };
  134. }
  135. private boolean hasPermission(String token, String path){
  136. //解码jwt token
  137. Claims claims = Jwts.parser().setSigningKey(authJwtProperties.getSecret()).parseClaimsJws(token).getBody();
  138. //获取token中的权限值
  139. String roles = (String) claims.get("roles");
  140. if(roles!=null){
  141. List<String> pathList = permissionService.pathGet(roles);
  142. path = path.replaceFirst("/api",StringUtils.EMPTY);
  143. for (String api:pathList){
  144. if(api.equals(path)){
  145. return true;
  146. }
  147. }
  148. }
  149. return false;
  150. }
  151. //请求token
  152. private String getToken(ServerHttpRequest request) {
  153. String token = request.getHeaders().getFirst(authJwtProperties.getHeader());
  154. // 如果前端设置了令牌前缀,则裁剪掉前缀
  155. if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX))
  156. {
  157. token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
  158. }
  159. return token;
  160. }
  161. //jwt鉴权失败处理类
  162. private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, ServerHttpResponse serverHttpResponse, ResponseCodeEnum responseCodeEnum) {
  163. log.warn("token异常处理,请求路径:{}", exchange.getRequest().getPath());
  164. serverHttpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
  165. serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
  166. CommonResult<Object> responseResult = new CommonResult<>(responseCodeEnum.getCode(),responseCodeEnum.getMessage());
  167. DataBuffer dataBuffer = serverHttpResponse.bufferFactory()
  168. .wrap(JSON.toJSONStringWithDateFormat(responseResult, JSON.DEFFAULT_DATE_FORMAT)
  169. .getBytes(StandardCharsets.UTF_8));
  170. return serverHttpResponse.writeWith(Flux.just(dataBuffer));
  171. }
  172. }

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

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

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

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

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

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

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

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

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

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

  1. @Configuration
  2. public class FeignConfig {
  3. @Bean
  4. @ConditionalOnMissingBean
  5. public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
  6. return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
  7. }
  8. }

5.实现效果

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

 

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

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

判断用户没有权限。

访问查询接口:

 拥有权限,查询成功。

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

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

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/302053
推荐阅读
相关标签
  

闽ICP备14008679号