본문 바로가기
IT/Spring boot

[Spring boot] Spring Security OAuth 구현

by kyu-nahc 2024. 9. 8.
반응형

 

Spring Security / OAuth

이전 포스트에서는 Spring Security의 기본적인 세션 인증 방식으로 구현해 보았다.

또한 OAuth의 기본 개념 및 프로세스에 대해서도 정리하였다.

Spring Security의 기본 구현 및 OAuth에 대한 내용은 아래 포스트를 참고하면 된다.

 

[Spring boot] Spring Security 기본 구현

Spring Security이전 포스트에서는 Spring Security의 기본 구조에 대해서 살펴보았다.Spring Security의 기본 구조에 대한 내용은 아래 포스트를 참고하면 된다. [Spring boot] Spring Security 구조 이해Spring Security

kyu-nahc.tistory.com

 

 

[Server] OAuth의 개념 및 프로세스

OAuth란?먼저 OAuth의 정의부터 살펴보면 다음과 같다. OAuth(”Open Authorization”)는 인터넷 사용자들이 비밀번호를 제공하지 않고,다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플

kyu-nahc.tistory.com

 

해당 포스트에서는 Spring Security를 사용하여 OAuth 소셜 로그인을 구현할 것이다.

소셜 로그인 대상으로는 Google / Naver / Kakao 로그인을 진행할 것이다.

OAuth 소셜 로그인도 기본 구현과 같은 메커니즘으로 작동하며 소셜 로그인이 추가된 것뿐이다.

먼저 개발 환경부터 살펴보면 다음과 같다.

 

개발 환경

  • Spring boot 3.3.3
  • Java 21
  • Gradle
  • Spring Security / Jpa / OAuth2 Client
  • Maria Database

주요 개발 환경은 다음과 같고, gradle 의존성은 다음과 같다.

 

build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

 

여기서 한 가지 살펴볼 것은 OAuth2 Client의 의존성 사용이다.

OAuth의 프로세스에서 본 것처럼 Authorization Code를 통해

AccessToken을 요청하는 것은 Server 개발자의 역량이었다.

하지만 OAuth2 Client 의존성을 사용한다면 해당 과정을 Spring이 직접 처리해 준다.

즉 OAuth2 Client는 Provider라는 것을 제공하는데,

이 Provider에 이미 등록되어 있는 Google, Facebook, Github와 같은 것은

Client ID, Secret, Redirect URI만 등록해 주면 AccessToken을 발급받는 과정까지

처리를 해주고 AccessToekn을 발급받은 후 처리할 로직 콜백함수만 작성하면 된다.

Naver, Kakao 등은 Provider로 등록되어있지 않으므로, 직접 등록해줘야 한다.

 

application.properties

이제 application.properties 부분이다.

각 소셜마다 Client ID, Secret, Redirect URI를 등록해 준 후,

Client ID / Secret / Redirect URI를 이용하여 application.properties를 작성해야 한다.

먼저 Provider로 등록되어 있는 Google은 다음과 같이 작성해 주면 된다.

# Oauth Google
spring.security.oauth2.client.registration.google.client-id={GOOGLE_CLIENT_ID}
spring.security.oauth2.client.registration.google.client-secret={GOOGLE_SECRET}
spring.security.oauth2.client.registration.google.scope=profile,email

 

Provider로 등록되어 있으므로 발급받은 Client ID와 Secret을 등록하고

내가 사용할 사용자 정보에 대해서만 명시해 주면 된다.

해당 예제에서는 이메일과 간단한 프로필 정보만 사용하므로 profile, email이라고 명시해 준다.

다음으로 직접 Provider로 등록해줘야 하는 Kakao와 Naver이다.

