SpringBoot Oauth Google Login - Security편

Juice🌱·2024년 1월 17일
1

SpringBoot

목록 보기
3/7
post-thumbnail

저번 velog에서 스프링부트 oauth2를 이용한 구글 로그인 기초 설정들에 대해 알아봤는데요. SpringBoot Oauth2 Google Login(Oauth2를 이용한 구글 로그인)

이번 velog에는 코드로 어떻게 구현하는지 알아가보면 좋을거 같습니다.

하기전에 잠깐 ✋

Spring Security?

Spring에 있는 프레임워크로 Security Filter로 인증(authentication), 인가(authorization)을 쉽게 할 수 있도록 도와주는 프레임워크

로그인 UI

프런트엔드 없이 백엔드만 만드는 관계로 UI는 mustache 간단하게 합시다:)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>LoginForm</title>
</head>
<body>
<form action="/login" method="post">
    <input type="text" name="username" placeholder="Username"> <br />
    <input type="password" name="password" placeholder="Password"> <br />
    <button type="submit">Login</button>
</form>
<a href="/oauth2/authorization/google">구글 로그인</a>
<a href="/joinForm">회원가입 아직 안하셨나요?</a>
</body>
</html>

저번 시간에 말했듯이 프론트엔드 쪽에서 구글로그인 버튼을 누르면 /oauth2/authorization/google 로 넘어오게 해야 합니다.

SecurityConfig.java

Oauth2로 로그인하는데, 세큐리티 사용하고 싶다? -> gradle에서

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

세큐리티 설정은 필수적입니다.

gradle 설정 완료 후,

config 폴더 생성, 안에 SecurityConfig.java 생성합니다.
(Spring Security ver.3입니다)

@Configuration
@EnableWebSecurity //security 지원 활성화
@EnableMethodSecurity(securedEnabled = true) //controller위에 secured 어노테이션 사용 가능하게 만듦
public class SecurityConfig{
  @Autowired
  private PrincipalOauth2UserService principalOauth2UserService;
  //Bean으로 등록해준다 -> 해당 메서드 리턴되는 오브젝트 IOC로 등록
  //password가 DB에 저장될 때, 그대로 저장되는것이 아닌 암호화되어서 저장된다.
  @Bean
  public BCryptPasswordEncoder encodePwd(){
      return new BCryptPasswordEncoder();
  }

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http)throws Exception{
      http.
              csrf(AbstractHttpConfigurer::disable); //다른 도메인에서 API호출되는거 막지 않겠다. Rest Api -> 브라우저 통해 request 받아서 꺼도 됨.
//                .cors(AbstractHttpConfigurer::disable); //이거 disable안하면 프런트에서 요청 보내면 response 안하고 에러 발생시킵니다
//                .sessionManagement(s->s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
      http.
              authorizeHttpRequests(au->
                      au.requestMatchers("/user").authenticated() //user로 시작하는 모든 요청은 인증이 되어야함
//                                Security3에서는 .accses.Role을 사용하지 않고 .hasRole을 사용, 기존에는 ROLE_을 붙여야 하지만 3에서는 자동적으로 ROLE_을 붙여준다
                              .requestMatchers("/manager").hasAnyRole("MANAGER","ADMIN") //manager로 시작하는 모든 요청은 ROLE_MANAGER, ROLE_ADMIN 권한이 있어야함
                              .requestMatchers("/admin").hasAnyRole("ADMIN") //admin으로 시작하는 모든 요청은 ROLE_ADMIN 권한이 있어야함
                              .anyRequest().permitAll() //그 외의 요청은 모두 허용
              );
      http.formLogin(f ->
                      f.loginPage("/loginForm")
                              .loginProcessingUrl("/login") // /login으로 호출오면 세큐리티가 낚아채서 로그인 진행
                              .defaultSuccessUrl("/") //loginForm으로 와서 로그인하면 /로 이동하는데, user로 와서 로그인하면 /user로 이동하게 설정
                              .permitAll())
              .httpBasic(AbstractHttpConfigurer::disable);

      http.oauth2Login(
              oauth -> oauth.loginPage("/loginForm") //구글 로그인이 완료된 뒤의 후처리. 엑세스토큰 + 사용자프로필정보 받아옴. 로그인 여기서 해야함
                      .defaultSuccessUrl("/home") //로그인 성공하고 URL Redirect
                      .userInfoEndpoint() //사용자 정보에 대한 엔드포인트 구성
                      .userService(principalOauth2UserService) //구글 로그인이 완료된 뒤의 후처리. 엑세스토큰 + 사용자프로필정보 받아옴
      );
      return http.build();
  }
}

