Skip to content

Commit

Permalink
[ARV-120] OAuth2 로그인 리다이렉트 추가 (#106)
Browse files Browse the repository at this point in the history
* [ARV-120] chore: application-dev gitignore 추가

* [ARV-120] fix: login시 리다이렉트 추가

* [ARV-120] refactor: member swagger 분리

* [ARV-120] feat: 회원 가입 및 회원정보 수정 시 이미지 저장 로직 추가

* [ARV-120] fix: oauth2 로그인 시 redirect 되도록 수정 및 refresh, access token을 cookie로 저장하도록 수정

* [ARV-120] fix: redirect 주석 해제

* [ARV-120] fix: main, concert, rent 일부 API permitAll
  • Loading branch information
sangcci authored Dec 6, 2024
1 parent 7929bd4 commit d4bc126
Show file tree
Hide file tree
Showing 26 changed files with 409 additions and 172 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ out/

/src/main/resources/application.yml
/src/main/resources/application-local.yml
/src/main/resources/application-dev.yml
/src/main/resources/application-test.yml
/src/test/resources/application-test.yml

Expand Down
6 changes: 2 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.3'
implementation 'org.springframework.cloud:spring-cloud-commons:4.1.4'

//jaxb
// jaxb
implementation 'io.github.openfeign:feign-jaxb:11.0'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
implementation 'org.glassfish.jaxb:jaxb-runtime:2.3.1'
Expand Down Expand Up @@ -72,6 +72,7 @@ dependencies {

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'it.ozimov:embedded-redis:0.7.2'

// aws s3
implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.0.0'
Expand All @@ -88,9 +89,6 @@ dependencies {
// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

//kafka
implementation 'org.springframework.kafka:spring-kafka'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package com.backend.allreva.artist.command;

import com.backend.allreva.artist.command.domain.Artist;
import com.backend.allreva.member.command.application.dto.MemberInfoRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.backend.allreva.member.command.application.dto.MemberArtistRequest;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
Expand All @@ -17,9 +16,9 @@ public class ArtistCommandService {

private final ArtistRepository artistRepository;

public void saveIfNotExist(final List<MemberInfoRequest.MemberArtistRequest> artists) {
public void saveIfNotExist(final List<MemberArtistRequest> artists) {
List<String> ids = artists.stream()
.map(MemberInfoRequest.MemberArtistRequest::spotifyArtistId)
.map(MemberArtistRequest::spotifyArtistId)
.toList();

List<Artist> existingEntities = artistRepository
Expand All @@ -31,7 +30,7 @@ public void saveIfNotExist(final List<MemberInfoRequest.MemberArtistRequest> art

List<Artist> list = artists.stream()
.filter(artist -> !existingIds.contains(artist.spotifyArtistId()))
.map(MemberInfoRequest.MemberArtistRequest::to)
.map(MemberArtistRequest::to)
.toList();

artistRepository.saveAll(list);
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/backend/allreva/auth/domain/RefreshToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.backend.allreva.auth.domain;

import lombok.Builder;
import lombok.Getter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

@Getter
@RedisHash(value = "refreshToken", timeToLive = 14440)
public class RefreshToken {

@Id
private String token;
private Long memberId;

@Builder
private RefreshToken(final String token, final Long memberId) {
this.token = token;
this.memberId = memberId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.backend.allreva.auth.domain;

import org.springframework.data.repository.CrudRepository;

public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
package com.backend.allreva.auth.oauth2.handler;

import java.io.IOException;

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

import com.backend.allreva.auth.application.dto.PrincipalDetails;
import com.backend.allreva.auth.application.dto.LoginSuccessResponse;
import com.backend.allreva.auth.domain.RefreshToken;
import com.backend.allreva.auth.domain.RefreshTokenRepository;
import com.backend.allreva.auth.util.CookieUtil;
import com.backend.allreva.auth.util.JwtProvider;
import com.backend.allreva.common.dto.Response;
import com.backend.allreva.member.command.domain.Member;
import com.fasterxml.jackson.databind.ObjectMapper;

import com.backend.allreva.member.command.domain.value.MemberRole;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

private static final String FRONT_BASE_URL = "http://localhost:8080";
private static final String FRONT_SIGNUP_URL = "/signup";

private final JwtProvider jwtProvider;
private final ObjectMapper objectMapper;
private final RefreshTokenRepository refreshTokenRepository;
@Value("${jwt.refresh.expiration}")
private Long REFRESH_TIME;
private int REFRESH_TIME;
@Value("${jwt.access.expiration}")
private int ACCESS_TIME;

/**
* OAuth2 인증 success시 JWT 반환하는 메서드
Expand All @@ -45,44 +47,32 @@ public void onAuthenticationSuccess(
Member member = oAuth2User.member();

// token 생성
String memberId = String.valueOf(member.getId());
String accessToken = jwtProvider.generateAccessToken(memberId);
String refreshToken = jwtProvider.generateRefreshToken(memberId);

// access token 응답객체 생성
LoginSuccessResponse loginSuccessResponse = LoginSuccessResponse.of(
accessToken,
refreshToken,
jwtProvider.getREFRESH_TIME(),
member.getEmail().getEmail(),
member.getMemberInfo().getProfileImageUrl());
Long memberId = member.getId();
String accessToken = jwtProvider.generateAccessToken(String.valueOf(memberId));
String refreshToken = jwtProvider.generateRefreshToken(String.valueOf(memberId));

// TODO: db or cache에 RefreshToken 저장
// redis에 RefreshToken 저장
RefreshToken refreshTokenEntity = RefreshToken.builder()
.token(refreshToken)
.memberId(memberId)
.build();
refreshTokenRepository.save(refreshTokenEntity);

// refreshToken 쿠키 등록
setHeader(response, refreshToken);
CookieUtil.addCookie(response, "accessToken", accessToken, ACCESS_TIME);
CookieUtil.addCookie(response, "refreshToken", refreshToken, REFRESH_TIME);

response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
Response<LoginSuccessResponse> apiResponse = Response.onSuccess(loginSuccessResponse);
String jsonResponse = objectMapper.writeValueAsString(apiResponse);
response.getWriter().write(jsonResponse);
sendRedirect(response, member);
}

// refreshToken 쿠키 설정
public void setHeader(final HttpServletResponse response, final String refreshToken) {
if (refreshToken != null) {
response.addHeader("refresh_token", refreshToken);
response.addHeader("Set-Cookie", createRefreshToken(refreshToken).toString());
private void sendRedirect(
final HttpServletResponse response,
final Member member
) throws IOException {
if (member.getMemberRole().equals(MemberRole.USER)) {
response.sendRedirect(FRONT_BASE_URL);
} else {
response.sendRedirect(FRONT_BASE_URL + FRONT_SIGNUP_URL);
}
}

// refreshToken 쿠키 생성
public ResponseCookie createRefreshToken(final String refreshToken) {
return ResponseCookie.from("refreshToken", refreshToken)
.path("/")
.maxAge(REFRESH_TIME)
.httpOnly(true)
.build();
}
}
20 changes: 12 additions & 8 deletions src/main/java/com/backend/allreva/auth/ui/OAuth2Controller.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
import com.backend.allreva.member.command.application.dto.MemberInfoRequest;
import com.backend.allreva.member.command.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequiredArgsConstructor
Expand All @@ -19,19 +21,21 @@ public class OAuth2Controller implements OAuth2ControllerSwagger{

private final MemberCommandFacade memberCommandFacade;

@Override
@GetMapping("/login")
public ResponseEntity<Void> login() {
return null;
}

@Override
@PostMapping("/register")
public ResponseEntity<Void> register(
final @AuthMember Member member,
final @RequestBody MemberInfoRequest memberInfoRequest
/**
* oauth2 회원가입
*/
@PostMapping(path = "/register", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Void> registerMember(
@AuthMember final Member member,
@RequestPart final MemberInfoRequest memberInfoRequest,
@RequestPart(value = "image", required = false) final MultipartFile image
) {
memberCommandFacade.registerMember(memberInfoRequest, member);
memberCommandFacade.registerMember(memberInfoRequest, member, image);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
package com.backend.allreva.auth.ui;

import com.backend.allreva.auth.application.AuthMember;
import com.backend.allreva.member.command.application.dto.MemberInfoRequest;
import com.backend.allreva.member.command.domain.Member;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Encoding;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;

@Tag(name = "OAuth2 로그인 API")
public interface OAuth2ControllerSwagger {

@Operation(
summary = "oauth2 로그인",
description = """
<b>oauth2 로그인 API</b>
먼저 로그인을 다음 주소로 login한 후 token 주소를 얻습니다.
- `http://{host}:{port}/api/v1/oauth2/login/{provider}`
- ex) `/api/v1/oauth2/login/kakao`
이후 token을 이용하여 회원가입 절차를 진행합니다.
Expand All @@ -25,9 +33,14 @@ public interface OAuth2ControllerSwagger {
)
ResponseEntity<Void> login();

@Operation(
summary = "oauth2 회원가입",
description = "oauth2 회원가입 시 회원 정보 입력 API"
)
ResponseEntity<Void> register(Member member, MemberInfoRequest memberInfoRequest);
@Operation(summary = "oauth2 회원가입", description = "oauth2 회원가입 시 USER 권한으로 승격됩니다.")
@RequestBody(
content = @Content(
encoding = @Encoding(
name = "memberInfoRequest", contentType = MediaType.APPLICATION_JSON_VALUE)))
ResponseEntity<Void> registerMember(
@AuthMember Member member,
@RequestPart MemberInfoRequest memberInfoRequest,
@RequestPart(value = "image", required = false) MultipartFile image
);
}
30 changes: 30 additions & 0 deletions src/main/java/com/backend/allreva/auth/util/CookieUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.backend.allreva.auth.util;

import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class CookieUtil {

private static final String COOKIE_DOMAIN = "localhost:3000";

// refreshToken 쿠키 생성
public static void addCookie(
final HttpServletResponse response,
final String name,
final String value,
final int maxAge
) {
ResponseCookie cookie = ResponseCookie.from(name, value)
//.domain(COOKIE_DOMAIN)
.path("/")
.maxAge(maxAge)
.httpOnly(true)
.build();

response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.backend.allreva.common.config;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import redis.embedded.RedisServer;

@Configuration
@EnableRedisRepositories
public class RedisEmbeddedConfig {

private final RedisServer redisServer;

public RedisEmbeddedConfig(
@Value("${spring.data.redis.port}") final int port,
@Value("${spring.data.redis.host}") final String host
) {
this.redisServer = new RedisServer(port);
}

@PostConstruct
public void startRedis() {
redisServer.start();
}

@PreDestroy
public void stopRedis() {
redisServer.stop();
}

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory();
}

@Bean
public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<byte[], byte[]> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
return template;
}
}
Loading

0 comments on commit d4bc126

Please sign in to comment.