# Oauth Naver
spring.security.oauth2.client.registration.naver.client-id=${NAVER_CLIENT_ID}
spring.security.oauth2.client.registration.naver.client-secret=${NAVER_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

# Oauth Naver 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


# Oauth Kakao Provider
spring.security.oauth2.client.registration.kakao.client-id=${KAKAO_CLIENT_ID}
spring.security.oauth2.client.registration.kakao.client-secret=${KAKAO_SECRET}
spring.security.oauth2.client.registration.kakao.scope=account_email,profile_nickname
spring.security.oauth2.client.registration.kakao.client-name=Kakao
spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/login/oauth2/code/kakao

# Oauth Kakao Provider
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

 

Google과 다르게 Provider 설정이 많아진 것을 볼 수 있다.

마찬가지로 Client ID / Secret / Scope는 동일하에 등록해 주고,

추가적으로 사용할 Resouce Sever의 이름, 인증 방법, 타입까지 설정이 필요하다.

여기서 Redirect URI는 반드시 Kakao와 Naver에서 설정한 Redirect URI와 동일해야 한다.

그 후 Authorization Code를 통해 AccessToken을 자동으로 발급받은 후

바로 사용자 정보를 접근하기 위해 추가적으로 설정을 해주어야 한다.

이것이 바로 token-uri / authorization-uri이다.

물론 해당 uri를 통해 직접 Rest Api를 요청하여,

AccessToken을 발급받은 후 사용자 정보를 요청해도 되지만,

Provider를 등록할 경우 콜백함수만 작성하면 되므로 훨씬 편리하게 소셜 로그인을 구현할 수 있다.

 

 

PrincipalDetails / PrincipalDetailsService

다음으로 PrincipalDetailsPrincipalDetailsService이다.

해당 클래스 객체에 대한 설명은 Spring Security 기본 구현에 자세히 나와있다.

단 여기서 주목할 점은 PrincipalDetails에서 기본 구현과 다르게 2개의 인터페이스를 구현한다.

소셜 로그인을 동반한다면, 소셜 로그인을 사용하여 로그인하는 회원과,

웹 애플리케이션의 Form Login을 사용하여 로그인하는 사용자 두 개의 분류가 존재한다.

따라서 UserDetails와 OAuth2User 2개의 인터페이스를 모두 구현하여,

어떤 사용자 분류든지 SecurityContextHolder에 PrincipalDetails를 저장하도록 구현해 준다.

PrincipalDetailsService의 로직은 기존과 동일하다.

@Data
public class PrincipalDetails implements UserDetails, OAuth2User {

    private Member member;
    private Map<String,Object> attributes;

    public PrincipalDetails(Member member){
        this.member = member;
    }
    public PrincipalDetails(Member member,Map<String,Object> attributes){
        this.member = member;
        this.attributes = attributes;
    }
    @Override
    public Map<String, Object> getAttributes() {
        return attributes;
    }
    @Override
    public String getName() {
        return null;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        member.getRoleList().forEach(role -> authorities.add(() -> role));
        return authorities;
    }
    @Override
    public String getPassword() {
        return member.getPassword();
    }

    @Override
    public String getUsername() {
        return member.getUserId();
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<Member> memberEntity = memberRepository.findByUserId(username);
        return memberEntity.map(PrincipalDetails::new).orElse(null);
    }
}

 

OauthUserInfo Interface 및 구현체

OAuth 객체에 대한 추상화를 인터페이스를 하나 정의한 후, 이에 대한 구현체 3개를 만들어준다.

Resource Server 마다 제공하는 사용자 데이터의 형태가 다르기 때문에,

공통로직을 추상화하기 위한 인터페이스 한 개와

이에 대한 구현체를 Resource Server마다 한 개씩 생성하는 것이다.

public interface OauthUserInfo {
    String getProviderId();
    String getProvider();
    String getEmail();
    String getName();
}
public class GoogleUserInfo implements OauthUserInfo{

    private final Map<String,Object> attributes;

    public GoogleUserInfo(Map<String,Object> attributes){
        this.attributes = 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();
    }
}
public class KakaoUserInfo implements OauthUserInfo{

    private final Map<String,Object> attributes;

    public KakaoUserInfo(Map<String,Object> attributes){
        this.attributes = attributes;
    }

    @Override
    public String getProviderId() {
        return attributes.get("id").toString();
    }

    @Override
    public String getProvider() {
        return "kakao";
    }

    @Override
    public String getEmail() {
        return (String) ((Map<?, ?>) attributes.get("kakao_account")).get("email");
    }

    @Override
    public String getName() {
        return (String) ((Map<?, ?>) attributes.get("properties")).get("nickname");
    }
}
public class NaverUserInfo implements OauthUserInfo{

    private final Map<String,Object> attributes;

    public NaverUserInfo(Map<String,Object> attributes){
        this.attributes = attributes;
    }

    @Override
    public String getProviderId() {
        return attributes.get("id").toString();
    }

    @Override
    public String getProvider() {
        return "naver";
    }

    @Override
    public String getEmail() {
        return attributes.get("email").toString();
    }

    @Override
    public String getName() {
        return attributes.get("name").toString();
    }
}

 

모두 생성자를 통해 Map<String,Object> 변수를 파라미터로 받아서, 멤버변수로 할당해 준다.

단 각 Resource Server마다 제공해 주는 key-value 값이 다르기 때문에,

제공해 주는 데이터 값을 확인한 후 Parsing을 진행하여 원하는 데이터를 추출할 필요가 있다.

따라서 현재 예제에서는 3개의 소셜 로그인을 사용하므로,

3개의 인터페이스 구현체를 생성하여 Parsing 로직을 구현한 것을 볼 수 있다.

 

다음으로 OAuth 프로세스에서 Authroization Code를 통해

AccessToken으로 발급받은 후, 사용자의 프로필까지 받았을 때 실행될 함수에 대한 코드이다.

 

PrinicipalOauthDetailsService

@Service
@RequiredArgsConstructor
@Slf4j
public class PrincipalOauthDetailsService extends DefaultOAuth2UserService {

    private final MemberService memberService;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest)
            throws OAuth2AuthenticationException {

        OAuth2User oAuth2User = super.loadUser(userRequest);
        OauthUserInfo oauth2UserInfo = null;
        if (userRequest.getClientRegistration().getRegistrationId().equals("google")){
            oauth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes());
        }else if (userRequest.getClientRegistration().getRegistrationId().equals("naver")){
            oauth2UserInfo = new NaverUserInfo((Map<String, Object>) oAuth2User.getAttributes().get("response"));
        }else if (userRequest.getClientRegistration().getRegistrationId().equals("kakao")){
            oauth2UserInfo = new KakaoUserInfo(oAuth2User.getAttributes());
        }

        assert oauth2UserInfo != null;
        String provider = oauth2UserInfo.getProvider();
        String providerId = oauth2UserInfo.getProviderId();
        String email = oauth2UserInfo.getEmail();
        String name = oauth2UserInfo.getName();
        String userId = provider+"_"+providerId;

        Member checkMember = memberService.findByUserIdProcess(userId);
        if (checkMember == null){
            // Oauth 로그인 강제 회원가입 진행
            checkMember = Member.builder()
                    .name(name)
                    .userId(userId)
                    .password("")
                    .email(email)
                    .roles("ROLE_USER")
                    .provider(provider)
                    .providerId(providerId)
                    .build();
            memberService.joinOauthProcess(checkMember);
        }
        return new PrincipalDetails(checkMember,oAuth2User.getAttributes());
    }
}

 

