소셜로그인 trouble shooting

문딤·2023년 1월 4일
0
post-custom-banner

Oauth2.0로그인 동작원리

개요

역사

2006년 트위터와 Ma.gnolia 가 주도적으로 개발하였다. 이후 1.0버전이 개선된 1.0a 버전이 출시되었으나 모바일 어플리케이션 등에서 안전하게 사용될 수 없는 사례가 존재했다. 이런 사례를 보완하고 기존 버전보다 조금 더 단순화한 OAuth 2.0 버전이 2012년에 등장하게 되었다.

Oauth 란?

다양한 플랫폼의 특정한 사용자 데이터에 접근하기위해 제3자에게 접근권한을 위임하여 정보를 받을수 있는 표준 프로토콜이다.

💨 우리가 만든 web application에서 유저가 로그인을 시도할 때, 해당 플랫폼에서 로그인을 수행하고, 해당정보 접근권한을 위임받아서 유저의 정보를 획득하는 일련의 작업.

관련용어

✅ Authorization & Resource Server

Authorization Server => Resource owner(유저, 정보주체)를 인증하고
Client(app)에게 엑세스 토큰을 발급해주는 서버

◻ Resource Server => 유저정보를 가지고있는 플랫폼서버
ex) 구글, 깃허브 ,카카오톡 등등

✅ Client

◻ Resource Server 자원을 이용하고자하는 app 우리가 개발하는 service

Trouble

구현을 위해 생각한 flow

  1. backend server에서 Authentication server와 Resource Server 모두 통신하여, 회원가입 완료후 JWT 토큰을 던져주는 방식
  2. 프론트에서 로그인 관련로직을 모두 처리하여 회원 정보를 client-server에 넘겨주는 방식

1번 backend에서 모든 요청을 처리

SecurityConfig 설정

	@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilter(corsConfig.corsFilter())
                .formLogin().disable()
                .httpBasic().disable()
                .authorizeRequests()
                .antMatchers("/api/user/**").access("hasRole('ROLE_USER') OR hasRole('ROLE_ADMIN')")
                .antMatchers("**/admin").access("hasRole('ROLE_ADMIN')")
                .anyRequest().permitAll()
                .and().logout().logoutSuccessUrl("/");
 				 .and()
                .oauth2Login().defaultSuccessUrl("/") 
                .successHandler(oAuth2SuccessHandler)
                .failureHandler(oAuth2AuthenticationFailureHandler)
                .userInfoEndpoint()
                .userService(principalOauth2UserService);
                
 http.addFilterBefore(new JwtAuthFilter(tokenService,principalDetailsService), UsernamePasswordAuthenticationFilter.class);
 return http.build();

get요청으로 Oauth2 라이브러리에서 내부로직에의해 Accesstoken을 발급받고 해당 accessToken을 통해 resource server에 저장된 정보를
받아서 회원가입과 토큰 발급을 진행.

PrincipalOauth2UserService
=> DefaultOAuth2UserService 상속


public class PrincipalOauth2UserService extends DefaultOAuth2UserService {


    private final UserRepository userRepository;

    @Override //후처리 용 함수  user 정보가 담겨있는 userRequest
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        log.info("OauthPrincipal login");

        OAuth2User oAuthUser = super.loadUser(userRequest);

        log.info("oAuth2User : "+oAuthUser);

        return processOAuth2User(oAuthUser);

    }

AuthenticationSuccessHandler 객체에 오기까지 디버깅

  1. 넘기면 OAuth2LoginAuthenticationProvider에서 새로운 Oauth2User 객체에 data를 담는다.

  2. mappedAuthorities객체에 해당 Authorities에 대한 정보를 저장

  3. OAuth2LoginAuthenticationToken authenticationResult라는 변수명의 토큰 생성

  4. return token.

  5. authenticate()메소드를 통해 인증

  6. copyDetails메소드를 통해 해당 정보 copy

  7. 인증완료 후 secret한 정보는 삭제.

  8. AuthenticationManager가 중복배포했는지 확인후 publish

  9. OAuth2LoginAuthenticationToken을 result라는 이름으로 return

  10. Oauth2LoginAuthenticationFilter에 있는 Oauth2LoginAuthentication authenticationResult 변수에 삽입.

  11. authorizedClient라는 이름으로 Session안의 Authentication 객체에 저장

