백엔드

[번역글] 스프링부트에서 JWT와 소셜로그인

오늘의 나1 2021. 11. 25. 00:04

원문: https://medium.com/javarevisited/jwt-and-social-authentication-using-spring-boot-90e4faaa9204

이 글은 스프링부트와 Facebook Graph API를 이용해서 JWT 인증을 구현하고 페이스북 인증에 적용하는 법에 대한 가이드이다.

이 글은 주로 페이스북 인증에 대해서 다룬다. 하지만 소셜로그인은 구글, 링크드인, 트위터 등의 소셜플랫폼에서도 통용되는 이야기이다.

아래 내용에 대한 이해가 권장된다:

아래 비디오에서 이 글에서 구현하는 것을 확인할 수 있다.

https://youtu.be/ruZB44KZoFk

소스코드는 여기에서 확인할 수 있다.

JWT 인증

JWT 인증 구현하기

소셜 로그인

소셜 로그인은 사용자들이 페이스북, 트위터, 링크드인 등의 소셜 계정으로 다른 웹사이트에 로그인할 수 있게 한다.

이는 웹사이트에 대한 사용자 경험을 향상시킨다. 회원가입 양식을 기입하거나 비밀번호를 기억할 필요가 없기 때문이다.

이 다이어그램은 소셜 로그인 흐름을 보여준다.

- 프론트엔드: 페이스북 로그인
- 페이스북: 페이스북 로그인 성공 시 엑세스토큰 반환
- 프론트엔드: 백엔드서버에 엑세스토큰과 함께 로그인 요청
- 백엔드서버: 페이스북에게 엑세스토큰 유효한지 확인 요청
- 페이스북: 엑세스토큰 유효성 검사. 유효한 경우 백엔드서버에 유저 프로필 반환
- 백엔드서버: 유저가 존재하지 않을 경우에 회원등록 + JWT 토큰 반환. 유저 존재할 경우에 JWT 토큰 반환
- 프론트엔드: JWT 토큰과 함께 유저 정보 요청
- 백엔드서버: JWT 토큰 유효성 검사. 유효한 경우 유저 프로필 반환

Facebook Graph API

페이스북이 제공하는 정보는 그래프 형식으로 표현된다. 그래프는 nodes(User나 Photo), edges(Photo의 comment) 그리고 fields(User의 birthday, email)로 구성된다. 예시로 든 그래프는 소셜 그래프라고 불린다.

Facebook Graph API에 대해 여기서 더 알아볼 수 있다.

페이스북 로그인 구현하기

프론트엔드에서 할 일

첫째로, 프론트엔드(클라이언트)는 페이스북으로부터 엑세스토큰을 받아서 이를 백엔드서버로 보내야 한다.

페이스북 개발자 계정과 페이스북 앱을 생성해야 한다. 여기의 가이드를 따라 앞의 두 가지를 생성할 수 있다.

그 다음 Facebook SDK를 포함하기 위해 index.html의 body 태그 바로 다음에 아래 스크립트를 추가한다.

<script async defer crossorigin="anonymous" src="https://connect.facebook.net/en_US/sdk.js"></script>

login.js 페이지에서 Facebook API를 초기화하기 위해 다음 코드를 추가한다.

const initFacebookLogin = () => {
    window.fbAsyncInit = function () {
      FB.init({
        appId: "{app_id}",
        autoLogAppEvents: true,
        xfbml: true,
        version: "v7.0",
      });
    };
  };

{app_id}는 실제 APP ID로 교체한다.

"페이스북으로 로그인하기" 버튼은 반드시 아래 함수를 호출해야 한다.

FB.login(function(response) {
  if (response.status === 'connected') {
    // Logged into your webpage and Facebook.
    const facebookLoginRequest = {
      accessToken: response.authResponse.accessToken,
    };
    facebookLogin(facebookLoginRequest)
      .then((response) => {
        localStorage.setItem("accessToken", response.accessToken);
       });
  } else {
    // The person is not logged into your webpage or we are unable to tell. 
  }
}, {scope: 'email'});

