Skip to content
Merged
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
11 changes: 9 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,21 @@ dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

// implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
// implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'

runtimeOnly 'com.mysql:mysql-connector-j'

implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'

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-data-redis'


// test
testRuntimeOnly 'com.h2database:h2'
testCompileOnly 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.cake.pop.domain.user.repository;

import java.util.Optional;

import com.cake.pop.domain.user.exception.UserErrorCode;
import com.cake.pop.entity.User;
import com.cake.pop.global.exception.RestApiException;
Expand All @@ -12,4 +14,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
default User getById(Long id){
return findById(id).orElseThrow(()->new RestApiException(UserErrorCode.USER_NOT_FOUND));
}

Optional<User> findByEmail(String email);
}
23 changes: 23 additions & 0 deletions src/main/java/com/cake/pop/domain/user/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
package com.cake.pop.domain.user.service;

import com.cake.pop.domain.user.repository.UserRepository;
import com.cake.pop.entity.User;
import com.cake.pop.global.auth.oauth.Oauth2Response;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;

public User saveOrUpdate(Oauth2Response oauth2Response) {
User user = userRepository.findByEmail(oauth2Response.getEmail())
.map(u -> {
u.updateEmail(oauth2Response.createSocialEmail());
// deleteRefreshTokenIfExists(m);
// deleteOauthAccessTokenIfExists(m);
// saveOauth2AccessToken(oauth2Response, m);
return u;
})
.orElseGet(() -> createMemberFromOauth2Response(oauth2Response));

return userRepository.save(user);
}

private User createMemberFromOauth2Response(Oauth2Response oauth2Response) {
User user = User.of(oauth2Response.getEmail());
// saveOauth2AccessToken(oauth2Response, member);
return user;
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/cake/pop/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@ public static User of(String email){
.email(email)
.build();
}

public void updateEmail(String email) {
this.email = email;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.cake.pop.global.auth.handler;

import java.io.IOException;
import java.util.Objects;
import java.util.stream.Collectors;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import com.cake.pop.global.exception.ErrorResponse;
import com.fasterxml.jackson.databind.ObjectMapper;

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

@Component
@Slf4j
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

private static final String ROLE_GUEST = "ROLE_GUEST";
private static final String ROLE_USER = "ROLE_USER";

private static boolean matchAuthenticationFromRole(Authentication authentication, String role) {
String authRole = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining());

return Objects.equals(authRole, role);
}

private static void setUpResponse(
HttpServletResponse response,
SecurityErrorCode securityErrorCode
) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 Unauthorized
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");

ErrorResponse errorResponse = new ErrorResponse(
securityErrorCode.getMessage(),
securityErrorCode.getHttpStatus().name()
);

ObjectMapper mapper = new ObjectMapper();
String jsonResponse = mapper.writeValueAsString(errorResponse);

response.getWriter().write(jsonResponse);
}

