SpringBoot权限控制(RBAC)设计详解:从入门到精通

一、RBAC基础概念与原理

1.1 什么是RBAC?

RBAC(Role-Based Access Control,基于角色的访问控制)是一种广泛使用的权限管理模型。它的核心思想是将权限与角色关联,用户通过成为适当角色的成员而获得这些角色的权限。

通俗理解:想象一家公司,有"经理"、“普通员工”、"财务"等角色。经理可以审批请假,财务可以查看工资,普通员工只能提交请假申请。RBAC就是这样的系统,不是直接给每个人分配权限,而是通过角色来间接分配。

1.2 RBAC核心组件

组件说明生活化例子
用户(User)系统的使用者公司员工张三
角色(Role)权限的集合"部门经理"这个职位
权限(Permission)对资源的操作权限“审批请假申请”、“查看财务报表”
资源(Resource)系统中被保护的对象请假申请单、财务报表
用户-角色关联用户和角色的关系张三被任命为部门经理
角色-权限关联角色和权限的关系部门经理可以审批请假

1.3 RBAC模型分类

模型类型特点适用场景
核心RBAC最基本的用户-角色-权限关系简单系统
层级RBAC角色之间有继承关系(上级角色包含下级角色的权限)组织结构分明的系统
约束RBAC增加了职责分离约束(如互斥角色)金融、政务等高安全性系统

二、Spring Security基础集成

2.1 添加Spring Security依赖

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

添加这个依赖后,Spring Boot会自动保护所有端点,并生成一个默认用户(user)和随机密码(在启动日志中)。

2.2 基础安全配置

