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

6주차 세미나 구현 과제 #5

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
3 changes: 2 additions & 1 deletion sixthAssignment/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

//JWT
Expand All @@ -37,6 +37,7 @@ dependencies {

//Security
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

//Multipart file
implementation("software.amazon.awssdk:bom:2.21.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ protected void doFilterInternal(
final String token = getJwtFromRequest(request);

if (jwtTokenProvider.validateToken(token) == VALID_JWT) {
Long memberId = jwtTokenProvider.getUserFromJwt(token);
Long memberId = jwtTokenProvider.getMemberIdFromToken(token);
setAuthentication(request, memberId);
}
} catch (Exception exception) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.sopt.common.auth.dto;

public record AuthTokenSet(
String accessToken,
String refreshToken
) {

public static AuthTokenSet of(String accessToken, String refreshToken) {
return new AuthTokenSet(accessToken, refreshToken);
}

public enum Type {
ACCESS_TOKEN,
REFRESH_TOKEN
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.sopt.common.auth.UserAuthentication;
import org.sopt.common.auth.dto.AuthTokenSet;
import org.sopt.common.auth.redis.service.TokenService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
Expand All @@ -16,36 +19,45 @@
public class JwtTokenProvider {

private static final String USER_ID = "userId";

private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14;
private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14;

private final TokenService tokenService;
@Value("${jwt.secret}")
private String JWT_SECRET;

public String issueToken(Long memberId, AuthTokenSet.Type type) {
Authentication authentication = UserAuthentication.createUserAuthentication(memberId);

public String issueAccessToken(final Authentication authentication) {
return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME);
}
if (type == AuthTokenSet.Type.ACCESS_TOKEN)
return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME);

public String issueRefreshToken(final Authentication authentication) {
return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME);
}
String refreshToken = generateToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME);
tokenService.save(memberId, refreshToken);

return refreshToken;
}

