Skip to content

Commit

Permalink
Merge pull request #236 from Codiary-UMC-6th/feature/#228-token-refresh
Browse files Browse the repository at this point in the history
#228 auth 관련 기능 구현
  • Loading branch information
ParkJh38 authored Oct 7, 2024
2 parents 03cca29 + 0b4b343 commit 6322bc0
Show file tree
Hide file tree
Showing 13 changed files with 139 additions and 135 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ dependencies {
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}


Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
package com.codiary.backend.domain.member.controller;

import com.codiary.backend.domain.member.converter.MemberConverter;
import com.codiary.backend.domain.member.dto.request.MemberRequestDTO;
import com.codiary.backend.domain.member.dto.response.MemberResponseDTO;
import com.codiary.backend.domain.member.entity.Member;
import com.codiary.backend.domain.member.service.MemberCommandService;
import com.codiary.backend.domain.member.service.MemberQueryService;
import com.codiary.backend.global.apiPayload.ApiResponse;
import com.codiary.backend.domain.member.dto.request.MemberRequestDTO;
import com.codiary.backend.domain.member.dto.response.MemberResponseDTO;
import com.codiary.backend.global.apiPayload.code.status.SuccessStatus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
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.RestController;
import com.codiary.backend.global.apiPayload.code.status.SuccessStatus;
import org.springframework.web.bind.annotation.*;

@RequiredArgsConstructor
Expand Down Expand Up @@ -48,26 +44,28 @@ public ApiResponse<String> checkNicknameDuplication(@RequestParam String nicknam
}

@PostMapping("/login")
@Operation(
summary = "로그인"
)
@Operation(summary = "로그인")
public ApiResponse<MemberResponseDTO.MemberTokenResponseDTO> login(@Valid @RequestBody MemberRequestDTO.MemberLoginRequestDTO request) {
return memberCommandService.login(request);
}