즉 spring security의 평범한 로그인 과정처럼 token으로 만들어져서 인증후에 다시 Authentication 객체안에 저장되고 이것을 LoginSuccessHandler에서 사용한다.

 @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {

        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
        String username = usernameMaker(oAuth2User);

        //토큰 생성
        Token token = tokenService.generateToken(username, "ROLE_USER");

        log.info("토큰 : {} ", token);

        String uri = UriComponentsBuilder.fromUriString("http://localhost:8080/api/oauth/redirect")
                .queryParam("token", token.getToken())
                .queryParam("refresh",token.getRefreshToken())
                .build().toUriString();


        response.sendRedirect(uri);
    }
    
    
     private void writeTokenResponse(HttpServletResponse response, Token token) throws IOException{
        response.setContentType("text/html;charset=UTF-8");
        response.addHeader("Authentication",token.getToken());
        response.addHeader("Refresh",token.getRefreshToken());
        response.setContentType("application/json;charset=UTF-8");
        //응답 스트림에 텍스트를 기록하기 위함
        PrintWriter out = response.getWriter();
        //스트림에 텍스트를 기록
        ObjectMapper objectMapper = new ObjectMapper();
        out.println(objectMapper.writeValueAsString(token));
        out.flush();
    }

[Trouble Shooting]
1. writeTokenResponse 메소드 느낌으로 response에 header를 붙여서
화면단에 전달하려했으나 해당request는 client로부터의 요청이아닌 securtiy 내부 로직에 의한 request였으므로 client에게 전달되지 않았다.

  1. queryParam 메소드를 통해 url에 토큰을 붙여 경로상의 토큰 value를 받을수 있지 않을까하고 사용 && sendRedirect를 통해 원하는 경로에서 token을 받아서 사용할 수 있지 않을까? 하고 시도.
    @GetMapping(value = "/api/oauth/redirect")
    public CommonDto<Token> getToken(HttpServletResponse response
                                    , @RequestParam(value = "token", required = false) String token
                                    , @RequestParam(value = "refresh", required = false) String refresh) {


        response.addHeader("Authentication", token);
        response.addHeader("Refresh", refresh);

        Token tokens = new Token(token,refresh);

        return new CommonDto<>(HttpStatus.OK.value(),tokens);
    }

axios 라이브러리를 이용해서 받을수 있지 않을까 했지만 해당 창에서 정보를 얻을수도, 버튼을 만들어 이동할수도 없다는 답변을 받았다.

Solve

결국 FE에서 소셜로그인 로직을 구현하여, 서버에 정보를 넘겨주면 JWT 인증방식을 적용하였으므로, TOKEN을 만들어 반환하는식으로 구현하였다.

	@PostMapping(value="/join")
    public CommonDto<?> JoinUsers(@RequestBody @Valid UserDto dto, BindingResult bindingResult){


         String username = userService.saveUserDetails(dto);

         Token token = tokenService.generateToken(username, RoleMaker(username));

         return new CommonDto<>(HttpStatus.OK.value(), token);

    }

느낀점

  • 매일 회의를 진행하였고, 요구사항이나 진행사항에대해 공유가 원활히되었을거라 생각했지만, 한번 더 물어보고 test해보는 습관을 들여야하는 필요성을 느끼는 계기가 되었다.

  • 서비스 로직을 구현하는 방향성에 대해서도 충분한 토의후에 진행하는것이
    비용을 줄이는 방법임을 절실히 깨달았다.

  • spring security 동작원리에 대해 어렴풋이 알고있었지만, 결국 큰 틀에서 Oauth2.0 로그인이나,세션 securtiy 로그인이나 같은 동작원리를 가졌음을 보고 디버깅이나 주변에 도움을 요청해서라도 서비스에대한 충분한 이해 후에 기능구현을 진행하는것이 실력향상에 중요한 부분임을 느꼈다.

profile
풀스택개발자가 될래요
post-custom-banner

0개의 댓글