用户权限管理体系 (RBAC)

用户权限管理体系发展史

最早期,我们通过从前台传递来 username(用户名) password(密码) role_status(角色状态)

传递这些信息到 Controller 层,之后 controller 会通过 if 判断来校验角色信息

不同的角色 controller 的响应对应不同的 service 和 view

不同的角色:

  • 获取的数据不同
  • 展示的页面不同
  • 具备的操作权限不同

这种实现形式具有很多的缺点:

  1. 代码耦合度非常高
  2. 扩展性很差(新增角色时操作繁琐)
  3. 维护难度很高

以角色为学生、教师为例子,大致流程图如下:

在这里插入图片描述

经过一段时期的发展,对于权限体系,我们可以采取加上 Filter 过滤器,来过滤所有的请求

这种方式在数据库中维护的资源是所有的请求 URI(统一资源标识符) ,即 RequestMapping 中的字符串

(在此阶段中也产生了一种形式:URL 结尾为 .action 和 .do 结尾,只拦截 .do 结尾的请求进行数据库的匹配)

此时,我们会遇到如以下的一些问题:

  1. 并不是所有的 URL 都要拦截
  2. 拦截规则应该添加白名单
  3. 登陆的请求不应该被拦截
  4. 静态资源不应该被拦截
  5. 没有统一的标准,所拦截的 URI 粒度太细,在资源层面无法统一
  6. 会增加一定的工作量
  7. 安全性、授权形式等均需要自行实现

再发展时,就对细粒度的 URI 进行了一定的归纳,形成了统一的字符串来标识一类资源权限

可以通过注解及 AOP 切面的形式,对不同的 controller 方法,根据权限需求,绑定统一的字符串

从而达到 用户对应角色,角色对应资源 的模式

此时就和如今的 RBAC 体系类似了。

注解及AOP实现RBAC体系的思路

在不使用类似 JWT 等框架的前提下,可以使用 UUID 来模拟生成随机的 token

模拟流程图如下:

在这里插入图片描述

STEP 1 登陆阶段

用户进行登陆,需要输入用户名以及密码

在用户名、密码校验通过之后,在后台通过 UUID 生成随机的 token

将生成的随机 token 与用户名存入 Redis 缓存中 (使用 Hash 的方式,Key 值存放用户名,Value 值存放生成的 token)

STEP 2 校验阶段

在用户登陆成功后,会在其用户信息中加入 token ,每次发起请求时,都会在请求头中带上此 token

当用户发起请求时,token 随请求传入后台

后台首先经过 token Filter 过滤器,判断其 token 是否合法及是否包含 token

如果 token 不合法或为空,则返回给前台 401 权限认证失败

如果 token 合法,则从 Redis 缓存中取出对应此 token 的用户名,根据用户名判断其角色及角色所对应的权限 (Redis 中不存在该 token 则返回权限认证失败)

STEP 3 定义注解

通过定义自定义注解,并在注解中加入 value 值标识对应的权限字段,来控制在不同的 controller 中需要的权限资源信息

STEP 4 AOP 切面校验权限

首先,查找到注解修饰的方法

其次,判断 token 对应的用户名对应的角色的权限中,是否包含有注解中所声明的权限信息

若没有对应的权限,则返回权限认证失败

若存在对应的权限信息,则进行访问该资源,并将数据返回至前台

代码模拟实现 RBAC

SpringBoot 配置文件 application.yml

token:  
  # header 值  
  header: Authorization  
  # token 前缀  
  prefix: "MELODY BEARER "  
  # 白名单,不过滤token  
  exclude: /auth/temp,/auth/login

TokenFilter.java

@Component  
public class TokenFilter implements Filter {  
    @Value("${token.header}")  
    private String header;  
    @Value("${token.prefix}")  
    private String prefix;  
    @Value("${token.exclude}")  
    private String exclude;  
    @Override  
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {  
        List<String> excludeList = Arrays.asList(exclude.split(","));  
        HttpServletRequest request = (HttpServletRequest) servletRequest;  
        String token = request.getHeader(header);  
        if(excludeList.contains(request.getRequestURI())) {  
            filterChain.doFilter(servletRequest, servletResponse);  
            return;  
        }  
        if(token == null || token.equals("") || !token.startsWith(prefix)) {  
            System.err.println("TOKEN ERROR");  
            HttpServletResponse response = (HttpServletResponse) servletResponse;  
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);  
            return;  
        } else {  
            filterChain.doFilter(servletRequest, servletResponse);  
        }  
    }  
}