@Configuration
@EnableWebSecurity
public class BasicSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/public/**").permitAll() // 公共资源允许所有人访问
                .anyRequest().authenticated() // 其他所有请求需要认证
            .and()
            .formLogin() // 启用表单登录
                .loginPage("/login") // 自定义登录页
                .permitAll()
            .and()
            .logout() // 启用注销功能
                .permitAll();
    }
    
    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        // 内存中的用户存储(仅用于演示,生产环境用数据库)
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build();
        
        UserDetails admin = User.withDefaultPasswordEncoder()
            .username("admin")
            .password("admin")
            .roles("ADMIN")
            .build();
            
        return new InMemoryUserDetailsManager(user, admin);
    }
}

代码解析

  1. @EnableWebSecurity 启用Spring Security
  2. configure(HttpSecurity http) 方法配置了哪些路径需要认证,哪些可以公开访问
  3. userDetailsService() 定义了两个内存用户(user和admin)及其角色

2.3 密码加密

重要:永远不要存储明文密码!Spring Security推荐使用BCrypt加密。

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(); // 使用BCrypt强哈希加密
}

// 使用示例
public class PasswordExample {
    public static void main(String[] args) {
        PasswordEncoder encoder = new BCryptPasswordEncoder();
        String rawPassword = "myPassword123";
        String encodedPassword = encoder.encode(rawPassword);
        System.out.println("加密后的密码: " + encodedPassword);
        
        // 验证密码
        boolean matches = encoder.matches(rawPassword, encodedPassword);
        System.out.println("密码匹配: " + matches);
    }
}

输出示例

加密后的密码: $2a$10$N9qo8uLOickgx2ZMRZoMy.Mrq4H3zQY1Q8qP3R2ZR6zBf1QwJQ1GW
密码匹配: true

三、数据库设计与实现

3.1 RBAC数据库表设计

-- 用户表
CREATE TABLE sys_user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(100) NOT NULL,
    enabled BOOLEAN DEFAULT TRUE,
    account_non_expired BOOLEAN DEFAULT TRUE,
    account_non_locked BOOLEAN DEFAULT TRUE,
    credentials_non_expired BOOLEAN DEFAULT TRUE
);

-- 角色表
CREATE TABLE sys_role (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL UNIQUE,
    description VARCHAR(255)
);

-- 权限表
CREATE TABLE sys_permission (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL UNIQUE,
    url VARCHAR(255),
    method VARCHAR(10),
    description VARCHAR(255)
);

-- 用户-角色关联表
CREATE TABLE sys_user_role (
    user_id BIGINT NOT NULL,
    role_id BIGINT NOT NULL,
    PRIMARY KEY (user_id, role_id),
    FOREIGN KEY (user_id) REFERENCES sys_user(id),
    FOREIGN KEY (role_id) REFERENCES sys_role(id)
);

-- 角色-权限关联表
CREATE TABLE sys_role_permission (
    role_id BIGINT NOT NULL,
    permission_id BIGINT NOT NULL,
    PRIMARY KEY (role_id, permission_id),
    FOREIGN KEY (role_id) REFERENCES sys_role(id),
    FOREIGN KEY (permission_id) REFERENCES sys_permission(id)
);

RBAC 数据库关系图 (ER图)

sys_user sys_user_role sys_role sys_role_permission sys_permission 1:N 1:N 1:N 1:N

3.2 JPA实体类实现

// 用户实体
@Entity
@Table(name = "sys_user")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true)
    private String username;
    
    private String password;
    
    private Boolean enabled = true;
    private Boolean accountNonExpired = true;
    private Boolean accountNonLocked = true;
    private Boolean credentialsNonExpired = true;
    
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "sys_user_role",
        joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
        inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")})
    private List<Role> roles;
    
    // 实现UserDetails接口的方法
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        this.roles.forEach(role -> {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
            role.getPermissions().forEach(permission -> {
                authorities.add(new SimpleGrantedAuthority(permission.getName()));
            });
        });
        return authorities;
    }
    
    // 其他UserDetails方法实现...
}

// 角色实体
@Entity
@Table(name = "sys_role")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Role {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true)
    private String name;
    
    private String description;
    
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "sys_role_permission",
        joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},
        inverseJoinColumns = {@JoinColumn(name = "permission_id", referencedColumnName = "id")})
    private List<Permission> permissions;
}

// 权限实体
@Entity
@Table(name = "sys_permission")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Permission {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true)
    private String name;
    
    private String url;
    private String method; // GET, POST, PUT, DELETE等
    private String description;
}

四、自定义认证与授权

4.1 自定义UserDetailsService

@Service
public class CustomUserDetailsService implements UserDetailsService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
        
        // 检查账户状态
        if (!user.isEnabled()) {
            throw new DisabledException("账户已禁用");
        }
        if (!user.isAccountNonExpired()) {
            throw new AccountExpiredException("账户已过期");
        }
        if (!user.isAccountNonLocked()) {
            throw new LockedException("账户已锁定");
        }
        if (!user.isCredentialsNonExpired()) {
            throw new CredentialsExpiredException("凭证已过期");
        }
        
        return user;
    }
}

4.2 安全配置进阶

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级安全控制
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private CustomUserDetailsService userDetailsService;
    
    @Autowired
    private AuthenticationSuccessHandler successHandler;
    
    @Autowired
    private AuthenticationFailureHandler failureHandler;
    
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // 禁用CSRF(根据需求配置)
            .csrf().disable()
            
            // 授权配置
            .authorizeRequests()
                .antMatchers("/login", "/register", "/api/public/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .antMatchers(HttpMethod.GET, "/api/products").hasAuthority("product:read")
                .antMatchers(HttpMethod.POST, "/api/products").hasAuthority("product:write")
                .anyRequest().authenticated()
            .and()
            
            // 表单登录配置
            .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/auth/login")
                .successHandler(successHandler)
                .failureHandler(failureHandler)
                .permitAll()
            .and()
            
            // 记住我配置
            .rememberMe()
                .tokenValiditySeconds(7 * 24 * 60 * 60) // 7天
                .key("uniqueAndSecret")
                .userDetailsService(userDetailsService)
            .and()
            
            // 注销配置
            .logout()
                .logoutUrl("/auth/logout")
                .logoutSuccessUrl("/login?logout")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID", "remember-me")
                .permitAll()
            .and()
            
            // 异常处理
            .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler);
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    // 自定义认证成功处理器
    @Bean
    public AuthenticationSuccessHandler successHandler() {
        return (request, response, authentication) -> {
            response.setContentType("application/json;charset=UTF-8");
            Map<String, Object> result = new HashMap<>();
            result.put("status", 200);
            result.put("msg", "登录成功");
            result.put("data", authentication);
            response.getWriter().write(new ObjectMapper().writeValueAsString(result));
        };
    }
    
    // 自定义认证失败处理器
    @Bean
    public AuthenticationFailureHandler failureHandler() {
        return (request, response, exception) -> {
            response.setContentType("application/json;charset=UTF-8");
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            Map<String, Object> result = new HashMap<>();
            result.put("status", 401);
            
            if (exception instanceof LockedException) {
                result.put("msg", "账户被锁定,请联系管理员");
            } else if (exception instanceof DisabledException) {
                result.put("msg", "账户被禁用,请联系管理员");
            } else if (exception instanceof AccountExpiredException) {
                result.put("msg", "账户已过期,请联系管理员");
            } else if (exception instanceof UsernameNotFoundException) {
                result.put("msg", "用户名不存在");
            } else if (exception instanceof BadCredentialsException) {
                result.put("msg", "用户名或密码错误");
            } else {
                result.put("msg", "登录失败");
            }
            
            response.getWriter().write(new ObjectMapper().writeValueAsString(result));
        };
    }
    
    // 自定义权限不足处理器
    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return (request, response, exception) -> {
            response.setContentType("application/json;charset=UTF-8");
            response.setStatus(HttpStatus.FORBIDDEN.value());
            Map<String, Object> result = new HashMap<>();
            result.put("status", 403);
            result.put("msg", "权限不足,无法访问");
            response.getWriter().write(new ObjectMapper().writeValueAsString(result));
        };
    }
}

五、权限控制实践

5.1 方法级权限控制

Spring Security提供了多种方法级权限控制注解:

注解说明示例
@PreAuthorize方法执行前检查权限@PreAuthorize(“hasRole(‘ADMIN’)”)
@PostAuthorize方法执行后检查返回值权限@PostAuthorize(“returnObject.owner == authentication.name”)
@Secured简单的角色检查@Secured(“ROLE_ADMIN”)
@RolesAllowedJSR-250标准角色检查@RolesAllowed(“ADMIN”)
@PreFilter方法执行前过滤集合参数@PreFilter(“filterObject.owner == authentication.name”)
@PostFilter方法执行后过滤返回值集合@PostFilter(“filterObject.isPublic or filterObject.owner == authentication.name”)

使用示例

@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    @GetMapping
    @PreAuthorize("hasAuthority('product:read')")
    public List<Product> getAllProducts() {
        // 返回所有产品
    }
    
    @PostMapping
    @PreAuthorize("hasAuthority('product:write')")
    public Product createProduct(@RequestBody Product product) {
        // 创建新产品
    }
    
    @GetMapping("/{id}")
    @PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
    public Product getProductById(@PathVariable Long id) {
        // 根据ID获取产品
    }
    
    @GetMapping("/mine")
    @PostFilter("filterObject.owner == authentication.name")
    public List<Product> getMyProducts() {
        // 返回当前用户的产品(管理员会看到所有,但会被过滤)
    }
}

5.2 动态权限控制

对于更复杂的权限需求,可以实现PermissionEvaluator接口:

public class CustomPermissionEvaluator implements PermissionEvaluator {
    
    @Autowired
    private PermissionService permissionService;
    
    @Override
    public boolean hasPermission(Authentication authentication, 
                               Object targetDomainObject, 
                               Object permission) {
        // 检查对特定对象的权限
        if (authentication == null || !authentication.isAuthenticated()) {
            return false;
        }
        
        String username = authentication.getName();
        String perm = permission.toString();
        
        return permissionService.hasPermission(username, targetDomainObject, perm);
    }
    
    @Override
    public boolean hasPermission(Authentication authentication, 
                               Serializable targetId, 
                               String targetType, 
                               Object permission) {
        // 通过ID和类型检查权限
        if (authentication == null || !authentication.isAuthenticated()) {
            return false;
        }
        
        String username = authentication.getName();
        String perm = permission.toString();
        
        return permissionService.hasPermission(username, targetType, targetId, perm);
    }
}

// 配置自定义PermissionEvaluator
@Configuration
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    
    @Autowired
    private CustomPermissionEvaluator permissionEvaluator;
    
    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler = 
            new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(permissionEvaluator);
        return expressionHandler;
    }
}

// 使用示例
@RestController
@RequestMapping("/api/documents")
public class DocumentController {
    
    @GetMapping("/{id}")
    @PreAuthorize("hasPermission(#id, 'document', 'read')")
    public Document getDocument(@PathVariable Long id) {
        // 获取文档
    }
    
    @PutMapping("/{id}")
    @PreAuthorize("hasPermission(#document, 'write')")
    public Document updateDocument(@PathVariable Long id, @RequestBody Document document) {
        // 更新文档
    }
}

六、JWT集成与无状态认证

6.1 JWT基础概念

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为JSON对象。

JWT结构

  • Header:包含令牌类型和签名算法
  • Payload:包含声明(用户信息、过期时间等)
  • Signature:验证令牌完整性的签名

6.2 JWT集成实现

添加依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>

JWT工具类

@Component
public class JwtTokenProvider {
    
    @Value("${app.jwt.secret}")
    private String jwtSecret;
    
    @Value("${app.jwt.expiration-in-ms}")
    private int jwtExpirationInMs;
    
    // 生成令牌
    public String generateToken(Authentication authentication) {
        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
        
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
        
        return Jwts.builder()
                .setSubject(Long.toString(userPrincipal.getId()))
                .setIssuedAt(new Date())
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS512, jwtSecret)
                .compact();
    }
    
    // 从令牌获取用户ID
    public Long getUserIdFromJWT(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(jwtSecret)
                .parseClaimsJws(token)
                .getBody();
        
        return Long.parseLong(claims.getSubject());
    }
    
    // 验证令牌
    public boolean validateToken(String authToken) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
            return true;
        } catch (SignatureException ex) {
            logger.error("Invalid JWT signature");
        } catch (MalformedJwtException ex) {
            logger.error("Invalid JWT token");
        } catch (ExpiredJwtException ex) {
            logger.error("Expired JWT token");
        } catch (UnsupportedJwtException ex) {
            logger.error("Unsupported JWT token");
        } catch (IllegalArgumentException ex) {
            logger.error("JWT claims string is empty.");
        }
        return false;
    }
}

JWT认证过滤器

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtTokenProvider tokenProvider;
    
    @Autowired
    private CustomUserDetailsService customUserDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                  HttpServletResponse response, 
                                  FilterChain filterChain) 
        throws ServletException, IOException {
        
        try {
            String jwt = getJwtFromRequest(request);
            
            if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
                Long userId = tokenProvider.getUserIdFromJWT(jwt);
                
                UserDetails userDetails = customUserDetailsService.loadUserById(userId);
                UsernamePasswordAuthenticationToken authentication = 
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception ex) {
            logger.error("Could not set user authentication in security context", ex);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

配置JWT安全

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class JwtSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;
    
    @Autowired
    private JwtTokenProvider tokenProvider;
    
    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() {
        return new JwtAuthenticationFilter();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .cors()
                .and()
            .csrf()
                .disable()
            .exceptionHandling()
                .authenticationEntryPoint(unauthorizedHandler)
                .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
            .authorizeRequests()
                .antMatchers("/api/auth/**").permitAll()
                .antMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated();
        
        // 添加JWT过滤器
        http.addFilterBefore(jwtAuthenticationFilter(), 
                           UsernamePasswordAuthenticationFilter.class);
    }
}

七、权限缓存优化

7.1 使用Redis缓存权限数据

@Configuration
@EnableCaching
public class RedisConfig {
    
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
                        .entryTtl(Duration.ofHours(1)) // 1小时过期
                        .disableCachingNullValues())
                .build();
    }
}

@Service
public class PermissionServiceImpl implements PermissionService {
    
    @Autowired
    private PermissionRepository permissionRepository;
    
    @Autowired
    private RoleRepository roleRepository;
    
    @Autowired
    private CacheManager cacheManager;
    
    @Cacheable(value = "userPermissions", key = "#userId")
    public Set<String> getUserPermissions(Long userId) {
        // 从数据库查询用户权限
        Set<String> permissions = new HashSet<>();
        List<Role> roles = roleRepository.findByUsers_Id(userId);
        
        for (Role role : roles) {
            List<Permission> rolePermissions = permissionRepository.findByRoles_Id(role.getId());
            for (Permission permission : rolePermissions) {
                permissions.add(permission.getName());
            }
        }
        
        return permissions;
    }
    
    @CacheEvict(value = "userPermissions", key = "#userId")
    public void clearUserPermissionsCache(Long userId) {
        // 方法体可以为空,注解会触发缓存清除
    }
}

7.2 自定义权限缓存过滤器

public class PermissionCacheFilter extends OncePerRequestFilter {
    
    @Autowired
    private PermissionService permissionService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                  HttpServletResponse response, 
                                  FilterChain filterChain) 
        throws ServletException, IOException {
        
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.isAuthenticated()) {
            // 获取当前用户ID
            Long userId = ((UserPrincipal) authentication.getPrincipal()).getId();
            
            // 从缓存获取权限
            Set<String> permissions = permissionService.getUserPermissions(userId);
            
            // 将权限存入请求属性
            request.setAttribute("userPermissions", permissions);
        }
        
        filterChain.doFilter(request, response);
    }
}

7.3 权限缓存流程图

存在
不存在
权限变更事件
清除Redis缓存
下次请求时
缓存是否存在?
直接返回缓存
查询数据库
写入Redis缓存
返回权限数据

八、权限系统监控与审计

8.1 权限操作日志

@Entity
@Table(name = "sys_audit_log")
@Data
public class AuditLog {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String username;
    private String operation;
    private String method;
    private String params;
    private Long time;
    private String ip;
    
    @CreatedDate
    private Date createTime;
}

@Aspect
@Component
public class AuditLogAspect {
    
    @Autowired
    private AuditLogRepository auditLogRepository;
    
    @Autowired
    private HttpServletRequest request;
    
    @Pointcut("@annotation(com.example.demo.annotation.RequiresPermission)")
    public void permissionPointcut() {}
    
    @AfterReturning("permissionPointcut()")
    public void afterReturning(JoinPoint joinPoint) {
        saveLog(joinPoint, null);
    }
    
    @AfterThrowing(pointcut = "permissionPointcut()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Throwable e) {
        saveLog(joinPoint, e.getMessage());
    }
    
    private void saveLog(JoinPoint joinPoint, String errorMsg) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        
        RequiresPermission requiresPermission = method.getAnnotation(RequiresPermission.class);
        
        AuditLog auditLog = new AuditLog();
        auditLog.setOperation(requiresPermission.value());
        auditLog.setMethod(method.getDeclaringClass().getName() + "." + method.getName());
        
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null) {
            auditLog.setUsername(authentication.getName());
        }
        
        Object[] args = joinPoint.getArgs();
        try {
            List<String> params = new ArrayList<>();
            for (Object arg : args) {
                if (arg != null && !(arg instanceof HttpServletRequest) 
                    && !(arg instanceof HttpServletResponse)) {
                    params.add(JSON.toJSONString(arg));
                }
            }
            auditLog.setParams(String.join(", ", params));
        } catch (Exception e) {
            auditLog.setParams("参数解析失败");
        }
        
        auditLog.setIp(IpUtils.getIpAddr(request));
        auditLog.setTime(System.currentTimeMillis());
        
        if (errorMsg != null) {
            auditLog.setOperation(auditLog.getOperation() + " 失败: " + errorMsg);
        }
        
        auditLogRepository.save(auditLog);
    }
}

8.2 权限使用统计

@Service
public class PermissionStatsService {
    
    @Autowired
    private AuditLogRepository auditLogRepository;
    
    public Map<String, Integer> getPermissionUsageStats(Date startDate, Date endDate) {
        List<Object[]> stats = auditLogRepository.countByOperationBetweenDates(startDate, endDate);
        
        Map<String, Integer> result = new HashMap<>();
        for (Object[] stat : stats) {
            result.put((String) stat[0], ((Number) stat[1]).intValue());
        }
        
        return result;
    }
    
    public Map<String, Map<String, Integer>> getUserPermissionStats(Date startDate, Date endDate) {
        List<Object[]> stats = auditLogRepository.countByUserAndOperationBetweenDates(startDate, endDate);
        
        Map<String, Map<String, Integer>> result = new HashMap<>();
        for (Object[] stat : stats) {
            String username = (String) stat[0];
            String operation = (String) stat[1];
            int count = ((Number) stat[2]).intValue();
            
            result.computeIfAbsent(username, k -> new HashMap<>())
                 .put(operation, count);
        }
        
        return result;
    }
}

九、前端权限控制

9.1 基于Vue的前端权限控制

权限指令

// permission.js
import store from '@/store'

export default {
  inserted(el, binding, vnode) {
    const { value } = binding
    const permissions = store.getters && store.getters.permissions
    
    if (value && value instanceof Array && value.length > 0) {
      const requiredPermissions = value
      const hasPermission = permissions.some(permission => {
        return requiredPermissions.includes(permission)
      })
      
      if (!hasPermission) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    } else {
      throw new Error(`需要指定权限,如 v-permission="['user:add']"`)
    }
  }
}

路由守卫

// permission.js
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

NProgress.configure({ showSpinner: false })

const whiteList = ['/login', '/auth-redirect']

router.beforeEach(async (to, from, next) => {
  NProgress.start()
  
  // 确定用户是否已登录
  const hasToken = store.getters.token
  
  if (hasToken) {
    if (to.path === '/login') {
      // 如果已登录,重定向到主页
      next({ path: '/' })
      NProgress.done()
    } else {
      // 检查用户是否已获取权限
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      
      if (hasRoles) {
        next()
      } else {
        try {
          // 获取用户信息
          const { roles } = await store.dispatch('user/getInfo')
          
          // 基于角色生成可访问路由
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
          
          // 动态添加路由
          router.addRoutes(accessRoutes)
          
          // 确保addRoutes已完成
          next({ ...to, replace: true })
        } catch (error) {
          // 移除token并重定向到登录页
          await store.dispatch('user/resetToken')
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    /* 未登录 */
    if (whiteList.indexOf(to.path) !== -1) {
      // 在白名单中直接放行
      next()
    } else {
      // 其他没有访问权限的页面被重定向到登录页面
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  NProgress.done()
})

