Skip to content

Commit

Permalink
#525 [release] v1.1.0
Browse files Browse the repository at this point in the history
#525 [release] v1.1.0
  • Loading branch information
sohyundoh authored Oct 9, 2024
2 parents 07ed71e + 6b35522 commit 450f20c
Show file tree
Hide file tree
Showing 115 changed files with 1,139 additions and 406 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ module-common/build/
module-domain/build/
module-auth/build/
module-api/src/main/resources/application.yml

module-domain/src/test/resources/application.yml
3 changes: 3 additions & 0 deletions module-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ dependencies {
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

//Sentry
implementation 'io.sentry:sentry-spring-boot-starter-jakarta:7.9.0'
}

tasks.named('test') {
Expand Down
52 changes: 38 additions & 14 deletions module-api/src/main/java/com/mile/common/auth/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.mile.common.auth;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mile.exception.message.ErrorMessage;
import com.mile.exception.model.BadRequestException;
import com.mile.exception.model.UnauthorizedException;
import com.mile.writername.domain.MoimRole;
import com.mile.writername.service.vo.WriterNameInfo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Header;
Expand All @@ -11,19 +15,26 @@
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.redisson.misc.Hash;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Service
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

private final ObjectMapper objectMapper;
private static final String MEMBER_ID = "memberId";
private static final String JOINED_ROLE = "joinedRole";
private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 4 * 60 * 60 * 1000L;
private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 60 * 60 * 24 * 1000L * 14;

Expand All @@ -32,7 +43,6 @@ public class JwtTokenProvider {

@PostConstruct
protected void init() {
//base64 라이브러리에서 encodeToString을 이용해서 byte[] 형식을 String 형식으로 변환
JWT_SECRET = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes(StandardCharsets.UTF_8));
}

Expand All @@ -45,36 +55,39 @@ private String getTokenFromHeader(final String token) {
return token.substring("Bearer ".length());
}

public String issueAccessToken(final Long userId) {
return issueToken(userId, ACCESS_TOKEN_EXPIRATION_TIME);
public String issueAccessToken(final Long userId, final Map<Long, WriterNameInfo> joinedRole) {
return issueToken(userId, joinedRole, ACCESS_TOKEN_EXPIRATION_TIME);
}


public String issueRefreshToken(final Long userId) {
return issueToken(userId, REFRESH_TOKEN_EXPIRATION_TIME);
public String issueRefreshToken(final Long userId, final Map<Long, WriterNameInfo> joinedRole) {
return issueToken(userId, joinedRole, REFRESH_TOKEN_EXPIRATION_TIME);
}

private String issueToken(
final Long userId,
final Map<Long, WriterNameInfo> role,
final Long expiredTime
) {
final Date now = new Date();

final Claims claims = Jwts.claims()
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + expiredTime)); // 만료 시간 설정
.setExpiration(new Date(now.getTime() + expiredTime));

claims.put(MEMBER_ID, userId);
claims.put(JOINED_ROLE, role);

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

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

public JwtValidationType validateToken(final String token) {
Expand All @@ -100,8 +113,19 @@ private Claims getBody(final String token) {
.getBody();
}

public Long getUserFromJwt(final String token) {
public Long getUserFromAuthHeader(final String token) {
Claims claims = getBody(getTokenFromHeader(token));
return Long.valueOf(claims.get(MEMBER_ID).toString());
}

public HashMap<Long, WriterNameInfo> getJoinedRoleFromHeader(final String token) {
return getJoinedRoleFromJwt(getTokenFromHeader(token));
}

public HashMap<Long, WriterNameInfo> getJoinedRoleFromJwt(final String token) {
Claims claims = getBody(token);
Object joinedRole = claims.get(JOINED_ROLE);
HashMap<Long, WriterNameInfo> roleMap = objectMapper.convertValue(joinedRole, new TypeReference<HashMap<Long, WriterNameInfo>>() {});
return roleMap;
}
}
36 changes: 36 additions & 0 deletions module-api/src/main/java/com/mile/common/auth/JwtTokenUpdater.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.mile.common.auth;

import com.mile.writername.service.vo.WriterNameInfo;
import com.mile.jwt.service.TokenService;
import com.mile.writername.domain.MoimRole;
import com.mile.writername.service.WriterNameRetriever;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
@RequiredArgsConstructor
public class JwtTokenUpdater {

private final JwtTokenProvider jwtTokenProvider;
private final TokenService tokenService;
private final WriterNameRetriever writerNameRetriever;

public String setAccessToken(final Long userId, final Long moimId, final Long writerNameId, final MoimRole moimRole) {

Map<Long, WriterNameInfo> moimRoleMap = writerNameRetriever.getJoinedRoleFromUserId(userId);

tokenService.deleteRefreshToken(userId);

moimRoleMap.put(moimId, WriterNameInfo.of(writerNameId, moimRole));

String newAccessToken = jwtTokenProvider.issueAccessToken(userId, moimRoleMap);
String newRefreshToken = jwtTokenProvider.issueRefreshToken(userId, moimRoleMap);

tokenService.saveRefreshToken(userId, newRefreshToken);

return newAccessToken;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.mile.common.auth.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface UserAuthAnnotation {
UserAuthenticationType value() default UserAuthenticationType.USER;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.mile.common.auth.annotation;

public enum UserAuthenticationType {
OWNER,
WRITER_NAME,
USER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.mile.common.auth.dto;

public record AccessTokenDto<T>(
String accessToken,
T response
) {
public static <T> AccessTokenDto<T> of(final T data, final String accessToken) {
return new AccessTokenDto<>(accessToken, data);
}
public static AccessTokenDto of(final String accessToken) {
return new AccessTokenDto(accessToken, null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.mile.common.interceptor;

import com.mile.common.auth.JwtTokenProvider;
import com.mile.common.auth.annotation.UserAuthAnnotation;
import com.mile.common.utils.thread.WriterNameContextUtil;
import com.mile.common.utils.SecureUrlUtil;
import com.mile.exception.message.ErrorMessage;
import com.mile.exception.model.ForbiddenException;
import com.mile.exception.model.UnauthorizedException;
import com.mile.writername.domain.MoimRole;
import com.mile.writername.service.vo.WriterNameInfo;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import static org.springframework.web.servlet.HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;

@Component
@RequiredArgsConstructor
public class MoimAuthInterceptor implements HandlerInterceptor {

private static final String MOIM_ID = "moimId";
private final JwtTokenProvider jwtTokenProvider;
private final SecureUrlUtil secureUrlUtil;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (handler instanceof ResourceHttpRequestHandler || Objects.equals(request.getMethod(), "OPTIONS")) {
return true;
}
HandlerMethod method = (HandlerMethod) handler;

UserAuthAnnotation annotation = method.getMethodAnnotation(UserAuthAnnotation.class);

if (annotation != null) {
final String userToken = getUserTokenFromHeader(request);

final HashMap<Long, WriterNameInfo> roleFromUser = jwtTokenProvider.getJoinedRoleFromHeader(userToken);
final Map<String, String> pathVariables = (Map<String, String>) request.getAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE);

return authenticateUserFromMap(annotation, roleFromUser, pathVariables);
}
return true;
}

private String getUserTokenFromHeader(final HttpServletRequest request) {
final String userToken = request.getHeader("Authorization");

if (userToken == null) {
throw new UnauthorizedException(ErrorMessage.UN_LOGIN_EXCEPTION);
}

return userToken;
}

private boolean authenticateUserFromMap(final UserAuthAnnotation annotation,
final HashMap<Long, WriterNameInfo> userRoles,
final Map<String, String> pathVariables) {
switch (annotation.value()) {
case OWNER -> {
final Long requestMoimId = secureUrlUtil.decodeUrl(pathVariables.get(MOIM_ID));
if (!userRoles.containsKey(requestMoimId) || !userRoles.get(requestMoimId).moimRole().equals(MoimRole.OWNER)) {
throw new ForbiddenException(ErrorMessage.MOIM_OWNER_AUTHENTICATION_ERROR);
}
WriterNameContextUtil.setWriterNameIdContext(userRoles.get(requestMoimId).writerNameId());
return true;
}
case WRITER_NAME -> {
final Long requestMoimId = secureUrlUtil.decodeUrl(pathVariables.get(MOIM_ID));
if (!userRoles.containsKey(requestMoimId)) {
throw new ForbiddenException(ErrorMessage.USER_MOIM_AUTHENTICATE_ERROR);
}
WriterNameContextUtil.setWriterNameIdContext(userRoles.get(requestMoimId).writerNameId());
return true;
}
case USER -> {
WriterNameContextUtil.setMoimWriterNameMapContext(userRoles);
return true;
}
}
return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
WriterNameContextUtil.clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import com.mile.exception.message.ErrorMessage;
import com.mile.exception.model.BadRequestException;
import com.mile.utils.SecureUrlUtil;
import com.mile.common.utils.SecureUrlUtil;
import com.mile.exception.model.NotFoundException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
Expand Down Expand Up @@ -36,7 +37,7 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m
try {
return secureUrlUtil.decodeUrl(id);
} catch (NumberFormatException e) {
throw new BadRequestException(ErrorMessage.INVALID_URL_EXCEPTION);
throw new NotFoundException(ErrorMessage.INVALID_URL_EXCEPTION);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import com.mile.exception.message.ErrorMessage;
import com.mile.exception.model.BadRequestException;
import com.mile.utils.SecureUrlUtil;
import com.mile.common.utils.SecureUrlUtil;
import com.mile.exception.model.NotFoundException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
Expand Down Expand Up @@ -36,7 +37,7 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m
try {
return secureUrlUtil.decodeUrl(moimId);
} catch (NumberFormatException e) {
throw new BadRequestException(ErrorMessage.INVALID_URL_EXCEPTION);
throw new NotFoundException(ErrorMessage.INVALID_URL_EXCEPTION);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import com.mile.exception.message.ErrorMessage;
import com.mile.exception.model.BadRequestException;
import com.mile.utils.SecureUrlUtil;
import com.mile.common.utils.SecureUrlUtil;
import com.mile.exception.model.NotFoundException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
Expand Down Expand Up @@ -35,7 +36,7 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m
try {
return secureUrlUtil.decodeUrl(postId);
} catch (NumberFormatException e) {
throw new BadRequestException(ErrorMessage.INVALID_URL_EXCEPTION);
throw new NotFoundException(ErrorMessage.INVALID_URL_EXCEPTION);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import com.mile.exception.message.ErrorMessage;
import com.mile.exception.model.BadRequestException;
import com.mile.utils.SecureUrlUtil;
import com.mile.common.utils.SecureUrlUtil;
import com.mile.exception.model.NotFoundException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
Expand Down Expand Up @@ -36,7 +37,7 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m
try {
return secureUrlUtil.decodeUrl(replyId);
} catch (NumberFormatException e) {
throw new BadRequestException(ErrorMessage.INVALID_URL_EXCEPTION);
throw new NotFoundException(ErrorMessage.INVALID_URL_EXCEPTION);
}
}
}
Loading

0 comments on commit 450f20c

Please sign in to comment.