SpringSecurity实现RBAC权限验证

本文介绍了如何在SpringBoot项目中实现基于角色的访问控制(RBAC)框架,包括用户注册、登录、token生成与验证,以及如何通过JWT进行身份管理和权限控制。同时,详细描述了权限表设计、用户信息检验流程以及如何利用SpringSecurity进行权限决策和跨域处理。
摘要由CSDN通过智能技术生成

这个是我毕设实现的关于RBAC权限控制部分,大致的流程是用户注册登陆后,产生一个token,token通过jwt封装用户id来进行用户身份识别。添加过滤器拦截请求,从请求中获取url,判断当前url是否能被当前用户访问(权限表内会将角色能够访问的url进行存储)。

一、什么是RBAC

基于角色的访问控制(RBAC)是一种访问控制策略,用于管理系统、应用程序或网络中的用户对资源的访问权限。RBAC 将用户分配给角色,然后将权限分配给角色,而不是直接将权限分配给个别用户。这种方式简化了权限管理,特别是在大型组织或系统中,可以更轻松地管理权限。
RBAC 的主要组成部分包括:

  1. 角色(Roles):角色是一组具有相似职责或权限需求的用户集合。例如,一个企业应用可能有角色如“管理员”、“普通用户”、“审计员”等。
  2. 权限(Permissions):权限是指用户或角色被允许执行的特定操作或访问资源的能力。例如,读取、写入、删除文件或访问特定功能。
  3. 用户(Users):用户是系统中的实体,可以被分配到一个或多个角色。
  4. 角色分配(Role Assignment):将用户分配给角色的过程。一旦分配了角色,用户就继承了与该角色相关联的权限。
  5. 权限授予(Permission Granting):将权限授予角色的过程。这确定了哪些操作或资源可供特定角色使用。
  6. 访问控制(Access Control):根据用户的角色来控制对资源的访问。这包括验证用户的身份,检查其所属的角色,然后根据角色的权限来决定是否允许访问特定资源。
    使用 RBAC 可以带来以下优点:
    简化权限管理:通过将权限与角色相关联,管理员可以更轻松地管理大量用户的权限,而不必为每个用户单独配置权限。
    降低管理成本:RBAC 可以降低权限管理的复杂性和成本,因为权限只需分配给角色,而不是给每个用户分配。
    提高安全性:RBAC 可以确保用户只能访问其所需的资源,因此可以减少潜在的安全漏洞。
    总的来说,基于角色的访问控制(RBAC)是一种灵活且高效的访问控制策略,广泛用于各种规模和类型的组织和系统中。

二、基于RBAC的表设计

在这里插入图片描述
etao_user为用户表,etao_role为角色表。多对多关系,一个用户可以拥有多种角色权限,一个角色权限对应多个用户。(中间表为etao_user_role)
etao_role为角色表,etao_permisson为权限表。多对多关系,etao_role_permisson为中间表。

三、用户信息检验流程

分两种情况,已登录用户与未登录用户

1、未登陆用户直接登陆

springsecurity检验,通过后拿到用户基本信息和生成token与刷新token。

    @Override
    @Transactional(rollbackFor = Exception.class)
    public ResponseResult login(String userName, String password) {

        //AuthenticationManager authenticate进行用户认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName,password);
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //如果认证没通过,给出对应的提示
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("登录失败");
        }
        //如果认证通过了,使用userid生成一个jwt jwt存入ResponseResult返回
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        User user = loginUser.getUser();
        Long userid =  user.getUserId();
        // long转tostring
        Short userState = user.getUserState();
        log.info("用户角色"+userState);

        // 15分钟accessToken, 40分钟refreshToken
        String jwt = JwtUtil.createJWT(userid.toString(),JwtUtil.TOKEN_TIMEOUT);
        String reJwt = JwtUtil.createJWT(userName, JwtUtil.REFRESH_TOKEN_TIMEOUT);
        Map<String,Object> map = new HashMap<>();
        // TODO 双重token
        map.put("accessToken",jwt);
        map.put("refreshToken",reJwt);
        map.put("id",userid.toString());
        map.put("nickName",user.getNickName());
        map.put("userName",userName);
        // 用户上线
        int update = userMapper.update(null, new UpdateWrapper<User>().eq("user_id",userid).set("user_tag",1).set("update_data",new Date()));
        String tokenKey = "etao_token"+userid;
        String refreshTokenKey = "etao_refreshToken"+userName;
        log.info("#JWT"+jwt);
        log.info("#REJET",reJwt);
        // 存入 15分钟的 token
        if (redisCache.exists(tokenKey)) {
            redisCache.deleteObject(tokenKey);
        }
        if (redisCache.exists(refreshTokenKey)) {
            redisCache.deleteObject(refreshTokenKey);
        }
        redisCache.setCacheObject(tokenKey,jwt,JwtUtil.TOKEN_TIMEOUT, TimeUnit.MILLISECONDS);
        // 存入 40 分钟的 refreshToken
        redisCache.setCacheObject(refreshTokenKey,reJwt,JwtUtil.REFRESH_TOKEN_TIMEOUT,TimeUnit.MILLISECONDS);
        redisCache.setCacheObject("login:"+userid,loginUser);
        if (userState == 1) {
            map.put("permission", loginUser.getPermissionsList());
            return new ResponseResult(200,"登录成功",map);
        }
        return new ResponseResult(200,"登录成功",map);
    }

