login register
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
package com.example.project_it207_server.config.security;
|
||||||
|
|
||||||
|
import com.example.project_it207_server.config.security.jwt.JWTAuthTokenFilter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableMethodSecurity
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private final JWTAuthTokenFilter jwtAuthTokenFilter;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.csrf(csrf -> csrf.disable())
|
||||||
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
// ✅ Cho phép truy cập các API public
|
||||||
|
.requestMatchers(
|
||||||
|
"/api/auth/register",
|
||||||
|
"/api/auth/login",
|
||||||
|
"/api/auth/forgot-password",
|
||||||
|
"/api/auth/confirm-password-change",
|
||||||
|
"/public/**"
|
||||||
|
).permitAll()
|
||||||
|
// ✅ Tất cả API khác yêu cầu xác thực
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
);
|
||||||
|
|
||||||
|
// ✅ Thêm JWT filter trước UsernamePasswordAuthenticationFilter
|
||||||
|
http.addFilterBefore(jwtAuthTokenFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||||
|
return config.getAuthenticationManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.example.project_it207_server.config.security.exception;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class AccessDeniedHandler implements org.springframework.security.web.access.AccessDeniedHandler {
|
||||||
|
@Override
|
||||||
|
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
|
||||||
|
response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403 Forbidden
|
||||||
|
response.setContentType("application/json");
|
||||||
|
response.getWriter().write("{\"error\": \"Forbidden\", \"message\": \"" + accessDeniedException.getMessage() + "\"}");
|
||||||
|
response.getWriter().flush();
|
||||||
|
response.getWriter().close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.example.project_it207_server.config.security.exception;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class AuthenticationEntryPoint implements org.springframework.security.web.AuthenticationEntryPoint {
|
||||||
|
@Override
|
||||||
|
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
response.setContentType("application/json");
|
||||||
|
response.getWriter().write("{\"error\": \"Unauthorized\", \"message\": \"" + authException.getMessage() + "\"}");
|
||||||
|
response.getWriter().flush();
|
||||||
|
response.getWriter().close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package com.example.project_it207_server.config.security.jwt;
|
||||||
|
|
||||||
|
import com.example.project_it207_server.redis.RedisService;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT Authentication Filter với kiểm tra Redis
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class JWTAuthTokenFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private final JWTProvider jwtProvider;
|
||||||
|
private final UserDetailsService userDetailsService;
|
||||||
|
private final RedisService redisService;
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
|
||||||
|
try {
|
||||||
|
String token = getTokenFromRequest(request);
|
||||||
|
|
||||||
|
if (token != null) {
|
||||||
|
// Kiểm tra token hợp lệ
|
||||||
|
if (jwtProvider.validateToken(token)) {
|
||||||
|
String username = jwtProvider.getUserNameFromToken(token);
|
||||||
|
|
||||||
|
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||||
|
|
||||||
|
// Kiểm tra token có tồn tại trong Redis không
|
||||||
|
String redisKey = "token:" + username + ":" + token;
|
||||||
|
if (!redisService.hasKey(redisKey)) {
|
||||||
|
log.warn("Token không tồn tại trong Redis cho user: {}", username);
|
||||||
|
handleAuthenticationError(response, "Token đã hết hạn hoặc không hợp lệ");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||||
|
|
||||||
|
if (userDetails != null) {
|
||||||
|
UsernamePasswordAuthenticationToken authentication =
|
||||||
|
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||||
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Lỗi trong quá trình xác thực: ", e);
|
||||||
|
handleAuthenticationError(response, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy JWT token từ request header
|
||||||
|
*/
|
||||||
|
private String getTokenFromRequest(HttpServletRequest request) {
|
||||||
|
String authHeader = request.getHeader("Authorization");
|
||||||
|
|
||||||
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
|
return authHeader.substring(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Xử lý khi token bị blacklist
|
||||||
|
*/
|
||||||
|
private void handleBlacklistedToken(HttpServletResponse response) throws IOException {
|
||||||
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
|
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
|
||||||
|
Map<String, Object> errorResponse = new HashMap<>();
|
||||||
|
errorResponse.put("success", false);
|
||||||
|
errorResponse.put("code", 401);
|
||||||
|
errorResponse.put("message", "Token đã bị vô hiệu hóa");
|
||||||
|
errorResponse.put("details", Map.of("error", "Token đã được đăng xuất"));
|
||||||
|
|
||||||
|
String jsonResponse = objectMapper.writeValueAsString(errorResponse);
|
||||||
|
response.getWriter().write(jsonResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Xử lý lỗi authentication
|
||||||
|
*/
|
||||||
|
private void handleAuthenticationError(HttpServletResponse response, String errorMessage) throws IOException {
|
||||||
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
|
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
|
||||||
|
Map<String, Object> errorResponse = new HashMap<>();
|
||||||
|
errorResponse.put("success", false);
|
||||||
|
errorResponse.put("code", 401);
|
||||||
|
errorResponse.put("message", "Xác thực thất bại");
|
||||||
|
errorResponse.put("details", Map.of("error", errorMessage));
|
||||||
|
|
||||||
|
String jsonResponse = objectMapper.writeValueAsString(errorResponse);
|
||||||
|
response.getWriter().write(jsonResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||||
|
String path = request.getRequestURI();
|
||||||
|
|
||||||
|
// Không filter các endpoint public
|
||||||
|
return path.startsWith("/api/auth/login") ||
|
||||||
|
path.startsWith("/api/auth/register") ||
|
||||||
|
path.startsWith("/api/auth/oauth2/firebase") ||
|
||||||
|
path.startsWith("/api/auth/forgot-password") ||
|
||||||
|
path.startsWith("/api/auth/confirm-password-change") ||
|
||||||
|
path.startsWith("/public/");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.example.project_it207_server.config.security.jwt;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.*;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class JWTProvider {
|
||||||
|
|
||||||
|
@Value("${jwt.secret-key}")
|
||||||
|
private String secretKey;
|
||||||
|
|
||||||
|
@Value("${jwt.expiration}")
|
||||||
|
private long jwtExpiration;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public String generateToken(UserDetails userDetails) {
|
||||||
|
Date today = new Date();
|
||||||
|
return Jwts.builder()
|
||||||
|
.setSubject(userDetails.getUsername())
|
||||||
|
.setIssuedAt(today)
|
||||||
|
.setExpiration(new Date(today.getTime() + jwtExpiration))
|
||||||
|
.signWith( SignatureAlgorithm.HS512, secretKey)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
public boolean validateToken(String token) {
|
||||||
|
try {
|
||||||
|
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
|
||||||
|
return true;
|
||||||
|
} catch (ExpiredJwtException e) {
|
||||||
|
log.error("JWT: message error expired:", e.getMessage());
|
||||||
|
} catch (UnsupportedJwtException e) {
|
||||||
|
log.error("JWT: message error unsupported:", e.getMessage());
|
||||||
|
} catch (MalformedJwtException e) {
|
||||||
|
log.error("JWT: message error not formated:", e.getMessage());
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
log.error("JWT: message error signature not math:", e.getMessage());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.error("JWT: message claims empty or argument invalid: ", e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Giai mã token
|
||||||
|
public String getUserNameFromToken(String token) {
|
||||||
|
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.example.project_it207_server.config.security.principal;
|
||||||
|
|
||||||
|
import com.example.project_it207_server.model.entity.Role;
|
||||||
|
import com.example.project_it207_server.model.entity.User;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lớp ánh xạ thông tin người dùng sang dạng UserDetails để Spring Security sử dụng
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class UserDetailsCus implements UserDetails {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String username;
|
||||||
|
private String email;
|
||||||
|
private String password;
|
||||||
|
private Role role;
|
||||||
|
private Collection<? extends GrantedAuthority> authorities;
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Xây dựng UserDetailsCus từ entity User
|
||||||
|
*/
|
||||||
|
public static UserDetailsCus build(User user) {
|
||||||
|
// ✅ Lấy quyền từ tên role (VD: "USER", "ADMIN")
|
||||||
|
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(user.getRole().getRoleName());
|
||||||
|
|
||||||
|
return UserDetailsCus.builder()
|
||||||
|
.id(user.getUserId())
|
||||||
|
.username(user.getUsername())
|
||||||
|
.email(user.getEmail())
|
||||||
|
.password(user.getPassword())
|
||||||
|
.role(user.getRole())
|
||||||
|
.authorities(Collections.singletonList(authority))
|
||||||
|
.enabled(true)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||||
|
return authorities;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsername() {
|
||||||
|
// Dùng email hoặc username đều được
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAccountNonExpired() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAccountNonLocked() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCredentialsNonExpired() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.example.project_it207_server.config.security.principal;
|
||||||
|
|
||||||
|
import com.example.project_it207_server.model.entity.User;
|
||||||
|
import com.example.project_it207_server.repository.UserRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy thông tin người dùng từ database để Spring Security xác thực
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserDetailsServiceCus implements UserDetailsService {
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
|
||||||
|
// ✅ Cho phép đăng nhập bằng email hoặc username
|
||||||
|
User user = userRepository.findByUsername(usernameOrEmail)
|
||||||
|
.or(() -> userRepository.findByEmail(usernameOrEmail))
|
||||||
|
.orElseThrow(() -> new UsernameNotFoundException("Không tìm thấy tài khoản: " + usernameOrEmail));
|
||||||
|
|
||||||
|
return UserDetailsCus.build(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.example.project_it207_server.controller;
|
||||||
|
|
||||||
|
import com.example.project_it207_server.model.dto.request.AuthRequest;
|
||||||
|
import com.example.project_it207_server.model.dto.response.AuthResponse;
|
||||||
|
import com.example.project_it207_server.service.AuthService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/auth")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class AuthController {
|
||||||
|
|
||||||
|
private final AuthService authService;
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
public ResponseEntity<AuthResponse> register(@RequestBody AuthRequest request) {
|
||||||
|
return ResponseEntity.ok(authService.register(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/login")
|
||||||
|
public ResponseEntity<AuthResponse> login(@RequestBody AuthRequest request) {
|
||||||
|
return ResponseEntity.ok(authService.login(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/logout")
|
||||||
|
public ResponseEntity<?> logout(@RequestHeader("Authorization") String authHeader) {
|
||||||
|
String token = authHeader.replace("Bearer ", "");
|
||||||
|
authService.logout(token);
|
||||||
|
return ResponseEntity.ok().body("Đăng xuất thành công");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.example.project_it207_server.model.dto.request;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class AuthRequest {
|
||||||
|
private String email;
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.example.project_it207_server.model.dto.response;
|
||||||
|
|
||||||
|
import com.example.project_it207_server.model.entity.Role;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class AuthResponse {
|
||||||
|
private String token;
|
||||||
|
private String email;
|
||||||
|
private Role role;
|
||||||
|
}
|
||||||
@@ -6,7 +6,11 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "users")
|
@Table(name = "users")
|
||||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
public class User {
|
public class User {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@@ -16,12 +20,12 @@ public class User {
|
|||||||
@Column(unique = true, nullable = false)
|
@Column(unique = true, nullable = false)
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private String passwordHash;
|
|
||||||
|
|
||||||
@Column(unique = true, nullable = false)
|
@Column(unique = true, nullable = false)
|
||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String password;
|
||||||
|
|
||||||
private String firstName;
|
private String firstName;
|
||||||
private String lastName;
|
private String lastName;
|
||||||
private String gender;
|
private String gender;
|
||||||
@@ -29,10 +33,16 @@ public class User {
|
|||||||
private String address;
|
private String address;
|
||||||
private String avatarUrl;
|
private String avatarUrl;
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne(fetch = FetchType.EAGER)
|
||||||
@JoinColumn(name = "role_id")
|
@JoinColumn(name = "role_id")
|
||||||
private Role role;
|
private Role role;
|
||||||
|
|
||||||
private LocalDateTime createdAt = LocalDateTime.now();
|
private LocalDateTime createdAt = LocalDateTime.now();
|
||||||
private LocalDateTime updatedAt = LocalDateTime.now();
|
private LocalDateTime updatedAt = LocalDateTime.now();
|
||||||
|
|
||||||
|
// ✅ Tự động cập nhật thời gian khi update entity
|
||||||
|
@PreUpdate
|
||||||
|
public void preUpdate() {
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.example.project_it207_server.repository;
|
||||||
|
|
||||||
|
import com.example.project_it207_server.model.entity.Role;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface RoleRepository extends JpaRepository<Role, Long> {
|
||||||
|
Optional<Role> findByRoleName(String roleName);
|
||||||
|
}
|
||||||
@@ -2,8 +2,11 @@ package com.example.project_it207_server.repository;
|
|||||||
|
|
||||||
import com.example.project_it207_server.model.entity.User;
|
import com.example.project_it207_server.model.entity.User;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
Optional<User> findByUsername(String username);
|
Optional<User> findByUsername(String username);
|
||||||
|
Optional<User> findByEmail(String email);
|
||||||
|
boolean existsByEmail(String email);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.example.project_it207_server.service;
|
||||||
|
|
||||||
|
import com.example.project_it207_server.config.security.jwt.JWTProvider;
|
||||||
|
import com.example.project_it207_server.model.dto.request.AuthRequest;
|
||||||
|
import com.example.project_it207_server.model.dto.response.AuthResponse;
|
||||||
|
import com.example.project_it207_server.model.entity.Role;
|
||||||
|
import com.example.project_it207_server.model.entity.User;
|
||||||
|
import com.example.project_it207_server.redis.RedisService;
|
||||||
|
import com.example.project_it207_server.repository.RoleRepository;
|
||||||
|
import com.example.project_it207_server.repository.UserRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuthService {
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final RoleRepository roleRepository;
|
||||||
|
private final JWTProvider jwtProvider;
|
||||||
|
private final RedisService redisService;
|
||||||
|
private final UserDetailsService userDetailsService;
|
||||||
|
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||||
|
|
||||||
|
public AuthResponse register(AuthRequest req) {
|
||||||
|
if (userRepository.existsByEmail(req.getEmail())) {
|
||||||
|
throw new RuntimeException("Email đã tồn tại!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 Lấy role mặc định (nếu chưa có, tự tạo)
|
||||||
|
Role defaultRole = roleRepository.findByRoleName("USER")
|
||||||
|
.orElseGet(() -> roleRepository.save(Role.builder().roleName("USER").build()));
|
||||||
|
|
||||||
|
User newUser = User.builder()
|
||||||
|
.username(req.getEmail())
|
||||||
|
.email(req.getEmail())
|
||||||
|
.password(passwordEncoder.encode(req.getPassword()))
|
||||||
|
.role(defaultRole)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
userRepository.save(newUser);
|
||||||
|
|
||||||
|
UserDetails userDetails = userDetailsService.loadUserByUsername(newUser.getEmail());
|
||||||
|
String token = jwtProvider.generateToken(userDetails);
|
||||||
|
|
||||||
|
String redisKey = "token:" + newUser.getEmail() + ":" + token;
|
||||||
|
redisService.setValueWithExpiry(redisKey, token, 1, TimeUnit.DAYS);// TTL 1 ngày
|
||||||
|
|
||||||
|
return new AuthResponse(token, newUser.getEmail(), newUser.getRole());
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthResponse login(AuthRequest req) {
|
||||||
|
User user = userRepository.findByEmail(req.getEmail())
|
||||||
|
.orElseThrow(() -> new RuntimeException("Tài khoản không tồn tại"));
|
||||||
|
|
||||||
|
if (!passwordEncoder.matches(req.getPassword(), user.getPassword())) {
|
||||||
|
throw new RuntimeException("Sai mật khẩu");
|
||||||
|
}
|
||||||
|
|
||||||
|
UserDetails userDetails = userDetailsService.loadUserByUsername(user.getEmail());
|
||||||
|
String token = jwtProvider.generateToken(userDetails);
|
||||||
|
|
||||||
|
String redisKey = "token:" + user.getEmail() + ":" + token;
|
||||||
|
redisService.setValueWithExpiry(redisKey, token, 86400L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
return new AuthResponse(token, user.getEmail(), user.getRole());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void logout(String token) {
|
||||||
|
String username = jwtProvider.getUserNameFromToken(token);
|
||||||
|
String redisKey = "token:" + username + ":" + token;
|
||||||
|
redisService.deleteKey(redisKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
spring.application.name=project_it207_server
|
spring.application.name=project_it207_server
|
||||||
|
|
||||||
server.port=8080
|
server.port=8080
|
||||||
|
server.address=0.0.0.0
|
||||||
spring.datasource.url=jdbc:mysql://localhost:3306/project_it207?createDatabaseIfNotExist=true
|
spring.datasource.url=jdbc:mysql://localhost:3306/project_it207?createDatabaseIfNotExist=true
|
||||||
spring.datasource.username=root
|
spring.datasource.username=root
|
||||||
spring.datasource.password=
|
spring.datasource.password=
|
||||||
|
|||||||
Reference in New Issue
Block a user