Controller

@RestController  
@RequestMapping("/auth")  
public class AuthController {  
  
    @Value("${token.prefix}")  
    private String prefix;  
  
    @Autowired  
    private RedisTemplate redisTemplate;  
  
    @Autowired  
    private MultiThreadQueryService multiThreadQueryService;  
  
    @AuthCheck(value = "/auth/test")  
    @GetMapping("/test")  
    public ResJson test() {  
        System.err.println("test");  
        return ResJson.yes();  
    }  
  
    @AuthCheck(value = "/auth/temp")  
    @GetMapping("/temp")  
    public ResJson temp() {  
        System.err.println("temp");  
        return ResJson.yes();  
    }  
  
    @PostMapping("/login")  
    public ResJson login(@RequestBody User usr) {  
        // 模拟用户登陆正确的用户名与密码  
        if(usr.getNickname().equals("admin") && usr.getPassword().equals("123456")) {  
            String token = prefix + UUID.randomUUID();  
            redisTemplate.opsForHash().put("token", usr.getNickname(), token);  
            Stack stack = new Stack<>();  
            stack.push(token);  
            stack.push(usr);  
            return ResJson.yes(stack);  
        }  
        return ResJson.no(ResCode.FAIL);  
    }  
}

自定义注解 AuthCheck

@Target({ ElementType.PARAMETER, ElementType.METHOD })  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
public @interface AuthCheck {  
  
    public String value() default "";  
  
}

AOP 切面

在切面中存在有两种方法
1 .细粒度的根据请求的 URI 来进行判断权限的方式 (way1部分)
2. 根据统一权限字符串判断权限的方式 (way2部分)

@Aspect  
@Component  
public class AuthAspect {  
  
    @Value("${token.header}")  
    private String header;  
  
    @Autowired  
    private HttpServletRequest request;  
  
    @Autowired  
    private RedisTemplate redisTemplate;  
  
    @Pointcut("@annotation(com.melody.auth.annotation.AuthCheck)")  
    public void authPointCut(){}  
  
    @Around("authPointCut()")  
    public Object authCheck(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {  
        String username = "";  
        try{  
            // 判断 TOKEN            
            if (redisTemplate.opsForHash().values("token").stream().anyMatch(str -> str.equals(request.getHeader(header)))) {  
                Map<String, String> map = redisTemplate.opsForHash().entries("token");  
                for(Map.Entry<String, String> entry : map.entrySet()) {  
                    if(entry.getValue().equals(request.getHeader(header))) {  
                        username = entry.getKey();  
                    }  
                }  
            }  
            if(username == null || username.equals("")){  
                throw new AuthException(ResCode.TOKEN_NOT_EXIST);  
            } else {  
                // 模拟根据用户名获取用户对应角色的有权限访问的(way1:资源列表)/(way2:权限字段资源列表)  
                List<String> resourceList = new ArrayList<>();  
                resourceList.add("/auth/test");  
                resourceList.add("/auth/login");  
  
                /****************/  
                // way1:根据URI来判断权限,此时数据库中存所有的URI,扩展性很差
                // boolean flag = resourceList.stream().anyMatch(str -> str.equals(request.getRequestURI()));   
                /****************/  
  
                /****************/                //从切面织入点处通过反射机制获取织入点处的方法  
                MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();  
                //获取切入点所在的方法  
                Method method = signature.getMethod();  
                AuthCheck ac = method.getAnnotation(AuthCheck.class);  
                boolean flag = false;  
                if(ac != null) {  
                    String auth = ac.value();  
                    // way2:数据库中存放权限字段,根据注解的value确定请求所需权限判断是否有权限进行访问  
                    flag = resourceList.stream().anyMatch(str -> str.equals(auth)); 
                }  
                /****************/  
  
                if(!flag) {  
                    throw new AuthException(ResCode.BANED_REQUEST);  // 自定义异常
                }  
            }  
        } catch(AuthException e) {  
            System.err.println(e.getResCode().getCode() + ":" + e.getResCode().getMsg());  
            return ResJson.no(e.getResCode());  
        }  
        Object res = proceedingJoinPoint.proceed();  
        return res;  
    }  
  
}

自定义异常 AuthException

@Getter  
public class AuthException extends RuntimeException{  
  
