一、数据访问层(DAO)
本文档包含用户认证与邮件服务的完整代码实现、功能描述以及 Postman API 测试指南。
邮件网页测试用的网页是 https://www.mailinator.com/v4/public/inboxes.jsp?msgid=pingguomiaomiao20-1776578910-012132816333012#
一、数据访问层(DAO)
1.1 VideoRepository - 视频数据仓库接口
路径:com.netflix.clone.demo.dao.VideoRepository
功能:提供视频实体的 CRUD 操作基础接口,继承 Spring Data JPA 的 JpaRepository,无需手动实现。
package com.netflix.clone.demo.dao;
import com.netflix.clone.demo.entity.Video;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* 视频数据仓库接口
* <p>
* 继承 JpaRepository,自动获得针对 {@link Video} 实体的基本增删改查、
* 分页、排序等方法。Spring Data JPA 会在运行时自动生成实现代理类。
* </p>
* <p>
* 泛型参数说明:
* <ul>
* <li>{@code Video}:实体类型</li>
* <li>{@code Long}:实体主键 ID 的类型</li>
* </ul>
* </p>
*/
@Repository
public interface VideoRepository extends JpaRepository<Video, Long> {
// 目前仅使用 JpaRepository 提供的基础方法,暂无自定义查询。
// 未来可根据业务需求在此添加方法签名,例如:
// List<Video> findByPublishedTrue();
// Page<Video> findByCategoriesContaining(String category, Pageable pageable);
}
二、业务服务接口层(Service)
2.1 AuthService - 认证服务接口
路径:com.netflix.clone.demo.service.AuthService
功能:定义用户注册、登录、邮箱验证、密码重置等核心认证业务方法契约。
package com.netflix.clone.demo.service;
import com.netflix.clone.demo.dto.request.UserRequest;
import com.netflix.clone.demo.dto.response.EamilValidationResponse;
import com.netflix.clone.demo.dto.response.LoginResponse;
import com.netflix.clone.demo.dto.response.MessageResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public interface AuthService {
MessageResponse signup(@Valid UserRequest userRequest);
LoginResponse login(String email, String password);
EamilValidationResponse validdateEmail(String email);
MessageResponse verifyEmail(String token);
MessageResponse resendVerificationEmail(@NotBlank(message = "Email is required")
@Email(message = "Invalid email format") String email);
MessageResponse forgotPassword(@NotBlank(message = "Email is required")
@Email(message = "Invalid email format") String email);
MessageResponse resetPassword(@NotBlank String token,
@NotBlank @Size(min = 6, message = "New password must be at lease 6 characters long") String newPassword);
MessageResponse changePassword(String email, String currentPassword, String newPassword);
LoginResponse currentUser(String email);
}
2.2 EmailService - 邮件发送服务接口
路径:com.netflix.clone.demo.service.EmailService
功能:定义邮件发送相关方法,包括邮箱验证邮件和密码重置邮件。
package com.netflix.clone.demo.service;
public interface EmailService {
void sendVerificationEmail(String toEmail, String token);
void sendPasswordResetEmail(String toEmail, String token);
}
三、业务服务实现层(Service.Impl)
3.1 AuthServiceImpl - 认证服务实现类
路径:com.netflix.clone.demo.service.Impl.AuthServiceImpl
功能:实现 AuthService 接口,处理用户注册、登录、邮箱验证、密码重置等具体业务逻辑。
package com.netflix.clone.demo.service.Impl;
import com.netflix.clone.demo.dao.UserRepository;
import com.netflix.clone.demo.dto.request.UserRequest;
import com.netflix.clone.demo.dto.response.EamilValidationResponse;
import com.netflix.clone.demo.dto.response.LoginResponse;
import com.netflix.clone.demo.dto.response.MessageResponse;
import com.netflix.clone.demo.entity.User;
import com.netflix.clone.demo.enums.Role;
import com.netflix.clone.demo.exception.*;
import com.netflix.clone.demo.security.JwtUtil;
import com.netflix.clone.demo.service.AuthService;
import com.netflix.clone.demo.service.EmailService;
import com.netflix.clone.demo.util.ServiceUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.UUID;
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private EmailService emailService;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private ServiceUtils serviceUtils;
@Override
public MessageResponse signup(UserRequest userRequest) {
if(userRepository.existsByEmail(userRequest.getEmail())){
throw new EmailAlreadyExistsException("Email already exists.");
}
User user = new User();
user.setEmail(userRequest.getEmail());
user.setPassword(passwordEncoder.encode(userRequest.getPassword()));
user.setFullName(userRequest.getFullName());
user.setRole(Role.USER);
user.setEmailVerified(false);
String verificationToken = UUID.randomUUID().toString();
user.setVerificationToken(verificationToken);
user.setVerificationTokenExpiry(Instant.now().plusSeconds(86400));
userRepository.save(user);
emailService.sendVerificationEmail(userRequest.getEmail(), verificationToken);
return new MessageResponse("Registration successful! Please check your email to verify your account");
}
@Override
public LoginResponse login(String email, String password) {
User user = userRepository
.findByEmail(email)
.filter(u -> passwordEncoder.matches(password, u.getPassword()))
.orElseThrow(() -> new BadCredentialsException("Invalid email or password"));
if(!user.isActive()){
throw new AccountDeactivatedException("You account has been deactivated. Please contact" +
"support for assistance");
}
if(!user.isEmailVerified()){
throw new EmailNotVerifiedException(
"Please verify your email address before loggin in.Check your inbox for the" +
"verification link."
);
}
final String token = jwtUtil.generateToken(user.getEmail(), user.getRole().name());
return new LoginResponse(token, user.getEmail(), user.getFullName(), user.getRole().name());
}
@Override
public EamilValidationResponse validdateEmail(String email) {
boolean exists = userRepository.existsByEmail(email);
return new EamilValidationResponse(exists, !exists);
}
@Override
public MessageResponse verifyEmail(String token) {
User user=
userRepository
.findByVerificationToken(token)
.orElseThrow(() -> new InvalidTokenException("Invalid or expired verification token"));
if (user.getVerificationTokenExpiry() == null
|| user.getVerificationTokenExpiry().isBefore(Instant.now())){
throw new InvalidTokenException("Verification link has expired. Please request a new one.");
}
user.setEmailVerified(true);
user.setVerificationToken(token);
user.setVerificationTokenExpiry(null);
userRepository.save(user);
return new MessageResponse("Email verified successfully! you can now login!");
}
@Override
public MessageResponse resendVerificationEmail(String email) {
User user = serviceUtils.getUserByEmailOrThrow(email);
String verificationToken = UUID.randomUUID().toString();
user.setVerificationToken(verificationToken);
user.setVerificationTokenExpiry(Instant.now().plusSeconds(86400));
userRepository.save(user);
emailService.sendVerificationEmail(email, verificationToken);
return new MessageResponse("Verification email resent successfully! Please check your inbox.");
}
@Override
public MessageResponse forgotPassword(String email) {
User user = serviceUtils.getUserByEmailOrThrow(email);
String resetToken = UUID.randomUUID().toString();
user.setPasswordResetToken(resetToken);
user.setPasswordResetTokenExpiry(Instant.now().plusSeconds(3600));
userRepository.save(user);
emailService.sendPasswordResetEmail(email, resetToken);
return new MessageResponse("Password reset email sent successfully! Please check your inbox.");
}
@Override
public MessageResponse resetPassword(String token, String newPassword) {
User user =
userRepository
.findByPasswordResetToken(token)
.orElseThrow(() -> new InvalidTokenException("Invalid or expired reset token"));
if (user.getPasswordResetTokenExpiry() == null || user.getPasswordResetTokenExpiry().isBefore(Instant.now())){
throw new InvalidTokenException("Reset token has expired");
}
user.setPassword(passwordEncoder.encode(newPassword));
user.setPasswordResetToken(null);
user.setPasswordResetTokenExpiry(null);
userRepository.save(user);
return new MessageResponse("Password reset successfully. You can now log in with your new password.");
}
@Override
public MessageResponse changePassword(String email, String currentPassword, String newPassword) {
User user = serviceUtils.getUserByEmailOrThrow(email);
if(!passwordEncoder.matches(currentPassword, user.getPassword())){
throw new InvalidCredentialsException("Current password is incorrect");
}
user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
return new MessageResponse("Password changed successfully.");
}
@Override
public LoginResponse currentUser(String email) {
User user = serviceUtils.getUserByEmailOrThrow(email);
return new LoginResponse(null, user.getEmail(), user.getFullName(), user.getRole().name());
}
}
3.2 EmailServiceImpl - 邮件发送服务实现类
路径:com.netflix.clone.demo.service.Impl.EmailServiceImpl
功能:实现 EmailService 接口,使用 Spring Mail 发送纯文本邮件,包括邮箱验证邮件和密码重置邮件。
package com.netflix.clone.demo.service.Impl;
import com.netflix.clone.demo.service.EmailService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
@Service
public class EmailServiceImpl implements EmailService {
private static final Logger logger = LoggerFactory.getLogger(EmailServiceImpl.class);
@Autowired
private JavaMailSender mailSender;
@Value("${app.frontend.url:http://localhost:4200}")
private String frontendUrl;
@Value("${spring.mail.username}")
private String fromEmail;
@Override
public void sendVerificationEmail(String toEmail, String token) {
try {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(fromEmail);
message.setTo(toEmail);
message.setSubject("Netflix Clone - Verify Your Email");
String verificationLink = frontendUrl + "/verify-email?token=" + token;
String emailBody =
"Welcome to Netflix Clone!\n\n"
+ "Thank you for registering. Please verify your email address by clicking the link below:\n\n"
+ verificationLink
+ "\n\n"
+ "This link will expire in 24 hours.\n\n"
+ "If you didn't create this account, please ignore this email.\n\n"
+ "Best regards,\n"
+ "Netflix Clone Team";
message.setText(emailBody);
mailSender.send(message);
logger.info("Verification email sent to {}", toEmail);
} catch (Exception ex) {
logger.error("Failed to send verification email to {}: {}", toEmail, ex.getMessage(), ex);
// 【修改1】统一异常处理行为:抛出运行时异常,让调用方感知失败
throw new RuntimeException("Failed to send verification email", ex);
}
}
@Override
public void sendPasswordResetEmail(String toEmail, String token) {
try {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(fromEmail);
// 【修改2】添加缺失的收件人设置
message.setTo(toEmail);
message.setSubject("Netflix Clone - Password Reset");
String resetLink = frontendUrl + "/reset-password?token=" + token;
// 【修改3】修正邮件正文模板,替换为密码重置专用文案
String emailBody =
"Hi,\n\n"
+ "We received a request to reset the password for your Netflix Clone account.\n\n"
+ "Please click the link below to set a new password:\n\n"
+ resetLink
+ "\n\n"
+ "This link will expire in 15 minutes.\n\n"
+ "If you did not request a password reset, please ignore this email. "
+ "Your account security will not be affected.\n\n"
+ "Best regards,\n"
+ "Netflix Clone Team";
message.setText(emailBody);
mailSender.send(message);
logger.info("Password reset email sent to {}", toEmail);
} catch (Exception ex) {
logger.error("Failed to send password reset email to {} : {}", toEmail, ex.getMessage(), ex);
throw new RuntimeException("Failed to send password reset email", ex);
}
}
}
四、控制器层(Controller)
4.1 AuthController - 认证控制器
路径:com.netflix.clone.demo.controller.AuthController
功能:暴露 RESTful API,处理来自前端的认证相关 HTTP 请求,包括注册、登录、邮箱验证、密码重置等。
package com.netflix.clone.demo.controller;
import com.netflix.clone.demo.dto.request.*;
import com.netflix.clone.demo.dto.response.EamilValidationResponse;
import com.netflix.clone.demo.dto.response.LoginResponse;
import com.netflix.clone.demo.dto.response.MessageResponse;
import com.netflix.clone.demo.service.AuthService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
public class AuthControler {
@Autowired
private AuthService authService;
@PostMapping("/signup")
public ResponseEntity<MessageResponse> signup(@Valid @RequestBody UserRequest userRequest){
return ResponseEntity.ok(authService.signup(userRequest));
}
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest loginRequest){
LoginResponse response = authService.login(loginRequest.getEmail(), loginRequest.getPassword());
return ResponseEntity.ok(response);
}
@GetMapping("/validate-email")
public ResponseEntity<EamilValidationResponse> validateEmail(@RequestParam String email){
return ResponseEntity.ok(authService.validdateEmail(email));
}
@GetMapping("/verify-email")
public ResponseEntity<MessageResponse> verifyEmail(@RequestParam String token){
return ResponseEntity.ok(authService.verifyEmail(token));
}
@PostMapping("/resend-verification")
public ResponseEntity<MessageResponse> resendVerification(@Valid @RequestBody EmailRequest emailRequest){
return ResponseEntity.ok(authService.resendVerificationEmail(emailRequest.getEmail()));
}
@PostMapping("/forget-password")
public ResponseEntity<MessageResponse> forgotPassword(
@Valid @RequestBody EmailRequest emailRequest
){
return ResponseEntity.ok(authService.forgotPassword(emailRequest.getEmail()));
}
@PostMapping("/reset-password")
public ResponseEntity<MessageResponse> resetPassword(
@Valid @RequestBody ResetPasswordRequest resetPasswordRequest
){
return ResponseEntity.ok(
authService.resetPassword(
resetPasswordRequest.getToken(), resetPasswordRequest.getNewPassword()
)
);
}
@PostMapping("/change-password")
public ResponseEntity<?> changePassword(
Authentication authentication,
@Valid @RequestBody ChangePasswordRequest changePasswordRequest
){
String email = authentication.getName();
return ResponseEntity.ok(
authService.changePassword(
email,
changePasswordRequest.getCurrentPassword(),
changePasswordRequest.getNewPassword()
)
);
}
@GetMapping("/current-user")
public ResponseEntity<LoginResponse> currentUser(
Authentication authentication
){
String email = authentication.getName();
return ResponseEntity.ok(authService.currentUser(email));
}
}
五、Postman API 测试指南
5.1 基础配置
- Base URL:
http://localhost:8080 - Content-Type:
application/json
5.2 接口测试详情
① 用户注册 – POST /api/auth/signup
请求体:
{
"email": "test@mailinator.com",
"password": "123456",
"fullName": "Test User"
}
预期响应:
{
"message": "Registration successful! Please check your email to verify your account"
}
② 邮箱验证 – GET /api/auth/verify-email?token=<verification_token>
示例 URL:
http://localhost:8080/api/auth/verify-email?token=681103c3-3385-4b05-b5c3-a540e124a888
预期响应:
{
"message": "Email verified successfully! you can now login!"
}
③ 用户登录 – POST /api/auth/login
请求体:
{
"email": "test@mailinator.com",
"password": "123456"
}
预期响应:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"email": "test@mailinator.com",
"fullName": "Test User",
"role": "USER"
}
④ 检查邮箱是否已注册 – GET /api/auth/validate-email?email=test@mailinator.com
预期响应:
{
"exists": true,
"available": false
}
⑤ 重发验证邮件 – POST /api/auth/resend-verification
请求体:
{
"email": "test@mailinator.com"
}
⑥ 忘记密码 – POST /api/auth/forget-password
请求体:
{
"email": "test@mailinator.com"
}
⑦ 重置密码 – POST /api/auth/reset-password
请求体:
{
"token": "从邮件中提取的 password_reset_token",
"newPassword": "newPassword123"
}
⑧ 修改密码(需认证) – POST /api/auth/change-password
Headers:Authorization: Bearer <JWT Token>
请求体:
{
"currentPassword": "123456",
"newPassword": "newSecurePassword"
}
⑨ 获取当前用户信息(需认证) – GET /api/auth/current-user
Headers:Authorization: Bearer <JWT Token>
预期响应:
{
"token": null,
"email": "test@mailinator.com",
"fullName": "Test User",
"role": "USER"
}
5.3 Postman 环境变量建议
| 变量名 | 示例值 |
|---|---|
baseUrl | http://localhost:8080 |
token | 登录成功后获取的 JWT |
在请求 URL 中使用 {{baseUrl}}/api/auth/...,在 Authorization 标签页选择 Bearer Token 并填入 {{token}}。
六、注意事项
- 密码加密:当前
SecurityConfig使用了NoOpPasswordEncoder,密码以明文存储,仅适用于开发测试。生产环境必须更换为BCryptPasswordEncoder。 - 邮件服务:需在
application.properties中正确配置spring.mail.*参数,推荐使用 QQ 邮箱或 Gmail 应用专用密码。 - Token 有效期:邮箱验证 Token 有效期为 24 小时,密码重置 Token 有效期为 1 小时。
- 数据库检查:可通过 MySQL 命令行查看
users表,确认email_verified、verification_token等字段的更新情况。
评论