跳到主要内容

访问控制与授权

访问控制(Access Control)和授权(Authorization)是确保用户只能访问被授权资源的关键机制。失效的访问控制是 OWASP Top 10 中最常见的问题之一。本章将详细介绍如何正确实现访问控制。

什么是访问控制?

  • 认证(Authentication) - 验证用户是谁
  • 授权(Authorization) - 验证用户可以做什么
  • 访问控制(Access Control) - 执行授权决策的机制

访问控制模型

1. 自主访问控制(DAC)

资源所有者决定谁可以访问他们的资源。

class File {
private String owner;
private Set<String> allowedUsers;

public boolean canAccess(User user) {
return user.getId().equals(owner) ||
allowedUsers.contains(user.getId());
}
}

2. 强制访问控制(MAC)

系统根据安全标签强制执行访问控制,通常用于军事和政府系统。

3. 基于角色的访问控制(RBAC)

用户被分配角色,角色决定权限。这是 Web 应用中最常用的模型。

// 角色定义
public enum Role {
ADMIN, // 管理员
MANAGER, // 经理
USER, // 普通用户
GUEST // 访客
}

// 权限定义
public enum Permission {
USER_READ,
USER_WRITE,
USER_DELETE,
ORDER_READ,
ORDER_WRITE,
SYSTEM_ADMIN
}

// 角色-权限映射
Map<Role, Set<Permission>> rolePermissions = Map.of(
Role.ADMIN, Set.of(Permission.values()),
Role.MANAGER, Set.of(Permission.USER_READ, Permission.USER_WRITE,
Permission.ORDER_READ, Permission.ORDER_WRITE),
Role.USER, Set.of(Permission.USER_READ, Permission.ORDER_READ),
Role.GUEST, Set.of()
);

4. 基于属性的访问控制(ABAC)

根据用户属性、资源属性和环境属性进行动态决策。

public boolean canAccess(User user, Resource resource, Map<String, Object> environment) {
// 用户是资源所有者
if (resource.getOwnerId().equals(user.getId())) {
return true;
}

// 用户角色有权限
if (user.getRole().hasPermission(resource.getRequiredPermission())) {
// 检查时间限制(工作时间才能访问)
int hour = (int) environment.get("currentHour");
if (hour < 9 || hour > 18) {
return false;
}
return true;
}

return false;
}

实施访问控制

1. 在控制器层检查权限

@RestController
@RequestMapping("/api/users")
public class UserController {

@Autowired
private UserService userService;

// 每个端点都应该进行权限检查
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
User currentUser = getCurrentUser();

// 检查:用户只能查看自己的信息,除非是管理员
if (!currentUser.getId().equals(id) &&
currentUser.getRole() != Role.ADMIN) {
throw new AccessDeniedException("无权访问");
}

return userService.findById(id);
}

@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
User currentUser = getCurrentUser();

// 检查:只有管理员可以删除用户
if (currentUser.getRole() != Role.ADMIN) {
throw new AccessDeniedException("无权删除");
}

userService.delete(id);
}
}

2. 使用注解进行声明式授权

// Spring Security 注解
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/users")
public List<User> getAllUsers() {
return userService.findAll();
}

@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
@GetMapping("/users/{userId}/profile")
public UserProfile getProfile(@PathVariable Long userId) {
return userService.getProfile(userId);
}

// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
Permission value();
}

// 使用
@RequirePermission(Permission.USER_DELETE)
public void deleteUser(Long userId) {
// 删除用户
}

3. 服务层权限检查

@Service
public class UserService {

@Autowired
private PermissionService permissionService;

public User updateUser(Long userId, UserUpdateRequest request) {
User currentUser = SecurityContext.getCurrentUser();

// 检查权限
if (!permissionService.hasPermission(currentUser, Permission.USER_WRITE)) {
throw new AccessDeniedException("无权修改用户");
}

// 检查是否是本人或管理员
if (!currentUser.getId().equals(userId) &&
currentUser.getRole() != Role.ADMIN) {
throw new AccessDeniedException("只能修改自己的信息");
}

return userRepository.save(user);
}
}

4. 数据层访问控制

确保数据库查询也执行访问控制:

@Repository
public class UserRepository {

// 不安全:返回所有用户
public List<User> findAll() {
return jdbc.query("SELECT * FROM users");
}

// 安全:只返回当前用户有权限看到的用户
public List<User> findAllForUser(User currentUser) {
if (currentUser.getRole() == Role.ADMIN) {
return jdbc.query("SELECT * FROM users");
}
// 普通用户只能看到公开信息
return jdbc.query(
"SELECT id, username, email FROM users WHERE visible = true"
);
}
}

5. 前端访问控制

前端也需要进行访问控制,但不要依赖它作为唯一防护:

// React 示例
function UserProfile() {
const user = useAuth();

// 根据用户角色渲染不同内容
if (user.role === 'admin') {
return <AdminPanel />;
}

return <UserPanel />;
}

