Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-quartz'
implementation 'org.springframework:spring-context-support'

// OAuth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.ctrls.auto_enter_view.component;

import com.ctrls.auto_enter_view.entity.CandidateEntity;
import com.ctrls.auto_enter_view.enums.UserRole;
import com.ctrls.auto_enter_view.repository.CandidateRepository;
import com.ctrls.auto_enter_view.security.JwtTokenProvider;
import com.ctrls.auto_enter_view.util.RandomGenerator;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
@RequiredArgsConstructor
@Slf4j
public class OAuth2GithubSuccessHandler implements AuthenticationSuccessHandler {

private final CandidateRepository candidateRepository;
private final JwtTokenProvider jwtTokenProvider;
private final KeyGenerator keyGenerator;
private final PasswordEncoder passwordEncoder;

@Override
@Transactional
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
String email = oAuth2User.getAttribute("email");
String name = oAuth2User.getAttribute("name");

CandidateEntity candidate = candidateRepository.findByEmail(email)
.orElseGet(() -> createNewCandidate(email, name));

String token = jwtTokenProvider.generateToken(candidate.getEmail(), candidate.getRole());

response.setHeader("Authorization", "Bearer " + token);
response.sendRedirect("/common/job-postings?page=1");
}

private CandidateEntity createNewCandidate(String email, String name) {

String randomPassword = RandomGenerator.generateTemporaryPassword();
String encodedPassword = passwordEncoder.encode(randomPassword);

CandidateEntity newCandidate = CandidateEntity.builder()
.candidateKey(keyGenerator.generateKey())
.email(email)
.name(name)
.password(encodedPassword)
.phoneNumber("temp_number")
.role(UserRole.ROLE_CANDIDATE)
.build();

return candidateRepository.save(newCandidate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ctrls.auto_enter_view.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class PasswordEncoderConfig {

@Bean
public PasswordEncoder passwordEncoder() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 은선님께서 말씀해주신 것처럼 PasswordEncoder를 Config로 따로 뺐더니 순환 참조 문제가 해결되었습니다! 감사해요 ㅎㅎ

return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.ctrls.auto_enter_view.dto.auth;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

public class GithubOAuthDto {

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class Response {
private String candidateKey;
private String email;
private String name;
}
}
4 changes: 3 additions & 1 deletion src/main/java/com/ctrls/auto_enter_view/enums/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public enum ErrorCode {
EMAIL_DUPLICATION(409, "이메일이 중복됩니다."),
COMPANY_NUMBER_DUPLICATION(409, "회사 전화번호가 중복됩니다."),
EMAIL_NOT_FOUND(404, "가입된 사용자 이메일이 없습니다."),
NAME_NOT_FOUND(404, "가입된 사용자 이름이 없습니다."),
EMAIL_SEND_FAILURE(500, "이메일 전송에 실패했습니다."),
INTERNAL_SERVER_ERROR(500, "내부 서버 오류입니다."),
INVALID_TOKEN(401, "유효하지 않은 토큰입니다."),
Expand Down Expand Up @@ -51,7 +52,8 @@ public enum ErrorCode {
UNSCHEDULE_FAILED(500, "스케줄링 취소에 실패하였습니다."),
FAILED_MAIL_SCHEDULING(500, "메일 예약 등록을 실패했습니다."),
FAILED_MAIL_UNSCHEDULING(500, "메일 예약 취소를 실패했습니다."),
INVALID_CURRENT_STEP_ID(400, "잘못된 채용 공고 단계 입니다.");
INVALID_CURRENT_STEP_ID(400, "잘못된 채용 공고 단계 입니다."),
JSON_PROCESSING_ERROR(500, "JSON 처리 중 오류가 발생했습니다.");

private final int status;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.ctrls.auto_enter_view.security;

import com.ctrls.auto_enter_view.component.OAuth2GithubSuccessHandler;
import com.ctrls.auto_enter_view.enums.UserRole;
import com.ctrls.auto_enter_view.service.GithubOAuthService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
Expand All @@ -11,8 +13,6 @@
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
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;
import org.springframework.web.cors.CorsConfiguration;
Expand All @@ -26,12 +26,9 @@
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final GithubOAuthService githubOAuthService;
private final OAuth2GithubSuccessHandler oAuth2GithubSuccessHandler;

@Bean
public PasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
Expand All @@ -58,8 +55,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/candidates/find-email").permitAll()
.requestMatchers("/common/**").permitAll()
.requestMatchers(HttpMethod.GET, "/companies/{companyKey}/information").permitAll()
.requestMatchers("/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**")
.permitAll()
.requestMatchers("/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**").permitAll()
.requestMatchers( "/oauth2/**", "/login/**").permitAll()

// 권한 필요 (candidate, company 둘 중 하나)
.requestMatchers("/common/signout", "common/{key}/password").authenticated()
Expand All @@ -76,7 +73,17 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/interview-schedule-participants/**")
.hasRole(UserRole.ROLE_COMPANY.name().substring(5))

.anyRequest().authenticated())
.anyRequest().authenticated()

)

// OAuth2 로그인 설정 추가
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userService(githubOAuthService))
.successHandler(oAuth2GithubSuccessHandler)
)


// JWT 필터 추가
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.ctrls.auto_enter_view.service;

import com.ctrls.auto_enter_view.enums.ErrorCode;
import com.ctrls.auto_enter_view.enums.UserRole;
import com.ctrls.auto_enter_view.exception.CustomException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
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.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

// GitHub OAuth 인증을 처리하는 서비스 클래스
@Service
@RequiredArgsConstructor
public class GithubOAuthService extends DefaultOAuth2UserService {

// OAuth2UserRequest를 기반으로 사용자 정보를 로드하는 메서드
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// 부모 클래스의 loadUser 메서드를 호출하여 기본 OAuth2User 객체를 얻음
OAuth2User oAuth2User = super.loadUser(userRequest);

// GitHub API에 접근하기 위한 액세스 토큰 설정
String token = userRequest.getAccessToken().getTokenValue();
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<String> entity = new HttpEntity<>(headers);

RestTemplate restTemplate = new RestTemplate();

// GitHub API를 통해 사용자의 이메일 정보 가져오기
ResponseEntity<String> emailResponse = restTemplate.exchange("https://api.github.com/user/emails", HttpMethod.GET, entity, String.class);
String email = extractEmail(emailResponse);

// GitHub API를 통해 사용자 정보 가져오기
ResponseEntity<String> userResponse = restTemplate.exchange("https://api.github.com/user", HttpMethod.GET, entity, String.class);
String name = extractName(userResponse);

// 이메일이 없으면 예외 발생
if (email == null) {
throw new CustomException(ErrorCode.EMAIL_NOT_FOUND);
}

// 이름이 없으면 예외 발생
if (name == null) {
throw new CustomException(ErrorCode.NAME_NOT_FOUND);
}

// 사용자 정보를 OAuth2User 형태로 반환하기 위해 속성 맵 생성
Map<String, Object> attributes = new HashMap<>(oAuth2User.getAttributes());
attributes.put("email", email);
attributes.put("name", name);

// DefaultOAuth2User 객체 생성 및 반환
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(UserRole.ROLE_CANDIDATE.name())),
attributes,
"id"
);
}

// GitHub API 응답에서 이메일 추출하는 메서드
private String extractEmail(ResponseEntity<String> response) {
try {
return new ObjectMapper().readTree(response.getBody()).get(0).get("email").asText();
} catch (JsonProcessingException e) {
throw new CustomException(ErrorCode.JSON_PROCESSING_ERROR);
}
}

// GitHub API 응답에서 이름 추출하는 메서드
private String extractName(ResponseEntity<String> response) {
try {
return new ObjectMapper().readTree(response.getBody()).get("name").asText();
} catch (JsonProcessingException e) {
throw new CustomException(ErrorCode.JSON_PROCESSING_ERROR);
}
}
}