十、高级主题与最佳实践

10.1 多租户权限系统

@Entity
@Table(name = "sys_tenant")
@Data
public class Tenant {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    private String code;
    private Boolean enabled;
}

@Entity
@Table(name = "sys_user")
@Data
public class User implements UserDetails {
    
    // 其他字段...
    
    @ManyToOne
    @JoinColumn(name = "tenant_id")
    private Tenant tenant;
}

// 租户过滤器
public class TenantFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                  HttpServletResponse response, 
                                  FilterChain filterChain) 
        throws ServletException, IOException {
        
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.isAuthenticated()) {
            UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
            Long tenantId = userPrincipal.getTenantId();
            
            // 设置当前租户上下文
            TenantContext.setCurrentTenant(tenantId);
        }
        
        try {
            filterChain.doFilter(request, response);
        } finally {
            // 清除租户上下文
            TenantContext.clear();
        }
    }
}

// 租户上下文
public class TenantContext {
    
    private static final ThreadLocal<Long> CURRENT_TENANT = new ThreadLocal<>();
    
    public static void setCurrentTenant(Long tenantId) {
        CURRENT_TENANT.set(tenantId);
    }
    
    public static Long getCurrentTenant() {
        return CURRENT_TENANT.get();
    }
    
    public static void clear() {
        CURRENT_TENANT.remove();
    }
}

