오늘의 목표
JWT를 이용해 인증 인가를 구현하던 api에 oauth2를 이용한 소셜 로그인 기능 추가하기
OAUTH2 인증&인가 진행 과정
기본 동작 과정은 저번 글에서 설명했듯이 다음과 같다.
(여기서 이야기하는 Client는 실제 사용자가 아닌 Oauth Provider에 등록한 우리의 서비스임을 주의하자)
- Resource Owner 가 Client에 로그인 요청
- Client는 Authorization Server에 다시 로그인 요청(Clitne Id, Redirect URI, Response Type, Scope 전달)
- Authorization Server에서 Client에 로그인 페이지 제공
- Resource Owner가 로그인 성공시 Authrorization code 와 함께 redirect uri로 리다이렉트
- redirect uri에서 Client는 Autorization Server에게 Authroization code를 사용해 Access Token 발급 요청
- Client에서 필요할때마다 Access Token으로 Resource 서버의 사용자 정보에 접근
사실 권한부여(Grant Type) 유형은 위와같은 Authorization code 유형이 아닌 다른 유형도 존재하지만,
가장 많이 사용하는 Authorization code 유형을 기준으로 진행했다.
다른 유형도 궁금하다면 OAuth2 Grant Type에 대해 검색해보자.
https://velog.io/@crow/OAuth-2.0-%EA%B6%8C%ED%95%9C%EB%B6%80%EC%97%AC-%EC%9C%A0%ED%98%95Grant-Type
OAuth 2.0 권한부여 유형(Grant Type)
123
velog.io
프론트와 백엔드가 나눠진 인증 과정
프론트와 백 어디가 주가 될 것인가 - Authorization Server Redirect URI를 어디로 설정하는 것이 좋은가?
Authorization Server의 역할을 다시 한 번 짚어볼 필요가 있다.
Authorization Server는 사용자 인증을 진행한 후 인증 성공시 code와 state를
미리 설정해놓은 Redirect URI에 쿼리 파라미터 형식으로 전달한다.
그리고 이 code와 Client Secret 등의 정보를 사용해 Autorization Server는 Access Token을 발급한다.
이 Access Token을 헤더로 사용해야 Resource Server에서 유저 정보를 가져올 수 있다.
저번 글에선 이 과정을 Spring Security의 필터에게 맡겼다.
그런데 프론트와 협업하는 상황에선 이 과정을 어떻게 처리해야할까?
이는 Authorization Server의 Redirect URI를 어떻게 설정하느냐와 깊은 연관이 있다.
1. 프론트가 모두 담당
프론트에서 Authorization Server에 접근해 code를 발급받고 Access Token을 발급받은 다음
Access Token을 백엔드로 넘겨주거나 혹은 유저 정보까지 조회한 후 필요한 유저 정보만 백엔드로 넘겨주는 방식이다.
그러면 백엔드에서 유저 정보를 사용해 서비스용 Access Token과 Refresh Token을 생성한 후 다시 프론트로 넘겨준다.
전자의 경우는 상당히 위험한데, 프론트에서 Access Token과 Refresh Token을 백엔드로 전송할때 탈취의 위험이 있고,
탈취시 심각한 개인정보 유출로 이어질 수 있다.
후자의 경우 OAuth2를 단순히 유저 인증용으로만 사용할 때(로그인 이후엔 유저 정보가 필요 없을 때) 사용할 것 같다.
2. 프론트로 RedirctURI를 설정하고 Authorization code만 다시 백엔드로 넘겨주기
구글링하면 가장 많이 나오는 방식이다.
프론트에서 Authorization Server에 접근해 code를 발급받은 후 그 코드만 백엔드에 넘겨주는 방식이다.
Authorization code는 유효기간도 매우 짧고 탈취당해도 client_secret을 알지 못한다면 소용이 없고
client_secret과 같은 정보는 모두 백에서 관리하므로 보안에 장점이 있는 방식이다.
문제는 Spring Security를 적용하기가 힘들다.
Spring Security의 oauth2 로그인 로직은
- OAuth2LoginAuthenticationFilter에서 생성해뒀던 OAuth2AuthorizationReques객체를 이용해
Access Token을 발급받는다.
OAuth2AuthorizationRequestRedirectFilter를 반드시 거쳐가야 하는데 이 필터는
/oauth2/authorization/** 와 같은 요청을 처리한다.
OAuth2LoginAuthenticationFilter가 처리하는 요청 url은 /login/oauth2/code/** 이다.
프론트에서 두 개의 url에 순서대로 요청하고 쿠키에 저장된 값을 꺼내 쓰고 하는게 너무 복잡하고 불편해보인다.
그렇다고 Security 안쓰고 직접 구현하는건 쉽냐? 그거도 아니다.
백엔드 서버에서 직접 Auhtorization Server랑 Resource Server에 접근해야하며,
결과 값 파싱까지 귀찮은게 한두가지가 아니다.
3. 백이 모두 담당
프론트에선 백의 인증 url만 호출하고 백엔드에서 인증 로직을 모두 전담하는 방식이다.
백엔드에서 모든 인증을 끝낸 후 프론트와 미리 약속한 redirect url에 (Authorization Server의 redirect URI와 다르다)
서비스용 access token, refresh token만 전달하면 된다.
이렇게하면 Authorization Server의 Redirect URI는 localhost:8080/login/oauth2/code/**
같은 백엔드 api 주소로 설정해 Spring Security를 사용하기 쉬워지며
Client Secret과 같은 보안 정보를 모두 백엔드에서 관리해 보안성도 높아진다.
나는 3번 방식을 사용하기로 결정했다.
우리가 선택한 방식의 인증 인가 진행 과정
- 프론트에서 백엔드에 로그인 페이지를 요청하는 것으로 시작
- 백엔드의 /oauth2/authorization/** 로 바로 리다이렉트 -> 로그인 페이지 제공
- 로그인 성공시 /login/oauth2/code/** 로 리다이렉트 -> 시큐리티 필터 동작
- 지난번에 만든 유저 서비스가 Authentication 객체 만들어 Security Context에 넣어둠
- SuccessHandler가 Authentication 객체에서 principal 꺼내서 JWT 제작
- 마지막에 프론트 리다이렉트 url로 토큰 반환
SecurityConfig 수정
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
private final CustomOAuth2UserService customOAuth2UserService;
private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
private final JwtProvider jwtProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin().disable()
.httpBasic().disable()
.addFilter(new JwtAuthorizationFilter(authenticationManager(), jwtProvider))
.authorizeRequests()
.antMatchers("/v1/login").permitAll()
.antMatchers("/v1/refresh").permitAll()
.antMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll()
.antMatchers("/admin/**", "/css/**", "/*.ico", "/error").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOAuth2UserService)
.and()
.successHandler(oAuth2AuthenticationSuccessHandler);
}
}
Session을 이용해 유저 정보를 저장하지 않기 때문에 sessionCreationPolicy를 STATELESS로 설정했다.
기존의 JWT 인증,인가 방식을 그대로 사용하며 OAuth2 소셜 로그인을 추가해야하므로
기존 JWT 설정 (JwtAuthorizationFilter, JwtProvider) 를 그대로 사용하기로 했다.
그리고 지난 글에서 사용했던 customOAuth2UserService, UserInfo, OAuth2Attributes를 그대로 사용하기로 했다.
기존 로직은 OAuth2 로그인 성공 후 SecurityContext에 Authentication 객체를 저장하기만 하면
SecurityContext가 세션에 저장되어 인증 인가를 처리할 수 있었다.
그러나 이젠 Session에 저장하는 것이 아닌 JWT를 만들어 발급해야 하므로 로그인 성공 후 토큰을 만들어야 하는데
이를 CustomSuccessHandler 를 구현해 처리할 예정이다.
CustomSuccessHandler 구현
@RequiredArgsConstructor
@Component
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtProvider jwtProvider;
private final String REDIRECT_URL = "http://localhost:3000/login/redirect";
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
UserInfo userInfo = (UserInfo) authentication.getPrincipal();
User loginUser = userInfo.getUser();
String accessToken = jwtProvider.createAccessToken(loginUser);
String refreshToken = jwtProvider.createRefreshToken(loginUser);
getRedirectStrategy().sendRedirect(request, response, getRedirectUrl(accessToken, refreshToken));
}
private String getRedirectUrl(String accessToken, String refreshToken){
return UriComponentsBuilder.fromUriString(REDIRECT_URL)
.queryParam("access_token", accessToken)
.queryParam("refresh_token", refreshToken)
.build().toUriString();
}
}
SimpleUrlAuthenticationSuccessHandler를 상속받아 구현했다.
인증 성공 후 생성된 Authentication에서 UserInfo를 다시 꺼낸 후 access token과 refresh token을 생성했다,
그리고 생성된 refresh token과 access token을 프론트와 미리 약속한 redirect url에 전송하는 로직을 처리한다.
인증, 인가는 공부를 할 수록 의문점만 생기는 부분인 것 같다.
프론트에서 백으로 access token을 전송하는건 위험하다는데 백에서 프론트로 전송하는 건 괜찮은건가?
프론트에서 보관하고 있는 access token을 어떻게 탈취한다는 걸까?
redirect url을 백엔드 api로 설정하면 유저 브라우저로 백엔드 api를 직접 호출하는 것인데 문제는 없는가?
협업 프로젝트를 진행해보며 더 공부해봐야할 내용인 것 같다.
'Spring > Spring Security' 카테고리의 다른 글
사지방에서 Spring Security 공부하기 #5 - OAuth2 + Session을 이용한 인증, 인가 구현 (4) | 2023.12.29 |
---|---|
사지방에서 Spring 공부하기 Spring Security #4 - JWT를 이용한 인증 인가 적용 코드 (0) | 2023.10.19 |
사지방에서 Spring 공부하기 Spring Security #3 - JWT를 이용한 인증 인가 적용하기 (3) | 2023.10.15 |
사지방에서 Spring 공부하기 - 기존 프로젝트에 Spring Security 적용기 (0) | 2023.09.27 |
사지방에서 Spring 공부하기 - Spring Security란? (3) | 2023.09.26 |