지난 포스트 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 를 다 사용할거야!)면 관리의 효율성과 편의성이 더 뛰어나기에 직접 구현보다는 더 좋은 선택지라고 생각합니다.