그 전의 로그인 과정을 정리하면 다음과 같다.

위의 로직은 소셜 로그인이 완료된 뒤 후처리 하는 로직이다.

  1. 사용자가 구글 로그인 버튼을 누른 후 직접 아이디와 패스워드를 입력 후
    구글 계정을 통해 로그인을 완료한다.
  2. Resource Server ( Google )은 인증 코드 Authorization Code를 제공해 준다.
  3. 추가 권한을 받기 위해 Authorization Code를 통해 Access Token을 요청한다.
  4. Resource Server ( Google )에서 AccessToken을 발급해 준다.
  5. AccessToken을 발급받은 후 해당 자격증명 토큰을 통해 사용자 프로필 정보를 접근

이때 Provider로 등록을 해주었기 때문에, 2~5번의 과정이 Spring boot에 의해 처리된 것이다.

따라서 해당 매개변수로 받은 OAuth2UserRequest 객체에는

AccessToken 및 사용자 프로필 정보 값이 모두 들어가 있는 것이다.

따라서 이에 대한 후처리 함수만 작성하면 된다.

이때 후처리 함수의 로직을 설명하면 다음과 같다.

OAuth2User oAuth2User = super.loadUser(userRequest);

 

해당 구문을 통해 OAuth2User 객체를 받아온 후