通过Authentication 返回的用户信息,组装成一个map返回给前端。token使用用户id进行生成,刷新token使用用户名称进行生成。
Authentication 通过UserDetailsService来获取用户的详细信息(包括用户名、密码和权限)自定义实现UserDetailsService,如下:

@Service
@Slf4j
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private UserRoleMapper userRoleMapper;
    @Autowired
    private RolePermissionMapper rolePermissionMapper;
    @Autowired
    private PermissionMapper permissionMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询用户信息
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName, username);
        User user = userMapper.selectOne(wrapper);
        //如果没有查询到
        if (Objects.isNull(user)) {
            throw new BusinessException(USER_EXCEPTION);
        }
        // 未审核用户禁止登录
        if (user.getUserState() == -1) {
            throw new BusinessException(USER_CHECK_EXCEPTION);
        }
        List<Integer> array = new ArrayList<>();
        List<String> arrays = new ArrayList<>();
        Integer roleId = userRoleMapper.selectOne(new LambdaQueryWrapper<UserRole>().eq(UserRole::getUserId, user.getUserId())).getRoleId();
        List<RolePermission> rolePermissions = rolePermissionMapper.selectList(new QueryWrapper<RolePermission>().select("perm_id").eq("role_id", roleId));
        rolePermissions.forEach(e -> {
            array.add(e.getPermId());
        });
        List<Permission> permissions = permissionMapper.selectList(new LambdaQueryWrapper<Permission>().in(Permission::getId, array));
        permissions.forEach(e -> {
            arrays.add(e.getPermissionUrl());
        });
        System.out.println(arrays);
        if (user.getUserState() == 1) {
            List<Permission> menuTreeRecursion = getMenuTreeRecursion(permissions);
            return new LoginUser(user,arrays,menuTreeRecursion);
        }
        return new LoginUser(user,arrays);
    }

    public static List<Permission> getMenuTreeRecursion(List<Permission> permissions){
        /**
         * 过滤分出父级菜单和子级菜单
         */
        List<Permission> parentSysMenuList = permissions.stream().filter(sysMenu -> sysMenu.getParentId() == 0).collect(Collectors.toList());
        List<Permission> childSysMenuList = permissions.stream().filter(sysMenu -> sysMenu.getParentId() > 0).collect(Collectors.toList());
        /**
         * 将子级目录菜单转换为map对象
         */
        Map<Integer, List<Permission>> map = childSysMenuList.stream().collect(Collectors.toMap(Permission::getParentId,
                // 此时的value 为集合,方便重复时操作
                s -> {
                    List<Permission> childSysMenuMap = new ArrayList<>();
                    childSysMenuMap.add(s);
                    return childSysMenuMap;
                },
                // 重复时将现在的值全部加入到之前的值内
                (List<Permission> value1, List<Permission> value2) -> {
                    value1.addAll(value2);
                    return value1;
                }
        ));

        /**
         * 循环对比,父级菜单和子级菜单,相同则加入父级对象中
         */
        parentSysMenuList.forEach(e -> {
            List<Permission> childSysMenus = map.get(e.getId());
            e.setChildSysMenu(childSysMenus);
        });
        /**
         * 需要对返回结果集排序,前端要展示第一个菜单项,做重定向
         */
        parentSysMenuList.sort(Comparator.comparing(Permission::getOrderNum));
        System.out.println(parentSysMenuList);
        return parentSysMenuList;
    }
}

校验用户是否存在,并将查询到的用户信息封装成LoginUser类返回。LoginUser类信息如下:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails , Serializable {
    private static final long serialVersionUID = -3210884885630038713L;

    private User user;
    //存储权限信息
    private List<String> permissions;

    private List<Permission> permissionsList;

    public LoginUser(User user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }
    //存储SpringSecurity所需要的权限信息的集合
    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;

    public LoginUser(User user, List<String> arrays, List<Permission> menuTreeRecursion) {
        this.user = user;
        this.permissions = arrays;
        this.permissionsList = menuTreeRecursion;
    }

    public List<String> getPermissions() {
        return permissions;
    }
    // TODO 权限字段
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities!=null){
            return authorities;
        }
        if (permissions == null) {
            authorities = new ArrayList<>();
            return authorities;
        }
        // 把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
        authorities= permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        // 用户登录 无权限校验
        return authorities;

    }
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

该类存放用户信息与权限信息

2、登陆后的用户