@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
// 현재 인증된 사용자 정보 가져오기
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
log.error(authentication.getName());
// 사용자 권한에 따라 다른 응답 제공
if (!Objects.isNull(accessDeniedException)) {
if (!matchAuthenticationFromRole(authentication, ROLE_USER)) {
// ROLE_USER 권한이 없는 경우
log.info(SecurityErrorCode.FORBIDDEN_USER.getMessage());
setUpResponse(response, SecurityErrorCode.FORBIDDEN_USER);
} else if (!matchAuthenticationFromRole(authentication, ROLE_GUEST)) {
// ROLE_GUEST 권한이 없는 경우
log.info(SecurityErrorCode.FORBIDDEN_GUEST.getMessage());
setUpResponse(response, SecurityErrorCode.FORBIDDEN_GUEST);
} else {
// 기타 권한이 없는 경우
log.info(SecurityErrorCode.FORBIDDEN_MISMATCH.getMessage());
setUpResponse(response, SecurityErrorCode.FORBIDDEN_MISMATCH);
}
} else {
log.info(SecurityErrorCode.FORBIDDEN_MISMATCH.getMessage());
setUpResponse(response, SecurityErrorCode.FORBIDDEN_MISMATCH);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.cake.pop.global.auth.handler;

import java.io.IOException;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import com.cake.pop.global.exception.ErrorResponse;
import com.fasterxml.jackson.databind.ObjectMapper;

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

@Slf4j
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {

log.error("비인가 사용자 요청 -> 예외 발생 : {}", authException.getMessage());

response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 Unauthorized
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");

ErrorResponse errorResponse = new ErrorResponse(SecurityErrorCode.UNAUTHORIZED_USER.getMessage(),
SecurityErrorCode.UNAUTHORIZED_USER.getMessage());

ObjectMapper mapper = new ObjectMapper();
String jsonResponse = mapper.writeValueAsString(errorResponse);

response.getWriter().write(jsonResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.cake.pop.global.auth.handler;

import java.io.IOException;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

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

@Component
@Slf4j
public class CustomOauth2FailureHandler implements AuthenticationFailureHandler {

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException {
log.error("소셜 로그인 실패", exception);
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "소셜 로그인에 실패하였습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.cake.pop.global.auth.handler;

import java.io.IOException;
import java.util.Date;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import com.cake.pop.domain.user.exception.UserErrorCode;
import com.cake.pop.domain.user.repository.UserRepository;
import com.cake.pop.entity.User;
import com.cake.pop.global.auth.jwt.CookieUtil;
import com.cake.pop.global.auth.jwt.TokenProvider;
import com.cake.pop.global.auth.oauth.CustomOauth2User;
import com.cake.pop.global.exception.RestApiException;

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

@Component
@RequiredArgsConstructor
public class CustomOauth2SuccessHandler implements AuthenticationSuccessHandler {

private final UserRepository userRepository;
private final TokenProvider tokenProvider;
private final CookieUtil cookieUtil;
@Value("${direct.home}")
private String REDIRECTION_HOME;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {

CustomOauth2User customOauth2User = (CustomOauth2User)authentication.getPrincipal();

String email = customOauth2User.getEmail();
User findUser = userRepository.findByEmail(email)
.orElseThrow(() -> new RestApiException(UserErrorCode.USER_NOT_FOUND));

String token = tokenProvider.generateAccessToken(findUser, customOauth2User, new Date());
tokenProvider.generateRefreshToken(findUser, customOauth2User, new Date());

response.addCookie(cookieUtil.createCookie(token));

response.sendRedirect(REDIRECTION_HOME);
}

private boolean isRoleGuest(String role) {
return "ROLE_GUEST".equals(role);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.cake.pop.global.auth.handler;


import org.springframework.http.HttpStatus;

import com.cake.pop.global.exception.ErrorCode;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum SecurityErrorCode implements ErrorCode {
UNAUTHORIZED_USER(HttpStatus.NOT_FOUND, "비인가 사용자 요청입니다."),
FORBIDDEN_USER(HttpStatus.NOT_FOUND, "ROLE_USER 권한이 필요합니다."),
FORBIDDEN_GUEST(HttpStatus.NOT_FOUND, "ROLE_GUEST 권한이 필요합니다."),
FORBIDDEN_MISMATCH(HttpStatus.NOT_FOUND, "어떤 권한도 매치되지 않습니다.");
private final HttpStatus httpStatus;
private final String message;
}
49 changes: 49 additions & 0 deletions src/main/java/com/cake/pop/global/auth/jwt/CookieUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.cake.pop.global.auth.jwt;

import org.springframework.stereotype.Component;

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

@Component
@RequiredArgsConstructor
@Slf4j
public class CookieUtil {

public Cookie createCookie(String token) {
Cookie cookie = new Cookie("Authorization", token);
cookie.setPath("/");
cookie.setMaxAge(60 * 180);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setAttribute("SameSite", "None");
return cookie;
}

public void deleteCookie(HttpServletResponse response) {
Cookie cookie = new Cookie("Authorization", null);
cookie.setPath("/");
cookie.setMaxAge(0);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setAttribute("SameSite", "None");

response.addCookie(cookie);
}


public String getCookieValue(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("Authorization".equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
}
17 changes: 17 additions & 0 deletions src/main/java/com/cake/pop/global/auth/jwt/CustomJwtException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.cake.pop.global.auth.jwt;


import com.cake.pop.global.exception.ErrorCode;

import lombok.Getter;

@Getter
public class CustomJwtException extends RuntimeException {

private final String code;

public CustomJwtException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getHttpStatus().name();
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/cake/pop/global/auth/jwt/JwtErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.cake.pop.global.auth.jwt;

import org.springframework.http.HttpStatus;

import com.cake.pop.global.exception.ErrorCode;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum JwtErrorCode implements ErrorCode {

MALFORMED_TOKEN(HttpStatus.NOT_FOUND, "알맞지 않은 형식의 토큰입니다."),
INVALID_TOKEN(HttpStatus.NOT_FOUND, "유효하지 않은 토큰입니다."),
EXPIRED_TOKEN(HttpStatus.NOT_FOUND, "만료된 토큰입니다.");

private final HttpStatus httpStatus;
private final String message;

}
Loading