Springboot权限管理

一、前言

最近在整合完单点登录后,又来了一个新活,好在这个任务已经比较成熟,实现的方式也比较多,也有比较成熟的框架已经实现了它,它就是权限管理。

二、实现过程

实现权限管理的方式有很多,我提出我通过查阅资料学习所知道的,如果有更多欢迎分享:

  1. 拦截器做鉴权
  2. AOP做鉴权
  3. shiro框架
  4. springsecurity框架

其中,1、2点都是基于在类或方法上加上一个自定义注解实现的,在通知或拦截器中通过获取类或方法上的注解的value值,来比较判断用户的权限,再决定是否放行执行我们的核心业务。

考虑到公司架构和其他原因,我使用到的是使用AOP进行鉴权认证。

实现过程如下:

2.1数据库设计   

2.1.1RBAC权限模型

RBAC是基于角色的访问控制,是用户通过角色与权限进行关联,为什么不直接给用户分配权限呢?简单来说,一个用户拥有多个角色,每个角色拥有若干权限。这样就构成了“用户-角色-权限”的授权模型。在这个模型中,用户与角色、角色与权限之间是多对多的关系。

根据角色授权的思想,我们需要设计五张表

​ 用户表(user)

​ 角色表(role)

​ 权限表(permission)

这三个表之间都是多对多的关系,所以衍生出两个关联表如下

 用户角色表(user_role)

​ 角色资源表(role_permission)

2.2.选择方案

2.2.1基于AOP的实现

1.定义注解

