ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • OAuth2 사용해서 react와 함께 소셜로그인 기능 만들기
    Spring 2021. 10. 26. 21:40

     

     

    로그인 창에서 많이 볼 수 있는 소셜로그인 기능을 만들어보자. 

    여기서 인증이 완료된 후 생성한 토큰을 어떻게 프론트엔드로 다시 보낼까에 대한 부분이 어려웠다 😵‍💫

    (결론은 쿠키를 통해서 확인된 uri에 생성된 토큰 붙인 새로운 uri 생성하고 다시 보내기...!)

     

     

    구글 프로젝트 계정 만들기

    https://console.cloud.google.com/apis

     

    Google Cloud Platform

    하나의 계정으로 모든 Google 서비스를 Google Cloud Platform을 사용하려면 로그인하세요.

    accounts.google.com

    먼저 홈페이지에 들어가서 로그인을 해준 후 , API 및 서비스 항목으로 들어간다. 

    적용할 프로젝트를 생성해준다.

    OAuth 동의화면으로 가서 앱 정보를 입력해준다. 

    사용자 정보 중 프로젝트 사이트에서 활용할 때 필요한 정보의 범위를 설정해준다. 

    새로 인증정보를 추가해준다.

    여기서 http://localhost:8080/oauth2/callback/google 사이트는 인증 완료 후에 돌아갈 리다이렉션 사이트이다.

    (프론트엔드의 리다이렉션 사이트와는 다르다)

     

    완료후에는 client ID와 secret 정보를 받게된다. 이를 프로젝트에 활용할 것이다. 

     

     

     

    네이버 프로젝트 계정 만들기

    https://developers.naver.com/main/

     

    NAVER Developers

    네이버 오픈 API들을 활용해 개발자들이 다양한 애플리케이션을 개발할 수 있도록 API 가이드와 SDK를 제공합니다. 제공중인 오픈 API에는 네이버 로그인, 검색, 단축URL, 캡차를 비롯 기계번역, 음

    developers.naver.com

     네이버 개발자 센터에 가입을 해준다. 

    내 어플리케이션란에서 새프로젝트를 등록해준다. 

    필요한 정보들을 등록설정한다. 

    google과 마찬가지로 리다이렉션 URL을 설정해준다. 

    생성된 client id와 client secret을 복사해둔다. 

     

    카카오 프로젝트 계정 만들기

    https://developers.kakao.com/

     

    Kakao Developers

    카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

    developers.kakao.com

    카카오 개발자 사이트에서 새 프로젝트를 등록해준다. 

    어플리케이션 선택 후 카카오 로그인 -> 동의항목을 설정해준다. (scope)

    카카오 로그인에서 활성화를 시켜주고 리다이렉트 uri를 설정해준다. 

    카카오로그인 -> 보안 창에서 secret코드를 활성화 하고 코드를 발급받는다. 

     

     

    dependency, application.properties 추가
    		<dependency>
    			<groupId>org.springframework.security</groupId>
    			<artifactId>spring-security-oauth2-client</artifactId>
    		</dependency>
    spring.security.oauth2.client.registration.google.client-id=
    spring.security.oauth2.client.registration.google.client-secret=
    spring.security.oauth2.client.registration.google.scope=profile,email
    spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/oauth2/callback/{registrationId}
    
    spring.security.oauth2.client.registration.naver.client-id=
    spring.security.oauth2.client.registration.naver.client-secret=
    spring.security.oauth2.client.registration.naver.client-authentication-method=post
    spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
    spring.security.oauth2.client.registration.naver.scope=name,email,profile_image
    spring.security.oauth2.client.registration.naver.client-name=Naver
    spring.security.oauth2.client.registration.naver.redirect-uri=http://localhost:8080/oauth2/callback/{registrationId}
    
    spring.security.oauth2.client.registration.kakao.client-id=
    spring.security.oauth2.client.registration.kakao.client-secret=
    spring.security.oauth2.client.registration.kakao.client-authentication-method=post
    spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
    spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email,profile_image
    spring.security.oauth2.client.registration.kakao.client-name=Kakao
    spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/oauth2/callback/{registrationId}
    
    
    # Provider
    spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize
    spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token
    spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
    spring.security.oauth2.client.provider.naver.user-name-attribute=response
    
    spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize
    spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token
    spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me
    spring.security.oauth2.client.provider.kakao.user-name-attribute=id
    
    app.oauth2.authorized-redirect-uris=http://localhost:3000/oauth2/redirect

    발급받은 client id와 client secret을 등록해주고, scope도 등록해준다. 

    구글, 페이스북과는 다르게 네이버와 카카오는 provider를 등록해주어야 한다. 

    spring.security.oauth2.client.registration.---.redirect-uri는 서버에서 인증마치고 돌아가는 callback uri이고, 

    oauth2.authorized-redirect-uris는 서버에서 인증을 마치고 프론트엔드로 돌아가 redirect uri 정보들이다. (다르다)

     

    참고로 프론트와 서버의 포트가 다르기 때문에 webConfig에서 cors 설정은 미리 해주어야 한다. 

     

     

    PoviderType 등록
    @Getter
    public enum ProviderType {
        GOOGLE,
        FACEBOOK,
        NAVER,
        KAKAO
    }
        @Column(name = "provider_type")
        @Enumerated(EnumType.STRING)
        private ProviderType providerType;

     

    열거형 클래스 provider type를 만들어주고, 이를 user에 추가해준다. 

     

     

     

    JwtUserDetails에 OAuth2User를 추가 상속
    public class JwtUserDetails implements UserDetails, OAuth2User {
    
        private User user;
    
        private Map<String, Object> attributes;
    
        public JwtUserDetails(User user) {
            super();
            this.user = user;
        }
    
        public JwtUserDetails(User user, Map<String, Object> attributes) {
            this.user = user;
            this.attributes = attributes;
        }
    
        @Override
        public Map<String, Object> getAttributes() {
            return attributes;
        }
    
    }

    Jwt인증을 위해서 생성한 JwtUserDetails에 OAuth2User를 추가로 상속시켜준다. 

    attributes를 추가해주고 생성자와 이를 반환하는 함수도 추가로 구현해준다. 

     

     

    OAuth2UserInfo
    public abstract class OAuth2UserInfo {
        protected Map<String, Object> attributes;
    
        public OAuth2UserInfo(Map<String, Object> attributes) {
            this.attributes = attributes;
        }
    
        public Map<String, Object> getAttributes() {
            return attributes;
        }
    
        public abstract String getId();
    
        public abstract String getName();
    
        public abstract String getEmail();
    
        public abstract String getImageUrl();
    
        public abstract String getFirstName();
    
        public abstract String getLastName();
    }

    사용자 정보는 provider타입에 따라서 각각 정보가 다른 이름이로 들어오게 된다. 

    이를 구현해서 정보를 담기위해서 필요한 정보의 항목만 추상클래스로 만들어 놓고 타입마다 다르게 각각 추상클래스를 상속받는 클래스를 구현해서 정보를 가져온다. 

     

     

    GoogleOAuth2UserInfo
    public class GoogleOAuth2UserInfo extends OAuth2UserInfo {
    
        public GoogleOAuth2UserInfo(Map<String, Object> attributes) {
            super(attributes);
        }
    
        @Override
        public String getId() {
            return (String) attributes.get("sub");
        }
    
        @Override
        public String getName() {
            return (String) attributes.get("name");
        }
    
        @Override
        public String getEmail() {
            return (String) attributes.get("email");
        }
    
        @Override
        public String getImageUrl() {
            return (String) attributes.get("picture");
        }
    
        @Override
        public String getFirstName(){
            return (String) attributes.get("given_name");
        }
    
        @Override
        public String getLastName(){
            return (String) attributes.get("family_name");
        }
    }

     

     

     

    FacebookOAuth2UserInfo
    public class FacebookOAuth2UserInfo extends OAuth2UserInfo{
    
        public FacebookOAuth2UserInfo(Map<String, Object> attributes) {
            super(attributes);
        }
    
        @Override
        public String getId() {
            return (String) attributes.get("id");
        }
    
        @Override
        public String getName() {
            return (String) attributes.get("name");
        }
    
        @Override
        public String getEmail() {
            return (String) attributes.get("email");
        }
    
        @Override
        public String getImageUrl() {
            return (String) attributes.get("imageUrl");
        }
    
        @Override
        public String getFirstName() {
            return null;
        }
    
        @Override
        public String getLastName() {
            return null;
        }
    }

     

     

     

    NaverOAuth2UserInfo
    public class NaverOAuth2UserInfo extends OAuth2UserInfo{
    
        public NaverOAuth2UserInfo(Map<String, Object> attributes) {
            super(attributes);
        }
    
        @Override
        public String getId() {
            Map<String, Object> response = (Map<String, Object>) attributes.get("response");
    
            if (response == null) {
                return null;
            }
            System.out.println(response);
    
            return (String) response.get("id");
        }
    
        @Override
        public String getName() {
            Map<String, Object> response = (Map<String, Object>) attributes.get("response");
    
            if (response == null) {
                return null;
            }
    
            return (String) response.get("name");
        }
    
        @Override
        public String getEmail() {
            Map<String, Object> response = (Map<String, Object>) attributes.get("response");
    
            if (response == null) {
                return null;
            }
    
            return (String) response.get("email");
        }
    
        @Override
        public String getImageUrl() {
            Map<String, Object> response = (Map<String, Object>) attributes.get("response");
    
            if (response == null) {
                return null;
            }
    
            return (String) response.get("profile_image");
        }
    
        @Override
        public String getFirstName() {
            Map<String, Object> response = (Map<String, Object>) attributes.get("response");
    
            if (response == null) {
                return null;
            }
            String name = (String)response.get("name");
    
            return name.substring(1);
        }
    
        @Override
        public String getLastName() {
            Map<String, Object> response = (Map<String, Object>) attributes.get("response");
    
            if (response == null) {
                return null;
            }
            String name = (String)response.get("name");
    
            return name.substring(0,1);
        }
    }

     

     

     

    KakaoOAuth2UserInfo
    public class KakaoOAuth2UserInfo extends OAuth2UserInfo{
    
        public KakaoOAuth2UserInfo(Map<String, Object> attributes) {
            super(attributes);
        }
        @Override
        public String getId() {
            return attributes.get("id").toString();
        }
    
        @Override
        public String getName() {
            Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
    
            if (properties == null) {
                return null;
            }
    
            return (String) properties.get("nickname");
        }
    
        @Override
        public String getEmail() {
            Map<String, Object> properties = (Map<String, Object>) attributes.get("kakao_account");
    
            if (properties == null) {
                return null;
            }
            return (String) properties.get("email");
        }
    
        @Override
        public String getImageUrl() {
            Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
    
            if (properties == null) {
                return null;
            }
    
            return (String) properties.get("thumbnail_image");
        }
    
        @Override
        public String getFirstName() {
            Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
    
            if (properties == null) {
                return null;
            }
    
            String name = (String) properties.get("nickname");
            return name.substring(1);
        }
    
        @Override
        public String getLastName() {
            Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
    
            if (properties == null) {
                return null;
            }
    
            String name = (String) properties.get("nickname");
            return name.substring(0,1);
        }
    }

     

     

     

    OAuth2UserInfoFactory
    public class OAuth2UserInfoFactory {
        public static OAuth2UserInfo getOAuth2UserInfo(ProviderType providerType, Map<String, Object> attributes) {
            switch (providerType) {
                case GOOGLE: return new GoogleOAuth2UserInfo(attributes);
                case FACEBOOK: return new FacebookOAuth2UserInfo(attributes);
                case NAVER: return new NaverOAuth2UserInfo(attributes);
                case KAKAO: return new KakaoOAuth2UserInfo(attributes);
                default: throw new IllegalArgumentException("Invalid Provider Type.");
            }
        }
    }

    switch를 통해서 provider타입에 따라서 각각 다른 userInfo 정보를 가져오도록 실행하도록 하는 팩토리 클래스를 구현해준다. 

     

     

     

    AppProperties
    @ConfigurationProperties(prefix = "app")
    public class AppProperties {
    
        private final Auth auth = new Auth();
    
        private final OAuth2 oauth2 = new OAuth2();
    
        public static class Auth {
    
            private String tokenSecret;
    
            private long tokenExpirationMsec;
    
            public String getTokenSecret() {
                return tokenSecret;
            }
    
            public void setTokenSecret(String tokenSecret) {
                this.tokenSecret = tokenSecret;
            }
    
            public long getTokenExpirationMsec() {
                return tokenExpirationMsec;
            }
    
            public void setTokenExpirationMsec(long tokenExpirationMsec) {
                this.tokenExpirationMsec = tokenExpirationMsec;
            }
        }
    
        public static final class OAuth2 {
    
            private List<String> authorizedRedirectUris = new ArrayList<>();
    
            public List<String> getAuthorizedRedirectUris() {
                return authorizedRedirectUris;
            }
            public OAuth2 authorizedRedirectUris(List<String> authorizedRedirectUris) {
                this.authorizedRedirectUris = authorizedRedirectUris;
                return this;
            }
        }
    
        public Auth getAuth() {
            return auth;
        }
        public OAuth2 getOauth2() {
            return oauth2;
        }
    }

    oauth2 정보를 가지고 있는 AppProperties configuration을 생성해준다.

    (인증 정보 redirectUri를 저장하고 가져올 수 있게 하는 역할)

     

     

     

    앱 application
    @EnableJpaAuditing
    @EnableConfigurationProperties(AppProperties.class)
    @SpringBootApplication
    public class WalkerholicApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(WalkerholicApplication.class, args);
    	}
    
    }

    어플리케이션에 AppProperties Configuration을 추가해준다. 

     

     

     

    CustomOAuth2UserService
    @Service
    @RequiredArgsConstructor
    public class CustomOauth2UserService extends DefaultOAuth2UserService {
    
        private final UserRepository userRepository;
    
        private final JwtTokenUtil jwtTokenUtil;
    
        @Override
        public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
            System.out.println("attributes" + super.loadUser(userRequest).getAttributes());
            OAuth2User user = super.loadUser(userRequest);
    		//여기서 attriutes를 찍어보면 모두 각기 다른 이름으로 데이터가 들어오는 것을 확인할 수 있다. 
            try{
                return process(userRequest, user);
            } catch (AuthenticationException ex){
                throw new OAuth2AuthenticationException(ex.getMessage());
            } catch (Exception ex){
                throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
            }
        }
    
    	//인증을 요청하는 사용자에 따라서 없는 회원이면 회원가입, 이미 존재하는 회원이면 업데이트를 실행한다. 
        private OAuth2User process(OAuth2UserRequest userRequest, OAuth2User user) {
            ProviderType providerType = ProviderType.valueOf(userRequest.getClientRegistration().getRegistrationId().toUpperCase());
    		//provider타입에 따라서 각각 다르게 userInfo가져온다. (가져온 필요한 정보는 OAuth2UserInfo로 동일하다)
            OAuth2UserInfo userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(providerType, user.getAttributes());
    
            User savedUser = userRepository.findByEmail(userInfo.getEmail());
    
            if (savedUser != null) {
                if (providerType != savedUser.getProviderType()) {
                    throw new OAuthProviderMissMatchException(
                            "Looks like you're signed up with " + providerType +
                                    " account. Please use your " + savedUser.getProviderType() + " account to login."
                    );
                }
                updateUser(savedUser, userInfo);
            } else {
                savedUser = createUser(userInfo, providerType);
            }
            System.out.println(jwtTokenUtil.generateToken(userInfo.getEmail()));
    
            return new JwtUserDetails(savedUser, user.getAttributes());
        }
    
    	//넘어온 사용자 정보를 통해서 회원가입을 실행한다. 
        private User createUser(OAuth2UserInfo userInfo, ProviderType providerType) {
            User user = new User();
            user.setFirstname(userInfo.getFirstName());
            user.setLastname(userInfo.getLastName());
            user.setRole(Role.USER);
            user.setLevel(Level.Starter);
            user.setPassword("");
            user.setEmail(userInfo.getEmail());
            user.setImageUrl(userInfo.getImageUrl());
            user.setProviderType(providerType);
    
            return userRepository.save(user);
        }
    
    	//사용자정보에 변경이 있다면 사용자 정보를 업데이트 해준다. 
        private User updateUser(User user, OAuth2UserInfo userInfo) {
            if (userInfo.getFirstName() != null && !user.getFullname().equals(userInfo.getName())) {
                user.setFirstname(userInfo.getFirstName());
            }
    
            if (userInfo.getLastName() != null && !user.getLastname().equals(userInfo.getLastName())){
                user.setLastname(userInfo.getLastName());
            }
    
            if (userInfo.getImageUrl() != null && !user.getImageUrl().equals(userInfo.getImageUrl())) {
                user.setImageUrl(userInfo.getImageUrl());
            }
    
            return user;
        }
    }

    넘어온 사용자 로그인 요청에 provider타입에 따라서 각각 다르게 userInfo를 실행해서 정보를 가져온다. 

    회원 등록 여부에 따라서 회원가입 혹은 업데이트를 진행한다. 

     

     

     

     

    HttpCookieOAuth2AuthorizationRequestRepository
    public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
    
        public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request";
    
        public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri";
    
        private static final int cookieExpireSeconds = 180;
    
    	//쿠키에 저장된 인증요청 정보를 가지고 온다
        @Override
        public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest httpServletRequest) {
            return CookieUtils.getCookie(httpServletRequest, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME)
                    .map(cookie -> CookieUtils.deserialize(cookie, OAuth2AuthorizationRequest.class))
                    .orElse(null);    }
    
    	//인증 요청 정보를 쿠키에 저장
        @Override
        public void saveAuthorizationRequest(OAuth2AuthorizationRequest oAuth2AuthorizationRequest, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
            if (oAuth2AuthorizationRequest == null) {
                CookieUtils.deleteCookie(httpServletRequest, httpServletResponse, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
                CookieUtils.deleteCookie(httpServletRequest, httpServletResponse, REDIRECT_URI_PARAM_COOKIE_NAME);
                return;
            }
            CookieUtils.addCookie(httpServletResponse, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtils.serialize(oAuth2AuthorizationRequest), cookieExpireSeconds);
            String redirectUriAfterLogin = httpServletRequest.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME);
            if (StringUtils.isNotBlank(redirectUriAfterLogin)) {
                CookieUtils.addCookie(httpServletResponse, REDIRECT_URI_PARAM_COOKIE_NAME, redirectUriAfterLogin, cookieExpireSeconds);
            }
        }
    
    	//쿠키에 등록된 인증 요청 정보를 삭제
        @Override
        public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest httpServletRequest) {
            return this.loadAuthorizationRequest(httpServletRequest);
        }
        public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
            CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
            CookieUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME);
        }
    }

    인증요청 정보 http request를 쿠키에 저장하고 저장한 것을 불러오고 삭제하는 authorizationRequestRepository를 상속받는  repository를 만들어준다. 

     

     

     

    CookieUtils
    public class CookieUtils {
        public static Optional<Cookie> getCookie(HttpServletRequest request, String name) {
            Cookie[] cookies = request.getCookies();
            if (cookies != null && cookies.length > 0) {
                for (Cookie cookie : cookies) {
                    if (cookie.getName().equals(name)) {
                        return Optional.of(cookie);
                    }
                }
            }
            return Optional.empty();
        }
        public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
            Cookie cookie = new Cookie(name, value);
            cookie.setPath("/");
            cookie.setHttpOnly(true);
            cookie.setMaxAge(maxAge);
            response.addCookie(cookie);
        }
        public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) {
            Cookie[] cookies = request.getCookies();
            if (cookies != null && cookies.length > 0) {
                for (Cookie cookie: cookies) {
                    if (cookie.getName().equals(name)) {
                        cookie.setValue("");
                        cookie.setPath("/");
                        cookie.setMaxAge(0);
                        response.addCookie(cookie);
                    }
                }
            }
        }
        public static String serialize(Object object) {
            return Base64.getUrlEncoder()
                    .encodeToString(SerializationUtils.serialize(object));
        }
        public static <T> T deserialize(Cookie cookie, Class<T> cls) {
            return cls.cast(SerializationUtils.deserialize(
                    Base64.getUrlDecoder().decode(cookie.getValue())));
        }
    }

    쿠키정보를 처리하는 cookieUtils 클래스를 생성해준다. 

     

     

     

     

    OAuth2AuthenticationSuccessHandler
    @Component
    @RequiredArgsConstructor
    public class OAuth2SuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    
        private final JwtTokenUtil jwtTokenUtil;
    
        private final AppProperties appProperties;
    
        private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;
    
    
    	//oauth2인증이 성공적으로 이뤄졌을 때 실행된다 
        //token을 포함한 uri을 생성 후 인증요청 쿠키를 비워주고 redirect 한다. 
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
            String targetUrl = determineTargetUrl(request, response, authentication);
            if (response.isCommitted()) {
                logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
                return;
            }
            clearAuthenticationAttributes(request, response);
            getRedirectStrategy().sendRedirect(request, response, targetUrl);
        }
        
        //token을 생성하고 이를 포함한 프론트엔드로의 uri를 생성한다. 
        protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
            Optional<String> redirectUri = CookieUtils.getCookie(request, HttpCookieOAuth2AuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME)
                    .map(Cookie::getValue);
            if(redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) {
                throw new BadRequestException("Sorry! We've got an Unauthorized Redirect URI and can't proceed with the authentication");
            }
            String targetUrl = redirectUri.orElse(getDefaultTargetUrl());
            String token = jwtTokenUtil.generateToken(authentication.getName());
            return UriComponentsBuilder.fromUriString(targetUrl)
                    .queryParam("token", token)
                    .build().toUriString();
        }
        
        //인증정보 요청 내역을 쿠키에서 삭제한다. 
        protected void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
            super.clearAuthenticationAttributes(request);
            httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
        }
        
        //application.properties에 등록해놓은 Redirect uri가 맞는지 확인한다. (app.redirect-uris)
        private boolean isAuthorizedRedirectUri(String uri) {
            URI clientRedirectUri = URI.create(uri);
            return appProperties.getOauth2().getAuthorizedRedirectUris()
                    .stream()
                    .anyMatch(authorizedRedirectUri -> {
                        // Only validate host and port. Let the clients use different paths if they want to
                        URI authorizedURI = URI.create(authorizedRedirectUri);
                        if(authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost())
                                && authorizedURI.getPort() == clientRedirectUri.getPort()) {
                            return true;
                        }
                        return false;
                    });
        }
    }

    oauth2인증이 성공한 후에 실행되는 OAuth2AuthenticationSuccessHandler를 작성해준다. 

    프론트엔드를 통해서 들어온 redirect-uri가 우리가 저장해놓은 app.redirect-uris의 항목과 일치하는지 확인하고 일치한다면 token을 포함한 uri생성 후 인증요청 쿠키를 비워주고 redirect 하도록 한다. 

     

     

     

    OAuth2AuthenticationFailureHandler
    @Component
    public class OAuth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    
        @Autowired
        HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;
    
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            super.onAuthenticationFailure(request, response, exception);
    
            String targetUrl = CookieUtils.getCookie(request, HttpCookieOAuth2AuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME)
                    .map(Cookie::getValue)
                    .orElse(("/"));
            targetUrl = UriComponentsBuilder.fromUriString(targetUrl)
                    .queryParam("error", exception.getLocalizedMessage())
                    .build().toUriString();
            httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
            getRedirectStrategy().sendRedirect(request, response, targetUrl);
        }
    
    
    }

    만약 인증이 실패한다면, 들어온 uri정보에 token정보 대신에 error 정보를 추가해서 리다이렉트 하도록 설정한다. 

     

     

    OAuthProviderMissMatchException
    public class OAuthProviderMissMatchException extends RuntimeException{
    
        public OAuthProviderMissMatchException(String message) {
            super(message);
        }
    
        public OAuthProviderMissMatchException(String message, Throwable cause) {
            super(message, cause);
        }
    
        public OAuthProviderMissMatchException(Throwable cause) {
            super(cause);
        }
    }
        @ExceptionHandler(OAuthProviderMissMatchException.class)
        public ResponseEntity<ErrorResponse> handleOAuthProviderMissMatchException(OAuthProviderMissMatchException e){
            ErrorResponse errorResponse = new ErrorResponse();
            HttpStatus status = HttpStatus.BAD_REQUEST;
            errorResponse.setStatus(status.value());
            errorResponse.setMessage(e.getMessage());
    
            return new ResponseEntity<>(errorResponse, status);
        }

    인증 provider가 불일치 할 경우 발생될 exception을 생성하고 exception핸들러에 추가해준다. 

     

     

     

     

     

    WebSecurityConfig
    @Configuration
    @EnableWebSecurity
    //@EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private CustomOauth2UserService customOauth2UserService;
    
        @Autowired
        private OAuth2SuccessHandler oAuth2SuccessHandler;
    
        @Autowired
        private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
    
        @Autowired
        private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;
    
        @Bean
        public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() {
            return new HttpCookieOAuth2AuthorizationRequestRepository();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            http.cors()
                        .and()
                    .csrf()
                        .disable()
                    .formLogin()
                        .disable()
                    .authorizeRequests()
                        .permitAll()
                    .anyRequest()
                        .permitAll()
                        .and()
                    .exceptionHandling()
                        .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                        .and()
                    .sessionManagement()
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                        .and()
                    .oauth2Login()
                        .authorizationEndpoint()
                        .authorizationRequestRepository(cookieAuthorizationRequestRepository())
                        .baseUri("/oauth2/authorization")
                        .and()
                        .redirectionEndpoint()
                        .baseUri("/oauth2/callback/*")
                        .and()
                        .userInfoEndpoint()
                        .userService(customOauth2UserService)
                        .and()
                        .successHandler(oAuth2SuccessHandler)
                        .failureHandler((AuthenticationFailureHandler) oAuth2AuthenticationFailureHandler);
    
    
            http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
        }

    webSecurityConfig에 HttpCookieRepository, CustomOAuth2UserService, OAuth2AuthenticationSuccessHandler, OAuth2AuthenticationFailureHandler를 추가해서 작성해준다. 

    여기서 baseUri는 프론트엔드에서 서버로 인증을 요청할 때 이동할 uri이고, redirectionEndpoint의 base uri는 어플리케이션 설정에서 정해주었던 callback uri 이다. 

     

     

     

    프론트엔드 설정
    //서버로 인증을 요청할 uri (서버의 webSecurityConfig의 base uri와 일치해야 한다)
    export const API_BASE_URL = 'http://localhost:8080';
    
    //서버에서 인증을 완료한 후에 프론트엔드로 돌아올 redirect uri (app.oauth2.authorized-redirect-uri와 일치해야 한다)
    export const OAUTH2_REDIRECT_URI = 'http://localhost:3000/oauth2/redirect';
    
    export const GOOGLE_AUTH_URL = API_BASE_URL + '/oauth2/authorization/google?redirect_uri=' + OAUTH2_REDIRECT_URI;
    export const FACEBOOK_AUTH_URL = API_BASE_URL + '/oauth2/authorization/facebook?redirect_uri=' + OAUTH2_REDIRECT_URI;
    export const NAVER_AUTH_URL = API_BASE_URL + '/oauth2/authorization/naver?redirect_uri=' + OAUTH2_REDIRECT_URI;
    export const KAKAO_AUTH_URL = API_BASE_URL + '/oauth2/authorization/kakao?redirect_uri=' + OAUTH2_REDIRECT_URI;
    //클릭후 해당 uri로 이동하는 버튼을 생성
    <div className="auth_oauth_button">
      <a href={GOOGLE_AUTH_URL}><img src={google} alt="" /></a>
      <a href={KAKAO_AUTH_URL}><img src={kakao} alt="" /></a>
      <a href={NAVER_AUTH_URL}><img src={naver} alt="" /></a>
    </div>
     //프론트엔드로 돌아올 uri에 스크린을 등록해준다.
     <Route exact path="/oauth2/redirect" component={HomeScreen}/>

    프론트엔드에서 uri설정을 완료하고 버튼을 누르면 해당 uri로 이동하도록 설정한다. 

    oauth2 설정이 완료되었다!

     

     

     

     

    ( 도움이 정말정말 많이 되었던 참고 사이트 🤎 )

    https://deeplify.dev/back-end/spring/oauth2-social-login#kakaooauth2userinfo

     

    [Spring Boot] OAuth2 소셜 로그인 가이드 (구글, 페이스북, 네이버, 카카오)

    스프링부트를 이용하여 구글, 페이스북, 네이버, 카카오 OAuth2 로그인 구현하는 방법에 대해서 소개합니다.

    deeplify.dev

    http://yoonbumtae.com/?p=3000

     

    스프링 부트(Spring Boot): SPA에서 사용할 수 있는 OAuth2 소셜 로그인 (구글, 페이스북, 깃허브) - BGSMM

    원문 바로가기1 바로가기2 원문에서는 프론트엔드 부분을 리액트로 설명하고 있는데, 저는 리액트를 사용하지 않아서 다음 글에서 Vue.js 로 대체해서 올리고, 이 글은 백엔드만 다루겠습니다. 일

    yoonbumtae.com

    https://engkimbs.tistory.com/849

     

    스프링 부트로 OAuth2 구현(페이스북, 구글, 카카오, 네이버)

    * 아래의 모든 코드는 깃 저장소에 올려놨습니다. 참고 부탁드려요 * OAuth란? OAuth(Open Authorization)는 토큰 기반의 인증 및 권한을 위한 표준 프로토콜입니다. OAuth와 같은 인증 프로토콜을 통해 유

    engkimbs.tistory.com

     

     

    (이곳은 코드 작성 내용을 참고했던 깃허브이다)

    https://github.com/callicoder/spring-boot-react-oauth2-social-login-demo/blob/master/spring-social/src/main/java/com/example/springsocial/SpringSocialApplication.java

     

    GitHub - callicoder/spring-boot-react-oauth2-social-login-demo: Spring Boot React OAuth2 Social Login with Google, Facebook, and

    Spring Boot React OAuth2 Social Login with Google, Facebook, and Github - GitHub - callicoder/spring-boot-react-oauth2-social-login-demo: Spring Boot React OAuth2 Social Login with Google, Facebook...

    github.com

     

Designed by Tistory.