oauthLogin부분에서 userInfoEndpoint().userService()부분은 로그인 하고 user의 accessToken과 프로필을 가져오는 로그인 후처리를 해주는 부분으로 코드로 우리가 일일이 code로 accessToken 가져오고, 그 accessToken으로 유저 정보를 가져오는것이 아니라 우리가 우리가 위에서 Autowired로 선언해준 principalOauth2UserService를 넣어주면 spring security에서 제공하는 oauthClient dependency로 자동적으로 유저 정보를 가져와서 편리하게 해결할 수 있습니다.

로그인 후처리하는 PrincipalOauth2UserService.java

@Service
@RequiredArgsConstructor
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
    //구글에서 받은 유저 데이타 후처리하는 함수
    
    @Autowired
    private UserRepo userRepo;
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        //userRequest에서는 구글에서 받은 유저 정보가 있다
        System.out.println("getClientRegistration : " + userRequest.getClientRegistration()); //어떤 OAuth로 로그인했는지 확인 가능

        //구글 로그인 버튼 클릭->구글 로그인 창->로그인 후 code리턴받고 이 code를 oauth client라이브러리가 받아서 access token 요청
        //userRequest 정보 -> loadUser 함수 호출 -> 구글로부터 회원프로필 받아준다
        OAuth2User oauth2User = super.loadUser(userRequest);
        System.out.println("attributes : " + oauth2User.getAttributes());

        //자동 회원가입
        String provider = userRequest.getClientRegistration().getClientId(); //google
        String providerId = oauth2User.getAttribute("sub"); //google의 primary key
        String username = provider + "_" + providerId; //google_sub -> 중복될 일 없음
        String email = oauth2User.getAttribute("email");
        String role = "ROLE_USER";

        User user= userRepo.findByUsername(username);
        if(user == null){
            user = User.builder()
                    .username(username)
                    .email(email)
                    .role(role)
                    .provider(provider)
                    .providerId(providerId)
                    .build();
            userRepo.save(user);
        }
        //oauth login하면 user와 attributes Map을 가지고 authentication을 만들어준다
        return new PrincipalDetails(user,oauth2User.getAttributes()); //PrincipalDetails가 Authentication에 들어간다
    }
}

여기까지 진행되었다면 UX상의로 유저가 구글 로그인에 성공을 했고, 구글 - > oauthClient(우리의 springboot 서비스)로 인증코드는 return해준 상태입니다.
우리가 할 일: 인증코드를 바탕으로 accessToken을 얻고, accessToken으로 user의 프로필 정보를 가져오자!
loadUser : OAuth2UserRequest 객체를 매개변수로 받습니다. 이 객체는 구글로부터 받은 사용자의 인증 정보를 포함하고 있고, loadUser 메서드는 이 인증 정보를 기반으로 구글에게 사용자 프로필 정보를 요청합니다.
super.loadUser() : 우리가 extends한 DefaultOAuth2UserService class의 loadUser메서드를 호출합니다. 이 코드로 인해 우리의 서비스가 userRequest안에 있는 인증 정보를 가지고 유저의 프로필 정보를 가져오는 역할을 합니다.
Oauth Login을 하면 구글에서는 OAuth2User 타입으로 되돌려줘서 OAuth2User type으로 받아줘야 합니다.
oauth2User : 이 객체 안에는 유저의 이름, 이메일, 프로필 url과 같은 정보들이 들어있습니다.

Login 성공하고 난 후


구글 로그인 성공하면 SpringSecurity에서 설정한 defaultSuccessUrl인 /home으로 이동한것을 볼 수 있고, 404에러는 UI가 없는건데, 이 부분은 제가 따로 UI를 만들지 않아서 나온 에러이고, 서버 에러는 아니니 걱정 안하셔도 됩니다. 두번째 사진은 인텔리제이 log에 찍힌 것을 가져왔는데 유저 프로필 정보도 잘 받아온 것을 알 수 있습니다.

Oauth2 Login 처리하는 Controller

// Oauth login은 PrincipalDetails/UserDetails 캐스팅 받을 수 없다
    @GetMapping("/test/oauth/login")
    public String testOauthLogin(Authentication authentication){ //DI로 PrincipalDetails를 받고, PrincipalDetails에는 User가 들어있다
        System.out.println("/test/oauth/login ===================");
        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
        System.out.println("authentication : " + oAuth2User.getAttributes()); //getAttributes -> user의 정보 Map<String,Object>로 받아온다

        return "Oauth Session Check";
    }

위 코드는 /loginForm에서 Google Login을 하고 로그인 하고, 정보를 잘 받아왔는지 확인하려고 만든 Api입니다.
/loginForm으로 구글 로그인을 한 후 /test/oauth/login 을 들어가봅니다.

Oauth를 이용해서 구글 로그인 잘 구현한걸 확인할 수 있습니다.

🤔 : 여기서 왜 Oauth2User 타입이 아닌 Authentication으로 받을까요??

Spring Security의 Security Session


Spring Security에서는 인증된 사용자 정보를 Security Session(Security Context)에 저장되는데, Security Session안에는 Authentication 객체밖에 들어가지 못합니다. 저장된 객체는 사용자의 HTTPSession에 저장되어 세션이 지속될 때까지 저장됩니다.