해당 객체를 통해 AccessToken 및 사용자 프로필 정보에 접근할 수 있다.

이때 OAuth2User.getAttributes()를 통해 Key-Value값으로 사용자 데이터에 접근 가능하다.

userRequest.getClientRegistration().getRegistrationId().equals("google")

 

해당 구문을 통해 로그인한 유저의 소셜 종류를 알 수 있으며,

소셜 로그인 종류에 따라 google, Naver, Kakao에 따라

OauthUserInfo 인터페이스 구현체 객체를 생성해 준다.

그 후 사용자 객체 생성에 필요한 이메일, 이름, Provider 정보들을 Parsing 하여 추출해 낸다.

 

PrincipalOauthDetailsService에서 getProvider()는 google, Naver, Kakao 등

Provider의 종류를 나타내며, ProviderId는 해당 Provider의 전체 사용자를 구분하기 위한 Id이다.

즉 네이버에서도 굉장히 많은 사용자가 존재하며, 이를 구분해야 하는 Primary_Key가 존재할 것이다.

이 Primary_Key를 추출하는 것이 getProviderId() 메소드이고

이 Primary_Key값과 Provider를 이용하여 UserId로 등록해 준다.

이를 통해 내가 개발하는 서버에서 유일한 UserId값으로 정의가 되는 것이다.

이를 통해 강제회원가입을 진행할 수 있다.

나의 웹 애플리케이션에 회원가입을 시키는 것이다.

만약 UserId를 통해 Entity를 찾고 없으면 회원가입을 진행한다.

마지막으로 OAuth2User 인터페이스를 구현한

PrincipalDetails 객체를 return 해주면 후처리 로직이 완료된다.

 

SecurityConfig / EncodeConfig

@Configuration
public class EncodeConfig {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {

    private final PrincipalOauthDetailsService principalOauthDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

        String loginErrorMessage = URLEncoder.encode(ErrorEncodedString.INVALID_CREDENTIALS.getMessage(), StandardCharsets.UTF_8);

        http
                .csrf(AbstractHttpConfigurer::disable)
                .headers((headerConfig) ->
                        headerConfig.frameOptions(
                                HeadersConfigurer.FrameOptionsConfig::disable
                        )
                )
                .authorizeHttpRequests((authorizeRequests) ->
                        authorizeRequests
                                .requestMatchers("/auth/**","/public/**",
                                "/css/**","/js/**").permitAll()
                                .requestMatchers("/api/user/**").authenticated()
                                .requestMatchers("/api/manager/**").hasAnyRole("ADMIN", "MANAGER")
                                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                                .anyRequest().authenticated()
                )
                .exceptionHandling((exceptionConfig) ->
                        exceptionConfig
                                .accessDeniedHandler((request, response, accessDeniedException) -> {
                                    response.sendRedirect("/public/except");
                                })
                )
                .formLogin((formLogin) ->
                        formLogin
                                .loginPage("/public/login")
                                .loginProcessingUrl("/auth/sign-in")
                                .usernameParameter("userId")
                                .passwordParameter("password")
                                .defaultSuccessUrl("/public/main",true)
                                .failureUrl("/public/login?error=true&errorMessage="+loginErrorMessage)
                                .permitAll()
                )
                .logout((logoutConfig) ->
                        logoutConfig
                            .logoutUrl("/auth/logout")
                            .deleteCookies("JSESSIONID")
                            .invalidateHttpSession(true)
                            .logoutSuccessUrl("/public/main")
                )
                .oauth2Login((oauthLogin) ->
                        oauthLogin
                                .loginPage("/public/login")
                                .userInfoEndpoint(userInfoEndpointConfig ->
                                        userInfoEndpointConfig.userService(principalOauthDetailsService))
                                .failureUrl("/public/login?error=true&errorMessage="+loginErrorMessage)
                                .defaultSuccessUrl("/public/main",true)
                                .permitAll()
                );
        return http.build();
    }
}

 

