[Spring security] OAuth2 로그인(SNS Login 관리)

WOOK JONG KIM·2022년 12월 10일
0

패캠_java&Spring

목록 보기
96/103
post-thumbnail

CommonOAuth2Provider

개발자가 Provider에게 Client 등록을 해야 사용자들에 대한 정보를 받을 수 있다

등록하는 장소

OKTA

추가 가능한 OAuth2Provider

OAuth2User

facebook, naver, kakao

  • OAuth2User : UserDetails 를 대체
  • OAuth2UserService : UserDetailsService 를 대체
    -> 기본 구현체는 DefaultOAuth2UserService

OAuth2 Provider의 Resource Server에는 사이트에 가입한 사용자 정보(ex: 아이디, 성별, 생일, 생년월일, 전화번호 등)가 들어있다

개발자가 보통 구현하는 것은 Client Server(인증 역할X)

-> Provider에게 사용자 정보를 달라고 요청(이 정보를 바탕으로 관리할려고 하는 것)
-> 로그인을 Provider에게 위임을 하고 인증이 완료되면 해당 사이트의 Provider의 Authentication Server가 사용자에게 3rd Party Server가 너에 대한 정보를 요청했어? 제공할래?
-> 제공을 하겠다 하면 AuthenticationServer가 서비스 사용자에게 키를 주고 이 키로 3rd Party Server는 Resource Server를 찾아가 정보를 요청 함
-> 우리 사용자가 당신에게 이 정보 가져가도 된데! 가져갈게!
-> 데이터를 주면 3rd Party Server는 DB에 사용자 정보를 저장
-> 이후 서비스를 이용하는 사용자가 똑같이 Provider를 통해서 로그인을 해주면 이 사용자는 기존에 등록되어 있던 사용자와 연결된 자신의 개인 정보를 가지고 서비스를 하게 되는 것

OidcUser

구글은 OAuth2가 아닌 OidcUser 방식의 서비스를 함
-> 이는 OAuth2를 기반으로 발전된 방법


코드 예시

application.yml

server:
  port: 9061

## 구글같은 경우에는 provider는 이미 등록이 되어있다
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: 클라이언트 아이디 넣기
            client-secret: 클라이언트 secret 키

        ## 네이버의 경우에는 프로바이더 등록해야함

#        provider:
#          naver:
#          kakao:

config

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SnsLoginSecurityConfig extends WebSecurityConfigurerAdapter {

    private OidcUserService oidcUserService; // 원리 이해할려면 이 코드 분석
    private OidcAuthorizationCodeAuthenticationProvider oidcAuthorizationCodeAuthenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .oauth2Login(); // oauth2 로그인 필터 동작
    }
}

controller

@RestController
public class HomeController {

    @PreAuthorize("isAuthenticated()")
    @GetMapping("/greeting")
    // OAuth2로 로그인을 하게 되면 userPrincipal에 OAuth2User가 오게 됨
    public OAuth2User greeting(@AuthenticationPrincipal OAuth2User user){
        return user;
    }
}


greeting이라는 서비스를 할려고 들어가면 구글 로그인으로 리다이렉 됨
-> OAuth2로그인을 제공할 수 있는 Provider는 현재 구글만 등록


코드 예시2

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name = "sp_oauth2_user")
public class SpOAuth2User {

    // 사이트를 통해 들어온 사용자의 정보를 이곳에 저장

    @Id
    private String oauth2UserId; // google-{id}, naver-{id} 이렇게 유니크한 아이디를 키 값으로 사용

    private Long userId; // SpUser

    private String name;
    private String email;
    private LocalDateTime created;
    private Provider provider;

    // 어떤 사용자 인지
    public static enum Provider{
        google{
            public SpOAuth2User convert(OAuth2User user){
                return SpOAuth2User.builder()
                        .oauth2UserId(format("%s_%s", name(), user.getAttribute("sub")))
                        .provider(google)
                        .email(user.getAttribute("email"))
                        .name(user.getAttribute("name"))
                        .created(LocalDateTime.now())
                        .build();
            }
        },
        naver{
            public SpOAuth2User convert(OAuth2User user){
                    Map<String, Object> resp = user.getAttribute("response");
                    return SpOAuth2User.builder()
                            .oauth2UserId(format("%s_%s", name(), resp.get("id")))
                            .provider(naver)
                            .email(""+resp.get("email"))
                            .name(""+resp.get("name"))
                            .build();
                }
            };

        public abstract SpOAuth2User convert(OAuth2User userInfo);

    }
}
public interface SpOAuth2Repository extends JpaRepository<SpOAuth2User, String> {
}
@Service
@Transactional
public class SpUserService implements UserDetailsService {

    @Autowired
    private SpUserRepository userRepository;

