Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-security") // Spring Security
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") // OAuth 2.0 Resource server
// TODO: Spring Security OAuth2 Resource Server 사용에 대해 검토해야 함
implementation("org.springframework.boot:spring-boot-starter-oauth2-client") // OAuth2 starter
implementation("io.jsonwebtoken:jjwt-api:0.12.3")
implementation("io.jsonwebtoken:jjwt-impl:0.12.3")
implementation("io.jsonwebtoken:jjwt-jackson:0.12.3")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf") //타임리프

// Spring Data Redis 추가
// implementation("org.springframework.boot:spring-boot-starter-data-redis")
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.codezerotoone.mvp.domain.member.auth.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class OAuth2PageTestController {
@GetMapping("/custom/login")
public String oAuth2LoginPage() {
return "login";
}
}
Original file line number Diff line number Diff line change
@@ -1,59 +1,59 @@
package com.codezerotoone.mvp.domain.member.auth.controller.errorhandler;

import com.codezerotoone.mvp.domain.member.auth.controller.AuthController;
import com.codezerotoone.mvp.global.api.format.ErrorResponse;
import com.codezerotoone.mvp.global.security.exception.errorcode.SecurityErrorCode;
import com.codezerotoone.mvp.global.security.token.exception.InvalidRefreshTokenException;
import com.codezerotoone.mvp.global.security.token.exception.UnsupportedCodeException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.util.UriComponentsBuilder;

@RestControllerAdvice(assignableTypes = AuthController.class)
@Slf4j
public class AuthErrorHandlingControllerAdvice {
private final String clientDomain;
private final String clientOrigin;

public AuthErrorHandlingControllerAdvice(@Value("${client.domain}") String clientDomain,
@Value("${client.origin}") String clientOrigin) {
this.clientDomain = clientDomain;
this.clientOrigin = clientOrigin;
}

@ExceptionHandler(UnsupportedCodeException.class)
public ResponseEntity<ErrorResponse> unsupportedCodeException(UnsupportedCodeException ex) {
log.info("{}", ex.getMessage());
// return new ResponseEntity<>(ErrorResponse.of(SecurityErrorCode.UNSUPPORTED_CODE), HttpStatus.UNAUTHORIZED);

HttpHeaders headers = new HttpHeaders();

String redirectionTo = UriComponentsBuilder.fromUriString(this.clientOrigin)
.path("/redirection")
.queryParam("type", "oauth2")
.queryParam("is-success", false)
.build()
.encode()
.toUriString();

headers.add(HttpHeaders.LOCATION, redirectionTo);
return new ResponseEntity<>(headers, HttpStatus.PERMANENT_REDIRECT);
}

@ExceptionHandler(InvalidRefreshTokenException.class)
public ResponseEntity<ErrorResponse> invalidRefreshTokenException(InvalidRefreshTokenException ex) {
log.info("{}", ex.getMessage());
return ResponseEntity.badRequest()
.body(
ErrorResponse.of(
SecurityErrorCode.INVALID_REFRESH_TOKEN,
ex.getMessage()
)
);
}
}
//package com.codezerotoone.mvp.domain.member.auth.controller.errorhandler;
//
//import com.codezerotoone.mvp.domain.member.auth.controller.AuthController;
//import com.codezerotoone.mvp.global.api.format.ErrorResponse;
//import com.codezerotoone.mvp.global.security.exception.errorcode.SecurityErrorCode;
//import com.codezerotoone.mvp.global.security.token.exception.InvalidRefreshTokenException;
//import com.codezerotoone.mvp.global.security.token.exception.UnsupportedCodeException;
//import lombok.extern.slf4j.Slf4j;
//import org.springframework.beans.factory.annotation.Value;
//import org.springframework.http.HttpHeaders;
//import org.springframework.http.HttpStatus;
//import org.springframework.http.ResponseEntity;
//import org.springframework.web.bind.annotation.ExceptionHandler;
//import org.springframework.web.bind.annotation.RestControllerAdvice;
//import org.springframework.web.util.UriComponentsBuilder;
//
//@RestControllerAdvice(assignableTypes = AuthController.class)
//@Slf4j
//public class AuthErrorHandlingControllerAdvice {
// private final String clientDomain;
// private final String clientOrigin;
//
// public AuthErrorHandlingControllerAdvice(@Value("${client.domain}") String clientDomain,
// @Value("${client.origin}") String clientOrigin) {
// this.clientDomain = clientDomain;
// this.clientOrigin = clientOrigin;
// }
//
// @ExceptionHandler(UnsupportedCodeException.class)
// public ResponseEntity<ErrorResponse> unsupportedCodeException(UnsupportedCodeException ex) {
// log.info("{}", ex.getMessage());
//// return new ResponseEntity<>(ErrorResponse.of(SecurityErrorCode.UNSUPPORTED_CODE), HttpStatus.UNAUTHORIZED);
//
// HttpHeaders headers = new HttpHeaders();
//
// String redirectionTo = UriComponentsBuilder.fromUriString(this.clientOrigin)
// .path("/redirection")
// .queryParam("type", "oauth2")
// .queryParam("is-success", false)
// .build()
// .encode()
// .toUriString();
//
// headers.add(HttpHeaders.LOCATION, redirectionTo);
// return new ResponseEntity<>(headers, HttpStatus.PERMANENT_REDIRECT);
// }
//
// @ExceptionHandler(InvalidRefreshTokenException.class)
// public ResponseEntity<ErrorResponse> invalidRefreshTokenException(InvalidRefreshTokenException ex) {
// log.info("{}", ex.getMessage());
// return ResponseEntity.badRequest()
// .body(
// ErrorResponse.of(
// SecurityErrorCode.INVALID_REFRESH_TOKEN,
// ex.getMessage()
// )
// );
// }
//}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.codezerotoone.mvp.domain.member.auth.dto;