다음으로 SecurityFilterChain 등록이다. 이는 기본구현의 Chaining과 비슷하며,

.oauth2Login()이 추가되었다. 이도 마찬가지로 직접 구현한 로그인 페이지가 존재한다면,

.loginPage()를 통해 등록해 주고, 성공 및 실패 시 redirect 할 Url까지 등록할 수 있다.

단 여기서 Provider를 통해 AccessToken 발급 및 사용자 프로필 정보까지 받았을 때,

이를 후처리 할 함수를 가지고 있는 Service 객체를 등록해주어야 한다.

이 Service 객체가 위에서 설명한 PrinicipalOauthDetailsService인 것이다.

다음으로 컨트롤러 및 서비스 계층의 로직이다.

 

MemberService

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    public boolean isExistUserIdProcess(String userId){
        return memberRepository.existsByUserId(userId);
    }

    @Transactional(readOnly = true)
    public Member findByUserIdProcess(String userId){
        Optional<Member> targetEntity = memberRepository.findByUserId(userId);
        return targetEntity.orElse(null);
    }

    public String joinProcess(Member newMember){
        if (isExistUserIdProcess(newMember.getUserId())){
            return "ID_DUPLICATED";
        }
        String encodePassword = passwordEncoder.encode(newMember.getPassword());
        newMember.setRoles("ROLE_USER");
        newMember.setPassword(encodePassword);
        newMember.setProvider("Form-Login");
        newMember.setProviderId("Form-Login"+newMember.getUserId());
        memberRepository.save(newMember);
        return "Join Success";
    }

    @Transactional
    public void joinOauthProcess(Member newMember) {
        String encodePassword = passwordEncoder.encode(newMember.getPassword());
        newMember.setPassword(encodePassword);
        memberRepository.save(newMember);
    }
}

 

서비스 계층인 MemberService 로직에서는 회원가입, 아이디 중복체크,

UserId를 통한 Entity 객체 탐색 로직 3개가 존재한다.

회원가입에서는 PasswordEncoder를 통해 비밀번호를 인코딩하여 데이터베이스에 저장한다.

OAuth를 이용하는 회원가입과 일반 Form Login을 사용하는 회원가입에는 차이가 존재한다.

OAuth를 이용할 경우 사용자 객체의 Provider와 ProviderId는 OAuth 후처리 함수에서 처리되지만,

Form Login에서는 후처리 함수 PrinicipalDetails가 아닌 MemberService에서 진행하도록 구현하였다.

 

MainController / ApiController

@Controller
@RequiredArgsConstructor
public class MainController {

    private final MemberService memberService;

    @GetMapping("/public/login")
    public String loginPage(
            @RequestParam(value="errorMessage",required=false) String errorMessage,
            @RequestParam(value = "error", required = false, defaultValue = "false") boolean error,
            Model model){
        if (error) {
            model.addAttribute("errorMessage", errorMessage);
        }
        return "login.html";
    }

    @GetMapping("/public/main")
    public String index(){
        return "main.html";
    }

    @GetMapping("/public/join")
    public String join(
            @RequestParam(value="errorMessage",required=false) String errorMessage,
            @RequestParam(value = "error", required = false, defaultValue = "false") boolean error,
            Model model){
        if (error) {
            model.addAttribute("errorMessage", errorMessage);
        }
        return "join.html";
    }

