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]: 소셜 로그인 과정 FE와 통합 #51

Merged
merged 4 commits into from
Aug 10, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 92 additions & 4 deletions server/src/main/java/com/talkka/server/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,42 +1,130 @@
package com.talkka.server.config;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import com.talkka.server.oauth.enums.AuthRole;
import com.talkka.server.oauth.filter.UnregisteredUserFilter;
import com.talkka.server.oauth.service.CustomOAuth2Service;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
private final CustomOAuth2Service customOAuth2Service;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/auth/login/**").permitAll()
.requestMatchers("/auth/**", "/login/**").permitAll()
.anyRequest().authenticated()
.requestMatchers(HttpMethod.POST, "/api/auth/register").hasAuthority(AuthRole.UNREGISTERED.getName())
.anyRequest().authenticated()//.hasAuthority(AuthRole.USER.getName())
)
.addFilterAfter(new UnregisteredUserFilter(), BasicAuthenticationFilter.class)
.oauth2Login(oauth -> oauth
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2Service))
.defaultSuccessUrl("/")
.successHandler(successHandler())
.authorizationEndpoint(authorization -> authorization
.baseUri("/api/auth/login")
// /* 붙이면 안됨
)
.redirectionEndpoint(
redirection -> redirection
.baseUri("/api/auth/login/*/code")
// 반드시 /* 으로 {registrationId}를 받아야 함 스프링 시큐리티의 문제!!
// https://github.com/spring-projects/spring-security/issues/13251
)
)
.logout(logout -> logout
.logoutUrl("/api/auth/logout")
.logoutSuccessUrl("http://localhost:3000")
.deleteCookies("JSESSIONID")
)
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint((request, response, authException) -> {
response.sendRedirect("/auth/login");
log.info(authException.getMessage());
response.sendError(401, "인증이 필요합니다.");
})
);
return http.build();
}

@Bean
public AuthenticationSuccessHandler successHandler() {
return new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
if (isUnregisteredUser(authentication)) {
response.sendRedirect("http://localhost:3000/register");
return;
}
response.sendRedirect("http://localhost:3000");
}

private boolean isSignUpRequest(HttpServletRequest request) {
return request.getRequestURI().equals("/auth/signUp");
}
Copy link
Collaborator

@ss0ngcode ss0ngcode Aug 9, 2024

Choose a reason for hiding this comment

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

해당 메서드 filter에 새로 작성한 것으로 판단되어 중복된 코드 확인 필요


private boolean isUnregisteredUser(Authentication authentication) {
if (authentication == null) {
return false;
}
return hasRole(authentication.getAuthorities(), AuthRole.UNREGISTERED);
}

private boolean hasRole(Collection<? extends GrantedAuthority> authorities, AuthRole role) {
return authorities.stream().anyMatch(authority -> authority.getAuthority().equals(role.getName()));
}
};
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();

