diff --git a/build.gradle b/build.gradle index 27b6502..62e3a0c 100644 --- a/build.gradle +++ b/build.gradle @@ -56,6 +56,8 @@ dependencies { implementation 'software.amazon.awssdk:rekognition' // ✅ SMTP implementation 'org.springframework.boot:spring-boot-starter-mail' + // ✅ Oauth2 + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // SpringAI API implementation 'io.grpc:grpc-xds:1.62.2' diff --git a/src/main/java/io/github/petty/config/SecurityConfig.java b/src/main/java/io/github/petty/config/SecurityConfig.java index cdef7eb..18dc2d7 100644 --- a/src/main/java/io/github/petty/config/SecurityConfig.java +++ b/src/main/java/io/github/petty/config/SecurityConfig.java @@ -3,6 +3,8 @@ import io.github.petty.users.jwt.JWTFilter; import io.github.petty.users.jwt.JWTUtil; import io.github.petty.users.jwt.LoginFilter; +import io.github.petty.users.oauth2.CustomOAuth2UserService; +import io.github.petty.users.oauth2.OAuth2SuccessHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; @@ -18,16 +20,22 @@ @EnableWebSecurity public class SecurityConfig { - //AuthenticationManager가 인자로 받을 AuthenticationConfiguraion 객체 생성자 주입 private final AuthenticationConfiguration authenticationConfiguration; private final JWTUtil jwtUtil; + private final CustomOAuth2UserService customOAuth2UserService; + private final OAuth2SuccessHandler oAuth2SuccessHandler; - public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil) { + public SecurityConfig( + AuthenticationConfiguration authenticationConfiguration, + JWTUtil jwtUtil, + CustomOAuth2UserService customOAuth2UserService, + OAuth2SuccessHandler oAuth2SuccessHandler) { this.authenticationConfiguration = authenticationConfiguration; this.jwtUtil = jwtUtil; + this.customOAuth2UserService = customOAuth2UserService; + this.oAuth2SuccessHandler = oAuth2SuccessHandler; } - //AuthenticationManager Bean 등록 @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); @@ -41,13 +49,28 @@ public BCryptPasswordEncoder bCryptPasswordEncoder() { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .csrf(csrf -> csrf.disable()) // CSRF 비활성화 - .formLogin(form -> form.disable()) // 로그인 비활성화 - .httpBasic(basic -> basic.disable()) // HTTP Basic 인증 비활성화 + .csrf(csrf -> csrf.disable()) + .formLogin(form -> form.disable()) + .httpBasic(basic -> basic.disable()) .authorizeHttpRequests((auth) -> auth .requestMatchers("/admin").hasRole("ADMIN") - .requestMatchers("/user").authenticated() // 로그인이 필요한 페이지 + .requestMatchers("/user").authenticated() + .requestMatchers("/login/**", "/oauth2/**").permitAll() .anyRequest().permitAll()) + .oauth2Login(oauth2 -> oauth2 + .loginPage("/login") + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuth2UserService) + ) + .successHandler(oAuth2SuccessHandler) + ) + .logout(logout -> logout + .logoutUrl("/logout") // 클라이언트 측 로그아웃 요청 URL과 일치 + .logoutSuccessUrl("/") // 로그아웃 성공 후 리다이렉트 URL + .deleteCookies("jwt") // 로그아웃 시 jwt 쿠키 삭제 + .invalidateHttpSession(true) + .clearAuthentication(true) + ) .addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class) .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class) .sessionManagement((session) -> session @@ -55,4 +78,4 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.build(); } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/petty/config/SupabaseDataSourceConfig.java b/src/main/java/io/github/petty/config/SupabaseDataSourceConfig.java index 290a46b..a4a6a72 100644 --- a/src/main/java/io/github/petty/config/SupabaseDataSourceConfig.java +++ b/src/main/java/io/github/petty/config/SupabaseDataSourceConfig.java @@ -2,6 +2,7 @@ import jakarta.persistence.EntityManagerFactory; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; @@ -62,13 +63,13 @@ public DataSource supabaseDataSource() { public LocalContainerEntityManagerFactoryBean supabaseEntityManagerFactory( // datasource 2개 이상일 경우 명시 @Qualifier("supabaseDataSource") DataSource dataSource, - EntityManagerFactoryBuilder builder - ) { + EntityManagerFactoryBuilder builder, + @Value("${spring.jpa.hibernate.ddl-auto}") String ddlAuto) { // ddl-auto 주입 Map jpaProperties = new HashMap<>(); jpaProperties.put("hibernate.physical_naming_strategy", "org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy"); // ddl.auto - jpaProperties.put("hibernate.hbm2ddl.auto", "update"); + jpaProperties.put("hibernate.hbm2ddl.auto", ddlAuto); return builder.dataSource(dataSource) .packages( diff --git a/src/main/java/io/github/petty/users/controller/UsersController.java b/src/main/java/io/github/petty/users/controller/UsersController.java index 185e8e3..da1c881 100644 --- a/src/main/java/io/github/petty/users/controller/UsersController.java +++ b/src/main/java/io/github/petty/users/controller/UsersController.java @@ -1,23 +1,37 @@ package io.github.petty.users.controller; +import io.github.petty.users.dto.CustomUserDetails; import io.github.petty.users.dto.JoinDTO; +import io.github.petty.users.dto.UserProfileEditDTO; +import io.github.petty.users.entity.Users; import io.github.petty.users.service.JoinService; +import io.github.petty.users.service.UserService; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import org.springframework.security.core.Authentication; +import java.util.UUID; + @Controller public class UsersController { private final JoinService joinService; + private final UserService userService; + - public UsersController(JoinService joinService) { + public UsersController(JoinService joinService, UserService userService) { this.joinService = joinService; + this.userService = userService; } @GetMapping("/join") @@ -27,9 +41,9 @@ public String joinForm(Model model) { } @PostMapping("/join") - public String joinProcess(JoinDTO joinDTO, RedirectAttributes redirectAttributes) { + public String joinProcess(JoinDTO JoinDTO, RedirectAttributes redirectAttributes) { - Boolean joinResult = joinService.joinProcess(joinDTO); + Boolean joinResult = joinService.joinProcess(JoinDTO); if (!joinResult) { redirectAttributes.addFlashAttribute("error", "이미 존재하는 계정입니다!"); return "redirect:/"; @@ -47,6 +61,49 @@ public String loginForm() { } return "login"; } + + @GetMapping("/profile/edit") + public String editProfileForm(Model model) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated() + || authentication instanceof AnonymousAuthenticationToken) { + return "redirect:/login"; + } + + Object principal = authentication.getPrincipal(); + UUID currentUserId = userService.getCurrentUserId(principal); + UserProfileEditDTO userProfile = userService.getUserById(currentUserId); + + model.addAttribute("userProfile", userProfile); + + return "profile_edit"; + } + + @PostMapping("/profile/update") + public String updateProfile(@ModelAttribute UserProfileEditDTO userProfileEditDTO, + RedirectAttributes redirectAttributes) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated() + || authentication instanceof AnonymousAuthenticationToken) { + return "redirect:/login"; + } + + Object principal = authentication.getPrincipal(); + UUID currentUserId = userService.getCurrentUserId(principal); + + try { + // 사용자 정보 수정 + userService.updateUserProfile(currentUserId, userProfileEditDTO); + redirectAttributes.addFlashAttribute("successMessage", "프로필이 성공적으로 수정되었습니다."); + } catch (Exception e) { + redirectAttributes.addFlashAttribute("errorMessage", "프로필 수정 중 오류가 발생했습니다: " + e.getMessage()); + } + + // 수정 완료 후 메인 페이지로 + return "redirect:/"; + } } diff --git a/src/main/java/io/github/petty/users/dto/UserProfileEditDTO.java b/src/main/java/io/github/petty/users/dto/UserProfileEditDTO.java new file mode 100644 index 0000000..98056a1 --- /dev/null +++ b/src/main/java/io/github/petty/users/dto/UserProfileEditDTO.java @@ -0,0 +1,15 @@ +package io.github.petty.users.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class UserProfileEditDTO { + private String displayName; // 사용자 이름 + private String phone; // 전화번호 +} diff --git a/src/main/java/io/github/petty/users/entity/EmailVerification.java b/src/main/java/io/github/petty/users/entity/EmailVerification.java index 23a5479..5836aec 100644 --- a/src/main/java/io/github/petty/users/entity/EmailVerification.java +++ b/src/main/java/io/github/petty/users/entity/EmailVerification.java @@ -5,6 +5,7 @@ import lombok.Setter; import org.hibernate.annotations.CreationTimestamp; +import javax.validation.constraints.Size; import java.time.LocalDateTime; @Entity @@ -15,9 +16,11 @@ public class EmailVerification { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) + @Column(nullable = false, length = 255) private String email; + @Column(nullable = false) + @Size(min = 6, max = 6) private String code; @CreationTimestamp diff --git a/src/main/java/io/github/petty/users/entity/Users.java b/src/main/java/io/github/petty/users/entity/Users.java index 609d57d..d08081d 100644 --- a/src/main/java/io/github/petty/users/entity/Users.java +++ b/src/main/java/io/github/petty/users/entity/Users.java @@ -1,13 +1,8 @@ package io.github.petty.users.entity; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.RequiredArgsConstructor; -import lombok.Setter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.*; import org.hibernate.annotations.CreationTimestamp; import java.time.LocalDateTime; @@ -20,12 +15,32 @@ public class Users { @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; + + @Column(nullable = false, unique = true, length = 255) private String username; // email + + @Column(nullable = false, length = 60) // BCrypt로 해시된 비밀번호는 보통 60자의 길이를 가짐 + @ToString.Exclude // toString()에서 비밀번호가 출력되지 않도록 제외 + @JsonIgnore // 응답에서 비밀번호가 직렬화되지 않도록 제외 private String password; + + @Column(nullable = false, length = 50) private String displayName; + + @Column(length = 20) private String phone; + + @Column(nullable = false, length = 20) private String role; + + // Oauth2 관련 + @Column(nullable = false, length = 20) + private String provider; // oauth2 provider + + @Column(length = 100) + private String providerId; // oauth2 provider id + @CreationTimestamp private LocalDateTime createdAt; } diff --git a/src/main/java/io/github/petty/users/jwt/JWTFilter.java b/src/main/java/io/github/petty/users/jwt/JWTFilter.java index b2a7b8a..ac4a2ae 100644 --- a/src/main/java/io/github/petty/users/jwt/JWTFilter.java +++ b/src/main/java/io/github/petty/users/jwt/JWTFilter.java @@ -4,8 +4,11 @@ import io.github.petty.users.entity.Users; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -16,6 +19,8 @@ public class JWTFilter extends OncePerRequestFilter { private final JWTUtil jwtUtil; + private static final Logger logger = LoggerFactory.getLogger(JWTFilter.class); + public JWTFilter(JWTUtil jwtUtil) { this.jwtUtil = jwtUtil; @@ -23,28 +28,39 @@ public JWTFilter(JWTUtil jwtUtil) { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String token = null; + + // 1. 쿠키에서 토큰 확인 + if (request.getCookies() != null) { + for (Cookie cookie : request.getCookies()) { + if ("jwt".equals(cookie.getName())) { + token = cookie.getValue(); + logger.debug("Token found in Cookie"); + break; + } + } + } - //request에서 Authorization 헤더를 찾음 - String authorization= request.getHeader("Authorization"); - - //Authorization 헤더 검증 - if (authorization == null || !authorization.startsWith("Bearer ")) { + // 2. 쿠키에 토큰이 없으면 Authorization 헤더에서 확인 + if (token == null) { + String authorization = request.getHeader("Authorization"); + if (authorization != null && authorization.startsWith("Bearer ")) { + token = authorization.split(" ")[1]; + logger.debug("Token found in Authorization header"); + } + } - System.out.println("token null"); + // 토큰이 없는 경우 필터 체인 계속 진행 + if (token == null) { + logger.debug("Token not found in either Cookie or Authorization header"); filterChain.doFilter(request, response); - - //조건이 해당되면 메소드 종료 (필수) return; } - System.out.println("authorization now"); - //Bearer 부분 제거 후 순수 토큰만 획득 - String token = authorization.split(" ")[1]; - - //토큰 소멸 시간 검증 + // 토큰 소멸 시간 검증 if (jwtUtil.isExpired(token)) { - - System.out.println("token expired"); + logger.debug("Token expired"); + SecurityContextHolder.clearContext(); // 인증정보 삭제 // 토큰이 만료되었을 때 401 Unauthorized 응답을 반환 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 상태 코드 response.getWriter().write("{\"error\": \"Token has expired\"}"); // 에러 메시지 diff --git a/src/main/java/io/github/petty/users/jwt/JWTUtil.java b/src/main/java/io/github/petty/users/jwt/JWTUtil.java index 7b84f50..4f9f75a 100644 --- a/src/main/java/io/github/petty/users/jwt/JWTUtil.java +++ b/src/main/java/io/github/petty/users/jwt/JWTUtil.java @@ -1,5 +1,6 @@ package io.github.petty.users.jwt; +import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import org.springframework.stereotype.Component; @@ -29,7 +30,14 @@ public String getRole(String token) { } public Boolean isExpired(String token) { - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date()); + try { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date()); + } catch (ExpiredJwtException e) { + return true; // 토큰 만료됨 + } catch (Exception e) { + // 다른 예외 처리 (토큰 형식 오류 등) + return false; + } } public String createJwt(String username, String role, Long expiredMs) { @@ -38,7 +46,7 @@ public String createJwt(String username, String role, Long expiredMs) { .claim("username", username) .claim("role", role) .issuedAt(new Date(System.currentTimeMillis())) - .expiration(new Date(System.currentTimeMillis() + expiredMs * 1000)) + .expiration(new Date(System.currentTimeMillis() + expiredMs)) .signWith(secretKey) .compact(); } diff --git a/src/main/java/io/github/petty/users/jwt/LoginFilter.java b/src/main/java/io/github/petty/users/jwt/LoginFilter.java index 974f0c1..5cf0329 100644 --- a/src/main/java/io/github/petty/users/jwt/LoginFilter.java +++ b/src/main/java/io/github/petty/users/jwt/LoginFilter.java @@ -4,8 +4,10 @@ import io.github.petty.users.dto.CustomUserDetails; import io.github.petty.users.dto.LoginDTO; import jakarta.servlet.FilterChain; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -22,6 +24,10 @@ public class LoginFilter extends UsernamePasswordAuthenticationFilter { private final AuthenticationManager authenticationManager; private final JWTUtil jwtUtil; + // expirationTime 주입 + @Value("${jwt.expiration-time}") + private long expirationTime; + public LoginFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil) { this.authenticationManager = authenticationManager; @@ -63,9 +69,18 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR GrantedAuthority auth = iterator.next(); String role = auth.getAuthority(); - String token = jwtUtil.createJwt(username, role, 60*60L); - - response.addHeader("Authorization", "Bearer " + token); + String token = jwtUtil.createJwt(username, role, 3600000L); // expirationTime + + // JWT 토큰을 쿠키에 저장 + Cookie jwtCookie = new Cookie("jwt", token); + jwtCookie.setHttpOnly(true); // JavaScript 접근 방지 (XSS 방어) + jwtCookie.setPath("/"); // 쿠키의 유효 경로 + // jwtCookie.setSecure(true); // HTTPS 환경에서만 전송 (로컬호스트에서는 생략) + int maxAgeSeconds = (int) (3600000L / 1000); // 만료 시간을 초 단위로 변환 + jwtCookie.setMaxAge(maxAgeSeconds); // 쿠키의 만료 시간 설정 + response.addCookie(jwtCookie); + +// response.addHeader("Authorization", "Bearer " + token); } //로그인 실패시 diff --git a/src/main/java/io/github/petty/users/oauth2/CustomOAuth2User.java b/src/main/java/io/github/petty/users/oauth2/CustomOAuth2User.java new file mode 100644 index 0000000..2bc2fd6 --- /dev/null +++ b/src/main/java/io/github/petty/users/oauth2/CustomOAuth2User.java @@ -0,0 +1,28 @@ +package io.github.petty.users.oauth2; + +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; + +import java.util.Collection; +import java.util.Map; + +@Getter +public class CustomOAuth2User extends DefaultOAuth2User { + + private String email; + private String provider; + private String providerId; + + public CustomOAuth2User(Collection authorities, + Map attributes, + String nameAttributeKey, + String email, + String provider, + String providerId) { + super(authorities, attributes, nameAttributeKey); + this.email = email; + this.provider = provider; + this.providerId = providerId; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/petty/users/oauth2/CustomOAuth2UserService.java b/src/main/java/io/github/petty/users/oauth2/CustomOAuth2UserService.java new file mode 100644 index 0000000..479eac3 --- /dev/null +++ b/src/main/java/io/github/petty/users/oauth2/CustomOAuth2UserService.java @@ -0,0 +1,96 @@ +package io.github.petty.users.oauth2; + +import io.github.petty.users.Role; +import io.github.petty.users.entity.Users; +import io.github.petty.users.repository.UsersRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Map; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService implements OAuth2UserService { + + private final UsersRepository usersRepository; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2UserService delegate = new DefaultOAuth2UserService(); + OAuth2User oAuth2User = delegate.loadUser(userRequest); + + // OAuth2 서비스 ID (github, google, naver 등) + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + + // OAuth2 로그인 진행 시 키가 되는 필드 값(PK) + String userNameAttributeName = userRequest.getClientRegistration() + .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); + + // Github 유저 정보 추출 + Map attributes = oAuth2User.getAttributes(); + + // GitHub 이메일 및 ID 추출 + String email = extractEmail(registrationId, attributes); + String providerId = attributes.get(userNameAttributeName).toString(); + + // 기존 회원인지 확인하고 없으면 회원가입 진행 + Users user = saveOrUpdate(email, registrationId, providerId, attributes); + + return new CustomOAuth2User( + Collections.singleton(new SimpleGrantedAuthority(user.getRole())), + attributes, + userNameAttributeName, + user.getUsername(), + registrationId, + providerId + ); + } + + private String extractEmail(String registrationId, Map attributes) { + if ("github".equals(registrationId)) { + if (attributes.containsKey("email") && attributes.get("email") != null) { + return (String) attributes.get("email"); + } + // 이메일이 null인 경우 대체 로직 (GitHub ID + @github.com) + return attributes.get("login") + "@github.com"; + } + return null; // 다른 OAuth2 제공자 추가 시 확장 + } + + // 유저 정보 저장 또는 업데이트 + private Users saveOrUpdate(String email, String provider, String providerId, Map attributes) { + Users user = usersRepository.findByUsername(email); + + if (user == null) { + // 새 사용자 생성 + user = new Users(); + user.setUsername(email); + user.setProvider(provider); + user.setProviderId(providerId); + user.setPassword(UUID.randomUUID().toString()); // 임의의 패스워드 설정 + user.setRole(Role.ROLE_USER.name()); + + // GitHub 사용자명을 displayName으로 사용 + if (attributes.containsKey("name") && attributes.get("name") != null) { + user.setDisplayName((String) attributes.get("name")); + } else if (attributes.containsKey("login")) { + user.setDisplayName((String) attributes.get("login")); + } else { + user.setDisplayName(email.split("@")[0]); + } + + return usersRepository.save(user); + } + + // 기존 사용자 업데이트 로직 (필요한 경우) + return user; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/petty/users/oauth2/OAuth2SuccessHandler.java b/src/main/java/io/github/petty/users/oauth2/OAuth2SuccessHandler.java new file mode 100644 index 0000000..bfd48cf --- /dev/null +++ b/src/main/java/io/github/petty/users/oauth2/OAuth2SuccessHandler.java @@ -0,0 +1,43 @@ +package io.github.petty.users.oauth2; + +import io.github.petty.users.jwt.JWTUtil; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final JWTUtil jwtUtil; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + + CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal(); + + // JWT 발급 + String token = jwtUtil.createJwt(oAuth2User.getEmail(), oAuth2User.getAuthorities().iterator().next().getAuthority(), 3600000L); + + // JWT 토큰을 쿠키에 저장 + Cookie jwtCookie = new Cookie("jwt", token); + jwtCookie.setHttpOnly(true); // JavaScript 접근 방지 (XSS 방어) + jwtCookie.setPath("/"); // 쿠키의 유효 경로 + // jwtCookie.setSecure(true); // HTTPS 환경에서만 전송 (로컬호스트에서는 생략) + int maxAgeSeconds = (int) (3600000L / 1000); // 만료 시간을 초 단위로 변환 + jwtCookie.setMaxAge(maxAgeSeconds); // 쿠키의 만료 시간 설정 + response.addCookie(jwtCookie); + + String targetUrl = "/"; + + getRedirectStrategy().sendRedirect(request, response, targetUrl); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/petty/users/service/EmailService.java b/src/main/java/io/github/petty/users/service/EmailService.java index 435894d..f3a7a3a 100644 --- a/src/main/java/io/github/petty/users/service/EmailService.java +++ b/src/main/java/io/github/petty/users/service/EmailService.java @@ -43,7 +43,7 @@ public boolean sendVerificationCode(String email) { // 4자리 랜덤 코드 생성 private String generateCode() { - return String.format("%04d", random.nextInt(10000)); // 0000 ~ 9999 + return String.format("%06d", random.nextInt(1000000)); // 000000 ~ 999999 } private void sendEmail(String toEmail, String code) throws MessagingException { @@ -53,7 +53,29 @@ private void sendEmail(String toEmail, String code) throws MessagingException { helper.setFrom("krpetty54@gmail.com"); // 보내는 사람 이메일 (본인의 실제 이메일 주소로 변경) helper.setTo(toEmail); helper.setSubject("[Petty] 이메일 인증 코드입니다."); - helper.setText(String.format("안녕하세요.\n\n요청하신 이메일 인증 코드는 다음과 같습니다.\n\n%s\n\n감사합니다.", code)); + + // HTML 형식의 이메일 본문 + String htmlContent = "
" + + "" + + "

Petty

" + + "
" + + "
" + + "

인증 번호

" + + "
" + + "
" + + "

" + code + "

" + + "
" + + "

" + + "Petty의 더 많은 서비스를 이용하려면 이메일 인증이 필요해요.
" + + "인증번호를 입력하고 인증을 완료해 주세요!
" + + "(스팸함에 있을 수도 있으니 한 번 확인 부탁드려요!)" + + "

" + + "
" + + "이 메일은 5분간 유효합니다." + + "
" + + "
"; + + helper.setText(htmlContent, true); javaMailSender.send(mimeMessage); log.info("이메일 ({})로 인증 코드 ({})를 성공적으로 전송했습니다.", toEmail, code); diff --git a/src/main/java/io/github/petty/users/service/JoinService.java b/src/main/java/io/github/petty/users/service/JoinService.java index 0c70b07..95dde10 100644 --- a/src/main/java/io/github/petty/users/service/JoinService.java +++ b/src/main/java/io/github/petty/users/service/JoinService.java @@ -33,6 +33,7 @@ public boolean joinProcess(JoinDTO joinDTO) { users.setDisplayName(displayName); users.setPhone(phone); users.setRole(Role.ROLE_USER.name()); + users.setProvider("local"); userRepository.save(users); return true; diff --git a/src/main/java/io/github/petty/users/service/UserService.java b/src/main/java/io/github/petty/users/service/UserService.java new file mode 100644 index 0000000..96b39d9 --- /dev/null +++ b/src/main/java/io/github/petty/users/service/UserService.java @@ -0,0 +1,74 @@ +package io.github.petty.users.service; + +import io.github.petty.users.dto.CustomUserDetails; +import io.github.petty.users.dto.UserProfileEditDTO; +import io.github.petty.users.entity.Users; +import io.github.petty.users.oauth2.CustomOAuth2User; +import io.github.petty.users.repository.UsersRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UsersRepository usersRepository; + + public UUID getCurrentUserId(Object principal) { + if (principal == null) { + return null; + } + + // CustomUserDetails 처리 (일반 로그인) + if (principal instanceof CustomUserDetails) { + // CustomUserDetails에서 username 얻기 + String username = ((CustomUserDetails) principal).getUsername(); + // username으로 사용자 찾기 + Users user = usersRepository.findByUsername(username); + return user != null ? user.getId() : null; + } + + // CustomOAuth2User 처리 (소셜 로그인) + if (principal instanceof CustomOAuth2User) { + // OAuth2에서 username 얻기 + String username = ((CustomOAuth2User) principal).getName(); + // username으로 사용자 찾기 + Users user = usersRepository.findByUsername(username); + return user != null ? user.getId() : null; + } + + // Users 객체 직접 처리 + if (principal instanceof Users) { + return ((Users) principal).getId(); + } + + return null; + } + + + + + + + // 사용자 정보 가져오기 + public UserProfileEditDTO getUserById(UUID userId) { + Users user = usersRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); + return new UserProfileEditDTO(user.getDisplayName(), user.getPhone()); + + } + + // 사용자 정보 수정 + public Users updateUserProfile(UUID userId, UserProfileEditDTO userProfileEditDTO) { + Users user = usersRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); + + user.setDisplayName(userProfileEditDTO.getDisplayName()); + user.setPhone(userProfileEditDTO.getPhone()); + + return usersRepository.save(user); + } +} diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 292e2cd..d168429 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -63,7 +63,8 @@

회원가입 에러 확인용

회원가입 @@ -76,21 +77,14 @@

사용자 정보

권한:

- - + \ No newline at end of file diff --git a/src/main/resources/templates/join.html b/src/main/resources/templates/join.html index 5e873bc..2c843ce 100644 --- a/src/main/resources/templates/join.html +++ b/src/main/resources/templates/join.html @@ -1,558 +1,562 @@ - - - - - - PETTY - 회원가입 - - - -
-