Skip to content

Commit

Permalink
Merge pull request #21 from NewsFit-jolp/feat/#20-jwt-logout
Browse files Browse the repository at this point in the history
Feat/#20 jwt logout
  • Loading branch information
k000927 authored Oct 9, 2024
2 parents 8248bc1 + b8590bf commit 2e6422f
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 36 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
compileOnly 'org.projectlombok:lombok:1.18.30'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,14 @@ public SuccessResponse<Boolean> postCommentLikes(@PathVariable("articleId") Stri
@PathVariable("commentId") String commentId) {
return SuccessResponse.createSuccess(articleService.postCommentLikes(articleId, commentId));
}

@Operation(summary = "댓글 좋아요 취소",
description = """
댓글 좋아요 취소 API입니다.
""")
@DeleteMapping("/{articleId}/comments/{commentId}/likes")
public SuccessResponse<Boolean> deleteCommentLikes(@PathVariable("articleId") String articleId,
@PathVariable("commentId") String commentId) {
return SuccessResponse.createSuccess(articleService.deleteCommentLikes(articleId, commentId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public Boolean deleteComment(String articleId, String commentId) {
Member member = memberRepository.findByMemberId(SecurityContextHolder.getContext().getAuthentication().getName())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

if(!comment.getMember().equals(member)){
if (!comment.getMember().equals(member)) {
throw new CustomException(ErrorCode.COMMENT_DELETE_FORBIDDEN);
}

Expand All @@ -148,7 +148,7 @@ public Boolean postArticleLikes(String articleId) {
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));


if(articleLikesRepository.findByMemberAndArticle(member, article).isPresent()){
if (articleLikesRepository.findByMemberAndArticle(member, article).isPresent()) {
throw new CustomException(ErrorCode.DUPLICATED_ARTICLE_LIKE);
}

Expand All @@ -172,7 +172,7 @@ public Boolean deleteArticleLikes(String articleId) {

Optional<Integer> removeLike = articleLikesRepository.removeByMemberAndArticle(member, article);

if(removeLike.isPresent() && removeLike.get() == 0){
if (removeLike.isPresent() && removeLike.get() == 0) {
throw new CustomException(ErrorCode.ARTICLE_LIKE_NOT_FOUND);
}

Expand All @@ -181,14 +181,14 @@ public Boolean deleteArticleLikes(String articleId) {
return true;
}

public Boolean postCommentLikes(String articleId, String commentId){
public Boolean postCommentLikes(String articleId, String commentId) {
Comment comment = commentRepository.findById(Long.parseLong(commentId))
.orElseThrow(() -> new CustomException(ErrorCode.COMMENT_NOT_FOUND));

Member member = memberRepository.findByMemberId(SecurityContextHolder.getContext().getAuthentication().getName())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

if(commentLikesRepository.findByMemberAndComment(member, comment).isPresent()){
if (commentLikesRepository.findByMemberAndComment(member, comment).isPresent()) {
throw new CustomException(ErrorCode.DUPLICATED_COMMENT_LIKE);
}

Expand All @@ -202,5 +202,23 @@ public Boolean postCommentLikes(String articleId, String commentId){

return true;
}

public Boolean deleteCommentLikes(String articleId, String commentId) {
Comment comment = commentRepository.findById(Long.parseLong(commentId))
.orElseThrow(() -> new CustomException(ErrorCode.COMMENT_NOT_FOUND));

Member member = memberRepository.findByMemberId(SecurityContextHolder.getContext().getAuthentication().getName())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

Optional<Integer> removeLike = commentLikesRepository.removeByMemberAndComment(member, comment);

if (removeLike.isPresent() && removeLike.get() == 0) {
throw new CustomException(ErrorCode.ARTICLE_LIKE_NOT_FOUND);
}

comment.subLikeCount();

return true;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import net.minidev.json.parser.ParseException;
import org.apache.commons.lang3.tuple.Pair;
Expand All @@ -34,16 +35,16 @@ public class MemberController {
@Operation(summary = "카카오 인증 서버를 통한 로그인",
description = """
카카오 인증 서버를 통해 로그인합니다.
**상태 코드에 따라 최초 회원가입, 기존 유저 로그인 여부를 알 수 있습니다.**
- statusCode가 200인 경우: 기존 유저 로그인
- statusCode가 201인 경우: 최초 회원가입
개발용 유저 삭제 API를 통해 최초 회원가입이 정상적으로 처리되는지 확인할 수 있습니다.
요청값:
- (Query Parameter) code: 카카오 인증서버에서 받은 인증 코드값입니다.
반환값:
- accessToken: 서버 내부에서 발급한 토큰입니다.
- refreshToken: 서버 내부에서 발급한 토큰입니다.
Expand All @@ -68,16 +69,16 @@ public SuccessResponse<TokenResponse> kakaoLogin(@Parameter(name = "code", descr
@Operation(summary = "구글 인증 서버를 통한 로그인",
description = """
구글 인증 서버를 통해 로그인합니다.
**상태 코드에 따라 최초 회원가입, 기존 유저 로그인 여부를 알 수 있습니다.**
- statusCode가 200인 경우: 기존 유저 로그인
- statusCode가 201인 경우: 최초 회원가입
개발용 유저 삭제 API를 통해 최초 회원가입이 정상적으로 처리되는지 확인할 수 있습니다.
요청값:
- (Query Parameter) code: 구글 인증서버에서 받은 인증 코드값입니다.
반환값:
- accessToken: 서버 내부에서 발급한 토큰입니다.
- refreshToken: 서버 내부에서 발급한 토큰입니다.
Expand All @@ -100,16 +101,16 @@ public SuccessResponse<TokenResponse> googleLogin(@Parameter(name = "code", desc
@Operation(summary = "네이버 인증 서버를 통한 로그인",
description = """
네이버 인증 서버를 통해 로그인합니다.
**상태 코드에 따라 최초 회원가입, 기존 유저 로그인 여부를 알 수 있습니다.**
- statusCode가 200인 경우: 기존 유저 로그인
- statusCode가 201인 경우: 최초 회원가입
개발용 유저 삭제 API를 통해 최초 회원가입이 정상적으로 처리되는지 확인할 수 있습니다.
요청값:
- (Query Parameter) code: 네이버 인증서버에서 받은 인증 코드값입니다.
반환값:
- accessToken: 서버 내부에서 발급한 토큰입니다.
- refreshToken: 서버 내부에서 발급한 토큰입니다.
Expand Down Expand Up @@ -185,37 +186,44 @@ public SuccessResponse<Boolean> deleteMember() {


@Operation(summary = "유저 선호 주제 조회하기",
description = """
유저 선호 주제를 조회합니다.
""")
description = """
유저 선호 주제를 조회합니다.
""")
@GetMapping("/categories")
public SuccessResponse<GetPreferredCategories> getPreferredCategories(){
public SuccessResponse<GetPreferredCategories> getPreferredCategories() {
return SuccessResponse.success(memberService.getPreferredCategories());
}

@Operation(summary = "유저 선호 언론사 조회하기",
description = """
유저 선호 언론사를 조회합니다.
""")
유저 선호 언론사를 조회합니다.
""")
@GetMapping("/press")
public SuccessResponse<GetPreferredPress> getPreferredPress(){
public SuccessResponse<GetPreferredPress> getPreferredPress() {
return SuccessResponse.success(memberService.getPreferredPress());
}

@Operation(summary = "(개발용) 유저 삭제하기",
description = """
개발용 API입니다. 로그인 한 유저를 삭제합니다.
**테스트용으로만 사용해야 합니다.**
에피소드, 활동 태그, 보석함 등의 유저 관련 모든 데이터가 삭제됩니다.
# **해당 API는 soft delete 하지 않고 데이터를 직접 삭제합니다. 사용에 주의하세요**
""")
@DeleteMapping("/delete")
public SuccessResponse<Boolean> deleteUser() {
return SuccessResponse.success(memberService.deleteUser());
}


@DeleteMapping("/logout")
public SuccessResponse<Boolean> logout(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");
String accessToken = authorization.split(" ")[1].trim();
return SuccessResponse.success(memberService.logout(accessToken));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.example.newsfit.domain.member.repository.MemberRepository;
import com.example.newsfit.global.error.exception.CustomException;
import com.example.newsfit.global.error.exception.ErrorCode;
import com.example.newsfit.global.util.RedisUtil;
import lombok.RequiredArgsConstructor;
import net.minidev.json.JSONArray;
import net.minidev.json.JSONObject;
Expand All @@ -31,6 +32,7 @@
public class MemberService {

private final MemberRepository memberRepository;
private final RedisUtil redisUtil;

public GetMemberInfo getMemberInfo() {
Member member = memberRepository.findByMemberId(SecurityContextHolder.getContext().getAuthentication().getName())
Expand All @@ -40,7 +42,7 @@ public GetMemberInfo getMemberInfo() {
}

@Transactional
public GetMemberInfo putMemberInfo(String requestBody) throws ParseException, java.text.ParseException {
public GetMemberInfo putMemberInfo(String requestBody) throws ParseException, java.text.ParseException {
Member member = memberRepository.findByMemberId(SecurityContextHolder.getContext().getAuthentication().getName())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

Expand Down Expand Up @@ -86,7 +88,7 @@ public GetPreferredPress putPreferredPress(String requestBody) throws ParseExcep
}

@Transactional
public Boolean deleteMember(){
public Boolean deleteMember() {
String memberId = SecurityContextHolder.getContext().getAuthentication().getName();

Member member = memberRepository.findByMemberId(memberId)
Expand All @@ -97,14 +99,14 @@ public Boolean deleteMember(){
return true;
}

public GetPreferredCategories getPreferredCategories(){
public GetPreferredCategories getPreferredCategories() {
Member member = memberRepository.findByMemberId(SecurityContextHolder.getContext().getAuthentication().getName())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

return GetPreferredCategories.of(member);
}

public GetPreferredPress getPreferredPress(){
public GetPreferredPress getPreferredPress() {
Member member = memberRepository.findByMemberId(SecurityContextHolder.getContext().getAuthentication().getName())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

Expand Down Expand Up @@ -150,4 +152,9 @@ public Pair<Member, Boolean> registerMemberIfNeed(MemberDto MemberInfo) {
}
return Pair.of(member, false);
}

public Boolean logout(String accessToken) {
redisUtil.setBlackList(accessToken, "logout", 8640000000L);
return true;
}
}
39 changes: 39 additions & 0 deletions src/main/java/com/example/newsfit/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.newsfit.global.config;

import lombok.RequiredArgsConstructor;
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 org.springframework.data.redis.serializer.StringRedisSerializer;

@RequiredArgsConstructor
@Configuration
@EnableRedisRepositories
public class RedisConfig {

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

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

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

@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();

template.setConnectionFactory(redisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());

return template;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public enum ErrorCode {
COMMENT_DELETE_FORBIDDEN(403, "C009", "댓글 삭제 권한이 없는 사용자입니다."),
DUPLICATED_ARTICLE_LIKE(400, "C010", "이미 좋아요를 누른 게시글입니다."),
ARTICLE_LIKE_NOT_FOUND(404, "C011", "좋아요를 누르지 않은 게시글입니다."),
DUPLICATED_COMMENT_LIKE(400, "C012", "이미 좋아요를 누른 댓글입니다.");
DUPLICATED_COMMENT_LIKE(400, "C012", "이미 좋아요를 누른 댓글입니다."),
TOKEN_EXPIRED(409, "C013", "만료된 토큰입니다.");



Expand Down
12 changes: 9 additions & 3 deletions src/main/java/com/example/newsfit/global/jwt/TokenService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.example.newsfit.domain.member.repository.MemberRepository;
import com.example.newsfit.global.error.exception.CustomException;
import com.example.newsfit.global.error.exception.ErrorCode;
import com.example.newsfit.global.util.RedisUtil;
import io.jsonwebtoken.*;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
Expand All @@ -18,14 +19,16 @@
import java.util.Base64;
import java.util.Date;

@Service @RequiredArgsConstructor
@Service
@RequiredArgsConstructor
public class TokenService {

@Value("${jwt.secret}")
private String secretKey;

private final JpaMemberDetailsService userDetailsService;
private final MemberRepository memberRepository;
private final RedisUtil redisUtil;

@PostConstruct
protected void init() {
Expand Down Expand Up @@ -94,6 +97,10 @@ public boolean validateToken(String token) {
} else {
token = token.split(" ")[1].trim();
}
if (redisUtil.hasKeyBlackList(token)) {
throw new CustomException(ErrorCode.TOKEN_EXPIRED);
}

JwtParser build = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build();
Expand All @@ -102,8 +109,7 @@ public boolean validateToken(String token) {

// 만료되었을 시 false
return !claims.getBody().getExpiration().before(new Date());
}
catch (Exception e) {
} catch (Exception e) {
return false;
}
}
Expand Down
Loading

0 comments on commit 2e6422f

Please sign in to comment.