赞
踩
目录
RBAC(Role Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联,而不是直接将权限赋予用户。
一个用户拥有若干个角色,每个角色拥有若干个权限,这样就构成了“用户-角色-权限”的授权模型。这种授权模型的好处在于,不必每次创建用户时都进行权限分配的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,减少频繁设置。
RBAC模型中,用户与角色之间、角色与权限之间,一般是多对多的关系。比如用户a可以同时拥有admin角色和user角色,user角色也可以同时被用户a及用户b拥有。
RBAC通过定义角色的权限,并对用户授予某个角色从而来控制用户的权限,实现了用户和权限的逻辑分离。简单来说就是,权限是属于角色的,想要获得某个权限,则用户必须先获得某个角色,通过角色操纵对应权限下的数据资源。
RBAC支持三个著名的安全原则:
RBAC模型的缺点是没有提供操作顺序的控制机制,这以缺陷使得RBAC模型很难 适应那些对操作顺序有严格要求的系统。、
下面对RBAC-0模型进行简单实现。
权限管理顾名思义,就是表与表之间的关系。所以必须要有三张表(用户表)(角色表)(数据资源表)
用户表:存储用户账号相关信息
角色表:存储角色相关信息
数据资源表:存储访问的api路由相关信息
其中用户表和角色表之间应建立映射关系:
表中存储用户账号和角色之间的关系,比如 :
此处只是简单应用,固一个账号只存放了一个角色,实践上一个账户应可存放多个角色,写成list形式以varchar格式存入数据库。
同时角色表和资源表之间也应建立映射关系:
至此,一个简单的RBAC权限模型下的数据库设计完毕。
然后,我们需要编写权限控制微服务,为角色分配权限和根据权限查询其拥有的权限:
- @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路由。
此处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()));
- }
- }
然后,我们对其进行实验,对应权限如下:
user角色拥有查询的权限,admin角色拥有查询和添加的权限。
首先登录拥有user角色的账号,获取对应的token。获取token后,携带token访问code:101下的api,进行添加地址:
判断用户没有权限。
访问查询接口:
拥有权限,查询成功。
切换拥有admin角色账号,获取token,访问添加接口:
访问成功,控制台打印如下;
先进行判断是否跳过鉴权;然后进行登录判断,是否携带token ,再进行权限判断,是否拥有权限。
以上即为用户权限判断全过程,RBAC模型的简单实现。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。