Skip to content

Commit

Permalink
#510 [feat] 인가 로직 개편 작업
Browse files Browse the repository at this point in the history
#510 [feat] 인가 로직 개편 작업
  • Loading branch information
sohyundoh authored Sep 18, 2024
2 parents 563f8b6 + be253ec commit 8475d2f
Show file tree
Hide file tree
Showing 27 changed files with 386 additions and 75 deletions.
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,94 @@
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 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) {
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 Map<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.setWriterNameContext(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.setWriterNameContext(userRoles.get(requestMoimId).writerNameId());
return true;
}
case USER -> {
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 @@ -39,7 +39,7 @@ public Long resolveArgument(@NotNull MethodParameter parameter, ModelAndViewCont
throw new UnauthorizedException(ErrorMessage.TOKEN_VALIDATION_ERROR);
}

return jwtTokenProvider.getUserFromJwt(token);
return jwtTokenProvider.getUserFromAuthHeader(token);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public String getUserIdFromContextHolder() {
throw new UnauthorizedException(ErrorMessage.TOKEN_VALIDATION_ERROR);
}

return jwtTokenProvider.getUserFromJwt(token).toString();
return jwtTokenProvider.getUserFromAuthHeader(token).toString();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.mile.common.utils.thread;

public class WriterNameContextUtil {
private static final ThreadLocal<Long> writerNameContext = new ThreadLocal<>();

public static void setWriterNameContext(Long writerNameId) {
writerNameContext.set(writerNameId);
}

public static Long getWriterNameContext() {
return writerNameContext.get();
}

public static void clear() {
writerNameContext.remove();
}
}
5 changes: 4 additions & 1 deletion module-api/src/main/java/com/mile/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


import com.mile.common.interceptor.DuplicatedInterceptor;
import com.mile.common.interceptor.MoimAuthInterceptor;
import com.mile.common.resolver.comment.CommentVariableResolver;
import com.mile.common.resolver.moim.MoimVariableResolver;
import com.mile.common.resolver.post.PostVariableResolver;
Expand All @@ -27,6 +28,7 @@ public class WebConfig implements WebMvcConfigurer {
private final CommentVariableResolver commentVariableResolver;
private final ReplyVariableResolver replyVariableResolver;
private final DuplicatedInterceptor duplicatedInterceptor;
private final MoimAuthInterceptor moimAuthInterceptor;
private final UserIdHeaderResolver userIdHeaderResolver;

@Value("${client.local}")
Expand Down Expand Up @@ -59,7 +61,8 @@ public void addCorsMappings(CorsRegistry registry) {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(duplicatedInterceptor)
.addPathPatterns("/api/post/temporary", "/api/post", "/api/post/{postId}/comment","/api/comment/{commentId}", "/api/moim/{moimId}/topic");
.addPathPatterns("/api/post/temporary", "/api/post", "/api/post/{postId}/comment", "/api/comment/{commentId}", "/api/moim/{moimId}/topic");
registry.addInterceptor(moimAuthInterceptor);
}

@Override
Expand Down
Loading

0 comments on commit 8475d2f

Please sign in to comment.