import com.codezerotoone.mvp.domain.member.member.dto.MemberDto;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;

@RequiredArgsConstructor
public class CustomOAuth2User implements OAuth2User {

private final MemberDto memberDto;

@Override
public <A> A getAttribute(String name) {
return OAuth2User.super.getAttribute(name);
}

@Override
public String getName() {
return memberDto.getOidcId();
}

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

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();

collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return memberDto.getRole();
}
});

return collection;
}

public Long getMemberId() {
return memberDto.getMemberId();
}

public String getRole() {
return memberDto.getRole();
}
}
Original file line number Diff line number Diff line change
@@ -1,48 +1,48 @@
package com.codezerotoone.mvp.domain.member.auth.service;

import com.codezerotoone.mvp.domain.member.auth.dto.response.LoginResult;
import com.codezerotoone.mvp.domain.member.member.entity.Member;
import com.codezerotoone.mvp.domain.member.member.repository.MemberRepository;
import com.codezerotoone.mvp.global.security.token.dto.GrantedTokenInfo;
import com.codezerotoone.mvp.global.security.token.dto.OAuth2UserInfo;
import com.codezerotoone.mvp.global.security.token.exception.UnsupportedCodeException;
import com.codezerotoone.mvp.global.security.token.support.TokenSupport;
import com.codezerotoone.mvp.global.security.token.vendor.AuthVendor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@RequiredArgsConstructor
@Slf4j
public class AuthService {
private final TokenSupport tokenSupport;
private final MemberRepository memberRepository;

@Transactional
public LoginResult loginByOAuth2(String code, String redirectUri, AuthVendor authVendor)
throws UnsupportedCodeException {
GrantedTokenInfo grantedTokenInfo = this.tokenSupport.grantToken(code, redirectUri, authVendor);
Optional<Member> memberOp = this.memberRepository.findByOdicId(grantedTokenInfo.id());
if (memberOp.isPresent()) {
Member member = memberOp.get();
return LoginResult.builder()
.newMember(false)
.accessToken(grantedTokenInfo.accessToken())
.refreshToken(grantedTokenInfo.refreshToken())
.memberId(member.getMemberId())
.build();
}
OAuth2UserInfo userInfo = this.tokenSupport.retrieveUserInfo(grantedTokenInfo.accessToken());
return LoginResult.builder()
.newMember(true)
.accessToken(grantedTokenInfo.accessToken())
.refreshToken(grantedTokenInfo.refreshToken())
.profileImageUrl(userInfo.profileImageUrl())
.userName(userInfo.name())
.build();
}
}
//package com.codezerotoone.mvp.domain.member.auth.service;
//
//import com.codezerotoone.mvp.domain.member.auth.dto.response.LoginResult;
//import com.codezerotoone.mvp.domain.member.member.entity.Member;
//import com.codezerotoone.mvp.domain.member.member.repository.MemberRepository;
//import com.codezerotoone.mvp.global.security.token.dto.GrantedTokenInfo;
//import com.codezerotoone.mvp.global.security.token.dto.OAuth2UserInfo;
//import com.codezerotoone.mvp.global.security.token.exception.UnsupportedCodeException;
//import com.codezerotoone.mvp.global.security.token.support.TokenSupport;
//import com.codezerotoone.mvp.global.security.token.vendor.AuthVendor;
//import lombok.RequiredArgsConstructor;
//import lombok.extern.slf4j.Slf4j;
//import org.springframework.stereotype.Service;
//import org.springframework.transaction.annotation.Transactional;
//
//import java.util.Optional;
//
//@Service
//@RequiredArgsConstructor
//@Slf4j
//public class AuthService {
// private final TokenSupport tokenSupport;
// private final MemberRepository memberRepository;
//
// @Transactional
// public LoginResult loginByOAuth2(String code, String redirectUri, AuthVendor authVendor)
// throws UnsupportedCodeException {
// GrantedTokenInfo grantedTokenInfo = this.tokenSupport.grantToken(code, redirectUri, authVendor);
// Optional<Member> memberOp = this.memberRepository.findByOdicId(grantedTokenInfo.id());
// if (memberOp.isPresent()) {
// Member member = memberOp.get();
// return LoginResult.builder()
// .newMember(false)
// .accessToken(grantedTokenInfo.accessToken())
// .refreshToken(grantedTokenInfo.refreshToken())
// .memberId(member.getMemberId())
// .build();
// }
// OAuth2UserInfo userInfo = this.tokenSupport.retrieveUserInfo(grantedTokenInfo.accessToken());
// return LoginResult.builder()
// .newMember(true)
// .accessToken(grantedTokenInfo.accessToken())
// .refreshToken(grantedTokenInfo.refreshToken())
// .profileImageUrl(userInfo.profileImageUrl())
// .userName(userInfo.name())
// .build();
// }
//}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.codezerotoone.mvp.domain.member.auth.service;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
Copy link
Collaborator