    @GetMapping("/public/except")
    @ResponseBody
    public String except(){
        return "Not Access This Page";
    }

    @PostMapping("/auth/join")
    public String joinProc(Member member){
        String result = memberService.joinProcess(member);
        String errorMessage;
        if (result.equals("Join Success")){
            return "redirect:/public/login";
        }else if (result.equals("ID_DUPLICATED")){
            errorMessage = URLEncoder.encode(ErrorEncodedString.ID_DUPLICATED.getMessage(), StandardCharsets.UTF_8);
        }else{
            errorMessage = URLEncoder.encode(ErrorEncodedString.FAIL_JOIN.getMessage(), StandardCharsets.UTF_8);
        }
        return "redirect:/public/join?error=true&errorMessage="+errorMessage;
    }
}
@RestController
@RequestMapping("/api")
@Slf4j
public class ApiController {

    @GetMapping("/user")
    public String user(@AuthenticationPrincipal PrincipalDetails userDetails){
        log.info(userDetails.getUsername());
        return "user";
    }

    @GetMapping("/manager")
    public String manager(@AuthenticationPrincipal PrincipalDetails userDetails){
        log.info(userDetails.getUsername());
        return "manager";
    }

    @GetMapping("/admin")
    public String admin(@AuthenticationPrincipal PrincipalDetails userDetails){
        log.info(userDetails.getUsername());
        return "admin";
    }
}

 

다음으로 컨트롤러 로직이다.

MainController에서는 기본적인 인증이 필요 없는 View를 제공하는 컨트롤러이다.

위에서 설명한 loginPage(), failureUrl(), accessDeniedHandler()에

등록한 Url의 Url Pattern 들의 처리를 담당하는 컨트롤러이다.

 

ApiController에서는 사용자의 권한에 따라 Api에 접근할 수 있다.

또한 기본 구현에서 살펴보았듯이 @AuthenticationPrincipal를 통해

컨트롤러에서 바로 인증이 완료된 객체를 접근하도록 할 수 있다.

SecurityContextHolder의 Session은 이용하기 때문에 세션 인증 객체에 접근가능한 것이다.

 

Login.html

<body>
    <h1>로그인</h1>
    <div class="login-wrapper">
        <form action="/auth/sign-in" method="post">
            <label for="userId"> User Id </label> <br>
            <input id="userId" type="text" name="userId" placeholder="User Id"/> <br>

            <label for="password"> User Password </label> <br>
            <input id="password" type="password" name="password" placeholder="User Password"> <br>
            <button>로그인</button>
            <div class="error-message-wrapper" th:if="${errorMessage != null}">
                <p class="error-message" th:text="${errorMessage}"></p>
            </div>
        </form>
        <a href="/oauth2/authorization/google">구글 로그인</a>
        <a href="/oauth2/authorization/naver">네이버 로그인</a>
        <a href="/oauth2/authorization/kakao">카카오 로그인</a>
        <a href="/public/join">회원가입</a>
    </div>
</body>

 

이제 마지막 HTML 코드이다.

<a href="">라고 표시되어 있는 href를 등록함으로써 소셜 로그인과 연동이 가능하다.

href의 규격은 정해져 있으며 Provider에 따라 변경하여 등록하면 된다.

이를 통해 소셜 로그인과 기본 Form Login을 병합하여 로그인 기능을 구현할 수 있다.

 

소셜 로그인을 진행할 경우 더욱 빠르게 회원가입 및 로그인을 처리할 수 있으며,

사용자의 추가적인 정보도 바로 접근가능하다는 장점이 존재한다.

Spring boot에서는 Provider를 제공하여 AccessToken을 통한 요청 없이

바로 사용자 프로필을 처리할 수 있으므로 더욱 편리하게 소셜 로그인을 구현할 수 있다.

 

반응형

loading