Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] 6차 세미나 과제 코드입니다. #8

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.sopt.practice.auth.redis.config;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
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;

@Configuration
@RequiredArgsConstructor
@EnableRedisRepositories
public class RedisRepositoryConfig {

private final RedisProperties redisProperties;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
}

@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.sopt.practice.auth.redis.controller;

import lombok.RequiredArgsConstructor;
import org.sopt.practice.auth.PrincipalHandler;
import org.sopt.practice.auth.redis.service.RedisTokenService;
import org.sopt.practice.service.dto.AccessTokenDto;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

@Transactional
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/member")
public class TokenController {

private final RedisTokenService redisTokenService;
private final PrincipalHandler principalHandler;

@PostMapping("/refresh-token")
public ResponseEntity<AccessTokenDto> refreshToken(){
Long userId = principalHandler.getUserIdFromPrincipal();
AccessTokenDto newAccessTokenResponse = redisTokenService.refreshToken(userId);
return ResponseEntity.status(HttpStatus.CREATED)
.body(newAccessTokenResponse);
}
}
Comment on lines +21 to +28

Choose a reason for hiding this comment

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

위의 메서드에서 @RequestBody로 RefreshToken을 받도록 수정하면 좀 더 안전하게 토큰을 갱신할 수 있을 것 같다는 생각이 드네요..!!

