일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- spring cloud
- 자바
- 배포
- Spring Security
- 데이터베이스
- Container
- computer science
- CI/CD
- Spring
- spring batch
- 스프링 시큐리티
- virtualization
- Java
- spring boot
- 백엔드
- 스프링 부트
- web server
- 영속성 컨텍스트
- HTTP
- ORM
- mysql
- vm
- JPA
- 스프링
- 컨테이너
- 스프링 배치
- 가상화
- 도커
- CS
- 웹 서버
- Today
- Total
개발 일기
[Spring Security] OAuth2.0 + JWT 토큰 기반 인증 인가 본문
혹시나 틀린 부분이나 더 나은 코드가 있다면 댓글로 의견 남겨주시면 정말 감사할 것 같습니다!!
Spring Boot 프로젝트를 하면서 OAuth2.0과 JWT 토큰 기반 인증 인가를 구현해봤다.
개발자 유미님의 유투브 강의와 깃허브의 여러 코드 그리고 구글링을 모두 반영하여 구현했다.
이전에 Express활용하여 개발을 할때 OAuth2 클라이언트 로그인에 대해서 모든 책임(redirect_url)을 백엔드가 가질 경우 JWT 발급한 것을 어떻게 프론트측에서 받게 만들지 막막했었다. 그래서 해답을 찾이 못하고 책임을 나눠서 개발했더니 위의 문제를 해결할 수 있었다.
그러나 카카오에 누군가 질문을 한 것을 찾아보거나 현업자들의 얘기를 들어보니 백엔드에서 OAuth2 로그인에 대한 모든 책임을 갖는 것을 지향한다고 해서 어떻게 해결해야할지 고민이 됐다. 그래서 여기저기 알아본 결과 SuccessHandler를 통해 처리한다는 것을 알게 됐다.
전체적인 구현 로직(프론트 요청 ~ Success Handler)
1. 프론트엔드에서 네이버 로그인 요청
: 프론트엔드에서 네이버 로그인 버튼을 클릭하면 http://localhost:8080/oauth2/authorization/{provider} 로 요청을 보낸다.
이때 provider은 naver, google 등이다. 이는 Spring Security OAuth2 Client에서 제공하는 엔드포인트로, 인증 프로세스를 시작하게 된다.
2. OAuth2 인증 필터 작동
: 백엔드에서는 OAuth2AuthorizationRequestRedirectFilter가 해당 요청을 가로챈다. 이 필터는 인증 서버(네이버)로 리디렉션하기 위한 인증 요청을 생성하고 클라이언트에게 응답한다.
3. 인증 서버 로그인 및 코드 획득
: 클라이언트는 인증 서버(Provider)로 리디렉션되어 로그인 페이지를 반환한다. 유저가 로그인에 성공하면 인증 서버는 승인 코드(authorization code)를 발급하고, 이를 다시 http://localhost:8080/login/oauth2/code/naver 로 리디렉션한다.
4. 토큰 획득 및 사용자 정보 조회
: 이번에는 백엔드의 OAuth2LoginAuthenticationFilter가 리디렉션된 요청을 가로챈다. 이 필터는 인증 서버로부터 로그인 성공하여 발급된 승인 코드를 사용하여 액세스 토큰을 획득하고. 그 후 OAuth2UserService를 통해 사용자 정보를 조회한다.
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final MemberRepository memberRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
log.info("--------------------------- CustomOAuth2UserService ---------------------------");
OAuth2User oAuth2User = super.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId(); // Provider 얻기
OAuth2Response oAuth2Response = null;
// 각 Provider에 따라 응답 규격이 다름.
if (registrationId.equals("naver")) {oAuth2Response = new NaverResponse(oAuth2User.getAttributes());}
else if (registrationId.equals("google")) {oAuth2Response = new GoogleResponse(oAuth2User.getAttributes());}
else {return null;}
String oAuth2Id = oAuth2Response.getProvider()+" "+oAuth2Response.getProviderId();
String name = oAuth2Response.getName();
String email = oAuth2Response.getEmail();
Optional<Member> byoAuth2Id = memberRepository.findByoAuth2Id(oAuth2Id);
Member savedMember = byoAuth2Id.orElseGet(() -> saveNewMember(oAuth2Id, name, email));
return new CustomOAuth2User(savedMember);
}
private Member saveNewMember(String oAuth2Id, String name, String email) {
Member newMember = Member.builder()
.oAuth2Id(oAuth2Id)
.name(name)
.email(email)
.roleType(RoleType.USER)
.build();
return memberRepository.save(newMember);
}
}
DefaultOAuth2UserService를 상속하여 직접 구현한 CustomOAuth2UserService를 정리해보자.
userRequest.getClientRegistration().getRegistrationId(); 를 통해 Provider를 얻어오고 super.loadUser(userRequest)는 부모 클래스인 DefaultOAuth2UserService의 loadUser 메서드를 호출하여 반환된 유저 정보가 담긴 OAuth2User를 받아온다.
위에서 알아낸 Provider에 따라 사용자 정보 응답을 NaverResponse 또는 GoogleResponse에 매핑해준다.
그 후 findByoAuth2Id 메서드를 사용하여 해당 oAuth2Id를 가진 회원이 이미 데이터베이스에 존재하는지 확인하고 존재하지 않는 경우, saveNewMember 메서드를 통해 새로운 회원을 저장하고 반환한다.
최종적으로 CustomOAuth2User 객체에 저장된 회원 정보를 담아 반환한다.
5. OAuth2User 구현체를 통한 인가 처리
: 위에서 OAuth2UserService를 통해 유저 정보를 인증 서비스로 부터 받아와 CustomOAuth2User객체에 담아 저장했다.
CustomOAuth2User은 OAuth2User라는 인터페이를 구현한 것으로 OAuth 2.0을 통해 인증된 사용자의 정보를 저장하고 제공하는 역할을 한다.
6. 성공 핸들러를 통한 JWT 전달
: 나는 백엔드에서 모든 OAuth2.0 회원가입 및 로그인에 대한 책임을 가져갔는데 인증 작업이 모두 성공적으로 처리됐을때 JWT 토큰을 프론트로 넘겨주기 위해 OAuth2LoginSuccessHandler를 도입하여 쿠키를 통해 넘겨줬다.
@Slf4j
@RequiredArgsConstructor
@Component
public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler {
private final MemberService memberService;
private final JwtUtil jwtUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("--------------------------- OAuth2LoginSuccessHandler ---------------------------");
CustomOAuth2User principal = (CustomOAuth2User) authentication.getPrincipal();
String oAuth2Id = principal.getOAuth2Id();
String authorities = principal.getAuthorities().toString();
String accessToken = jwtUtil.generateToken("access", oAuth2Id, authorities, jwtUtil.accessTokenExpireLength);
String refreshToken = jwtUtil.generateToken("refresh", oAuth2Id, authorities, jwtUtil.refreshTokenExpireLength);
response.addCookie(createCookie("access", accessToken));
response.addCookie(createCookie("refresh", refreshToken));
memberService.updateRefreshToken(oAuth2Id, refreshToken);
response.sendRedirect(jwtUtil.JWT_REDIRECT);
}
private Cookie createCookie(String key, String value) {
Cookie cookie = new Cookie(key, value);
cookie.setMaxAge(24*60*60);
//cookie.setSecure(true);
cookie.setPath("/");
cookie.setHttpOnly(true); // JS가 가져 가지 못하게(XSS 방지)
return cookie;
}
}
우선 유저 정보를 들고오기 위해서 Authentication 객체에서 principal을 추출한다. 필요한 유저 정보와 주입받은 JwtUtil 클래스를 통해AccessToken과 RefreshToken을 생성했고 각각을 쿠키로 추가했다.
이때 JWT 토큰에 포함시키는 정보는 서비스 상에서 필요한 정보들을 최소한으로 담는 것이 좋다고 한다.
그러면 브라우저를 통해 확인해보면 쿠키에 이렇게 엑세스 토큰과 리프레쉬 토큰이 등록된 것을 확인할 수 있었다. 이제 프론트에서 이 코드들을 쿠키 또는 로컬 스토리지 등에 저장해놓고 인가가 필요한 요청이나 엑세스 토큰 재발급 시에 꺼내 사용할 수 있을 것이다.
추후에 인가 부분에 쓰이는 JwtAuthenticationFilter와 이를 위해 쓰이는 JwtUtil 클래스 그리고 Refresh Token 재발급 로직에 대해 작성해보겠다.
'Back-End > Spring' 카테고리의 다른 글
[Spring Boot] 스프링 프로젝트에 로깅 적용하기 (0) | 2024.05.27 |
---|---|
[Spring Boot] 로깅 레벨(Logging Level) 정리 (0) | 2024.05.27 |
[Spring Security] Spring Security Authentication Architecture (0) | 2024.05.03 |
[Spring Security] SecurityFilterChain 내부 구조 (0) | 2024.05.02 |
[Spring Security] 스프링 시큐리티 - Filter, DelegatingFilterProxy, FilterChainProxy, SecurityFilterChain (1) | 2024.05.02 |