完成作业
Some checks failed
autograde-final-vibevault / check-trigger (push) Successful in 12s
autograde-final-vibevault / grade (push) Failing after 43s

This commit is contained in:
VibeVault User 2025-12-14 01:10:20 +08:00
parent 8866f3f31a
commit d6a4bd92e6
12 changed files with 389 additions and 36 deletions

View File

@ -39,13 +39,31 @@ public class SecurityConfig {
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// TODO: 配置安全规则 http
// 提示 // 配置路径权限
// - 使用 http.authorizeHttpRequests() 配置路径权限 .authorizeHttpRequests(authorizeRequests -> authorizeRequests
// - 使用 http.csrf(csrf -> csrf.disable()) 禁用 CSRF // 公开接口无需认证
// - 使用 http.sessionManagement() 配置无状态会话 .requestMatchers("/api/auth/**").permitAll()
// - 使用 http.exceptionHandling() 配置 401 响应 .requestMatchers(HttpMethod.GET, "/api/playlists").permitAll()
// - 使用 http.addFilterBefore() 添加 JWT 过滤器 .requestMatchers(HttpMethod.GET, "/api/playlists/{id}").permitAll()
// 其他接口需要认证
.anyRequest().authenticated()
)
// 禁用 CSRF
.csrf(csrf -> csrf.disable())
// 配置无状态会话
.sessionManagement(sessionManagement -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 配置 401 响应
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Unauthorized: Missing or invalid token");
})
)
// 添加 JWT 过滤器
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build(); return http.build();
} }

View File