위의 함수는 페이스북 로그인을 실행해서 엑세스토큰을 가져오고, 백엔드서버를 호출해 로그인 후 JWT 토큰을 로컬스토리지에 저장한다.

여기서 scope는 사용자에게 이메일 정보에 대한 접근 권한을 요청한다는 것을 의미한다.

백엔드서버에서 할 일

FacebookService 코드를 보고 서버에서 어떻게 구현하고 있는 지 확인하자

public String loginUser(String fbAccessToken) {
   var facebookUser = facebookClient.getUser(fbAccessToken);

   return userService.findById(facebookUser.getId())
           .or(() -> Optional.ofNullable(userService.registerUser(convertTo(facebookUser), Role.FACEBOOK_USER)))
        .map(InstaUserDetails::new)
        .map(userDetails -> new UsernamePasswordAuthenticationToken(
            userDetails, null, userDetails.getAuthorities()))
        .map(tokenProvider::generateToken)
        .orElseThrow(() ->
            new InternalServerException("unable to login facebook user id " + facebookUser.getId()));
}

Line 2, FacebookClient.getUser 메서드를 호출한다. 이에 대해 뒤에 다룰 것이다.

Line 4, 사용자가 DB에 존재하는 지 확인한다.

Line 5, 사용자가 없으면 사용자를 등록한다.

Line 7-9, JWT 토큰을 생성하여 프론트엔드에 반환한다.

private User convertTo(FacebookUser facebookUser) {
    return User.builder()
        .id(facebookUser.getId())
        .email(facebookUser.getEmail())
        .username(generateUsername(facebookUser.getFirstName(), facebookUser.getLastName()))
        .password(generatePassword(8))
        .userProfile(Profile.builder()
            .displayName(String
                .format("%s %s", facebookUser.getFirstName(), facebookUser.getLastName()))
            .profilePictureUrl(facebookUser.getPicture().getData().getUrl())
            .build())
        .build();
}

물론, 위에서 언급했듯이, 사용자를 새로 추가할 때, 임의의 사용자명과 임의의 페스워드를 생성한다.

드디어, FacebookClient를 확인하자.

@Autowired private RestTemplate restTemplate;

private final String FACEBOOK_GRAPH_API_BASE = "https://graph.facebook.com";

public FacebookUser getUser(String accessToken) {
    var path = "/me?fields={fields}&redirect={redirect}&access_token={access_token}";
    var fields = "email,first_name,last_name,id,picture.width(720).height(720)";
    final Map<String, String> variables = new HashMap<>();
    variables.put("fields", fields);
    variables.put("redirect", "false");
    variables.put("access_token", accessToken);
    return restTemplate
            .getForObject(FACEBOOK_GRAPH_API_BASE + path, FacebookUser.class, variables);
}

이 함수는 다음 Facebook Graph API에 대한 단순한 GET 호출이다.

https://graph.facebook.com/me?fields=email,first_name,last_name,id,picture.width(720).height(720)&access_token={access_token}

흥미로운 점은 field 키를 통해 필요한 필드에 대해 명시한다는 점이다.

결론

소셜 로그인과 JWT 인증을 엮는 일은 꽤 작업이 필요하다. 그러나 이는 사용자 경험을 향상시켜 사용자가 회원가입 폼 입력없이 쉽게 웹사이트에 가입하게 한다.

좋아할만한 다른 Java와 Spring과 관련된 아티클이다.

https://medium.com/javarevisited/10-advanced-spring-boot-courses-for-experienced-java-developers-5e57606816bd

https://medium.com/javarevisited/12-advanced-spring-framework-courses-for-java-programmers-a273f6e4448c

https://javarevisited.blogspot.com/2021/09/microservices-design-patterns-principles.html