// 数据过滤拦截器
@Interceptor
public class TenantDataInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) {
        Long tenantId = TenantContext.getCurrentTenant();
        if (tenantId != null) {
            // 设置数据源路由
            DynamicDataSourceContextHolder.setDataSourceKey("tenant_" + tenantId);
        }
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, 
                              HttpServletResponse response, 
                              Object handler, 
                              Exception ex) {
        DynamicDataSourceContextHolder.clearDataSourceKey();
    }
}

10.2 权限系统性能优化建议

  1. 缓存策略

    • 使用Redis缓存用户权限数据
    • 实现多级缓存(本地缓存+分布式缓存)
    • 设置合理的过期时间
  2. 数据库优化

    • 为常用查询字段添加索引
    • 使用连接查询替代多次单表查询
    • 定期清理无用权限数据
  3. 权限检查优化

    • 批量检查权限而非单个检查
    • 使用位运算存储简单权限标志
    • 实现权限检查短路逻辑(一旦满足立即返回)
  4. 前端优化

    • 按需加载权限相关组件
    • 预加载用户权限数据
    • 实现权限数据的本地存储
  5. 监控与调优

    • 记录权限检查耗时
    • 监控权限缓存命中率
    • 定期分析权限使用模式

十一、完整示例项目结构

springboot-rbac-demo/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── example/
│   │   │           └── demo/
│   │   │               ├── config/               # 配置类
│   │   │               │   ├── SecurityConfig.java
│   │   │               │   ├── RedisConfig.java
│   │   │               │   └── WebMvcConfig.java
│   │   │               ├── controller/          # 控制器
│   │   │               │   ├── AuthController.java
│   │   │               │   ├── UserController.java
│   │   │               │   └── RoleController.java
│   │   │               ├── model/               # 数据模型
│   │   │               │   ├── entity/          # JPA实体
│   │   │               │   ├── dto/             # 数据传输对象
│   │   │               │   └── vo/             # 视图对象
│   │   │               ├── repository/          # 数据访问层
│   │   │               ├── service/             # 业务逻辑层
│   │   │               ├── security/            # 安全相关
│   │   │               │   ├── jwt/             # JWT相关
│   │   │               │   ├── oauth2/          # OAuth2相关
│   │   │               │   └── user/            # 用户详情
│   │   │               ├── util/                # 工具类
│   │   │               └── DemoApplication.java # 启动类
│   │   └── resources/
│   │       ├── application.yml                  # 应用配置
│   │       └── static/                          # 静态资源
│   └── test/                                    # 测试代码
└── pom.xml                                      # Maven配置

