diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index a7fbe1c..1da65cc 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -86,6 +86,12 @@ jobs: port: 22 key: ${{ secrets.PRIVATE_KEY }} script: | + # 기존 컨테이너 중지 및 제거 + if [ "$(sudo docker ps -q -f name=xcellent-be)" ]; then + sudo docker stop xcellent-be + sudo docker rm xcellent-be + fi + sudo docker ps sudo docker pull ${{ secrets.DOCKER_USERNAME }}/xcellent-be sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/xcellent-be diff --git a/.gitignore b/.gitignore index 772e3ec..97d0405 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ application.yaml application-dev.yaml application-local.yaml application-jwt.yaml +application-oauth.yaml +application-mail.yaml ### macOS ### .DS_Store diff --git a/Dockerfile b/Dockerfile index 4b5b42a..7a0929f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,4 +9,4 @@ ARG JAR_FILE=build/libs/*.jar COPY ${JAR_FILE} app.jar # 운영 및 개발에서 사용되는 환경 설정을 분리 -ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=dev,jwt", "/app.jar"] +ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=dev,jwt,oauth,mail", "/app.jar"] diff --git a/build.gradle b/build.gradle index e50197b..e387028 100644 --- a/build.gradle +++ b/build.gradle @@ -54,6 +54,13 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-jackson:0.12.6' implementation 'com.auth0:java-jwt:4.4.0' + // OAuth2 + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + // SMTP, Redis + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' diff --git a/src/main/java/com/leets/xcellentbe/domain/user/domain/User.java b/src/main/java/com/leets/xcellentbe/domain/user/domain/User.java index 22d6421..89c7887 100644 --- a/src/main/java/com/leets/xcellentbe/domain/user/domain/User.java +++ b/src/main/java/com/leets/xcellentbe/domain/user/domain/User.java @@ -7,7 +7,7 @@ import com.leets.xcellentbe.domain.shared.BaseTimeEntity; import com.leets.xcellentbe.domain.shared.UserStatus; - +import com.leets.xcellentbe.domain.user.Role; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -75,6 +75,12 @@ public class User extends BaseTimeEntity { @Enumerated(EnumType.STRING) private UserStatus userStatus; + @NotNull + @Column + @Enumerated(EnumType.STRING) + private Role userRole; + + @NotNull @Column(length=4) private int userBirthYear; @@ -88,7 +94,7 @@ public class User extends BaseTimeEntity { private int userBirthDay; @Builder - private User(String customId, String email, String userName, String password, String phoneNumber, String description, int userBirthDay, int userBirthMonth, int userBirthYear) { + private User(String customId, String email, String userName, String password, String phoneNumber, String description, int userBirthDay, int userBirthMonth, int userBirthYear, String socialEmail, Role userRole) { this.customId = customId; this.email = email; this.userName = userName; @@ -96,6 +102,7 @@ private User(String customId, String email, String userName, String password, St this.phoneNumber= phoneNumber; this.description = description; this.userStatus = UserStatus.ACTIVE; + this.userRole = userRole; this.userBirthMonth = userBirthMonth; this.userBirthYear = userBirthYear; this.userBirthDay = userBirthDay; @@ -115,6 +122,15 @@ public static User create(String customId, String email, String userName, String .userBirthDay(userBirthDay) .userBirthYear(userBirthYear) .userBirthMonth(userBirthMonth) + .userRole(Role.USER) + .build(); + } + + public static User socialCreate(String email, String socialEmail) { + return User.builder() + .email(email) + .userRole(Role.GUEST) + .socialEmail(socialEmail) .build(); } diff --git a/src/main/java/com/leets/xcellentbe/domain/user/domain/repository/UserRepository.java b/src/main/java/com/leets/xcellentbe/domain/user/domain/repository/UserRepository.java index 3988549..ed4938f 100644 --- a/src/main/java/com/leets/xcellentbe/domain/user/domain/repository/UserRepository.java +++ b/src/main/java/com/leets/xcellentbe/domain/user/domain/repository/UserRepository.java @@ -10,4 +10,5 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); Optional findByCustomId(String customId);; Optional findByRefreshToken(String refreshToken); + } diff --git a/src/main/java/com/leets/xcellentbe/global/auth/email/AuthCodeAlreadySentException.java b/src/main/java/com/leets/xcellentbe/global/auth/email/AuthCodeAlreadySentException.java new file mode 100644 index 0000000..c3cbc4d --- /dev/null +++ b/src/main/java/com/leets/xcellentbe/global/auth/email/AuthCodeAlreadySentException.java @@ -0,0 +1,10 @@ +package com.leets.xcellentbe.global.auth.email; + +import com.leets.xcellentbe.global.error.ErrorCode; +import com.leets.xcellentbe.global.error.exception.CommonException; + +public class AuthCodeAlreadySentException extends CommonException { + public AuthCodeAlreadySentException() { + super(ErrorCode.AUTH_CODE_ALREADY_SENT); + } +} diff --git a/src/main/java/com/leets/xcellentbe/global/auth/email/EmailCannotBeSent.java b/src/main/java/com/leets/xcellentbe/global/auth/email/EmailCannotBeSent.java new file mode 100644 index 0000000..5cd6ce9 --- /dev/null +++ b/src/main/java/com/leets/xcellentbe/global/auth/email/EmailCannotBeSent.java @@ -0,0 +1,12 @@ +package com.leets.xcellentbe.global.auth.email; + +import com.leets.xcellentbe.global.error.ErrorCode; +import com.leets.xcellentbe.global.error.exception.CommonException; + +public class EmailCannotBeSent extends CommonException +{ + public EmailCannotBeSent() + { + super(ErrorCode.EMAIL_CANNOT_BE_SENT); + } +} diff --git a/src/main/java/com/leets/xcellentbe/global/auth/email/EmailCheckDto.java b/src/main/java/com/leets/xcellentbe/global/auth/email/EmailCheckDto.java new file mode 100644 index 0000000..ecd5986 --- /dev/null +++ b/src/main/java/com/leets/xcellentbe/global/auth/email/EmailCheckDto.java @@ -0,0 +1,13 @@ +package com.leets.xcellentbe.global.auth.email; + +import jakarta.validation.constraints.Email; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class EmailCheckDto { + @Email + private String email; + private String authNum; +} diff --git a/src/main/java/com/leets/xcellentbe/global/auth/email/EmailRequestDto.java b/src/main/java/com/leets/xcellentbe/global/auth/email/EmailRequestDto.java new file mode 100644 index 0000000..fcbb8ce --- /dev/null +++ b/src/main/java/com/leets/xcellentbe/global/auth/email/EmailRequestDto.java @@ -0,0 +1,8 @@ +package com.leets.xcellentbe.global.auth.email; + +import lombok.Getter; + +@Getter +public class EmailRequestDto { + private String email; +} diff --git a/src/main/java/com/leets/xcellentbe/global/auth/email/EmailService.java b/src/main/java/com/leets/xcellentbe/global/auth/email/EmailService.java new file mode 100644 index 0000000..72aa5ec --- /dev/null +++ b/src/main/java/com/leets/xcellentbe/global/auth/email/EmailService.java @@ -0,0 +1,79 @@ +package com.leets.xcellentbe.global.auth.email; + +import org.springframework.stereotype.Service; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import java.util.Random; + +import com.leets.xcellentbe.global.error.exception.custom.InvalidInputValueException; + +@Transactional +@RequiredArgsConstructor +@Service +@Slf4j +public class EmailService { + private final RedisService redisService; + private final JavaMailSender mailSender; + private int authNumber; + + //임의의 6자리 양수를 반환합니다. + public void makeRandomNumber() { + Random r = new Random(); + this.authNumber = 100000 + r.nextInt(900000); + } + + + //mail을 어디서 보내는지, 어디로 보내는지 , 인증 번호를 html 형식으로 어떻게 보내는지 작성합니다. + public String joinEmail(String email) throws MessagingException { + makeRandomNumber(); + String toMail = email; + String title = "회원 가입 인증 이메일 입니다."; // 이메일 제목 + String content = + "인증 번호는 " + authNumber + "입니다." + + "
" + + "인증번호를 제대로 입력해주세요"; //이메일 내용 삽입 + mailSend(toMail, title, content); + return Integer.toString(authNumber); + } + + //이메일을 전송합니다. + public void mailSend(String toMail, String title, String content) throws MessagingException { + + if(redisService.getData(toMail)!=null){ + throw new AuthCodeAlreadySentException(); + } + + try { + MimeMessage message = mailSender.createMimeMessage();//JavaMailSender 객체를 사용하여 MimeMessage 객체를 생성 + MimeMessageHelper helper = new MimeMessageHelper(message, true, + "utf-8");// true를 전달하여 multipart 형식의 메시지를 지원하고, "utf-8"을 전달하여 문자 인코딩을 설정 + helper.setTo("h");//이메일의 수신자 주소 설정 + helper.setSubject(title);//이메일의 제목을 설정 + helper.setText(content, true);//이메일의 내용 설정 두 번째 매개 변수에 true를 설정하여 html 설정으로한다. + mailSender.send(message); + } + catch(Exception e) { + throw new EmailCannotBeSent(); + } + redisService.setDataExpire(toMail,Integer.toString(authNumber)); + } + + public String checkAuthNum(String email, String authNum) { + String storedAuthNum = redisService.getData(email); + + if (storedAuthNum == null) { + return "인증번호가 만료되었습니다."; + } + else if(!storedAuthNum.equals(authNum)){ + throw new InvalidInputValueException(); + } + else { + return "인증에 성공하였습니다."; + } + } +} diff --git a/src/main/java/com/leets/xcellentbe/global/auth/email/RedisService.java b/src/main/java/com/leets/xcellentbe/global/auth/email/RedisService.java new file mode 100644 index 0000000..8be9cd5 --- /dev/null +++ b/src/main/java/com/leets/xcellentbe/global/auth/email/RedisService.java @@ -0,0 +1,33 @@ +package com.leets.xcellentbe.global.auth.email; + +import java.time.Duration; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.data.redis.core.StringRedisTemplate; +@Service +@RequiredArgsConstructor +public class RedisService { + private static final long AUTH_CODE_EXPIRE_SECONDS = 300; + + private final StringRedisTemplate redisTemplate;//Redis에 접근하기 위한 Spring의 Redis 템플릿 클래스 + + public String getData(String key){//지정된 키(key)에 해당하는 데이터를 Redis에서 가져오는 메서드 + ValueOperations valueOperations=redisTemplate.opsForValue(); + return valueOperations.get(key); + } + public void setData(String key,String value){//지정된 키(key)에 값을 저장하는 메서드 + ValueOperations valueOperations=redisTemplate.opsForValue(); + valueOperations.set(key,value); + } + public void setDataExpire(String key,String value){//지정된 키(key)에 값을 저장하고, 지정된 시간(duration) 후에 데이터가 만료되도록 설정하는 메서드 + ValueOperations valueOperations=redisTemplate.opsForValue(); + Duration expireDuration=Duration.ofSeconds(AUTH_CODE_EXPIRE_SECONDS); + valueOperations.set(key,value,expireDuration); + } + public void deleteData(String key){//지정된 키(key)에 해당하는 데이터를 Redis에서 삭제하는 메서드 + redisTemplate.delete(key); + } +} diff --git a/src/main/java/com/leets/xcellentbe/global/auth/jwt/JwtAuthenticationFilter.java b/src/main/java/com/leets/xcellentbe/global/auth/jwt/JwtAuthenticationFilter.java index dc702ef..b474a43 100644 --- a/src/main/java/com/leets/xcellentbe/global/auth/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/leets/xcellentbe/global/auth/jwt/JwtAuthenticationFilter.java @@ -140,7 +140,7 @@ public void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpSe public void saveAuthentication(User myUser) { String password = myUser.getPassword(); // if (password == null) { // OAuth2 사용시 소셜로그인 비밀번호 생성 코드 - // password = PasswordUtil.generateRandomPassword(); + // password = PasswordUtils.generateRandomPassword(); // } UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder() .username(myUser.getEmail()) diff --git a/src/main/java/com/leets/xcellentbe/global/auth/login/AuthController.java b/src/main/java/com/leets/xcellentbe/global/auth/login/AuthController.java index 756b8d4..e569b16 100644 --- a/src/main/java/com/leets/xcellentbe/global/auth/login/AuthController.java +++ b/src/main/java/com/leets/xcellentbe/global/auth/login/AuthController.java @@ -10,9 +10,14 @@ import com.leets.xcellentbe.domain.user.dto.UserLoginRequestDto; import com.leets.xcellentbe.domain.user.dto.UserSignUpRequestDto; import com.leets.xcellentbe.domain.user.service.UserService; +import com.leets.xcellentbe.global.auth.email.EmailCheckDto; +import com.leets.xcellentbe.global.auth.email.EmailRequestDto; +import com.leets.xcellentbe.global.auth.email.EmailService; +import com.leets.xcellentbe.global.error.exception.custom.InvalidInputValueException; import com.leets.xcellentbe.global.response.GlobalResponseDto; import io.swagger.v3.oas.annotations.Operation; +import jakarta.mail.MessagingException; import lombok.RequiredArgsConstructor; @RestController @@ -20,6 +25,7 @@ @RequiredArgsConstructor public class AuthController { private final UserService userService; + private final EmailService emailService; @PostMapping("/register") @Operation(summary = "회원가입", description = "회원가입을 합니다.") @@ -34,4 +40,17 @@ public String login(@RequestBody UserLoginRequestDto userLoginRequestDto) { // 로그인 로직 처리 return "로그인 성공"; } + + @PostMapping("/email/send") + public ResponseEntity> mailSend(@RequestBody EmailRequestDto emailRequestDto) throws + MessagingException { + emailService.joinEmail(emailRequestDto.getEmail()); + return ResponseEntity.status(HttpStatus.OK).body(GlobalResponseDto.success()); + } + + @PostMapping("/email/check") + public ResponseEntity> AuthCheck(@RequestBody EmailCheckDto emailCheckDto) { + return ResponseEntity.status(HttpStatus.OK).body(GlobalResponseDto.success(emailService.checkAuthNum(emailCheckDto.getEmail(), emailCheckDto.getAuthNum()))); + } } + diff --git a/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/CustomOAuthUser.java b/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/CustomOAuthUser.java new file mode 100644 index 0000000..5251e18 --- /dev/null +++ b/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/CustomOAuthUser.java @@ -0,0 +1,28 @@ +package com.leets.xcellentbe.global.auth.login.oauth; + +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.core.GrantedAuthority; +import java.util.Collection; +import java.util.Map; + +import com.leets.xcellentbe.domain.user.Role; + +import lombok.Getter; + +/** + * DefaultOAuth2User를 상속하고, email과 role 필드를 추가로 가진다. + */ +@Getter +public class CustomOAuthUser extends DefaultOAuth2User { + + private String email; + private Role role; + + public CustomOAuthUser(Collection authorities, + Map attributes, String nameAttributeKey, + String email, Role role) { + super(authorities, attributes, nameAttributeKey); + this.email = email; + this.role = role; + } +} diff --git a/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/CustomOAuthUserService.java b/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/CustomOAuthUserService.java new file mode 100644 index 0000000..7c74a28 --- /dev/null +++ b/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/CustomOAuthUserService.java @@ -0,0 +1,59 @@ +package com.leets.xcellentbe.global.auth.login.oauth; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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 com.leets.xcellentbe.domain.user.domain.User; +import com.leets.xcellentbe.domain.user.domain.repository.UserRepository; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CustomOAuthUserService implements OAuth2UserService { + + private final UserRepository userRepository; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + log.info("CustomOAuth2UserService.loadUser() 실행 - OAuth2 로그인 요청 진입"); + + OAuth2UserService delegate = new DefaultOAuth2UserService(); + OAuth2User oAuth2User = delegate.loadUser(userRequest); + + String userNameAttributeName = userRequest.getClientRegistration() + .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); + Map attributes = oAuth2User.getAttributes(); + + OAuthAttributes extractAttributes = OAuthAttributes.of(userNameAttributeName, attributes); + + User createdUser = getUser(extractAttributes); + + return new CustomOAuthUser( + Collections.singleton(new SimpleGrantedAuthority(createdUser.getUserRole().getKey())), + attributes, + extractAttributes.getNameAttributeKey(), + createdUser.getEmail(), + createdUser.getUserRole() + ); + } + + private User getUser(OAuthAttributes attributes) { + return userRepository.findByEmail(attributes.getGoogleOAuthUserInfo().getEmail()) + .orElseGet(() -> saveUser(attributes)); + } + + private User saveUser(OAuthAttributes attributes) { + User createdUser = attributes.toEntity(attributes.getGoogleOAuthUserInfo()); + return userRepository.save(createdUser); + } +} diff --git a/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/GoogleOAuthUserInfo.java b/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/GoogleOAuthUserInfo.java new file mode 100644 index 0000000..62735b8 --- /dev/null +++ b/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/GoogleOAuthUserInfo.java @@ -0,0 +1,16 @@ +package com.leets.xcellentbe.global.auth.login.oauth; + +import java.util.Map; + +public class GoogleOAuthUserInfo { + + private final Map attributes; + + public GoogleOAuthUserInfo(Map attributes) { + this.attributes = attributes; + } + public String getEmail() { + return (String) attributes.get("email"); + } + +} diff --git a/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/OAuthAttributes.java b/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/OAuthAttributes.java new file mode 100644 index 0000000..f96c2f9 --- /dev/null +++ b/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/OAuthAttributes.java @@ -0,0 +1,42 @@ +package com.leets.xcellentbe.global.auth.login.oauth; + +import lombok.Builder; +import lombok.Getter; + +import java.util.Map; +import java.util.UUID; + +import com.leets.xcellentbe.domain.user.Role; +import com.leets.xcellentbe.domain.user.domain.User; + +@Getter +public class OAuthAttributes { + + private final String nameAttributeKey; + private final GoogleOAuthUserInfo googleOAuthUserInfo; + private final PasswordUtil passwordUtil; + + @Builder + private OAuthAttributes(String nameAttributeKey, GoogleOAuthUserInfo googleOAuthUserInfo, PasswordUtil passwordUtil) { + this.nameAttributeKey = nameAttributeKey; + this.googleOAuthUserInfo = googleOAuthUserInfo; + this.passwordUtil = passwordUtil; + } + + public static OAuthAttributes of(String userNameAttributeName, Map attributes) { + return OAuthAttributes.builder() + .nameAttributeKey(userNameAttributeName) + .googleOAuthUserInfo(new GoogleOAuthUserInfo(attributes)) + .build(); + } + + public User toEntity(GoogleOAuthUserInfo googleOAuthUserInfo) { + return User.builder() + .email(googleOAuthUserInfo.getEmail()) + .password(passwordUtil.generateHashedRandomPassword()) + .customId("customId") + .userName("userName") + .userRole(Role.GUEST) + .build(); + } +} diff --git a/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/OAuthLoginSuccessHandler.java b/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/OAuthLoginSuccessHandler.java new file mode 100644 index 0000000..39769a9 --- /dev/null +++ b/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/OAuthLoginSuccessHandler.java @@ -0,0 +1,79 @@ +package com.leets.xcellentbe.global.auth.login.oauth; + +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.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.leets.xcellentbe.domain.user.Role; +import com.leets.xcellentbe.domain.user.domain.User; +import com.leets.xcellentbe.domain.user.domain.repository.UserRepository; +import com.leets.xcellentbe.global.auth.jwt.JwtService; +import com.leets.xcellentbe.global.response.GlobalResponseDto; + +@Slf4j +@Component +@RequiredArgsConstructor +public class OAuthLoginSuccessHandler implements AuthenticationSuccessHandler { + + private final JwtService jwtService; + private final UserRepository userRepository; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + log.info("OAuth2 Login 성공!"); + + Map tokenMap = new HashMap<>(); + + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + CustomOAuthUser oAuth2User = (CustomOAuthUser) authentication.getPrincipal(); + Optional user = userRepository.findByEmail(oAuth2User.getEmail()); + // User의 Role이 GUEST일 경우 처음 요청한 회원이므로 회원가입 페이지로 리다이렉트 + if (oAuth2User.getRole() == Role.GUEST) { + String accessToken = jwtService.createAccessToken(oAuth2User.getEmail()); + + tokenMap.put("accessToken", accessToken); + GlobalResponseDto> responseDto = GlobalResponseDto.success(tokenMap); + + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.addHeader(jwtService.getAccessHeader(), "Bearer " + accessToken); + + String json = mapper.writeValueAsString(responseDto); + + response.getWriter().write(json); + // response.sendRedirect("/oauth2/sign-up.html"); + + jwtService.sendAccessAndRefreshToken(response, accessToken, null); + } else { + loginSuccess(response, oAuth2User); // 로그인에 성공한 경우 access, refresh 토큰 생성 + } + } + + private void loginSuccess(HttpServletResponse response, CustomOAuthUser oAuth2User) throws IOException { + String accessToken = jwtService.createAccessToken(oAuth2User.getEmail()); + String refreshToken = jwtService.createRefreshToken(); + response.addHeader(jwtService.getAccessHeader(), "Bearer " + accessToken); + response.addHeader(jwtService.getRefreshHeader(), "Bearer " + refreshToken); + + jwtService.sendAccessAndRefreshToken(response, accessToken, refreshToken); + jwtService.updateRefreshToken(oAuth2User.getEmail(), refreshToken); + } +} diff --git a/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/PasswordUtil.java b/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/PasswordUtil.java new file mode 100644 index 0000000..7c21841 --- /dev/null +++ b/src/main/java/com/leets/xcellentbe/global/auth/login/oauth/PasswordUtil.java @@ -0,0 +1,49 @@ +package com.leets.xcellentbe.global.auth.login.oauth; + +import java.security.SecureRandom; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +public class PasswordUtil { + + private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+"; + private static final int PASSWORD_LENGTH = 12; // 원하는 비밀번호 길이 + + private final PasswordEncoder passwordEncoder; + + public PasswordUtil() { + this.passwordEncoder = new BCryptPasswordEncoder(); + } + + // 랜덤 비밀번호 생성 및 해싱하여 반환 + public String generateHashedRandomPassword() { + String randomPassword = generateRandomPassword(); + return hashPassword(randomPassword); + } + + // 랜덤 비밀번호 생성 + private String generateRandomPassword() { + SecureRandom random = new SecureRandom(); + StringBuilder password = new StringBuilder(PASSWORD_LENGTH); + + for (int i = 0; i < PASSWORD_LENGTH; i++) { + int index = random.nextInt(CHARACTERS.length()); + password.append(CHARACTERS.charAt(index)); + } + + return password.toString(); + } + + // 비밀번호 해싱 + private String hashPassword(String password) { + return passwordEncoder.encode(password); + } + + public static void main(String[] args) { + PasswordUtil passwordUtil = new PasswordUtil(); + + // 랜덤 비밀번호 생성 및 해싱 + String hashedPassword = passwordUtil.generateHashedRandomPassword(); + System.out.println("해싱된 랜덤 비밀번호: " + hashedPassword); + } +} diff --git a/src/main/java/com/leets/xcellentbe/global/config/EmailConfig.java b/src/main/java/com/leets/xcellentbe/global/config/EmailConfig.java new file mode 100644 index 0000000..67e8dce --- /dev/null +++ b/src/main/java/com/leets/xcellentbe/global/config/EmailConfig.java @@ -0,0 +1,42 @@ +package com.leets.xcellentbe.global.config; + +import java.util.Properties; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +@Configuration +public class EmailConfig { + + @Value("${mail.username}") + private String username; + + @Value("${mail.password}") + private String password; + + @Bean + public JavaMailSender mailSender() {//JAVA MAILSENDER 인터페이스를 구현한 객체를 빈으로 등록하기 위함. + + JavaMailSenderImpl mailSender = new JavaMailSenderImpl();//JavaMailSender 의 구현체를 생성하고 + mailSender.setHost("smtp.gmail.com");// 속성을 넣기 시작합니다. 이메일 전송에 사용할 SMTP 서버 호스트를 설정 + mailSender.setPort(587);// 587로 포트를 지정 + mailSender.setUsername(username);//구글계정을 넣습니다. + mailSender.setPassword(password);//구글 앱 비밀번호를 넣습니다. + + Properties javaMailProperties = new Properties();//JavaMail의 속성을 설정하기 위해 Properties 객체를 생성 + javaMailProperties.put("mail.transport.protocol", "smtp");//프로토콜로 smtp 사용 + javaMailProperties.put("mail.smtp.auth", "true");//smtp 서버에 인증이 필요 + javaMailProperties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");//SSL 소켓 팩토리 클래스 사용 + javaMailProperties.put("mail.smtp.starttls.enable", "true");//STARTTLS(TLS를 시작하는 명령)를 사용하여 암호화된 통신을 활성화 + javaMailProperties.put("mail.debug", "true");//디버깅 정보 출력 + javaMailProperties.put("mail.smtp.ssl.trust", "smtp.gmail.com");//smtp 서버의 ssl 인증서를 신뢰 + javaMailProperties.put("mail.smtp.ssl.protocols", "TLSv1.2");//사용할 ssl 프로토콜 버젼 + + mailSender.setJavaMailProperties(javaMailProperties);//mailSender에 우리가 만든 properties 넣고 + + return mailSender;//빈으로 등록한다. + } +} diff --git a/src/main/java/com/leets/xcellentbe/global/config/SecurityConfig.java b/src/main/java/com/leets/xcellentbe/global/config/SecurityConfig.java index d48a522..0074c62 100644 --- a/src/main/java/com/leets/xcellentbe/global/config/SecurityConfig.java +++ b/src/main/java/com/leets/xcellentbe/global/config/SecurityConfig.java @@ -33,6 +33,7 @@ import com.leets.xcellentbe.global.auth.login.handler.LoginFailureHandler; import com.leets.xcellentbe.global.auth.login.handler.LoginSuccessHandler; import com.leets.xcellentbe.global.auth.jwt.JwtAuthenticationFilter; +import com.leets.xcellentbe.global.auth.login.oauth.OAuthLoginSuccessHandler; @Configuration @EnableWebSecurity @@ -43,6 +44,7 @@ public class SecurityConfig { private final JwtService jwtService; private final UserRepository userRepository; private final ObjectMapper objectMapper; + private final OAuthLoginSuccessHandler oAuthLoginSuccessHandler; @Bean public PasswordEncoder passwordEncoder() { @@ -51,7 +53,7 @@ public PasswordEncoder passwordEncoder() { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - return http + http .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .cors(withDefaults()) @@ -71,13 +73,15 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { authorize -> authorize .requestMatchers("/v3/api-docs", "/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**", - "/swagger/**", "/api/auth/register", "/api/auth/login").permitAll() + "/swagger/**", "/api/auth/register", "/api/auth/login", "/index.html", "/api/auth/**").permitAll() .anyRequest().authenticated() ) + .oauth2Login(oauth2 -> oauth2.successHandler(oAuthLoginSuccessHandler)); + // .failureHandler(oAuth2LoginFailureHandler) + http.addFilterAfter(customJsonAuthenticationFilter(), LogoutFilter.class); + http.addFilterBefore(jwtAuthenticationFilter(), CustomJsonAuthenticationFilter.class); - .addFilterAfter(customJsonAuthenticationFilter(), LogoutFilter.class) - .addFilterBefore(jwtAuthenticationFilter(), CustomJsonAuthenticationFilter.class) - .build(); + return http.build(); } @Bean diff --git a/src/main/java/com/leets/xcellentbe/global/error/ErrorCode.java b/src/main/java/com/leets/xcellentbe/global/error/ErrorCode.java index 4cb13a1..b0d965f 100644 --- a/src/main/java/com/leets/xcellentbe/global/error/ErrorCode.java +++ b/src/main/java/com/leets/xcellentbe/global/error/ErrorCode.java @@ -17,7 +17,9 @@ public enum ErrorCode { ARTICLE_NOT_FOUND(404, "ARTICLE_NOT_FOUND", "게시물을 찾을 수 없습니다."), CHAT_ROOM_NOT_FOUND(404, "CHAT_ROOM_NOT_FOUND", "채팅방을 찾을 수 없습니다."), REJECT_DUPLICATION(409,"REJECT_DUPLICATION","중복된 값입니다."), - INTERNAL_SERVER_ERROR(500, "INTERNAL_SERVER_ERROR", "서버 오류가 발생했습니다."); + AUTH_CODE_ALREADY_SENT(429, "AUTH_CODE_ALREADY_SENT", "이미 인증번호를 전송했습니다."), + INTERNAL_SERVER_ERROR(500, "INTERNAL_SERVER_ERROR", "서버 오류가 발생했습니다."), + EMAIL_CANNOT_BE_SENT(500, "EMAIL_CANNOT_BE_SENT", "이메일 전송에 실패했습니다."); private final int status; diff --git a/src/main/java/com/leets/xcellentbe/global/error/exception/GlobalExceptionHandler.java b/src/main/java/com/leets/xcellentbe/global/error/exception/GlobalExceptionHandler.java index 9c7ce36..bc2e940 100644 --- a/src/main/java/com/leets/xcellentbe/global/error/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/leets/xcellentbe/global/error/exception/GlobalExceptionHandler.java @@ -6,25 +6,26 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import com.leets.xcellentbe.global.error.*; +import com.leets.xcellentbe.global.response.GlobalResponseDto; @Slf4j @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(CommonException.class) // Custom Exception을 포괄적으로 처리 - public ResponseEntity handleCommonException(CommonException ex) { + public ResponseEntity> handleCommonException(CommonException ex) { ErrorCode errorCode = ex.getErrorCode(); // 전달된 예외에서 에러 코드 가져오기 ErrorResponse errorResponse = new ErrorResponse(errorCode); showErrorLog(errorCode); - return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(errorCode.getStatus())); + return ResponseEntity.status(HttpStatus.valueOf(errorCode.getStatus())).body(GlobalResponseDto.fail(errorCode, errorResponse.getMessage())); } @ExceptionHandler(Exception.class) - public ResponseEntity handleGenericException(Exception ex) { + public ResponseEntity handleGenericException(Exception ex) { ErrorCode errorCode = ErrorCode.INTERNAL_SERVER_ERROR; ErrorResponse errorResponse = new ErrorResponse(errorCode); showErrorLog(errorCode); - return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + return ResponseEntity.status(HttpStatus.valueOf(errorCode.getStatus())).body(GlobalResponseDto.fail(errorCode, errorResponse.getMessage())); } diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..9d9d664 --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1 @@ +Google Login
diff --git a/src/main/resources/static/oauth2/sign-up.html b/src/main/resources/static/oauth2/sign-up.html new file mode 100644 index 0000000..aa9add1 --- /dev/null +++ b/src/main/resources/static/oauth2/sign-up.html @@ -0,0 +1 @@ +
테스트 페이지