Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] 네이버 소셜 로그인 구현 #36

Merged
merged 12 commits into from
Aug 3, 2024
20 changes: 12 additions & 8 deletions server/src/main/java/com/talkka/server/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import com.talkka.server.oauth.OAuth2LoginFailureHandler;
import com.talkka.server.oauth.OAuth2LoginSuccessHandler;
import com.talkka.server.oauth.filter.UnregisteredUserFilter;
import com.talkka.server.oauth.service.CustomOAuth2Service;

import lombok.RequiredArgsConstructor;
Expand All @@ -17,21 +17,25 @@
public class SecurityConfig {

private final CustomOAuth2Service customOAuth2Service;
private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
private final OAuth2LoginFailureHandler oAuth2LoginFailureHandler;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated())
.requestMatchers("/auth/**", "/login/**").permitAll()
.anyRequest().authenticated()
)
.addFilterAfter(new UnregisteredUserFilter(), BasicAuthenticationFilter.class)
.oauth2Login(oauth -> oauth
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2Service))
.successHandler(oAuth2LoginSuccessHandler)
.failureHandler(oAuth2LoginFailureHandler)
.defaultSuccessUrl("/")
)
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint((request, response, authException) -> {
response.sendRedirect("/auth/login");
})
);
return http.build();
}
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.talkka.server.oauth.domain.AuthUserDto;
import com.talkka.server.oauth.domain.CustomUserPrincipal;
import com.talkka.server.oauth.domain.OAuth2UserInfo;
import com.talkka.server.user.dto.UserCreateDto;
import com.talkka.server.user.dto.UserDto;
import com.talkka.server.user.service.UserService;
Expand All @@ -30,32 +29,30 @@ public String loginForm() {
}