Session??
웹 서비스 - 서버가 상호작용을 일정시간동안 추적하는 방법.
일반적인 웹 HTTP Session은 일반적으로 Stateless(Request & Response) 한번 완료되면 종료됩니다. 하지만 많은 서비스는 사용자가 어떤 활동을 하는지 관측해하고, 페이지가 이동하더라도 유저의 정보를 사용해야하기 때문에 정보를 유지해야 하기 때문에 Session을 이용하는 것입니다.

Authentication에 유저 정보 넣고 DB에 저장 -

이제 oauth로 구글 로그인 받아오는 것은 완료되었습니다!

할 일 -> 받아온 정보 자동적으로 우리 DB에 넣어서 회원가입하기

🤔 : 왜??

😎 : 유저가 로그인하고 또 우리 서비스에서 회원가입 하는거는 UX상 비효율적이여서 자동 회원가입 해줍시다~

DB에 있는 User의 구조는 Oauth2User와 다르기 때문에 Oauth2User 객체를 DB에 그대로 넣을 수 없습니다. 아래 코드가 DB에 넣을 수 있는 타입입니다.

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String username;
    private String password;
    private String email;
    private String role;
    private String provider;    //google
    private String providerId;  //google의 id 이 두개로 oauth2로 로그인한 유저인지 판단
    private Timestamp loginDate;
    @CreationTimestamp
    private Timestamp createDate;
    }

PrincipalOauth2UserService.java 에서

//자동 회원가입
        String provider = userRequest.getClientRegistration().getClientId(); //google
        String providerId = oauth2User.getAttribute("sub"); //google의 primary key
        String username = provider + "_" + providerId; //google_sub -> 중복될 일 없음
        String email = oauth2User.getAttribute("email");
        String role = "ROLE_USER";

        User user= userRepo.findByUsername(username);
        if(user == null){
            user = User.builder()
                    .username(username)
                    .email(email)
                    .role(role)
                    .provider(provider)
                    .providerId(providerId)
                    .build();
            userRepo.save(user);
        }
        //oauth login하면 user와 attributes Map을 가지고 authentication을 만들어준다
        return new PrincipalDetails(user,oauth2User.getAttributes()); //PrincipalDetails가 Authentication에 들어간다
    }

provider,providerId,username,email,role을 Oauth2User에 있는 값들에서 전부 String으로 추출하고, builder를 이용해서 User객체로 만들고, saveJPA 쿼리문을 이용해 DB에 넣어줍니다.

PrincipalDetails.java

/loginForm으로 로그인이 완료되면, session에 Authentication객체가 들어간다고 했는데, Authentication객체 안에는 User정보 넣어주는 코드도 구현해줘야 합니다.

@Data
public class PrincipalDetails implements UserDetails, OAuth2User {
    //loadByUser실행될 때 PrincipalDetails가 Authentication에 들어간다
    private User user; //composition
    private Map<String,Object> attributes; //OAuth인증 시 사용되는 사용자의 추가 정보를 저장
    //생성자에서 user 받아서 PrincipalDetails에 넣어준다
    public PrincipalDetails(User user){
        this.user = user;
    }

    //Oauth2로 로그인을 하면 사용하는 생성
    //attributes로 user를 만들어준다
    public PrincipalDetails(User user,Map<String,Object> attributes)
    {
        this.user = user;
        this.attributes = attributes;
    }
    
    //아래는 인터페이스 구현하는데 필요한 추가 메서드들
    @Override
    public Map<String, Object> getAttributes() {
        return attributes;
    }

    //해당 User의 권한을 리턴하는 곳
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(new GrantedAuthority(){
            @Override
            public String getAuthority() {
                return user.getRole();
            }
        });
        return collection;
    }
    @Override
    public String getUsername() {
        return user.getUsername();
    }
 }
  • UserDetails, OAuth2User를 implements하는 이유
    -> Authentication 객체 안에는 UserDetails, OAuth2User 이 두개의 타입만 들어갈 수 있습니다. 따라서 Spring Security가 일반로그인과 소셜 로그인 모두 처리할 수 있게 설정해줘야 합니다.

  • OAuth와 같은 소셜로그인이 아니라 서비스 내 로그인과 회원가입이라면 ?UserDetails

  • OAuth를 이용한 소셜 로그인 서비스라면? OAuth2User
    이 두개의 타입만 Authentication안에 User객체를 넣어줍니다. HTTP 요청이 들어오면 Security는 Authentication을 찾는데, Authentication에 들어오는 값이 위 두개의 type이 아니라면 잘못된 type이라고 에러가 발생합니다.

여기까지 하면 OAuth를 이용한 스프링부트 소셜 로그인 구현 끝입니다! 🎉

profile
선한 영향력으로 세상을 변화시키는 새싹개발자

0개의 댓글

관련 채용 정보