
지난 포스트 GitHub OAuth2 + JWT 활용 인증 구현하기에서 아쉬움으로 남았던 직접 리다이렉트 방식의 수정과 세션이 stateless한 상태에서의 인증을 구현하기 위해 리펙토링을 진행하기로 했습니다

기존 인증 프로세스에서 변경된 방식은
GithubOAuth2Manager를 통해 GitHub 서버와 OAuth2 인증 과정을 진행합니다.AuthService가 GitHub 유저 정보를 기반으로 기반으로 MoGakGo 유저 관리, JWT 토큰 생성 및 저장을 진행합니다.자세한 코드는 레포지토리에서 확인할 수 있습니다.
GithubOAuth2Manager는 Spring OAuth2 Client를 대신해 Github 로그인을 담당하는 컴포넌트입니다.WebClient를 활용해 구현이 이루어졌으며 로그인 과정은 동기로 이루어집니다.getAccessToken() 메서드에서 사용하며, Github Resource Server에 접근 가능한 Access Token을 발급 받습니다.getGithubUserInfo() 메서드는 Github Resource Server에 접근, 사용자 정보를 받아옵니다.@Component
public class GithubOAuth2Manager {
private final String clientId;
private final String clientSecret;
public GithubOAuth2Manager(@Value("${github.oauth2.registration.client-id}") String clientId,
@Value("${github.oauth2.registration.client-secret}") String clientSecret) {
this.clientId = clientId;
this.clientSecret = clientSecret;
}
public String getAccessToken(String code) {
WebClient webClient = WebClient.builder()
.baseUrl("https://github.com/login/oauth/access_token")
.defaultHeader(ACCEPT, APPLICATION_JSON_VALUE)
.build();
var result = webClient.post().uri(uriBuilder ->
uriBuilder
.queryParam("client_id", clientId)
.queryParam("client_secret", clientSecret)
.queryParam("code", code)
.build()
).retrieve().bodyToMono(new ParameterizedTypeReference<Map<String, String>>() {
}).block();
return Objects.requireNonNull(result).get("access_token");
}
public Map<String, Object> getGithubUserInfo(String accessToken) {
WebClient webClient = WebClient.builder()
.baseUrl("https://api.github.com/user")
.build();
return webClient.get().header(AUTHORIZATION, "Bearer " + accessToken).retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
}).doOnError(error -> {
throw new AuthException(ErrorCode401.AUTH_MISSING_CREDENTIALS);
}).block();
}
}
AuthService는 OAuth2AuthenticationSuccessHandler를 대신해 API 호출로 전달된 로그인 요청을 처리하는 서비스 레이어입니다.@Slf4j
@RequiredArgsConstructor
@Service
public class AuthService {
private final JwtRedisDao jwtRedisDao;
private final JwtHelper jwtHelper;
private final AuthUserService authUserService;
private final GithubOAuth2Manager githubOAuth2Manager;
@Transactional
public AuthLoginResponse loginViaGithubCode(String code) {
verifyCode(code);
var githubAccessToken = githubOAuth2Manager.getAccessToken(code);
var githubUserInfo = githubOAuth2Manager.getGithubUserInfo(githubAccessToken);
var userOAuth2Response = authUserService.manageOAuth2User(githubUserInfo);
var jwtToken = generateJwtToken(userOAuth2Response);
return AuthLoginResponse.of(userOAuth2Response, jwtToken);
}
private void verifyCode(String code) {
if (code == null || code.isBlank()) {
throw new AuthException(ErrorCode401.AUTH_MISSING_CREDENTIALS);
}
}
private String generateAccessToken(String expiredAccessToken) {
Map<String, Claim> claims = jwtHelper.verifyWithoutExpiry(expiredAccessToken);
long userId = claims.get(JwtHelper.USER_ID_STR).asLong();
String[] roles = claims.get(JwtHelper.ROLES_STR).asArray(String.class);
return jwtHelper.sign(userId, roles, expiredAccessToken).getAccessToken();
}
...
}
API 호출에 의해 응답을 전달할 수 있게 되어 클라이언트의 뷰와 강결합을 맺지 않아도 된다는 장점과, 세션 상태를 stateless하게 가져갈 수 있다는 점을 모두 챙길 수 있는 리펙토링 방식이었습니다.
기존 Spring OAuth2 Client는 로그인 페이지를 무조건 생성한다는 점과 위의 단점이 존재했지만, 프로젝트에서 OAuth2 로그인을 다중으로 사용한다(ex. 카카오, 네이버, Google, Github 를 다 사용할거야!)면 관리의 효율성과 편의성이 더 뛰어나기에 직접 구현보다는 더 좋은 선택지라고 생각합니다.