Backend

[Spring Security] OAuth 네이버 로그인하기

연_우리 2022. 2. 2. 20:21
반응형

목차

     

    썸네일

     

    이전글

    https://lotuus.tistory.com/79

     

    [Spring Security] OAuth 구글 로그인하기

    목차 [이전 게시글] 꼭! 봐주세여 [Spring Security] 동작방법 및 Form, OAuth 로그인하기 (Feat.Thymeleaf 타임리프) 목차 Spring Security란? Spring을 사용할 때 애플리케이션에 대한 인증, 권한 부여 등의 보..

    lotuus.tistory.com

     

     

    Spring Security는 각 유명한 사이트들(구글, 깃허브, 페이스북, okta)의 OAuth2를 미리 설정해두었다.

    public enum CommonOAuth2Provider {
    	GOOGLE {
    		@Override
    		public Builder getBuilder(String registrationId) {
    			ClientRegistration.Builder builder = getBuilder(registrationId,
    					ClientAuthenticationMethod.CLIENT_SECRET_BASIC, DEFAULT_REDIRECT_URL);
    			builder.scope("openid", "profile", "email");
    			builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
    			builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
    			builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs");
    			builder.issuerUri("https://accounts.google.com");
    			builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
    			builder.userNameAttributeName(IdTokenClaimNames.SUB);
    			builder.clientName("Google");
    			return builder;
    		}
    	},
        ....
    }

     

    그래서 구글 로그인을 연동할때 application.properties에 기재하는 내용이 크게 없었다 (이미 설정되어있었기때문)

    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

     

     

    하지만 네이버의 경우는 기본 Provider가 아니기때문에 application.properties에 부가적인 내용을 등록해주어야한다!

    OAuth2 설정 파트에서 확인하자.

     

     

     

     

     

     

    Spring Security - OAuth2 네이버 로그인하기

     

    네이버 설정

    1. 네이버 개발자 센터 접속

     

    NAVER Developers

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

    developers.naver.com

     

    2. Application > 애플리케이션 등록

     

    3. 약관동의, 계정정보등록 후 애플리케이션 등록

        > 애플리케이션 이름 작성 > 사용 API : 네이버 로그인 클릭 > 회원이름, 이메일주소 클릭 > 서비스환경 PC웹 클릭 

     

    4. 서비스 URL : http://localhost:8080 입력

       콜백 URL : http://localhost:8080/login/oauth2/code/naver 입력 후 등록

     

    5. 아이디와 비밀번호 기억해두자

     

     

     

    OAuth2 설정

    build.gradle : 이전과 동일함

     

    application.properties

    # naver
    spring.security.oauth2.client.registration.naver.client-id = 
    spring.security.oauth2.client.registration.naver.client-secret = 
    spring.security.oauth2.client.registration.naver.scope = name, email
    spring.security.oauth2.client.registration.naver.client-name = Naver
    spring.security.oauth2.client.registration.naver.authorization-grant-type = authorization_code
    spring.security.oauth2.client.registration.naver.redirect-uri = http://localhost:8080/login/oauth2/code/naver
    
    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
    registration.naver.client-name 자동 생성되는 로그인 페이지에서 노출하는 등에 사용한다(꼭 있어야하는지는 모르겠다..)
    registration.naver.authorization-grant-type OAuth2는 4가지 Authorization Grant유형이 있다
    네이버는 가장 많이 사용되는 authorization_code 방식을 이용한다 (참고)
    registration.naver.redirect-uri 네이버가 사용자 확인 후 정보를 내 프로젝트로 보내주는 주소
    구글처럼 패턴을 가지고 있다. "{baseUrl}/login/oauth2/code/{registrationId}"
    provider.naver.authorization-uri 인증을 요청하는 url을 작성한다 (#3.4.2)
    provider.naver.token-uri 토근을 요청하는 url을 작성한다 (#3.4.4)
    provider.naver.user-info-uri 회원 정보를 가져오는 url을 작성한다 (#3.4.5)
    provider.naver.user-name-attribute { "resultCode":~, "message":~, "response": { "email":~, "name":~, ... } }
    네이버는 사용자정보를 이런식으로 반환해주는데,
    spring security에서는 하위 필드를 명시할 수 없고 최상위 필드만 user_name으로 지정할 수 있다.
    네이버의 최상위필드는 resultCode, message, response이기때문에 3개 중에 골라야하는데, 하위필드가 존재하는 response를 user_name으로 지정한다.
    (#3.4.5)

     

     

     

    OAuth2UserInfo 인터페이스와 GoogleUserInfo 클래스, NaverUserInfo 클래스

    기존 구글 로그인에서는 providerId값을 "sub"로 받았지만 네이버의 경우는 "id"로 받아야한다.

    다른 OAuth2 로그인을 추가하게된다면 경우에 따라서 providerId값이 아닌 email, name 값을 다르게 설정해야할수도있다. 추후 유지보수를 위해 인터페이스를 생성하자!

    //구글의 경우
    String providerId = oAuth2User.getAttribute("sub");
    
    //네이버의 경우
    String providerId = oAuth2User.getAttributes().get("response").get("id").toString();
    { "resultCode":~, "message":~, "response": { "id":~, "email":~, "name":~, ... } }

     

    패키지 > auth 폴더 > userinfo 폴더 > OAuth2UserInfo.java

    public interface OAuth2UserInfo {
        Map<String, Object> getAttributes();
        String getProviderId();
        String getProvider();
        String getEmail();
        String getName();
    }

     

    패키지 > auth 폴더 > userinfo 폴더 > GoogleUserInfo.java

    public class GoogleUserInfo implements OAuth2UserInfo{
        private Map<String, Object> attributes; 
    
        public GoogleUserInfo(Map<String, Object> attributes) {
            this.attributes = attributes;
        }
        
        @Override
        public Map<String, Object> getAttributes() {
            return attributes;
        }
    
        @Override
        public String getProviderId() {
            return attributes.get("sub").toString();
        }
    
        @Override
        public String getProvider() {
            return "google";
        }
    
        @Override
        public String getEmail() {
            return attributes.get("email").toString();
        }
    
        @Override
        public String getName() {
            return attributes.get("name").toString();
        }
    }

     

    패키지 > auth 폴더 > userinfo 폴더 > NaverUserInfo.java

    public class NaverUserInfo implements OAuth2UserInfo{
        private Map<String, Object> attributes; //OAuth2User.getAttributes();
        private Map<String, Object> attributesResponse;
    
        public NaverUserInfo(Map<String, Object> attributes) {
            this.attributes = (Map<String, Object>) attributes.get("response");
            this.attributesResponse = (Map<String, Object>) attributes.get("response");
        }
        
        @Override
        public Map<String, Object> getAttributes() {
            return attributes;
        }
    
        @Override
        public String getProviderId() {
            return attributesResponse.get("id").toString();
        }
    
        @Override
        public String getProvider() {
            return "naver";
        }
    
        @Override
        public String getEmail() {
            return attributesResponse.get("email").toString();
        }
    
        @Override
        public String getName() {
            return attributesResponse.get("name").toString();
        }
    }

     

     

     

    OAuth2Login loadUserByUsername

    패키지 > auth 폴더 > PrincipalOauth2UserService.java

    package com.example.demo.oauth;
    
    @Service
    public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
    
        @Autowired private UserRepository userRepository;
        @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder;
    
        @Override
        public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
    
            OAuth2User oAuth2User = super.loadUser(userRequest);
            
            OAuth2UserInfo oAuth2UserInfo = null;	//추가
            String provider = userRequest.getClientRegistration().getRegistrationId();    
            
            //추가
            if(provider.equals("google")){
                oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes());
            }
            else if(provider.equals("naver")){
                oAuth2UserInfo = new NaverUserInfo(oAuth2User.getAttributes());
            }
            
            String providerId = oAuth2UserInfo.getProviderId();	//수정
            String username = provider+"_"+providerId;  			
    
            String uuid = UUID.randomUUID().toString().substring(0, 6);
            String password = bCryptPasswordEncoder.encode("패스워드"+uuid); 
    
            String email = oAuth2UserInfo.getEmail();	//수정
            Role role = Role.ROLE_USER;
    
            User byUsername = userRepository.findByUsername(username);
            
            //DB에 없는 사용자라면 회원가입처리
            if(byUsername == null){
                byUsername = User.oauth2Register()
                        .username(username).password(password).email(email).role(role)
                        .provider(provider).providerId(providerId)
                        .build();
                userRepository.save(byUsername);
            }
    
            return new PrincipalDetails(byUsername, oAuth2UserInfo);	//수정
        }
    }

     

     

     

    OAuth2Login Authentication OAuth2User

    패키지 > auth 폴더 > PrincipalDetails.java

    package com.example.demo.auth;
    
    @Getter 
    @ToString
    public class PrincipalDetails implements UserDetails, OAuth2User {
    
        private User user;
        //private Map<String, Object> attributes;
        private OAuth2UserInfo oAuth2UserInfo;
    
        //UserDetails : Form 로그인 시 사용
        public PrincipalDetails(User user) {
            this.user = user;
        }
    
        //OAuth2User : OAuth2 로그인 시 사용
        //public PrincipalDetails(User user, Map<String, Object> attributes) {
        //    //PrincipalOauth2UserService 참고
        //    this.user = user;
        //    this.attributes = attributes;
        //}
        
        public PrincipalDetails(User user, OAuth2UserInfo oAuth2UserInfo) {
            this.user = user;
            this.oAuth2UserInfo = oAuth2UserInfo;
        }
        
    
        /**
         * UserDetails 구현
         * 해당 유저의 권한목록 리턴
         */
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
    
            Collection<GrantedAuthority> collect = new ArrayList<>();
            collect.add(new GrantedAuthority() {
                @Override
                public String getAuthority() {
                    return user.getRole().toString();
                }
            });
            return collect;
        }
    
        /**
         * UserDetails 구현
         * 비밀번호를 리턴
         */
        @Override
        public String getPassword() {
            return user.getPassword();
        }
    
        /**
         * UserDetails 구현
         * PK값을 반환해준다
         */
        @Override
        public String getUsername() {
            return user.getUsername();
        }
    
        /**
         * UserDetails 구현
         * 계정 만료 여부
         *  true : 만료안됨
         *  false : 만료됨
         */
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        /**
         * UserDetails 구현
         * 계정 잠김 여부
         *  true : 잠기지 않음
         *  false : 잠김
         */
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        /**
         * UserDetails 구현
         * 계정 비밀번호 만료 여부
         *  true : 만료 안됨
         *  false : 만료됨
         */
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        /**
         * UserDetails 구현
         * 계정 활성화 여부
         *  true : 활성화됨
         *  false : 활성화 안됨
         */
        @Override
        public boolean isEnabled() {
            return true;
        }
    
    
        /**
         * OAuth2User 구현
         * @return
         */
        @Override
        public Map<String, Object> getAttributes() {
            //return attributes;
            return oAuth2UserInfo.getAttributes();
        }
    
        /**
         * OAuth2User 구현
         * @return
         */
        @Override
        public String getName() {
            //String sub = attributes.get("sub").toString();
            //return sub;
            return oAuth2UserInfo.getProviderId();
        }
    }

     

     

     

    OAuth2Login Domain구현

    패키지 > domain 폴더 > Role.java : 이전과 같음 

    패키지 > domain 폴더 > User.java : 이전과 같음

     

     

     

     

    OAuth2Login Controller 구현

    패키지 > controller 폴더 > UserController.java : 이전과 같음

     

     

     

    OAuth2Login View구현

    resources > templates > join.html : 이전과 같음 

    resources > templates > login.html

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
    <head>
        <meta charset="UTF-8">
        <title>로그인 페이지</title>
    </head>
    <body>
    <h1>로그인 페이지</h1>
    <hr/>
    
    <h2>로그인 유저 : </h2>
    <p sec:authentication="principal"></p>
    
    <div sec:authorize="isAnonymous()" style="background-color:pink; padding:1em;">
        <form action="/login" method="post" >
            <input type="text" name="username" />
            <input type="password" name="password" />
            <button>로그인</button>
        </form>
        
        <a href="/oauth2/authorization/google">구글 로그인</a>
        <a href="/oauth2/authorization/naver">네이버 로그인</a>
        <!-- /oauth2/authorization/{registrationId}에 요청이 들어오면, 
        스프링 시큐리티가 provider의 authorization-uri로 요청을 전달한다-->
        <br><br>
    
        <a href="/joinForm">회원가입하기</a><br>
    </div>
    
    <div sec:authorize="isAuthenticated()" style="background-color:pink; padding:1em;">
        <a href="/logout">로그아웃</a>
    </div>
    
    <br><br>
    <a href="/user">유저</a><br>
    <a href="/manager" sec:authorize="hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')">매니저</a><br>
    <a href="/admin" sec:authorize="hasRole('ROLE_ADMIN')">어드민</a><br>
    
    <br><br>
    <a href="/form/loginInfo">Form 로그인 정보</a>
    <a href="/oauth/loginInfo">OAuth2 로그인 정보</a>
    <a href="/loginInfo">로그인 정보</a>
    
    </body>
    </html>

     

     

     

     

     

     

     

     

    반응형
    • 네이버 블러그 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 구글 플러스 공유하기
    • 카카오톡 공유하기