@PostMapping("/refresh-token")
public ResponseEntity<AccessTokenDto> refreshToken(@RequestBody String refreshToken){
    Long userId = principalHandler.getUserIdFromPrincipal();
    AccessTokenDto newAccessTokenResponse = redisTokenService.refreshToken(userId, refreshToken);
    return ResponseEntity.status(HttpStatus.CREATED).body(newAccessTokenResponse);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.sopt.practice.auth.redis.domain;

import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;

@RedisHash(value = "", timeToLive = 60 * 60 * 24 * 1000L * 14) //TTL 설정
//value = ""이렇게 하면, 객체가 Redis에 저장될 때 클래스의 전체 이름이 해시 키로 사용

@AllArgsConstructor
@Getter
@Builder
public class Token {

@Id
private Long id;

@Indexed //Redis에서 Indexed 어노테이션 사용 시 이 값으로 객체 값 찾을 수 있다. 주로 검색 조건으로 사용되는 필드에 적용!
private String refreshToken;

public static Token of(
final Long id,
final String refreshToken
){
return Token.builder()
.id(id)
.refreshToken(refreshToken)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.sopt.practice.auth.redis.repository;

import org.sopt.practice.auth.redis.domain.Token;
import org.springframework.data.repository.CrudRepository;

import java.util.Optional;

public interface RedisTokenRepository extends CrudRepository<Token, Long> {
/*
* Optional은 메소드의 결과가 null이 될 수 있으며, null에 의해 오류가 발생할 가능성이 매우 높을 때 반환값으로만 사용
* */

Optional<Token> findByRefreshToken(final String refreshToken);
Optional<Token> findById(final Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.sopt.practice.auth.redis.service;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.sopt.practice.auth.redis.domain.Token;
import org.sopt.practice.auth.redis.repository.RedisTokenRepository;
import org.sopt.practice.common.dto.ErrorMessage;
import org.sopt.practice.common.jwt.JwtTokenProvider;
import org.sopt.practice.common.jwt.JwtValidationType;
import org.sopt.practice.exception.UnauthorizedException;
import org.sopt.practice.service.dto.AccessTokenDto;
import org.sopt.practice.auth.UserAuthentication;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class RedisTokenService {

private final RedisTokenRepository redisTokenRepository;
private final JwtTokenProvider jwtTokenProvider;

@Transactional
public AccessTokenDto refreshToken(Long userId) {
// Redis에서 Refresh Token을 조회
Token token = redisTokenRepository.findById(userId)
.orElseThrow(() -> new UnauthorizedException(ErrorMessage.REFRESH_TOKEN_NOT_FOUND));

// Refresh Token 검증
JwtValidationType validationType = jwtTokenProvider.validateToken(token.getRefreshToken());
if (validationType == JwtValidationType.EXPIRED_JWT_TOKEN) {
throw new UnauthorizedException(ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION);
} else if (validationType != JwtValidationType.VALID_JWT) {
throw new UnauthorizedException(ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION);
}

// 새로운 Access Token 발급
String newAccessToken = jwtTokenProvider.newAccessToken(token.getRefreshToken());

// 새로운 Access Token을 포함한 응답 객체 반환
return AccessTokenDto.of(newAccessToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package org.sopt.practice.common.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.sopt.practice.auth.UserAuthentication;
import org.sopt.practice.service.dto.UserJoinResponse;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Value;

import javax.crypto.SecretKey;
import java.util.Base64;
import java.util.Date;

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

private static final String USER_ID = "userId";

//AccessToken 관련 로직
private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 2;

@Value("${jwt.secret}") //application.yml 파일에 설정한 암호화 키를 가져옴
private String JWT_SECRET;


public String issueAccessToken(final Authentication authentication) {
return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME);
}

// RefreshToken 관련 로직
private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 60 * 60 * 24 * 1000L * 14;

public String issueRefreshToken(final Authentication authentication) {
return generateToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME);
}

public String generateToken(Authentication authentication, Long tokenExpirationTime) {
final Date now = new Date();
final Claims claims = Jwts.claims()
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + tokenExpirationTime)); // 만료 시간

claims.put(USER_ID, authentication.getPrincipal());

return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) // Header
.setClaims(claims) // Claim
.signWith(getSigningKey()) // Signature
.compact();
}

private SecretKey getSigningKey() {
String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); //SecretKey 통해 서명 생성
return Keys.hmacShaKeyFor(encodedKey.getBytes()); //일반적으로 HMAC (Hash-based Message Authentication Code) 알고리즘 사용
}

public JwtValidationType validateToken(String token) {
try {
final Claims claims = getBody(token);
return JwtValidationType.VALID_JWT;
} catch (MalformedJwtException ex) {
return JwtValidationType.INVALID_JWT_TOKEN;
} catch (ExpiredJwtException ex) {
return JwtValidationType.EXPIRED_JWT_TOKEN;
} catch (UnsupportedJwtException ex) {
return JwtValidationType.UNSUPPORTED_JWT_TOKEN;
} catch (IllegalArgumentException ex) {
return JwtValidationType.EMPTY_JWT;
}
}

private Claims getBody(final String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}

public Long getUserFromJwt(String token) {
Claims claims = getBody(token);
return Long.valueOf(claims.get(USER_ID).toString());
}

// 리프레시 토큰이 유효하면 새로운 access token 발급
public String newAccessToken(String refreshToken){
Claims claims = getBody(refreshToken);
Long userId = Long.valueOf(claims.get(USER_ID).toString());

Authentication authentication = UserAuthentication.createUserAuthentication(userId);
return issueAccessToken(authentication);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.sopt.practice.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.sopt.practice.auth.PrincipalHandler;
import org.sopt.practice.common.dto.SuccessMessage;
import org.sopt.practice.common.dto.SuccessStatusResponse;
import org.sopt.practice.service.BlogService;
import org.sopt.practice.service.dto.BlogCreateRequest;
import org.sopt.practice.service.dto.BlogTitleUpdateRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;

@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class BlogController {

private final BlogService blogService;

// @PostMapping("/blog")
// public ResponseEntity<SuccessStatusResponse> createBlog(
// @RequestHeader(name = "memberId") Long memberId,
// @RequestBody BlogCreateRequest blogCreateRequest
// ) {
// return ResponseEntity.status(HttpStatus.CREATED).header(
// "Location",
// blogService.create(memberId, blogCreateRequest))
// .body(SuccessStatusResponse.of(SuccessMessage.BLOG_CREATE_SUCCESS));
// }

//5.18
private final PrincipalHandler principalHandler;
//
// @PostMapping("/blog")
// public ResponseEntity createBlog(
// BlogCreateRequest blogCreateRequest
// ) {
// return ResponseEntity.created(URI.create(blogService.create(
// principalHandler.getUserIdFromPrincipal(), blogCreateRequest))).build();
// }

//5.18
@PostMapping("/blog")
public ResponseEntity createBlog(
BlogCreateRequest blogCreateRequest
) {
return ResponseEntity.created(URI.create(blogService.create(
principalHandler.getUserIdFromPrincipal(), blogCreateRequest))).build();
}

@PatchMapping("/blog/{blogId}/title")
public ResponseEntity updateBlogTitle(
@PathVariable Long blogId,
@Valid @RequestBody BlogTitleUpdateRequest blogTitleUpdateRequest
) {
blogService.updateTitle(blogId, blogTitleUpdateRequest);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,72 @@
import jakarta.persistence.EntityNotFoundException;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.apache.catalina.User;
import org.sopt.practice.auth.UserAuthentication;
import org.sopt.practice.auth.redis.domain.Token;
import org.sopt.practice.auth.redis.repository.RedisTokenRepository;
import org.sopt.practice.common.dto.ErrorMessage;
import org.sopt.practice.common.jwt.JwtTokenProvider;
import org.sopt.practice.exception.NotFoundException;
import org.sopt.practice.service.dto.MemberCreateDto;
import org.sopt.practice.domain.Member;
import org.sopt.practice.repository.MemberRepository;
import org.sopt.practice.service.dto.MemberFindDto;
import org.sopt.practice.service.dto.UserJoinResponse;
import org.springframework.stereotype.Service;

import java.util.ArrayList;

@Service
@RequiredArgsConstructor
public class MemberService {
public class MemberService { //Service 에는 비즈니스 로직을 구현한다.

private final MemberRepository memberRepository;
private final JwtTokenProvider jwtTokenProvider;
private final RedisTokenRepository redisTokenRepository;

// @Transactional
// public String createMember(
// MemberCreateDto memberCreate
// ) { //포스트맨에서 POST로 멤버 생성 가능
// Member member = memberRepository.save(Member.create(memberCreate.name(), memberCreate.part(), memberCreate.age()));
// return member.getId().toString();
// }

// 5.18 세미나 부분
@Transactional
public String createMember(
public UserJoinResponse createMember(
MemberCreateDto memberCreate
) {
Member member = memberRepository.save(Member.create(memberCreate.name(), memberCreate.part(), memberCreate.age()));
return member.getId().toString();
Member member = memberRepository.save(
Member.create(memberCreate.name(), memberCreate.part(), memberCreate.age())
);
Long memberId = member.getId();
String accessToken = jwtTokenProvider.issueAccessToken( //사용자의 인증 정보를 이용하여 토큰을 발급
UserAuthentication.createUserAuthentication(memberId)
);
String refreshToken = jwtTokenProvider.issueRefreshToken(
UserAuthentication.createUserAuthentication(memberId)
);
redisTokenRepository.save(Token.of(memberId,refreshToken));
return UserJoinResponse.of(accessToken, refreshToken, memberId.toString());
}

public MemberFindDto findMemberById(Long memberId){

public Member findById(Long memberId) {
return memberRepository.findById(memberId).orElseThrow(
() -> new NotFoundException(ErrorMessage.MEMBER_NOT_FOUND)
);
}

public MemberFindDto findMemberById(Long memberId){ //ID를 기준으로 Member 찾기
Member member = memberRepository.findById(memberId).orElseThrow(
()->new EntityNotFoundException("ID에 해당하는 사용자가 존재하지 않습니다."));
return MemberFindDto.of(member);
}

@org.springframework.transaction.annotation.Transactional
public void deleteMemberById(Long memberId){
@Transactional
public void deleteMemberById(Long memberId){ //ID를 기준으로 Member 를 삭제
Member member=memberRepository.findById(memberId).orElseThrow(
()->new EntityNotFoundException("ID에 해당하는 사용자가 존재하지 않습니다."));
memberRepository.delete(member);
Expand Down
Loading