Spring Security与业务逻辑:Todo应用用户数据隔离实现问询
解决方案:实现Todo数据的用户隔离访问
首先修正你的Item实体中的明显问题,再逐步实现用户数据隔离的核心逻辑:
1. 修正Item实体定义
你的Item存在几个不符合规范的问题,修正后如下:
@Entity @Table(name = "item") public class Item { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String description; private String completionDate; private boolean isDone; // 替换String类型为boolean,符合业务语义 @ManyToOne @JoinColumn(name = "user_id", nullable = false) // 指定关联字段,添加非空约束 private User user; public Item() {} public Item(Long id, String description, String completionDate, boolean isDone, User user) { this.id = id; this.description = description; this.completionDate = completionDate; this.isDone = isDone; this.user = user; } // 修正getUser方法,返回正确的User对象 public User getUser() { return user; } public void setUser(User user) { this.user = user; } // 遵循JavaBean规范的isDone getter public boolean isDone() { return isDone; } public void setDone(boolean done) { isDone = done; } // 其他getter/setter保持正常 public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getCompletionDate() { return completionDate; } public void setCompletionDate(String completionDate) { this.completionDate = completionDate; } @Override public int hashCode() { return Objects.hash(id); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Item other = (Item) obj; return Objects.equals(id, other.id); } }
2. 完善安全配置
确保Todo相关接口被正确保护,同时修复H2控制台访问问题:
@Configuration @EnableWebSecurity @EnableMethodSecurity // 可选:开启方法级权限控制 public class SpringSecurityBasicAuthConfig { private static final Logger logger = LogManager.getLogger(SpringSecurityBasicAuthConfig.class); @Autowired UserDetailsService user; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { String methodName = "filterChain"; logger.info("Executing, " + methodName); http.authorizeHttpRequests() .requestMatchers("/h2/**").permitAll() .requestMatchers("/api/v1/registration/**").permitAll() .requestMatchers("/users/**", "/api/v1/items/**").hasAuthority("USER") // 保护Todo接口 .anyRequest().authenticated() // 其他接口强制认证 .and() .formLogin() .and() .csrf().disable() .headers().frameOptions().sameOrigin(); // 允许H2控制台iframe访问 return http.build(); } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(user); provider.setPasswordEncoder(passwordEncoder()); return provider; } }
3. 核心业务逻辑实现
数据层:添加用户关联查询方法
在ItemRepository中定义根据用户查询Todo的方法:
public interface ItemRepository extends JpaRepository<Item, Long> { List<Item> findByUser(User user); }
控制层:实现用户数据隔离
通过Principal获取当前登录用户,确保操作仅针对自身Todo:
@RestController @RequestMapping("/api/v1/items") public class ItemController { private final ItemRepository itemRepository; private final UserRepository userRepository; public ItemController(ItemRepository itemRepository, UserRepository userRepository) { this.itemRepository = itemRepository; this.userRepository = userRepository; } // 创建Todo时自动关联当前用户 @PostMapping public ResponseEntity<Item> createItem(@RequestBody Item item, Principal principal) { User currentUser = userRepository.findByUsername(principal.getName()) .orElseThrow(() -> new RuntimeException("用户不存在")); item.setUser(currentUser); return ResponseEntity.ok(itemRepository.save(item)); } // 查询当前用户的所有Todo @GetMapping public List<Item> getCurrentUserItems(Principal principal) { User currentUser = userRepository.findByUsername(principal.getName()) .orElseThrow(() -> new RuntimeException("用户不存在")); return itemRepository.findByUser(currentUser); } // 更新Todo前验证归属 @PutMapping("/{id}") public ResponseEntity<Item> updateItem(@PathVariable Long id, @RequestBody Item updatedItem, Principal principal) { User currentUser = userRepository.findByUsername(principal.getName()) .orElseThrow(() -> new RuntimeException("用户不存在")); Item existingItem = itemRepository.findById(id) .orElseThrow(() -> new RuntimeException("Todo不存在")); if (!existingItem.getUser().getId().equals(currentUser.getId())) { throw new RuntimeException("无权限修改该Todo"); } existingItem.setDescription(updatedItem.getDescription()); existingItem.setCompletionDate(updatedItem.getCompletionDate()); existingItem.setDone(updatedItem.isDone()); return ResponseEntity.ok(itemRepository.save(existingItem)); } // 删除Todo前验证归属 @DeleteMapping("/{id}") public ResponseEntity<Void> deleteItem(@PathVariable Long id, Principal principal) { User currentUser = userRepository.findByUsername(principal.getName()) .orElseThrow(() -> new RuntimeException("用户不存在")); Item existingItem = itemRepository.findById(id) .orElseThrow(() -> new RuntimeException("Todo不存在")); if (!existingItem.getUser().getId().equals(currentUser.getId())) { throw new RuntimeException("无权限删除该Todo"); } itemRepository.delete(existingItem); return ResponseEntity.noContent().build(); } }
4. 可选优化:方法级权限控制
如果想简化Controller的验证逻辑,可以用@PreAuthorize在Service层做权限校验:
@Service public class ItemService { private final ItemRepository itemRepository; private final UserRepository userRepository; public ItemService(ItemRepository itemRepository, UserRepository userRepository) { this.itemRepository = itemRepository; this.userRepository = userRepository; } public Item createItem(Item item, String username) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("用户不存在")); item.setUser(user); return itemRepository.save(item); } public List<Item> getCurrentUserItems(String username) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("用户不存在")); return itemRepository.findByUser(user); } @PreAuthorize("#item.user.username == authentication.name") public Item updateItem(Item item) { return itemRepository.save(item); } @PreAuthorize("@itemService.getItemById(#id).user.username == authentication.name") public void deleteItem(Long id) { itemRepository.deleteById(id); } public Item getItemById(Long id) { return itemRepository.findById(id) .orElseThrow(() -> new RuntimeException("Todo不存在")); } }
核心思路总结
- 实体层建立
Item与User的多对一关联,确保每条Todo归属明确 - 业务操作时始终绑定当前登录用户,查询时仅返回当前用户的数据
- 通过接口权限控制+数据归属验证,实现用户间的完全数据隔离
内容的提问来源于stack exchange,提问作者Andrew Johnson
相关产品推荐
相关产品推荐