@GetMapping("/signUp")
public String signUpForm(Model model, @AuthenticationPrincipal CustomUserPrincipal principal) {
AuthUserDto userDto = principal.getUser();
model.addAttribute("user", userDto);
public String signUpForm(Model model, @AuthenticationPrincipal OAuth2UserInfo principal) {
model.addAttribute("name", principal.getName());
model.addAttribute("email", principal.getEmail());
return "signUpForm";
Comment on lines +32 to 35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앞으로 Session 에 저장된 유저 인가 정보를 조회할 때 @AuthenticationPrincipal 을 사용하면 된다고 이해하면 될까요?
그리고 OAuthUserInfo 의 역할도 궁금합니다.

Copy link
Collaborator

@ss0ngcode ss0ngcode Aug 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 아직 시큐리티와 세션쪽을 잘 몰라서
@AuthenticationPrincipal외에도 세션을 관리할 수 있는 것들이 많다고 들었는데
해당 어노테이션을 선택하신 이유가 궁금합니다!

Copy link
Collaborator Author

@Gyaak Gyaak Aug 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AuthenticationPrincipal를 사용해 현재 사용자의 인증 정보를 가져오는게 맞습니다.

OAuthUserInfo는 인증객체 + dto 느낌으로 생각하면 될 것 같습니다.
기본적인 인증 정보들과 함께 db에 보관중인 추가 정보를 넣을 수 있게 OAuth2User를 상속했습니다.

SecurityContextAuthentication 객체로 주입받을 경우
getPrincipal()로 인증정보(OAuth2UserInfo)를 한번 더 꺼내야 하는 번거로움이 있어
@AuthenticationPrincipalOAuth2UserInfo를 직접 주입받았습니다.

}

@SuppressWarnings("checkstyle:WhitespaceAround")
@PostMapping("/signUp")
public String signUp(@RequestParam("nickname") String nickname,
Model model,
@AuthenticationPrincipal CustomUserPrincipal principal,
@AuthenticationPrincipal OAuth2UserInfo principal,
HttpServletRequest request) {
if (userService.isDuplicatedNickname(nickname)) {
return "signUpForm";
}
AuthUserDto authUserDto = principal.getUser();
UserCreateDto userCreateDto = UserCreateDto.builder()
.name(authUserDto.getName())
.email(authUserDto.getEmail())
.oauthProvider(authUserDto.getProvider())
.name(principal.getName())
.email(principal.getEmail())
.oauthProvider(principal.getProvider())
.nickname(nickname)
.accessToken(authUserDto.getAccessToken())
.accessToken(principal.getAccessToken())
.build();
UserDto user = userService.createUser(userCreateDto);
request.getSession().invalidate();
return "redirect:/";
return "redirect:/oauth2/authorization/naver";
}

@GetMapping("/login/naver")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.talkka.server.oauth.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import com.talkka.server.oauth.domain.OAuth2UserInfo;

// 인증 테스트를 위한 임시 컨트롤러
@Controller
public class BaseController {
@GetMapping("/")
public String authIndex(Model model, @AuthenticationPrincipal OAuth2UserInfo userInfo) {
model.addAttribute("name", userInfo.getName());
model.addAttribute("email", userInfo.getEmail());
model.addAttribute("nickname", userInfo.getNickName());
model.addAttribute("oauth2Id", userInfo.getOAuth2Id());
model.addAttribute("provider", userInfo.getProvider());
model.addAttribute("accessToken", userInfo.getAccessToken());
return "index";
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.talkka.server.oauth.domain;

import java.util.Collection;
import java.util.Map;

import org.springframework.security.core.GrantedAuthority;

public class NaverOAuth2User extends OAuth2UserInfo {

public NaverOAuth2User(Map<String, Object> attributes) {
super((Map<String, Object>)attributes.get("response"));
}

public NaverOAuth2User(Map<String, Object> attributes, Collection<? extends GrantedAuthority> authorities) {
super((Map<String, Object>)attributes, authorities);
}

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.talkka.server.oauth.domain;

import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;

public abstract class OAuth2UserInfo implements OAuth2User {

protected final Map<String, Object> attributes;
private final Collection<? extends GrantedAuthority> authorities;

public OAuth2UserInfo(Map<String, Object> attributes, Collection<? extends GrantedAuthority> authorities) {
this.attributes = attributes;
this.authorities = authorities;
}

// OAuth2 인증까지만 통과하면 UNREGISTERED 권한 부여
public OAuth2UserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
this.authorities = List.of(new SimpleGrantedAuthority("UNREGISTERED"));
}

@Override
public Map<String, Object> getAttributes() {
return attributes;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}

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

public String getOAuth2Id() {
return (String)attributes.get("id");
}

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

public String getAccessToken() {
return (String)attributes.get("accessToken");
}

public String getNickName() {
return (String)attributes.get("nickname");
}
Comment on lines +38 to +56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(String) 으로 강제 캐스팅하는 것이 꺼림칙한데, 어떤 이유가 있는지 궁금합니다

  • Attributes 는 어디서 오는 것인지 제가 잘 몰라서 해당 부분 한번만 설명해주시면 감사...

Copy link
Collaborator

@ss0ngcode ss0ngcode Aug 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동일한 질문입니다!
강제 캐스팅의 경우 넘어오는 값이 null이라도 NullPointException이 안뜨는데 괜찮은 경우일까요?
만약 OAuth를 통해 값이 잘못 넘어오는 경우가 있다면 고민을 해봐야 할 것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OAuth2UserInfo가 상속받고있는 OAuth2AuthenticatedPrincipal에서 Map<String, Object> getAttributes();를 정의하고 있어
attribute를 Map<String, Object>타입으로 정의했습니다.

현재 OAuth2 인증객체(OAuth2UserInfo)를 OAuth2인증시에만 사용하고
db에서 회원 정보를 가져와 별도의 인증dto를 만들어 SecurityContext에 넣는것을 고려하고 있습니다.
-> @AuthenticationPrincipal로 새로 정의한 dto를 받아올수 있게 만드는것을 고려중
아마 그 과정에서 강제 캐스팅 문제를 해결할 수 있을것으로 보입니다.


// oauth provider 는 각 제공자 별로 구현
public abstract String getProvider();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.talkka.server.oauth.filter;

import java.io.IOException;
import java.util.Collection;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class UnregisteredUserFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpServletResponse httpResponse = (HttpServletResponse)response;

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication != null) {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals("UNREGISTERED")
&& !httpRequest.getRequestURI().equals("/auth/signUp")) {
httpResponse.sendRedirect("/auth/signUp");
return;
}
}
}

chain.doFilter(request, response);
Comment on lines +28 to +44
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final 로 Object reference 바뀌는지 안바뀌는지 확실히 해주는 것이 좋아보입니다.

}

@Override
public void destroy() {
}

}
Loading
Loading