@Retention(value = RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
@Inherited
@Documented
public @interface PreAuthorize {
    String value();
}

注解的value值代表的是方法所需的条件或权限

2.AuthorizeAspect切面定义


@Aspect
@Component
public class AuthorizeAspect {

    @Autowired
    private ZsjScadaUserService userService;

    @Autowired
    private ZsjScadaUserRoleService userRoleService;

    @Autowired
    private ZsjScadaRolePermissionService rolePermissionService;

    @Autowired
    private ZsjScadaPermissionService permissionService;

    //定义切点
    @Pointcut("@annotation(com.metastar.vip.scada.service.annotation.PreAuthorize)")
    public void logPointCut(){

    }
    //鉴权通知
    //环绕通知选择原因:取消方法执行:在环绕通知中,我们可以选择不执行目标方法(JointPoint),从而取消方法的执行。这在鉴权过程中非常有用,如果用户没有相应的权限,我们可以直接返回错误信息,而不必执行目标方法。
    @Around("logPointCut()")
    public Object authAround(ProceedingJoinPoint joinPoint) throws Throwable{

        // TODO: 2023/8/31 获取目标方法中的HttpRequest -> 获取请求头携带的Cookie
        Object[] args = joinPoint.getArgs();
        String name = joinPoint.getSignature().getName();
        HttpServletRequest request = (HttpServletRequest) args[0];
        Object proceed = null;
        HttpSession session = request.getSession(false);
        if (session == null){
      
            return proceed = Result.fail("请先登录");
        }
//         TODO: 2023/8/31  根据cookie获取当前会话信息 -> 获取当前登录用户
        Assertion assertion = (Assertion) session.getAttribute("_const_cas_assertion_");
        Map attributes = assertion.getPrincipal().getAttributes();
        String user = attributes.get("USER").toString();
        JSONObject userJsonObject = JSON.parseObject(user);
        String userId = userJsonObject.getString("userId");
//         TODO: 2023/8/31 根据用户ID查询该用户角色
        QueryWrapper queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("id",userId).eq("in_use",1);
        UserDO userDO = userService.getOne(queryWrapper);
        if (user == null){
            throw new RuntimeException("该用户不存在");
        }
//         TODO: 2023/8/31 根据该角色去查询所拥有的权限
        QueryWrapper userRoleDOQueryWrapper = new QueryWrapper<>();
        userRoleDOQueryWrapper.eq("user_id",userDO.getId());
        UserRoleDO one = userRoleService.getOne(userRoleDOQueryWrapper);
        Long roleId = one.getRoleId();
        QueryWrapper rolePermissionDOQueryWrapper = new QueryWrapper<>();
        rolePermissionDOQueryWrapper.eq("role_id",roleId);
        List permissionIds = rolePermissionService.list(rolePermissionDOQueryWrapper).stream().map(RolePermissionDO::getPermissionId).collect(Collectors.toList());
//         TODO: 2023/8/31 查看该权限所能访问的资源uri
//         TODO: 2023/8/31 查看当前目标类+方法的URI是否存在与当前用户权限所对应的资源uri列表中
//         TODO: 2023/8/31 存在->继续指定控制器中的方法 不存在->返回错误信息
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Object target = joinPoint.getTarget();
        //获取注解标注的类(鉴权以类为单位)
//        PreAuthorize annotation1 = target.getClass().getAnnotation(PreAuthorize.class);
//        String value = annotation1.value();
        //获取注解标注的方法(鉴权以方法为单位)
        Method method = target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
        //通过方法获取注解
        PreAuthorize annotation = method.getAnnotation(PreAuthorize.class);
        //获取注解中的值
        String permissionValue = annotation.value();
        Integer count = 0;
        for (Long permissionId : permissionIds) {

            String permissionName = permissionService.getById(permissionId).getPermissionName();
            if (permissionName.equals(permissionValue)){
                //该角色含有该权限 访问方法
                proceed = joinPoint.proceed(args);
                break;
            }
            count++;
        }
        if (count == permissionIds.size()){
            //没有该权限 抛出异常
            return proceed = Result.fail("权限不足,无法访问");
    
        }
        return proceed;
    }

}

3.测试

@RestController
@RequestMapping("/auth")
public class authTestController {

    @PostMapping("/test1")
    @PreAuthorize(value = "用户模块")
    public Result Test1(HttpServletRequest request,String userName){

        return Result.ok(userName);
    }
}

Postman访问该url后,拦截后执行环绕通知进行鉴权,通过后访问该控制器中的Test1方法响应结果。

2.2.2基于拦截器的实现

1.同样是定义自定义注解

@Target({ElementType.TYPE, ElementType.METHOD}) //注解的作用域,即此注解应该被用在什么地方
@Retention(RetentionPolicy.RUNTIME)  //注解的生命周期,即注解在什么范围内有效
@Inherited  //标识注解,允许子类继承
@Documented //标识注解,生成javadoc文档时,会包含此注解
public @interface RoleNum {
//    int value(); //default 1;
    String value();
}

2.定义拦截器

@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 目标方法执行之前(Controller方法调用之前)
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // TODO: 2023/7/21 获取前端Authorization的token值 访问redis
        // TODO: 2023/7/21 命中 放行 刷新redis中token的TTL
        // TODO: 2023/7/21 未命中(过期) 重新向登录页面 拦截
        // TODO: 2023/7/24 在验证到相应登陆的Token后 需要查看当前用户Role值 检验是否有该路径的访问权限
        String token = request.getHeader("Authorization");
        Long size = stringRedisTemplate.opsForHash().size(RedisConstant.LOGIN_INFO + token);
        if (size == 0){
            //重定向到登录页面
//            response.sendRedirect("/login");
            //拦截
            response.getWriter().write("{\"code\":-1,\"msg\":\"please login first\",\"data\":null}");
            return false;
        }else {
            String role = (String) stringRedisTemplate.opsForHash().get(RedisConstant.LOGIN_INFO + token, "role");
            if (hasPermission(handler,role)) {
                //权限足够
                //放行 并 刷新 用户信息token TTL
                stringRedisTemplate.expire(RedisConstant.LOGIN_INFO + token,RedisConstant.LOGIN_TTL, TimeUnit.MINUTES);
                return true;
            } else {
                //权限不足
                response.getWriter().write("{\"code\":-1,\"msg\":\"privilege is not enough\",\"data\":null}");
                //放行 并 刷新 用户信息token TTL
                stringRedisTemplate.expire(RedisConstant.LOGIN_INFO + token,RedisConstant.LOGIN_TTL, TimeUnit.MINUTES);
                return false;
            }
        }
    }

    /**
     * 验证权限是否足够
     * @param handler
     * @param role
     * @return
     */
    private boolean hasPermission(Object handler, String role) {

        if (handler instanceof HandlerMethod){
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            //获取方法上的注解
            RoleNum roleNum = handlerMethod.getMethod().getAnnotation(RoleNum.class);
            //如果方法上的注解为空 则获取类的注解
            if (roleNum == null){
                roleNum = handlerMethod.getMethod().getDeclaringClass().getAnnotation(RoleNum.class);
            }
            //如果标记了注解,则判断权限
            if (roleNum != null){
                if (roleNum.value().equals("2")){  //该通用身份即可访问
//                    System.err.println("权限足够,正在访问");
                    return true;
                }
                if (roleNum.value().equals("1")) {  //Normal  0
                    if (role.equals("1")){
                        return true;
                    }else if (role.equals("0")){
                        return true;
                    }
                }
                if (roleNum.value().equals("0")){
                    if (role.equals("0")){
                        return true;
                    }else {
                        return false;
                    }
                }
            }
        }

        return false;
    }

注意:上述用户的角色权限信息是存储在Redis中的。

三、最后

现在鉴权模块已经非常成熟,以上仅为个人心得和实践,还有很多改进的地方欢迎评论,后续还会补充通过Shiro框架实现的鉴权这一模块。

你可能感兴趣的:(java,开发语言,spring,boot,spring)