    private ResCode resCode;  
  
    public AuthException(ResCode resCode) {  
        this.resCode = resCode;  
    }  
  
}

自定义 Json 字符串 ResJson

@Data  
public class ResJson<T> implements Serializable {  
  
    private int code;  
  
    private String msg;  
  
    private T data;  
  
    public static ResJson yes() {  
        return yes("");  
    }  
  
    public static ResJson yes(Object o) {  
        return new ResJson(ResCode.SUCCESS, o);  
    }  
  
    public static ResJson no(ResCode resCode) {  
        return new ResJson(resCode);  
    }  
  
    public static  ResJson no(ResCode resCode, Object o) {  
        return new ResJson(resCode, o);  
    }  
  
    public ResJson(){}  
  
    public ResJson(ResCode resCode) {  
        setResCode(resCode);  
    }  
  
    public ResJson(ResCode resCode, T data) {  
        setResCode(resCode);  
        this.data = data;  
    }  
  
    public void setResCode(ResCode resCode) {  
        this.code = resCode.getCode();  
        this.msg = resCode.getMsg();  
    }  
  
    @Override  
    public String toString() {  
        return "ResJson{" +  
                "\"code\":" + code +  
                ",\"msg\":" + msg + '\'' +  
                ",\"data\":" + data +  
                '}';  
    }  
}

自定义状态码 ResCode

package com.melody.auth.model;  
  
public enum ResCode {  
  
    SUCCESS(200, "获取数据成功"),  
  
    FAIL(400, "参数或语法错误"),  
  
    TOKEN_NOT_EXIST(401, "TOKEN不存在,认证失败"),  
  
    BANED_REQUEST(403, "无权访问该资源"),  
  
    FATAL("未知致命错误"),  
    ;  
  
    private int code;  
  
    private String msg;  
  
    ResCode(int code, String msg) {  
        this.code = code;  
        this.msg = msg;  
    }  
  
    ResCode(String msg) {  
        this.code = -999;  
        this.msg = msg;  
    }  
  
    public int getCode() {  
        return this.code;  
    }  
  
    public String getMsg() {  
        return this.msg;  
    }  
  
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
你想要了解有关权限管理的演示,使用的是基于角色的访问控制(RBAC)和 Casbin 的解决方案。Casbin 是一个强大的授权库,它提供了灵活的访问控制模型和规则引擎。下面是一个简单的 RBAC 和 Casbin 的权限管理演示: 1. 安装 Casbin: ``` pip install casbin ``` 2. 创建一个示例访问控制模型(model.conf),定义了用户、角色、资源和操作: ``` [request_definition] r = sub, obj, act [policy_definition] p = sub, obj, act [role_definition] g = _, _ [policy_effect] e = some(where (p.eft == allow)) [matchers] m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act ``` 3. 创建一个权限管理策略文件(policy.csv),定义了角色与资源的访问权限: ``` p, alice, data1, read p, bob, data2, write g, alice, admin ``` 4. 在 Python 中使用 Casbin 进行权限验证: ```python from casbin import Enforcer # 加载访问控制模型和策略文件 enforcer = Enforcer("model.conf", "policy.csv") # 验证用户的访问权限 if enforcer.enforce("alice", "data1", "read"): print("alice 有读取 data1 的权限") else: print("alice 没有读取 data1 的权限") if enforcer.enforce("bob", "data2", "write"): print("bob 有写入 data2 的权限") else: print("bob 没有写入 data2 的权限") ``` 这只是一个简单的 RBAC 和 Casbin 的权限管理演示,你可以根据实际需求自定义更复杂的访问控制模型和策略。希望这个演示对你有帮助!如果你对 Casbin 想要了解更多信息,可以参考官方文档:https://casbin.org/

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值