// 隐藏不应该显示的按钮
function ActionButtons({ userId }) {
const currentUser = useAuth();

return (
<div>
<Button>查看</Button>
{(currentUser.id === userId || currentUser.role === 'admin') && (
<Button>编辑</Button>
)}
{currentUser.role === 'admin' && (
<Button>删除</Button>
)}
</div>
);
}

常见访问控制漏洞

1. 垂直越权

普通用户获得了管理员的权限:

// 不安全:隐藏按钮但后端没有检查
@PostMapping("/admin/delete")
public void deleteUser(@RequestParam Long userId) {
// 任何用户都可以调用这个接口!
userService.delete(userId);
}

// 安全:添加权限检查
@PostMapping("/admin/delete")
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(@RequestParam Long userId) {
userService.delete(userId);
}

2. 水平越权

用户访问了其他用户的资源:

// 不安全:没有验证资源归属
@GetMapping("/api/documents/{id}")
public Document getDocument(@PathVariable Long id) {
return documentService.findById(id); // 任何用户都可以查看任何文档
}

// 安全:验证资源归属
@GetMapping("/api/documents/{id}")
public Document getDocument(@PathVariable Long id) {
User currentUser = getCurrentUser();
Document doc = documentService.findById(id);

if (!doc.getOwnerId().equals(currentUser.getId()) &&
currentUser.getRole() != Role.ADMIN) {
throw new AccessDeniedException("无权访问此文档");
}

return doc;
}

3. 不安全的直接对象引用(IDOR)

通过修改参数访问未授权资源:

// 不安全:直接使用用户输入的资源 ID
@GetMapping("/api/orders/{orderId}")
public Order getOrder(@PathVariable Long orderId) {
return orderService.findById(orderId);
}

// 安全:验证资源属于当前用户
@GetMapping("/api/orders/{orderId}")
public Order getOrder(@PathVariable Long orderId) {
User currentUser = getCurrentUser();
Order order = orderService.findById(orderId);

if (!order.getUserId().equals(currentUser.getId())) {
throw new AccessDeniedException("无权访问");
}

return order;
}

4. 缺失功能级访问控制

管理功能没有正确保护:

// 缺少权限检查的 API
@GetMapping("/api/export")
public void exportData(HttpServletResponse response) {
// 应该检查用户是否有导出权限
// 没有检查 = 任何登录用户都可以导出数据!
}

最佳实践

1. 默认拒绝

// Spring Security 配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/api/**").authenticated()
.anyRequest().denyAll() // 默认拒绝所有
.and()
.csrf().disable();
}
}

2. 集中式权限管理

// 权限检查服务
@Service
public class PermissionService {

public boolean canAccess(User user, Resource resource) {
// 检查用户角色
if (resource.getRequiredRole() != null) {
if (user.getRole().ordinal() >= resource.getRequiredRole().ordinal()) {
return true;
}
}

// 检查资源所有权
if (resource.getOwnerId() != null) {
if (resource.getOwnerId().equals(user.getId())) {
return true;
}
}

return false;
}
}

3. 审计日志

@Aspect
@Component
public class AccessControlLogging {

@Before("@annotation(RequirePermission)")
public void logAccess(JoinPoint joinPoint) {
User user = SecurityContext.getCurrentUser();
String method = joinPoint.getSignature().toShortString();

logger.info("User {} attempting to execute {}", user.getUsername(), method);
}

@AfterReturning("@annotation(RequirePermission)")
public void logSuccess(JoinPoint joinPoint) {
User user = SecurityContext.getCurrentUser();
logger.info("User {} successfully executed {}",
user.getUsername(),
joinPoint.getSignature().toShortString());
}
}

4. 定期安全审查

// 权限矩阵检查
public class AccessControlReview {
public void reviewPermissions() {
// 检查每个 API 端点是否有正确的权限标注
// 检查角色层级是否合理
// 检查是否有遗漏的访问控制
}
}

访问控制检查清单

在开发过程中,确保完成以下检查:

  1. 每个 API 端点

    • 是否需要身份验证?
    • 什么角色可以访问?
    • 是否有资源归属检查?
  2. 数据访问

    • 查询是否过滤了用户权限?
    • 是否验证了数据归属?
  3. 前端

    • 是否隐藏了未授权的 UI 元素?
    • 按钮/链接是否正确禁用?
  4. 日志

    • 是否记录了访问控制失败事件?
  5. 测试

    • 是否测试了各种角色的访问权限?
    • 是否测试了水平越权场景?

小结

访问控制是 Web 安全的核心:

  1. 理解概念

    • 认证:确认用户身份
    • 授权:确认用户权限
    • 访问控制:执行授权决策
  2. 实施要点

    • 默认拒绝所有访问
    • 每个端点都要检查权限
    • 服务层也要执行权限检查
  3. 常见漏洞

    • 垂直越权:普通用户获得管理员权限
    • 水平越权:用户访问其他用户的数据
    • IDOR:直接修改参数访问未授权资源
  4. 最佳实践

    • 使用 RBAC 模型管理角色和权限
    • 使用注解进行声明式授权
    • 实施审计日志
    • 定期进行安全审查

记住:永远不要信任客户端提交的任何数据,包括用户角色和资源 ID!