@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //放行
             filterChain.doFilter(request, response);
             return;
        }
        //解析token
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new SystemException(TOKEN_EXCEPTION);
        }
        //从redis中获取用户信息
        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if (Objects.isNull(loginUser)){
            throw new SystemException(LOGIN_EXCEPTION);
        }
        //存入SecurityContextHolder
        // 普通用户 没有权限信息
        if (loginUser.getAuthorities() == null){
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(loginUser,null,null);
        } else {
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        //放行
        filterChain.doFilter(request, response);
    }
}

Objects.isNull(loginUser)判断用户登陆是否过期或用户是否已经是退出登录的状态
loginUser.getAuthorities(),用户存在权限就存入权限信息,不存在则只用存入基本信息。

四、用户权限验证

通过authentication获取权限信息,比对url判断是否有权限进行访问

@Slf4j
public class MyAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        HttpServletRequest request = ((FilterInvocation) object).getRequest();
        String requestUrl = request.getRequestURI();
        log.info("#MyAccessDecisionManager#requestUrl"+requestUrl);
        // 遍历用户拥有的权限,与当前请求的 URL 进行匹配
        for (GrantedAuthority authority : authentication.getAuthorities()) {
            log.info("#MyAccessDecisionManager#authority.getAuthority()"+authority.getAuthority());
            if (requestUrl.contains(authority.getAuthority())) {
                // 如果匹配成功,直接返回,表示有权限访问
                return;
            }
        }

        // 如果没有匹配成功,表示没有权限访问,抛出 AccessDeniedException 异常
        throw new AccessDeniedException("Access Denied");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

配置springsecurity,添加权限验证过滤器与token拦截过滤器

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

       @Autowired
      private AccessDeniedHandler accessDeniedHandler;


    @Override
    public void configure(WebSecurity web) throws Exception {
        //解决静态资源被拦截的问题
        web.ignoring()
                .antMatchers(HttpMethod.GET, "/v1.0/index/product/**")
                // 登录,注册,验证邮箱,验证昵称放行
                .antMatchers("/v1.0/user/checkNickName/**")
                .antMatchers("/v1.0/user/register")
                .antMatchers("/v1.0/user/login")
                .antMatchers("/v1.0/user/sendMail")
                .antMatchers("/v1.0/user/checkMail")
                // 公告信息
                .antMatchers("/v1.0/index/notice/**")
                // 聊天放行
                .antMatchers("/v1.0/chat/**")
                // 刷新token放行
                .antMatchers("/v1.0/user/refreshToken")
                // 上传图片不需要Toekn检查
                .antMatchers("/v1.0/deal/img")
                .antMatchers("/v1.0/manage/register")
                .antMatchers("/v1.0/user/logout/**")
                .antMatchers("/v1.0/user/userDetail/**")
                .antMatchers("/v1.0/manage/register")
                .antMatchers("/v1.0/user/logout/**")
                .antMatchers(HttpMethod.OPTIONS)
                .antMatchers(HttpMethod.PATCH);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()

                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                .authorizeRequests()
                // 对于商品信息获取接口放行
                .antMatchers(HttpMethod.GET, "/v1.0/index/product/**").permitAll()
                // 登录,注册,验证邮箱,验证昵称放行
                .antMatchers("/v1.0/user/checkNickName/**").permitAll()
                .antMatchers("/v1.0/user/register").anonymous()
                .antMatchers("/v1.0/user/login").anonymous()
                .antMatchers("/v1.0/user/logout/**").anonymous()
                .antMatchers("/v1.0/user/sendMail").anonymous()
                .antMatchers("/v1.0/user/checkMail").anonymous()
                // 公告信息
                .antMatchers("/v1.0/index/notice/**").permitAll()
                // 聊天放行
                .antMatchers("/v1.0/chat/**").permitAll()
                // 刷新token放行
                .antMatchers("/v1.0/user/refreshToken").anonymous()
                // 上传图片不需要Toekn检查
                .antMatchers("/v1.0/deal/img").anonymous()

                .antMatchers("/v1.0/user/userDetail/**").permitAll()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .antMatchers(HttpMethod.PATCH).permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated().accessDecisionManager(new MyAccessDecisionManager());
        //添加过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);


        //配置异常处理器
        http.exceptionHandling()
                //配置认证失败处理器
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);

        //允许跨域.
        http.cors();

    }


    @Bean
    public AccessDecisionManager accessDecisionManager() {
        List<AccessDecisionVoter<?>> decisionVoters = Arrays.asList(
                new MyAccessDecisionVoter(),
                new RoleVoter(),
                new AuthenticatedVoter()
        );
        return new AffirmativeBased(decisionVoters);
    }
}

总结:整体逻辑并不难,需要再进行改进的:

  1. 内部密码校验可以自定义指定
  2. SecurityConfig太多冗余的,就是因为MyAccessDecisionVoter拦截器拦截了所有请求,public void configure(WebSecurity web)这个方法才能放行
  3. LoginUser类中不应该直接存User全部信息,应该再进行拆解
  • 46
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值