🌿 Spring

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

연_우리 2022. 2. 2. 02:31
반응형

목차

     

    썸네일

     

    [이전 게시글] 꼭! 봐주세여

     

    [Spring Security] 동작방법 및 Form, OAuth 로그인하기 (Feat.Thymeleaf 타임리프)

    목차 Spring Security란? Spring을 사용할 때 애플리케이션에 대한 인증, 권한 부여 등의 보안 기능을 제공하는 프레임워크이다. 다양한 로그인 방법(Form태그, OAuth2, JWT...)에 대해 Spring이 어느정도 구현

    lotuus.tistory.com

    간단히 요약하면

    1. 아이디, 비밀번호를 가진 요청이 들어온다

    2. Form 로그인이면 UserDetailsService의 loadUserByUsername메서드가 실행되고

       OAuth2 로그인이면 OAuth2UserService의 loadUserByUsername메서드가 실행된다.

    3. loadUserByUsername메서드는 "이런 정보가 들어왔는데 얘 혹시 회원이야?" 라고 묻는 메서드이다.

        = loadUserByUsername에서는 회원을 찾아주는 로직을 구현하면된다.

    4. 이때, 회원정보는 Form 로그인이면 UserDetails타입으로, OAuth2 로그인이면 OAuth2User타입으로 반환해준다.

    5. UserDetails 또는 OAuth2User를 반환하면 Spring에서 알아서 Session에 저장해준다.

     

     

     ++ 

    FormLogin으로 로그인 하는 경우엔,

    사용자가 직접 회원가입을 거친 후 로그인하기때문에 loadUserByUsername에서 회원을 찾고 정보를 반환해주었다.

     

    OAuth2로 로그인 하는 경우엔,

    사용자가 회원가입을 하지 않기때문에 loadUserByUsername에서 회원을 찾고, 

    없는 회원이라면 회원가입 처리해주어야한다!! 

    있는 회원이라면 FormLogin과 동일하게 정보를 반환해주면된다.

     

     

     

     

    Spring Security - OAuth2 구글 로그인하기

    완성된 모습

     

     

     

     

    구글 설정

    https://console.cloud.google.com/apis/dashboard  접속

     

    Google Cloud Platform

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

    accounts.google.com

     

    1. 상단 프로젝트부분 클릭

     

    2. 새 프로젝트 클릭 > 만들기 클릭

     

    3. 방금 생성한 프로젝트 선택 > OAuth 동의화면 > 외부 > 만들기

     

    4. 1단계 필수부분 작성,

        2단계 범위 설정 건들지 않고 다음,

        3단계 테스트 사용자 건들지 않고 다음,

        4단계 요약 확인

     

    5. 사용자 인증정보 > 사용자 인증정보 만들기 클릭

     

    6. 애플리케이션 유형 : 웹 애플리케이션 선택, 승인된 리디렉션URI 입력

     

    승인된 리디렉션URI에는 "http://localhost:8080/login/oauth2/code/google" 을 입력한다. (빨간색은 정해진 내용이다)

    스프링 부트 공식문서에 나와있는 내용이다.

    https://docs.spring.io/spring-security/reference/servlet/oauth2/login/core.html

     

    7. 클라이언트ID와 보안 비밀번호를 기억해두자

     

     

     

    OAuth2 설정

    build.gradle > dependency 추가

    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:2.6.2'

     

    application.properties

    # google
    spring.security.oauth2.client.registration.google.client-id = 7번의 클라이언트ID 입력
    spring.security.oauth2.client.registration.google.client-secret = 7번의 클라이언트 비밀번호 입력
    spring.security.oauth2.client.registration.google.scope = profile, email

     

     

     

     

    패키지 > config 폴더 > SecurityConfig.java 

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired private PrincipalOauth2UserService principalOauth2UserService;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            
            http.authorizeRequests()
                    .antMatchers("/user/**").authenticated()     
                    .antMatchers("/manager/**").access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")	
                    .antMatchers("/admin/**").hasRole("ROLE_ADMIN")	
                    .anyRequest().permitAll()
          	  .and()					//추가
                .oauth2Login()				// OAuth2기반의 로그인인 경우
                    .loginPage("/loginForm")		// 인증이 필요한 URL에 접근하면 /loginForm으로 이동
                    .defaultSuccessUrl("/")			// 로그인 성공하면 "/" 으로 이동
                    .failureUrl("/loginForm")		// 로그인 실패 시 /loginForm으로 이동
                    .userInfoEndpoint()			// 로그인 성공 후 사용자정보를 가져온다
                    .userService(principalOauth2UserService);	//사용자정보를 처리할 때 사용한다
        }
    }
    .oauth2Login() OAuth2기반으로 로그인 할 경우의 설정을 추가할 수 있다.
    OAuth2LoginConfigurer를 불러온다
    .loginPage(String url) 로그인 페이지 경로를 호출한다. 
    .defaultSuccessUrl(String url) 로그인 성공 시 url로 이동한다.
    .failureUrl(String url) 로그인 실패 시 url로 이동한다.
    .userInfoEndpoint() 로그인 성공 후 사용자 정보를 가져온다
    .userService(Class) userInfoEndpoint()로 가져온 사용자 정보를 처리할 때 사용한다.

     

     

     

    OAuth2Login loadUserByUsername

    패키지 > auth 폴더 > PrincipalOauth2UserService.java

    OAuth2UserService ← DefaultOAuth2UserService (DefaultOAuth2UserService는 OAuth2UserService 를 구현한다)

    DefaultOAuth2UserService는 OAuth2로그인 시 loadUserByUsername메서드로 로그인한 유저가 DB에 저장되어있는지를 찾는다.

    OAuth2로 로그인하는 사용자는 회원가입을 거치지 않기 때문에 DB에 유저가 없다면 회원가입처리,

    유저가 있다면 Authentication(OAuth2User를 구현한 PrincipalDetails)를 반환하여 SecurityContextHolder에 저장할 수 있게 한다.

    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);
    
            String provider = userRequest.getClientRegistration().getRegistrationId();    //google
            String providerId = oAuth2User.getAttribute("sub");
            String username = provider+"_"+providerId;  			// 사용자가 입력한 적은 없지만 만들어준다
    
            String uuid = UUID.randomUUID().toString().substring(0, 6);
            String password = bCryptPasswordEncoder.encode("패스워드"+uuid);  // 사용자가 입력한 적은 없지만 만들어준다
    
            String email = oAuth2User.getAttribute("email");
            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, oAuth2User.getAttributes());
        }
    }

     

    ++ 참고

    public class OAuth2UserRequest {
    
    	private final ClientRegistration clientRegistration;
    	private final OAuth2AccessToken accessToken;
    	private final Map<String, Object> additionalParameters;	//이건 여기서 안쓴다
        ....
    }
    
    //인증서버(구글)의 정보를 가져온다
    userRequest.getClientRegistration();
    	결과 : ClientRegistration{
                    registrationId='google', 
                    clientId='~', 
                    clientSecret='~', 
                    clientAuthenticationMethod=org.springframework.security.oauth2.core.ClientAuthenticationMethod@4fcef9d3, 
                    authorizationGrantType=org.springframework.security.oauth2.core.AuthorizationGrantType@5da5e9f3, 
                    redirectUri='{baseUrl}/{action}/oauth2/code/{registrationId}', 
                    scopes=[profile, email], 
                    providerDetails=org.springframework.security.oauth2.client.registration.ClientRegistration$ProviderDetails@5b8ec6e5, 
                    clientName='Google'
                }
    
    //인증 토큰값을 가져온다
    userRequest.getAccessToken().getTokenValue();
    	결과 : Tokenvalue~~
    
    //유저의 정보를 가져온다
    super.loadUser(userRequest).getAttributes();
    	결과 : {
                    sub=~(PK 같은거), 
                    name=~, 
                    given_name=~, 
                    family_name=~, 
                    picture=~, 
                    email=~, 
                    email_verified=true, 
                    locale=ko
                }

     

     

     

    OAuth2Login Authentication OAuth2User

    패키지 > auth 폴더 > PrincipalDetails.java

    FormLogin 방법, OAuth2Login 방법 구분없이 한 객체로만 관리하기위해

    기존 PrincipalDetails에 OAuth2User도 implements한다

    package com.example.demo.auth;
    
    @Getter 
    @ToString
    public class PrincipalDetails implements UserDetails, OAuth2User {
    
        private User user;
        private Map<String, Object> attributes;
    
        //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;
        }
    
        /**
         * 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;
        }
    
        /**
         * OAuth2User 구현
         * @return
         */
        @Override
        public String getName() {
            String sub = attributes.get("sub").toString();
            return sub;
        }
    }

     

     

     

     

    OAuth2Login Domain구현

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

     

    패키지 > domain 폴더 > User.java 

    package com.example.demo.domain;
    
    @Entity
    @Getter @ToString
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public class User {
        @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        private String username;
    
        @Setter
        private String password;
    
        private String email;
    
        @Enumerated(EnumType.STRING)
        @Setter
        private Role role;
    
        @CreationTimestamp  //자동으로 만들어준다
        private Timestamp createTime;
    
        private String provider;    // oauth2를 이용할 경우 어떤 플랫폼을 이용하는지
        private String providerId;  // oauth2를 이용할 경우 아이디값
    
    
        @Builder(builderClassName = "UserDetailRegister", builderMethodName = "userDetailRegister")
        public User(String username, String password, String email, Role role) {
            this.username = username;
            this.password = password;
            this.email = email;
            this.role = role;
        }
    
        @Builder(builderClassName = "OAuth2Register", builderMethodName = "oauth2Register")
        public User(String username, String password, String email, Role role, String provider, String providerId) {
            this.username = username;
            this.password = password;
            this.email = email;
            this.role = role;
            this.provider = provider;
            this.providerId = providerId;
        }
    }

     

     

     

     

    OAuth2Login Controller 구현 (Controller에서 인증 객체 가져오기)

    패키지 > controller 폴더 > UserController.java

    package com.example.demo.controller;
    
    @Controller
    public class UserController {
        @Autowired private UserRepository userRepository;
        @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder;
    
       @GetMapping("/loginForm")
        public String loginForm(){
            return "login";
        }
    
        @GetMapping("/joinForm")
        public String joinForm(){
            return "join";
        }
    
        @PostMapping("/join")
        public String join(@ModelAttribute User user){
            user.setRole(Role.ROLE_USER);
    
            String encodePwd = bCryptPasswordEncoder.encode(user.getPassword());
            user.setPassword(encodePwd);
    
            userRepository.save(user);  //반드시 패스워드 암호화해야함
            return "redirect:/loginForm";
        }
    
        @GetMapping("/user")
        @ResponseBody
        public String user(){
            return "user";
        }
    
        @GetMapping("/manager")
        @ResponseBody
        public String manager(){
            return "manager";
        }
    
        @GetMapping("/admin")
        @ResponseBody
        public String admin(){
            return "admin";
        }
        
        
        
        // !!!! OAuth로 로그인 시 이 방식대로 하면 CastException 발생함
        @GetMapping("/form/loginInfo")
        @ResponseBody
        public String formLoginInfo(Authentication authentication, @AuthenticationPrincipal PrincipalDetails principalDetails){
    
            PrincipalDetails principal = (PrincipalDetails) authentication.getPrincipal();
            User user = principal.getUser();
            System.out.println(user);
            //User(id=2, username=11, password=$2a$10$m/1Alpm180jjsBpYReeml.AzvGlx/Djg4Z9/JDZYz8TJF1qUKd1fW, email=11@11, role=ROLE_USER, createTime=2022-01-30 19:07:43.213, provider=null, providerId=null)
    
            User user1 = principalDetails.getUser();
            System.out.println(user1);
            //User(id=2, username=11, password=$2a$10$m/1Alpm180jjsBpYReeml.AzvGlx/Djg4Z9/JDZYz8TJF1qUKd1fW, email=11@11, role=ROLE_USER, createTime=2022-01-30 19:07:43.213, provider=null, providerId=null)
            //user == user1
    
            return user.toString();
       }
        
        
        @GetMapping("/oauth/loginInfo")
        @ResponseBody
        public String oauthLoginInfo(Authentication authentication, @AuthenticationPrincipal OAuth2User oAuth2UserPrincipal){
            OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
            Map<String, Object> attributes = oAuth2User.getAttributes();
            System.out.println(attributes);
            // PrincipalOauth2UserService의 getAttributes내용과 같음
    
            Map<String, Object> attributes1 = oAuth2UserPrincipal.getAttributes();
            // attributes == attributes1
    
           return attributes.toString();     //세션에 담긴 user가져올 수 있음음
        }
        
        
        @GetMapping("/loginInfo")
        @ResponseBody
        public String loginInfo(Authentication authentication, @AuthenticationPrincipal PrincipalDetails principalDetails){
            String result = "";
    
            PrincipalDetails principal = (PrincipalDetails) authentication.getPrincipal();
            if(principal.getUser().getProvider() == null) {
                result = result + "Form 로그인 : " + principal;
            }else{
                result = result + "OAuth2 로그인 : " + principal;
            }
            return result; 
        }
    
    }

     

     

     

     

     

    OAuth2Login View구현

    resources > templates > join.html : FormLogin과 같음 

    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>
        <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>
    반응형
    • 네이버 블러그 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 구글 플러스 공유하기
    • 카카오톡 공유하기