config.setAllowCredentials(true);
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setExposedHeaders(List.of("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}

@Bean
public HeaderWriterLogoutHandler addLogoutHandler() {
return new HeaderWriterLogoutHandler(
new ClearSiteDataHeaderWriter(ClearSiteDataHeaderWriter.Directive.COOKIES));
}
}
Original file line number Diff line number Diff line change
@@ -1,62 +1,59 @@
package com.talkka.server.oauth.controller;

import org.springframework.http.ResponseEntity;
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 org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.talkka.server.common.dto.ApiRespDto;
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.dto.UserCreateReqDto;
import com.talkka.server.user.service.UserService;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Controller
@Slf4j
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
@RequestMapping("/auth")
public class AuthController {

private final UserService userService;

@GetMapping("/login")
public String loginForm() {
return "loginForm";
}

@GetMapping("/signUp")
public String signUpForm(Model model, @AuthenticationPrincipal OAuth2UserInfo principal) {
model.addAttribute("name", principal.getName());
model.addAttribute("email", principal.getEmail());
return "signUpForm";
}

@PostMapping("/signUp")
public String signUp(@RequestParam("nickname") String nickname,
Model model,
@AuthenticationPrincipal OAuth2UserInfo principal,
@PostMapping("/register")
public ResponseEntity<ApiRespDto<Void>> register(
@AuthenticationPrincipal OAuth2UserInfo userInfo,
@RequestBody @Valid UserCreateReqDto userCreateReqDto,
HttpServletRequest request) {
String nickname = userCreateReqDto.getNickname();
if (userService.isDuplicatedNickname(nickname)) {
return "signUpForm";
return ResponseEntity.badRequest().body(
ApiRespDto.<Void>builder()
.statusCode(400)
.message("중복된 닉네임입니다.")
.build()
);
}

UserCreateDto userCreateDto = UserCreateDto.builder()
.name(principal.getName())
.email(principal.getEmail())
.oauthProvider(principal.getProvider())
.name(userInfo.getName())
.email(userInfo.getEmail())
.oauthProvider(userInfo.getProvider())
.nickname(nickname)
.accessToken(principal.getAccessToken())
.accessToken(userInfo.getAccessToken())
.build();
UserDto user = userService.createUser(userCreateDto);
userService.createUser(userCreateDto);
request.getSession().invalidate();
return "redirect:/oauth2/authorization/naver";
}

@GetMapping("/login/naver")
public String loginWithNaver() {
return "redirect:https://nid.naver.com/oauth2.0/authorize";
return ResponseEntity.ok(
ApiRespDto.<Void>builder()
.statusCode(200)
.message("회원가입이 완료되었습니다.")
.build()
);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 컨트롤러의 용도가 궁금합니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

기존에 수형님께서 타임리프로 처리해두셨던 컨트롤러입니다. 임시로 보존해두었고, 추후 프론트엔드 올라가면서 삭제해야합니다

Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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 org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

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;

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Controller
@RequiredArgsConstructor
@RequestMapping("/auth")
public class AuthViewController {

private final UserService userService;

@GetMapping("/login")
public String loginForm() {
return "loginForm";
}

@GetMapping("/signUp")
public String signUpForm(Model model, @AuthenticationPrincipal OAuth2UserInfo principal) {
log.info("signUpForm principal: {}", principal);
model.addAttribute("name", principal.getName());
model.addAttribute("email", principal.getEmail());
return "signUpForm";
}

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

@GetMapping("/login/naver")
public String loginWithNaver() {
return "redirect:https://nid.naver.com/oauth2.0/authorize";
}
}
16 changes: 16 additions & 0 deletions server/src/main/java/com/talkka/server/oauth/enums/AuthRole.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.talkka.server.oauth.enums;

public enum AuthRole {
UNREGISTERED("UNREGISTERED"),
USER("USER");

private final String name;

AuthRole(String name) {
this.name = name;
}

public String getName() {
return name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

import com.talkka.server.oauth.enums.AuthRole;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
Expand All @@ -15,37 +17,44 @@
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class UnregisteredUserFilter implements Filter {

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

@Override
public void destroy() {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
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;
}
}
if (isUnregisteredUser(authentication) && !isSignUpRequest(httpRequest)) {
httpResponse.sendRedirect("/register");
return;
}

chain.doFilter(request, response);
}

@Override
public void destroy() {
private boolean isSignUpRequest(HttpServletRequest request) {
return request.getRequestURI().equals("/api/auth/register") && request.getMethod().equals("POST");
}

private boolean isUnregisteredUser(Authentication authentication) {
if (authentication == null) {
return false;
}
return hasRole(authentication.getAuthorities(), AuthRole.UNREGISTERED);
}

private boolean hasRole(Collection<? extends GrantedAuthority> authorities, AuthRole role) {
return authorities.stream().anyMatch(authority -> authority.getAuthority().equals(role.getName()));
}
}
Loading
Loading