访问控制与授权
访问控制(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 端点是否有正确的权限标注
// 检查角色层级是否合理
// 检查是否有遗漏的访问控制
}
}
访问控制检查清单
在开发过程中,确保完成以下检查:
-
每个 API 端点
- 是否需要身份验证?
- 什么角色可以访问?
- 是否有资源归属检查?
-
数据访问
- 查询是否过滤了用户权限?
- 是否验证了数据归属?
-
前端
- 是否隐藏了未授权的 UI 元素?
- 按钮/链接是否正确禁用?
-
日志
- 是否记录了访问控制失败事件?
-
测试
- 是否测试了各种角色的访问权限?
- 是否测试了水平越权场景?
小结
访问控制是 Web 安全的核心:
-
理解概念
- 认证:确认用户身份
- 授权:确认用户权限
- 访问控制:执行授权决策
-
实施要点
- 默认拒绝所有访问
- 每个端点都要检查权限
- 服务层也要执行权限检查
-
常见漏洞
- 垂直越权:普通用户获得管理员权限
- 水平越权:用户访问其他用户的数据
- IDOR:直接修改参数访问未授权资源
-
最佳实践
- 使用 RBAC 模型管理角色和权限
- 使用注解进行声明式授权
- 实施审计日志
- 定期进行安全审查
记住:永远不要信任客户端提交的任何数据,包括用户角色和资源 ID!