十二、常见问题与解决方案

12.1 权限缓存一致性问题

问题:当用户权限变更后,缓存中的权限数据未及时更新。

解决方案

  1. 权限变更时主动清除相关缓存
  2. 使用发布-订阅模式通知所有节点更新缓存
  3. 设置较短的缓存过期时间
@Service
public class PermissionServiceImpl implements PermissionService {
    
    @CacheEvict(value = "userPermissions", key = "#userId")
    public void updateUserPermissions(Long userId, List<String> permissions) {
        // 更新用户权限逻辑
    }
    
    @CachePut(value = "userPermissions", key = "#userId")
    public Set<String> refreshUserPermissions(Long userId) {
        // 重新加载用户权限
        return getUserPermissionsFromDB(userId);
    }
}

12.2 权限细粒度控制问题

问题:如何实现字段级的权限控制?

解决方案

  1. 使用注解标记敏感字段
  2. 在DTO转换时过滤无权限字段
  3. 使用动态代理拦截字段访问
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface PermissionField {
    String value(); // 需要的权限
}

public class UserDTO {
    private String username;
    
    @PermissionField("user:view:email")
    private String email;
    
    @PermissionField("user:view:phone")
    private String phone;
}

// 字段过滤工具
public class PermissionFieldFilter {
    
    public static <T> T filterFields(T dto, Set<String> permissions) {
        try {
            Class<?> clazz = dto.getClass();
            T filteredDto = (T) clazz.newInstance();
            
            for (Field field : clazz.getDeclaredFields()) {
                field.setAccessible(true);
                
                PermissionField annotation = field.getAnnotation(PermissionField.class);
                if (annotation == null || permissions.contains(annotation.value())) {
                    field.set(filteredDto, field.get(dto));
                }
            }
            
            return filteredDto;
        } catch (Exception e) {
            throw new RuntimeException("字段过滤失败", e);
        }
    }
}

关注不关注,你自己决定(但正确的决定只有一个)。

喜欢的点个关注,想了解更多的可以关注微信公众号 “Eric的技术杂货库” ,提供更多的干货以及资料下载保存!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Clf丶忆笙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值