@PostMapping("/logout")
@Operation(summary = "로그아웃")
public ApiResponse<String> logout(@RequestHeader("Authorization") String token) {
Member member = memberCommandService.getRequester();
String jwtToken = token.substring(7);
String response = memberCommandService.logout(jwtToken, member);
public ApiResponse<String> logout(@Valid @RequestBody MemberRequestDTO.refreshRequestDTO request) {
String response = memberCommandService.logout(request.refreshToken());
return ApiResponse.onSuccess(SuccessStatus.MEMBER_OK, response);
}

@PostMapping("/refresh")
@Operation(summary = "액세스 토큰 재할당")
public ApiResponse<MemberResponseDTO.TokenRefreshResponseDTO> refresh(@Valid @RequestBody MemberRequestDTO.refreshRequestDTO request) {
String jwtToken = request.refreshToken();
MemberResponseDTO.TokenRefreshResponseDTO response = memberCommandService.refresh(jwtToken);
return ApiResponse.onSuccess(SuccessStatus.MEMBER_OK, response);
}

@PatchMapping(path = "/profile-image", consumes = "multipart/form-data")
@Operation(
summary = "프로필 사진 설정"
)
@Operation(summary = "프로필 사진 설정")
public ApiResponse<MemberResponseDTO.MemberImageDTO> updateProfileImage(@ModelAttribute MemberRequestDTO.MemberProfileImageRequestDTO request) {
Member member = memberCommandService.getRequester();

Expand All @@ -76,7 +74,7 @@ public ApiResponse<MemberResponseDTO.MemberImageDTO> updateProfileImage(@ModelAt

@DeleteMapping("/profile-image")
@Operation(summary = "프로필 사진 삭제")
public ApiResponse<String> deleteProflieImage() {
public ApiResponse<String> deleteProfileImage() {
Member member = memberCommandService.getRequester();
return memberCommandService.deleteProfileImage(member);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.codiary.backend.domain.member.dto.request;

import com.codiary.backend.domain.member.entity.Member;
import com.codiary.backend.global.apiPayload.code.status.ErrorStatus;
import com.codiary.backend.global.apiPayload.exception.handler.MemberHandler;
import com.codiary.backend.domain.member.entity.Member;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.multipart.MultipartFile;
Expand Down Expand Up @@ -45,13 +45,19 @@ public UsernamePasswordAuthenticationToken toAuthentication() {
}

// 프로필 이미지 요청 DTO
public record MemberProfileImageRequestDTO(MultipartFile image) {}
public record MemberProfileImageRequestDTO(MultipartFile image) {
}

// 회원 정보 요청 DTO
public record MemberInfoRequestDTO(
String birth,
String introduction,
String github,
String linkedin,
String discord) {}
String discord) {
}

public record refreshRequestDTO(
String refreshToken) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ public record MemberTokenResponseDTO (
TokenInfo tokenInfo,
String email,
String nickname,
Long memberId) {
}

@Builder
public record TokenRefreshResponseDTO(
String accessToken,
String email,
String nickname,
Long memberId) {}

@Builder
Expand Down
28 changes: 0 additions & 28 deletions src/main/java/com/codiary/backend/domain/member/entity/Token.java

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
import com.codiary.backend.domain.member.dto.response.MemberResponseDTO;
import com.codiary.backend.domain.member.entity.Member;
import com.codiary.backend.domain.member.entity.MemberImage;
import com.codiary.backend.domain.member.entity.Token;
import com.codiary.backend.domain.member.entity.Uuid;
import com.codiary.backend.domain.member.repository.MemberImageRepository;
import com.codiary.backend.domain.member.repository.MemberRepository;
import com.codiary.backend.domain.member.repository.TokenRepository;
import com.codiary.backend.domain.member.repository.UuidRepository;
import com.codiary.backend.global.apiPayload.ApiResponse;
import com.codiary.backend.global.apiPayload.code.status.ErrorStatus;
Expand All @@ -19,6 +17,7 @@
import com.codiary.backend.global.jwt.TokenInfo;
import com.codiary.backend.global.s3.AmazonS3Manager;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
Expand All @@ -29,6 +28,7 @@

import java.util.Date;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
Expand All @@ -42,7 +42,7 @@ public class MemberCommandService {
private final UuidRepository uuidRepository;
private final AmazonS3Manager s3Manager;
private final MemberImageRepository memberImageRepository;
private final TokenRepository tokenRepository;
private final RedisTemplate<String, String> redisTemplate;

@Transactional
public ApiResponse<String> signUp(MemberRequestDTO.MemberSignUpRequestDTO signUpRequest) {
Expand Down Expand Up @@ -119,18 +119,43 @@ public ApiResponse<MemberResponseDTO.MemberTokenResponseDTO> login(MemberRequest
}

@Transactional
public String logout(String token, Member member) {
if (!tokenRepository.existsByNotAvailableToken(token)) {
Date expiryTime = new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24);
Token tokenEntity = Token.builder()
.expiryTime(expiryTime)
.notAvailableToken(token)
.build();
tokenRepository.save(tokenEntity);
public String logout(String refreshToken) {
if (redisTemplate.hasKey(refreshToken)) {
throw new MemberHandler(ErrorStatus.MEMBER_ALREADY_LOGGED_OUT);
}

if (jwtTokenProvider.validateToken(refreshToken)) {
Date expirationDate = jwtTokenProvider.getExpirationTimeFromToken(refreshToken);
long expirationTime = (expirationDate.getTime() - (new Date()).getTime()) / 1000;
redisTemplate.opsForValue().set(refreshToken, "blacklisted", expirationTime, TimeUnit.SECONDS);
} else {
throw new MemberHandler(ErrorStatus.MEMBER_WRONG_TOKEN);
}
return "로그아웃되었습니다.";
}

@Transactional
public MemberResponseDTO.TokenRefreshResponseDTO refresh(String refreshToken) {
if (!jwtTokenProvider.validateToken(refreshToken)) {
throw new MemberHandler(ErrorStatus.MEMBER_WRONG_TOKEN);
}

if (redisTemplate.hasKey(refreshToken)) {
throw new MemberHandler(ErrorStatus.MEMBER_ALREADY_LOGGED_OUT);
}

String userEmail = jwtTokenProvider.getUserEmailFromToken(refreshToken);
String newAccessToken = jwtTokenProvider.createAccessToken(userEmail);
Member member = memberRepository.findByEmail(userEmail).orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND));

return MemberResponseDTO.TokenRefreshResponseDTO.builder()
.accessToken(newAccessToken)
.email(userEmail)
.nickname(member.getNickname())
.memberId(member.getMemberId())
.build();
}

@Transactional
public Member getRequester() {
String userEmail = SecurityUtil.getCurrentMemberEmail();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ public enum ErrorStatus implements BaseErrorCode {

// 가장 일반적인 응답
_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
_BAD_REQUEST(HttpStatus.BAD_REQUEST,"COMMON400","잘못된 요청입니다."),
_UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"COMMON401","인증이 필요합니다."),
_BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."),
_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."),
_FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),

// 회원 관려 에러 1000
Expand All @@ -27,6 +27,9 @@ public enum ErrorStatus implements BaseErrorCode {
MEMBER_WRONG_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER_1008", "비밀번호 형식이 올바르지 않습니다."),
MEMBER_EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER_1009", "이미 가입된 이메일입니다."),
MEMBER_NICKNAME_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER_1010", "이미 존재하는 닉네임입니다."),
MEMBER_REFRESH_FAIL(HttpStatus.BAD_REQUEST, "MEMBER_1011", "토큰 갱신에 실패했습니다."),
MEMBER_ALREADY_LOGGED_OUT(HttpStatus.BAD_REQUEST, "MEMBER_1012", "로그아웃된 유저입니다."),
MEMBER_WRONG_TOKEN(HttpStatus.BAD_REQUEST, "MEMBER_1013", "잘못된 토큰입니다."),
MEMBER_SELF_FOLLOW(HttpStatus.BAD_REQUEST, "MEMBER_1100", "셀프 팔로우 기능은 제공하지 않습니다"),
TECH_STACK_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER_1111", "이미 존재하는 기술스택입니다."),
PROJECT_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER_1112", "이미 존재하는 프로젝트입니다."),
Expand All @@ -52,7 +55,6 @@ public enum ErrorStatus implements BaseErrorCode {
COMMENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "COMMENT_4005", "댓글이 없습니다."),



// 북마크 관련 에러 6000
// BOOKMARK_CREATE_UNAUTHORIZED(HttpStatus.BAD_REQUEST, "BOOKMARK_6001", "북마크 추가 권한이 없습니다."),
// BOOKMARK_VIEW_UNAUTHORIZED(HttpStatus.BAD_REQUEST, "BOOKMARK_6002", "북마크 게시글 리스트 조회 권한이 없습니다."),
Expand All @@ -66,11 +68,7 @@ public enum ErrorStatus implements BaseErrorCode {
MEMBERCATEGORY_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBERCATEGORY_8001", "회원별 관심 카테고리가 없습니다."),

// 프로젝트 관련 에러 9000
PROJECT_NOT_FOUND(HttpStatus.BAD_REQUEST, "PROJECT_3009", "프로젝트가 없습니다.")


;

PROJECT_NOT_FOUND(HttpStatus.BAD_REQUEST, "PROJECT_3009", "프로젝트가 없습니다.");


private final HttpStatus httpStatus;
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/com/codiary/backend/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.codiary.backend.global.config;

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.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

@Value("${spring.data.redis.host}")
private String host;

@Value("${spring.data.redis.port}")
private int port;

@Bean
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}

@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.codiary.backend.global.jwt.EmailPasswordAuthenticationFilter;
import com.codiary.backend.global.jwt.JwtAuthenticationFilter;
import com.codiary.backend.global.jwt.JwtTokenProvider;
import com.codiary.backend.domain.member.repository.TokenRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -24,7 +23,6 @@
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
private final TokenRepository tokenRepository;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
Expand Down Expand Up @@ -53,7 +51,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
authorize -> authorize
// Member 관련 접근
.requestMatchers("/api/v2/members/sign-up", "/api/v2/members/sign-up/check-email", "api/v2/members/sign-up/check-nickname").permitAll()
.requestMatchers("/api/v2/members/login", "api/v2/members/logout").permitAll()
.requestMatchers("/api/v2/members/login", "/api/v2/members/refresh", "api/v2/members/logout").permitAll()
// Post 관련 접근
// Comment 관련 접근
// Team 관련 접근
Expand All @@ -65,7 +63,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.requestMatchers("/", "/api-docs/**", "/api-docs/swagger-config/*", "/swagger-ui/*", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, tokenRepository), EmailPasswordAuthenticationFilter.class).build();
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), EmailPasswordAuthenticationFilter.class).build();
}

@Bean
Expand Down
Loading

0 comments on commit 6322bc0

Please sign in to comment.