    @Autowired
    private SpOAuth2Repository spOAuth2Repository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findUserByEmail(username).orElseThrow(
                ()->new UsernameNotFoundException(username));
    }

    public Optional<SpUser> findUser(String email) {
        return userRepository.findUserByEmail(email);
    }

    public SpUser save(SpUser user) {
        return userRepository.save(user);
    }

    public void addAuthority(Long userId, String authority){
        userRepository.findById(userId).ifPresent(user->{
            SpAuthority newRole = new SpAuthority(user.getUserId(), authority);
            if(user.getAuthorities() == null){
                HashSet<SpAuthority> authorities = new HashSet<>();
                authorities.add(newRole);
                user.setAuthorities(authorities);
                save(user);
            }else if(!user.getAuthorities().contains(newRole)){
                HashSet<SpAuthority> authorities = new HashSet<>();
                authorities.addAll(user.getAuthorities());
                authorities.add(newRole);
                user.setAuthorities(authorities);
                save(user);
            }
        });
    }

    public void removeAuthority(Long userId, String authority){
        userRepository.findById(userId).ifPresent(user->{
            if(user.getAuthorities()==null) return;
            SpAuthority targetRole = new SpAuthority(user.getUserId(), authority);
            if(user.getAuthorities().contains(targetRole)){
                user.setAuthorities(
                        user.getAuthorities().stream().filter(auth->!auth.equals(targetRole))
                                .collect(Collectors.toSet())
                );
                save(user);
            }
        });
    }

    public SpUser load(SpOAuth2User oAuth2User){
        SpOAuth2User dbUser = spOAuth2Repository.findById(oAuth2User.getOauth2UserId())
                // 사용자가 등록되어있지 않다면 직접 Db에 등록하고 가져와야 함
                .orElseGet(() -> {
                    SpUser user = new SpUser();
                    // 같은 사용자여도 어떤 플랫폼을 통해 로그인을 했냐에 따라 다른 사용자로 가정
                    user.setEmail(oAuth2User.getEmail());
                    user.setName(oAuth2User.getName());
                    user.setEnabled(true);
                    userRepository.save(user);
                    addAuthority(user.getUserId(), "ROLE_USER");

                    oAuth2User.setUserId(user.getUserId());
                    return spOAuth2Repository.save(oAuth2User);
        });
        return userRepository.findById(dbUser.getUserId()).get();
    }
}

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SnsLoginSecurityConfig extends WebSecurityConfigurerAdapter {

//    @Autowired
//    private SpOAuth2UserService oAuth2UserService;
//
//    @Autowired
//    private SpOidcUserService OidcUserService;

    @Autowired
    private SpOAuth2SuccessHandler successHandler; // 이를 통해 유저를 맵핑

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .oauth2Login(oauth2->oauth2
//                        .userInfoEndpoint(
//                            userInfo->userInfo.userService(oAuth2UserService)
//                            .oidcUserService(OidcUserService)
//                        ).successHandler()
                                .successHandler(successHandler)
                );
    }
}
@Component
public class SpOAuth2SuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private SpUserService userService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException
    {
        Object principal = authentication.getPrincipal();
        if(principal instanceof OidcUser){
            // OidcUser가 하위 객체라 먼저 살펴봐야 함
            // 구글 사용자
            SpOAuth2User oauth = SpOAuth2User.Provider.google.convert((OidcUser) principal);
            SpUser user = userService.load(oauth);
            SecurityContextHolder.getContext().setAuthentication(
                    new UsernamePasswordAuthenticationToken(user, "", user.getAuthorities())
            );

        }else if(principal instanceof OAuth2User){
            // naver 사용자

            SpOAuth2User oauth = SpOAuth2User.Provider.naver.convert((OAuth2User) principal);
            SpUser user = userService.load(oauth);
            SecurityContextHolder.getContext().setAuthentication(
                    new UsernamePasswordAuthenticationToken(user, "", user.getAuthorities())
            );
        }
        request.getRequestDispatcher("/").forward(request, response); // 루트 페이지로 강제 리다이렉
    }
}
@Component
public class SpOAuth2UserService extends DefaultOAuth2UserService {

    // 유저 정보가(request) 오면 여기서 OAuth2User를 로딩하는 부분 추가
    // 페이스북, 카카오와 같은 벤더들은 OAuth2 User 스펙을 따르기에 이에 대한 정보를 줌
    // 구글은 Odic User 스펙을 따름
    // loadUser메서드를 추가해주면,  OAuth or Oidc 유저를 통해 사용자가 들어왔을 때 이쪽으로 들어옴
    // 여기서 사용자를 SpUser 사용자로 변환을 하거나 등록하는 과정 거칠수도 있음

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        return super.loadUser(userRequest);
    }
}
@Component
public class SpOidcUserService extends OidcUserService {

    @Override
    public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
        return super.loadUser(userRequest);
    }
}
@RestController
public class HomeController {

    @PreAuthorize("isAuthenticated()")
    @GetMapping("/")
    // OAuth2로 로그인을 하게 되면 userPrincipal에 OAuth2User가 오게 됨
    public Object greeting(@AuthenticationPrincipal Object user){
        return user;
    }
}
profile
Journey for Backend Developer

0개의 댓글