Choose a reason for hiding this comment

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

공통 에러 응답을 여기서 반환하면 403 에러가 왜 발생했는지 클라이언트에서 구체적으로 알 수 있을 거라고 생각합니다

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.codezerotoone.mvp.domain.member.auth.service;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {


@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
if (request.getRequestURI().equals("/login")) {
response.sendRedirect("/login");
return;
}

response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
Copy link
Collaborator

Choose a reason for hiding this comment

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

공통 에러 응답을 여기서 반환하면 401 에러가 왜 발생했는지 클라이언트에서 구체적으로 알 수 있을 거라고 생각합니다. 그에 따라 클라이언트에서도 상황에 맞게 대처할 수 있을 거고요

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.codezerotoone.mvp.domain.member.auth.service;

import com.codezerotoone.mvp.domain.member.auth.dto.CustomOAuth2User;
import com.codezerotoone.mvp.domain.member.auth.entity.Role;
import com.codezerotoone.mvp.domain.member.member.dto.MemberDto;
import com.codezerotoone.mvp.domain.member.member.entity.Member;
import com.codezerotoone.mvp.domain.member.member.repository.MemberRepository;
import com.codezerotoone.mvp.global.security.token.exception.UnsupportedCodeException;
import com.codezerotoone.mvp.global.security.token.support.OAuth2Response;
import com.codezerotoone.mvp.global.security.token.vendor.AuthSocial;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

@Service
@Slf4j
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final MemberRepository memberRepository;

@Override
@Transactional
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
log.info("oAuth2User: " + oAuth2User);

String registrationId = userRequest.getClientRegistration().getRegistrationId();

AuthSocial social = AuthSocial.fromName(registrationId);
OAuth2Response oAuth2Response = social.createResponse(oAuth2User.getAttributes());
String oidcId = oAuth2Response.getProviderId();

Member existData = memberRepository.findByOdicId(oidcId).orElse(null);

if (existData != null) {
MemberDto userDto = MemberDto.fromEntity(existData);

return new CustomOAuth2User(userDto);
} else {
Member member = Member.createGeneralMemberBySocialLogin("프로필이름", oidcId);
memberRepository.save(member);

MemberDto memberDto = MemberDto.fromEntity(member);
return new CustomOAuth2User(memberDto);
}
}
}
Loading