@ -36,9 +36,43 @@ public class AuthController {
this.jwtService = jwtService; this.jwtService = jwtService;
} }
// TODO: 实现 POST /api/auth/register (状态码 201) // POST /api/auth/register - 用户注册
@PostMapping("/register")
@ResponseStatus(HttpStatus.CREATED)
public RegisterResponse register(@RequestBody RegisterRequest request) {
// 检查用户名是否已存在
if (userRepository.existsByUsername(request.username())) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "Username already exists");
}
// TODO: 实现 POST /api/auth/login // 创建新用户加密密码
User user = new User();
user.setUsername(request.username());
user.setPassword(passwordEncoder.encode(request.password()));
// 保存用户到数据库
userRepository.save(user);
return new RegisterResponse("User registered successfully", user.getUsername());
}
// POST /api/auth/login - 用户登录
@PostMapping("/login")
public LoginResponse login(@RequestBody LoginRequest request) {
// 检查用户名是否存在
User user = userRepository.findByUsername(request.username())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid username or password"));
// 验证密码
if (!passwordEncoder.matches(request.password(), user.getPassword())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid username or password");
}
// 生成JWT token
String token = jwtService.generateToken(user.getUsername());
return new LoginResponse(token, user.getUsername());
}
} }
/** /**

View File

@ -39,19 +39,61 @@ public class PlaylistController {
this.playlistService = playlistService; this.playlistService = playlistService;
} }
// TODO: 实现 GET /api/playlists // GET /api/playlists - 获取所有歌单公开
@GetMapping
public List<PlaylistDTO> getAllPlaylists() {
return playlistService.getAllPlaylists();
}
// TODO: 实现 GET /api/playlists/{id} // GET /api/playlists/{id} - 获取指定歌单公开
@GetMapping("/{id}")
public PlaylistDTO getPlaylistById(@PathVariable Long id) {
return playlistService.getPlaylistById(id);
}
// TODO: 实现 POST /api/playlists (状态码 201) // POST /api/playlists - 创建歌单需认证
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public PlaylistDTO createPlaylist(@RequestBody PlaylistCreateDTO playlistCreateDTO, Authentication authentication) {
String username = authentication.getName();
return playlistService.createPlaylist(playlistCreateDTO.getName(), username);
}
// TODO: 实现 POST /api/playlists/{id}/songs (状态码 201) // POST /api/playlists/{id}/songs - 添加歌曲需认证
@PostMapping("/{id}/songs")
@ResponseStatus(HttpStatus.CREATED)
public PlaylistDTO addSongToPlaylist(@PathVariable Long id, @RequestBody SongCreateDTO songCreateDTO, Authentication authentication) {
String username = authentication.getName();
return playlistService.addSongToPlaylist(id, songCreateDTO, username);
}
// TODO: 实现 DELETE /api/playlists/{playlistId}/songs/{songId} (状态码 204) // DELETE /api/playlists/{playlistId}/songs/{songId} - 移除歌曲需认证
@DeleteMapping("/{playlistId}/songs/{songId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void removeSongFromPlaylist(@PathVariable Long playlistId, @PathVariable Long songId, Authentication authentication) {
String username = authentication.getName();
playlistService.removeSongFromPlaylist(playlistId, songId, username);
}
// TODO: 实现 DELETE /api/playlists/{id} (状态码 204) // DELETE /api/playlists/{id} - 删除歌单需认证
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deletePlaylist(@PathVariable Long id, Authentication authentication) {
String username = authentication.getName();
playlistService.deletePlaylist(id, username);
}
// TODO [Advanced]: 实现 GET /api/playlists/search?keyword=xxx // [Advanced] GET /api/playlists/search?keyword=xxx - 搜索歌单
@GetMapping("/search")
public List<PlaylistDTO> searchPlaylists(@RequestParam String keyword) {
return playlistService.searchPlaylists(keyword);
}
// TODO [Advanced]: 实现 POST /api/playlists/{id}/copy?newName=xxx (状态码 201) // [Advanced] POST /api/playlists/{id}/copy?newName=xxx - 复制歌单
@PostMapping("/{id}/copy")
@ResponseStatus(HttpStatus.CREATED)
public PlaylistDTO copyPlaylist(@PathVariable Long id, @RequestParam String newName, Authentication authentication) {
String username = authentication.getName();
return playlistService.copyPlaylist(id, newName, username);
}
} }

View File

@ -0,0 +1,12 @@
package com.vibevault.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.FORBIDDEN)
public class UnauthorizedAccessException extends RuntimeException {
public UnauthorizedAccessException(String message) {
super(message);
}
}

View File

@ -16,14 +16,22 @@ import java.util.List;
* - 一个歌单包含多首歌曲一对多关系 * - 一个歌单包含多首歌曲一对多关系
* - 删除歌单时应级联删除其中的歌曲 * - 删除歌单时应级联删除其中的歌曲
*/ */
@Entity
@Table(name = "playlists")
public class Playlist { public class Playlist {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@Column(nullable = false)
private String name; private String name;
@ManyToOne
@JoinColumn(name = "owner_id", nullable = false)
private User owner; private User owner;
@OneToMany(mappedBy = "playlist", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Song> songs = new ArrayList<>(); private List<Song> songs = new ArrayList<>();
protected Playlist() { protected Playlist() {
@ -59,7 +67,8 @@ public class Playlist {
* 提示需要维护双向关系 * 提示需要维护双向关系
*/ */
public void addSong(Song song) { public void addSong(Song song) {
// TODO: 实现添加歌曲逻辑 songs.add(song);
song.setPlaylist(this);
} }
/** /**
@ -67,6 +76,7 @@ public class Playlist {
* 提示需要维护双向关系 * 提示需要维护双向关系
*/ */
public void removeSong(Song song) { public void removeSong(Song song) {
// TODO: 实现移除歌曲逻辑 songs.remove(song);
song.setPlaylist(null);
} }
} }

View File

@ -10,16 +10,25 @@ import jakarta.persistence.*;
* - id 作为自增主键 * - id 作为自增主键
* - 每首歌曲属于一个歌单多对一关系 * - 每首歌曲属于一个歌单多对一关系
*/ */
@Entity
@Table(name = "songs")
public class Song { public class Song {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@Column(nullable = false)
private String title; private String title;
@Column(nullable = false)
private String artist; private String artist;
@Column(nullable = false)
private int durationInSeconds; private int durationInSeconds;
@ManyToOne
@JoinColumn(name = "playlist_id")
private Playlist playlist; private Playlist playlist;
public Song() { public Song() {

View File

@ -12,15 +12,22 @@ import jakarta.persistence.*;
* - password 不能为空 * - password 不能为空
* - [Challenge] 支持用户角色 ROLE_USER, ROLE_ADMIN * - [Challenge] 支持用户角色 ROLE_USER, ROLE_ADMIN
*/ */
@Entity
@Table(name = "users")
public class User { public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@Column(unique = true, nullable = false)
private String username; private String username;
@Column(nullable = false)
private String password; private String password;
// [Challenge] 用户角色默认为 ROLE_USER // [Challenge] 用户角色默认为 ROLE_USER
@Column(nullable = false)
private String role = "ROLE_USER"; private String role = "ROLE_USER";
protected User() { protected User() {
@ -49,6 +56,14 @@ public class User {
return password; return password;
} }
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public String getRole() { public String getRole() {
return role; return role;
} }

View File

@ -18,5 +18,9 @@ import java.util.List;
*/ */
@Repository @Repository
public interface PlaylistRepository extends JpaRepository<Playlist, Long> { public interface PlaylistRepository extends JpaRepository<Playlist, Long> {
// TODO [Advanced]: 添加高级查询方法 // 按所有者查询歌单
List<Playlist> findByOwner(User owner);
// [Advanced]: 按名称模糊搜索歌单
List<Playlist> findByNameContainingIgnoreCase(String keyword);
} }

View File

@ -15,5 +15,9 @@ import java.util.Optional;
*/ */
@Repository @Repository
public interface UserRepository extends JpaRepository<User, Long> { public interface UserRepository extends JpaRepository<User, Long> {
// TODO: 添加必要的查询方法 // 根据用户名查找用户
Optional<User> findByUsername(String username);
// 检查用户名是否已存在
boolean existsByUsername(String username);
} }

View File

@ -45,17 +45,41 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
@NonNull FilterChain filterChain @NonNull FilterChain filterChain
) throws ServletException, IOException { ) throws ServletException, IOException {
// TODO: 实现 JWT 认证逻辑
// 1. 从请求头获取 Authorization // 1. 从请求头获取 Authorization
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String username;
// 2. 检查是否以 "Bearer " 开头 // 2. 检查是否以 "Bearer " 开头
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// 3. 提取 token 并验证 // 3. 提取 token 并验证
jwt = authHeader.substring(7);
username = jwtService.extractUsername(jwt);
// 4. 如果有效创建 Authentication 并设置到 SecurityContextHolder // 4. 如果有效创建 Authentication 并设置到 SecurityContextHolder
// if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 提示 User user = userRepository.findByUsername(username)
// - 使用 request.getHeader("Authorization") 获取头 .orElse(null);
// - 使用 jwtService.extractUsername() jwtService.isTokenValid()
// - 使用 UsernamePasswordAuthenticationToken 创建认证对象 if (user != null && jwtService.isTokenValid(jwt, user)) {
// - 使用 SecurityContextHolder.getContext().setAuthentication() 设置 // 设置用户角色当前只有默认角色
List<SimpleGrantedAuthority> authorities = Collections.singletonList(
new SimpleGrantedAuthority("ROLE_USER")
);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
user, null, authorities
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }

View File

@ -30,26 +30,54 @@ public class JwtService {
* 为用户生成 JWT token * 为用户生成 JWT token
*/ */
public String generateToken(String username) { public String generateToken(String username) {
// TODO: 实现 token 生成 Date now = new Date();
// 提示使用 Jwts.builder() Date expirationDate = new Date(now.getTime() + expiration);
throw new UnsupportedOperationException("待实现");
return Jwts.builder()
.subject(username)
.issuedAt(now)
.expiration(expirationDate)
.signWith(getSigningKey())
.compact();
} }
/** /**
* token 中提取用户名 * token 中提取用户名
*/ */
public String extractUsername(String token) { public String extractUsername(String token) {
// TODO: 实现用户名提取 return Jwts.parser()
// 提示使用 Jwts.parser() .verifyWith(getSigningKey())
throw new UnsupportedOperationException("待实现"); .build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
} }
/** /**
* 验证 token 是否有效 * 验证 token 是否有效
*/ */
public boolean isTokenValid(String token, String username) { public boolean isTokenValid(String token, String username) {
// TODO: 实现 token 验证 try {
throw new UnsupportedOperationException("待实现"); String extractedUsername = extractUsername(token);
Date expirationDate = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload()
.getExpiration();
Date now = new Date();
return username.equals(extractedUsername) && expirationDate.after(now);
} catch (Exception e) {
return false;
}
}
/**
* 验证 token 是否对特定用户有效
*/
public boolean isTokenValid(String token, com.vibevault.model.User user) {
return isTokenValid(token, user.getUsername());
} }
/** /**

View File

@ -0,0 +1,153 @@
package com.vibevault.service.impl;
import com.vibevault.dto.PlaylistDTO;
import com.vibevault.dto.SongCreateDTO;
import com.vibevault.exception.ResourceNotFoundException;
import com.vibevault.exception.UnauthorizedAccessException;
import com.vibevault.model.Playlist;
import com.vibevault.model.Song;
import com.vibevault.model.User;
import com.vibevault.repository.PlaylistRepository;
import com.vibevault.repository.UserRepository;
import com.vibevault.service.PlaylistService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class PlaylistServiceImpl implements PlaylistService {
private final PlaylistRepository playlistRepository;
private final UserRepository userRepository;
@Autowired
public PlaylistServiceImpl(PlaylistRepository playlistRepository, UserRepository userRepository) {
this.playlistRepository = playlistRepository;
this.userRepository = userRepository;
}
@Override
public List<PlaylistDTO> getAllPlaylists() {
return playlistRepository.findAll().stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
@Override
public PlaylistDTO getPlaylistById(Long id) {
Playlist playlist = playlistRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Playlist not found with id: " + id));
return convertToDTO(playlist);
}
@Override
@Transactional
public PlaylistDTO createPlaylist(String name, String ownerUsername) {
User owner = userRepository.findByUsername(ownerUsername)
.orElseThrow(() -> new ResourceNotFoundException("User not found with username: " + ownerUsername));
Playlist playlist = new Playlist();
playlist.setName(name);
playlist.setOwner(owner);
Playlist savedPlaylist = playlistRepository.save(playlist);
return convertToDTO(savedPlaylist);
}
@Override
@Transactional
public PlaylistDTO addSongToPlaylist(Long playlistId, SongCreateDTO songDTO, String username) {
Playlist playlist = getPlaylistWithOwnershipCheck(playlistId, username);
Song song = new Song();
song.setTitle(songDTO.getTitle());
song.setArtist(songDTO.getArtist());
song.setDurationInSeconds(songDTO.getDurationInSeconds());
playlist.addSong(song);
Playlist savedPlaylist = playlistRepository.save(playlist);
return convertToDTO(savedPlaylist);
}
@Override
@Transactional
public void removeSongFromPlaylist(Long playlistId, Long songId, String username) {
Playlist playlist = getPlaylistWithOwnershipCheck(playlistId, username);
Song song = playlist.getSongs().stream()
.filter(s -> s.getId().equals(songId))
.findFirst()
.orElseThrow(() -> new ResourceNotFoundException("Song not found in playlist with id: " + songId));
playlist.removeSong(song);
playlistRepository.save(playlist);
}
@Override
@Transactional
public void deletePlaylist(Long playlistId, String username) {
Playlist playlist = getPlaylistWithOwnershipCheck(playlistId, username);
playlistRepository.delete(playlist);
}
// ========== Advanced 方法选做==========
@Override
public List<PlaylistDTO> searchPlaylists(String keyword) {
return playlistRepository.findByNameContainingIgnoreCase(keyword).stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
@Override
@Transactional
public PlaylistDTO copyPlaylist(Long playlistId, String newName, String username) {
Playlist originalPlaylist = playlistRepository.findById(playlistId)
.orElseThrow(() -> new ResourceNotFoundException("Playlist not found with id: " + playlistId));
User newOwner = userRepository.findByUsername(username)
.orElseThrow(() -> new ResourceNotFoundException("User not found with username: " + username));
Playlist copiedPlaylist = new Playlist();
copiedPlaylist.setName(newName);
copiedPlaylist.setOwner(newOwner);
// 复制歌曲
for (Song originalSong : originalPlaylist.getSongs()) {
Song copiedSong = new Song();
copiedSong.setTitle(originalSong.getTitle());
copiedSong.setArtist(originalSong.getArtist());
copiedSong.setDurationInSeconds(originalSong.getDurationInSeconds());
copiedPlaylist.addSong(copiedSong);
}
Playlist savedPlaylist = playlistRepository.save(copiedPlaylist);
return convertToDTO(savedPlaylist);
}
// 辅助方法检查歌单所有权
private Playlist getPlaylistWithOwnershipCheck(Long playlistId, String username) {
Playlist playlist = playlistRepository.findById(playlistId)
.orElseThrow(() -> new ResourceNotFoundException("Playlist not found with id: " + playlistId));
// 检查是否为歌单所有者
if (!playlist.getOwner().getUsername().equals(username)) {
throw new UnauthorizedAccessException("You don't have permission to modify this playlist");
}
return playlist;
}
// 辅助方法 Playlist 转换为 PlaylistDTO
private PlaylistDTO convertToDTO(Playlist playlist) {
PlaylistDTO dto = new PlaylistDTO();
dto.setId(playlist.getId());
dto.setName(playlist.getName());
dto.setOwnerUsername(playlist.getOwner().getUsername());
dto.setSongCount(playlist.getSongs().size());
return dto;
}
}