public String generateToken(Authentication authentication, Long tokenExpirationTime) {
Claims claims = generateClaims(authentication, tokenExpirationTime);

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

private Claims generateClaims(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();
return claims;
}

private SecretKey getSigningKey() {
Expand Down Expand Up @@ -77,7 +89,7 @@ private Claims getBody(final String token) {
.getBody();
}

public Long getUserFromJwt(String token) {
public Long getMemberIdFromToken(String token) {
Claims claims = getBody(token);
return Long.valueOf(claims.get(USER_ID).toString());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.sopt.common.auth.redis.domain;

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

@RedisHash(timeToLive = 60 * 60 * 24 * 1000L * 14)
@AllArgsConstructor
@Builder
@Getter
public class Token {

@Id
private Long memberId;

@Indexed
private String refreshToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.sopt.common.auth.redis.repository;

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

import java.util.Optional;

public interface TokenRepository extends CrudRepository<Token, Long> {

Optional<Token> findByMemberId(Long memberId);
Optional<Token> findByMemberIdAndRefreshToken(Long memberId, String refreshToken);
Optional<Token> findByRefreshToken(String refreshToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.sopt.common.auth.redis.service;

import lombok.RequiredArgsConstructor;
import org.sopt.common.auth.redis.domain.Token;
import org.sopt.common.auth.redis.repository.TokenRepository;
import org.sopt.common.exception.NotFoundException;
import org.sopt.common.exception.message.ErrorMessage;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class TokenService {

private final TokenRepository tokenRepository;

public void save(Long memberId, String refreshToken) {
tokenRepository.save(Token.builder()
.memberId(memberId)
.refreshToken(refreshToken)
.build());
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package org.sopt.common.dto.response;

import org.sopt.common.auth.dto.AuthTokenSet;

public record MemberJoinResponse(
String accessToken,
String refreshToken,
String userId
) {

public static MemberJoinResponse of(
String accessToken,
AuthTokenSet token,
String userId
) {
return new MemberJoinResponse(accessToken, userId);
return new MemberJoinResponse(token.accessToken(), token.refreshToken(), userId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.sopt.common.dto.response;

public record MemberTokenRefreshResponse(
String accessToken
) {

public static MemberTokenRefreshResponse of(String accessToken) {
return new MemberTokenRefreshResponse(accessToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
public enum ErrorMessage {

MEMBER_NOT_FOUND_BY_ID_EXCEPTION(HttpStatus.NOT_FOUND.value(), "ID에 해당하는 사용자가 존재하지 않습니다."),
MEMBER_NOT_FOUND_BY_REFRESH_TOKEN_EXCEPTION(HttpStatus.NOT_FOUND.value(), "리프레시 토큰에 해당하는 사용자가 존재하지 않습니다."),
BLOG_NOT_FOUND_BY_ID_EXCEPTION(HttpStatus.NOT_FOUND.value(), "ID에 해당하는 블로그가 존재하지 않습니다."),
JWT_UNAUTHORIZED_EXCEPTION(HttpStatus.UNAUTHORIZED.value(), "사용자의 로그인 검증을 실패했습니다."),
;
Expand Down
32 changes: 32 additions & 0 deletions sixthAssignment/src/main/java/org/sopt/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.sopt.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.RedisConnectionFactory;
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("${redis.host}")
private String host;

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

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@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
Expand Up @@ -5,11 +5,14 @@
import org.sopt.common.dto.response.MemberGetAllResponse;
import org.sopt.common.dto.response.MemberGetResponse;
import org.sopt.common.dto.response.MemberJoinResponse;
import org.sopt.common.dto.response.MemberTokenRefreshResponse;
import org.sopt.service.MemberService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.security.Principal;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/member")
Expand All @@ -28,11 +31,20 @@ public ResponseEntity<MemberJoinResponse> postMember(
.body(memberJoinResponse);
}

@GetMapping("/refresh")
public ResponseEntity<MemberTokenRefreshResponse> refreshAccessToken(
@RequestHeader("Authorization") String refreshToken
) {
return ResponseEntity.status(HttpStatus.OK)
.body(memberService.refreshAccessToken(refreshToken));
}

@GetMapping("/{memberId}")
public ResponseEntity<MemberGetResponse> findMemberById(
@PathVariable Long memberId
) {
return ResponseEntity.ok(memberService.findMemberById(memberId));
return ResponseEntity.status(HttpStatus.OK)
.body(MemberGetResponse.of(memberService.findById(memberId)));
}

@DeleteMapping("/{memberId}")
Expand Down
47 changes: 24 additions & 23 deletions sixthAssignment/src/main/java/org/sopt/service/MemberService.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package org.sopt.service;

import lombok.RequiredArgsConstructor;
import org.sopt.common.auth.UserAuthentication;
import org.sopt.common.auth.dto.AuthTokenSet;
import org.sopt.common.auth.redis.service.TokenService;
import org.sopt.common.dto.request.MemberJoinRequest;
import org.sopt.common.dto.response.MemberJoinResponse;
import org.sopt.common.auth.jwt.JwtTokenProvider;
import org.sopt.common.dto.response.MemberTokenRefreshResponse;
import org.sopt.common.exception.UnauthorizedException;
import org.sopt.domain.Member;
import org.sopt.common.dto.request.MemberCreateRequest;
import org.sopt.common.dto.response.MemberGetAllResponse;
import org.sopt.common.dto.response.MemberGetResponse;
import org.sopt.common.exception.NotFoundException;
Expand All @@ -15,46 +17,45 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.security.Principal;
import java.util.List;

import static org.sopt.common.auth.dto.AuthTokenSet.Type.ACCESS_TOKEN;
import static org.sopt.common.auth.dto.AuthTokenSet.Type.REFRESH_TOKEN;
import static org.sopt.common.auth.jwt.JwtValidationType.VALID_JWT;

@Service
@RequiredArgsConstructor
public class MemberService {

private final MemberRepository memberRepository;
private final JwtTokenProvider jwtTokenProvider;

@Transactional
public String createMemberWithoutAuth(
MemberCreateRequest request
) {
Member member = request.toEntity();
memberRepository.save(member);

return member.getId().toString();
}
private final TokenService tokenService;

@Transactional
public MemberJoinResponse createMember(
MemberJoinRequest request
) {
Member member = memberRepository.save(request.toEntity());
String accessToken = getToken(member.getId());
AuthTokenSet token = getTokenSet(member.getId());

return MemberJoinResponse.of(accessToken, member.getId().toString());
return MemberJoinResponse.of(token, member.getId().toString());
}

private String getToken(Long memberId) {
UserAuthentication authentication = UserAuthentication.createUserAuthentication(memberId);

return jwtTokenProvider.issueAccessToken(authentication);
private AuthTokenSet getTokenSet(Long memberId) {
return AuthTokenSet.of(
jwtTokenProvider.issueToken(memberId, ACCESS_TOKEN),
jwtTokenProvider.issueToken(memberId, REFRESH_TOKEN)
);
}

public MemberGetResponse findMemberById(
Long memberId
) {
return MemberGetResponse.of(memberRepository.findById(memberId)
.orElseThrow(() -> new NotFoundException(ErrorMessage.MEMBER_NOT_FOUND_BY_ID_EXCEPTION)));
public MemberTokenRefreshResponse refreshAccessToken(String refreshToken) {
if (jwtTokenProvider.validateToken(refreshToken) != VALID_JWT)
throw new UnauthorizedException(ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION);

Long memberId = jwtTokenProvider.getMemberIdFromToken(refreshToken);
String accessToken = jwtTokenProvider.issueToken(memberId, ACCESS_TOKEN);
return MemberTokenRefreshResponse.of(accessToken);
}

public Member findById(Long memberId) {
Expand Down