Skip to content

Commit

Permalink
Merge pull request #170 from daadaadaah/feat/add-AuthenticationService
Browse files Browse the repository at this point in the history
임시로 JWT를 활용하여 인증 기능 추가
  • Loading branch information
daadaadaah authored Aug 22, 2023
2 parents 87d09f4 + ad86ed4 commit 11a382d
Show file tree
Hide file tree
Showing 23 changed files with 672 additions and 291 deletions.
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.redisson:redisson-spring-boot-starter:3.21.3'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'

// devtools
developmentOnly 'org.springframework.boot:spring-boot-devtools'

Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/hcommerce/heecommerce/auth/AuthUserInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.hcommerce.heecommerce.auth;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class AuthUserInfo {

private final int userId;

public AuthUserInfo(int userId) {
this.userId = userId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.hcommerce.heecommerce.auth;

import com.hcommerce.heecommerce.common.utils.JwtUtils;
import com.hcommerce.heecommerce.user.UserQueryRepository;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AuthenticationService {

private final String AUTH_TYPE = "Bearer";

private final JwtUtils jwtUtils;

private final UserQueryRepository userQueryRepository;

@Autowired
public AuthenticationService(JwtUtils jwtUtils, UserQueryRepository userQueryRepository) {
this.jwtUtils = jwtUtils;
this.userQueryRepository = userQueryRepository;
}

// TODO : 테스트용으로 일단 간단하게 구현함.
public String login(int userId) {
return jwtUtils.encode(userId);
}

/**
* isAuthenticatedUser는 HTTP 요청이 인증된 사용자에 의한 것인지를 판단하는 함수이다.
*/
public boolean isAuthenticatedUser(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");

if(authorization == null || authorization.isBlank()) {
return false;
}

if(!isValidAuthType(authorization)) {
return false;
}

String accessToken = extractAccessToken(authorization);

if(accessToken == null || accessToken.isBlank()) {
return false;
}

AuthUserInfo authUserInfo = parseAccessToken(accessToken);

if(authUserInfo == null) {
return false;
}

boolean hasUserId = userQueryRepository.hasUserId(authUserInfo.getUserId());

if(!hasUserId) {
return false;
}

return true;
}

/**
* parseAuthorization는 HTTP Header 의 authorization 를 피상해서 accessToken에 담긴 정보를 리턴하는 함수이다.
* 각 단계별로 유효성 검사를 할 수 있겠지만, 다른 기능 구현에 집중하기 위해 시간 관계상
* AuthInterceptor 에서 authorization 의 유효성이 모두 유효하게 판단된 상황을 가정해서 따로 추가하지 않았다.
*/
public AuthUserInfo parseAuthorization(String authorization) {
String accessToken = extractAccessToken(authorization);

AuthUserInfo authUserInfo = parseAccessToken(accessToken);

return authUserInfo;
}

/**
* isValidAuthType 는 HTTP Header 의 authorization 의 인증 유형이 유효한 인증 유형인지를 판단하는 함수이다.
*/
private boolean isValidAuthType(String authorization) {
return authorization.startsWith(AUTH_TYPE);
}

/**
* isValidAuthType 는 HTTP Header 의 authorization로부터 accessToken을 추춣하는 함수이다.
*/
private String extractAccessToken(String authorization) {
String[] authorizationUnit = authorization.split(AUTH_TYPE+" ");

if(authorizationUnit.length < 2) {
return null;
}

String accessToken = authorizationUnit[1];

if(accessToken.isBlank()) {
return null;
}

return accessToken;
}

/**
* parseAccessToken 는 accessToken에 파싱하여 저장된 사용자 인증 정보를 리턴하는 함수이다.
*/
private AuthUserInfo parseAccessToken(String accessToken) {
Claims claims = jwtUtils.decode(accessToken);

if(claims == null) {
return null;
}

int userId = claims.get("userId", Integer.class);

return new AuthUserInfo(userId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.hcommerce.heecommerce.auth;

public class InvalidTokenException extends RuntimeException {
public InvalidTokenException(String token) {
super(token +" : 유효하지 않은 토큰 입니다.");
}
}
17 changes: 0 additions & 17 deletions src/main/java/com/hcommerce/heecommerce/common/AuthHelper.java

This file was deleted.

45 changes: 16 additions & 29 deletions src/main/java/com/hcommerce/heecommerce/common/AuthInterceptor.java
Original file line number Diff line number Diff line change
@@ -1,57 +1,44 @@
package com.hcommerce.heecommerce.common;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.hcommerce.heecommerce.auth.AuthenticationService;
import com.hcommerce.heecommerce.common.dto.ResponseDto;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class AuthInterceptor implements HandlerInterceptor {

private final AuthenticationService authenticationService;

private final ObjectMapper objectMapper = new ObjectMapper();

@Autowired
public AuthInterceptor(AuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

// TODO : 추후 삭제 필요! 이거 주석처리 후 테스트해야 제대로 테스트 됨
// HttpSession session = request.getSession();
// session.setAttribute("isAdmin", true);

if (!AuthHelper.isLogin(request)) {
if(!authenticationService.isAuthenticatedUser(request)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());

response.setContentType("application/json; charset=UTF-8");

response.getWriter().append(
objectMapper.writeValueAsString(ResponseDto.builder()
.code(HttpStatus.UNAUTHORIZED.name())
.message("로그인 후에 이용할 수 있습니다.")
.build())
objectMapper.writeValueAsString(ResponseDto.builder()
.code(HttpStatus.UNAUTHORIZED.name())
.message("로그인 후에 이용할 수 있습니다.")
.build())
);

return false;
}

if(!AuthHelper.isAdmin(request)) {
response.setStatus(HttpStatus.FORBIDDEN.value());

response.setContentType("application/json; charset=UTF-8");

response.getWriter().append(
objectMapper.writeValueAsString(ResponseDto.builder()
.code(HttpStatus.FORBIDDEN.name())
.message("관리자만 이용 가능합니다.")
.build())
);
return false;
}

return true;
}

// TODO : 추후 삭제 필요!
private boolean isRandomAdmin() {
return System.currentTimeMillis() % 2 == 0;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.hcommerce.heecommerce.common;

import com.hcommerce.heecommerce.auth.InvalidTokenException;
import com.hcommerce.heecommerce.common.dto.ResponseDto;
import com.hcommerce.heecommerce.order.exception.InvalidPaymentAmountException;
import com.hcommerce.heecommerce.order.exception.MaxOrderQuantityExceededException;
Expand Down Expand Up @@ -83,6 +84,17 @@ public ResponseDto invalidPaymentAmountExceptionHandler(InvalidPaymentAmountExce
.build();
}

@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler
public ResponseDto invalidTokenExceptionHandler(InvalidTokenException e) {
log.error("class = {}, message = {}, cause = {}", e.getClass(), e.getMessage(), e.getCause());

return ResponseDto.builder()
.code(HttpStatus.UNAUTHORIZED.name())
.message(e.getMessage())
.build();
}

/**
* @ResponseStatus 어노테이션을 안 붙인 이유는 토스페이먼츠의 에러 코드가 400대일 수도 있고 500일 수도 있어어서, 동적으로 상태코드를 변경할 수 있도록 하기 위해서이다.
*/
Expand All @@ -100,7 +112,7 @@ public ResponseEntity tosspaymentsExceptionHandler(TosspaymentsException e) {
@ExceptionHandler
public ResponseDto fallbackExceptionHandler(Exception e) {
log.error("class = {}, message = {}, cause = {}", e.getClass(), e.getMessage(), e.getCause());
log.debug("stackTrace = {}", e.getStackTrace());
log.error("stackTrace = {}", e.getStackTrace());

return ResponseDto.builder()
.code(HttpStatus.INTERNAL_SERVER_ERROR.name())
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/com/hcommerce/heecommerce/common/WebConfig.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
package com.hcommerce.heecommerce.common;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

private final AuthInterceptor authInterceptor;

@Autowired
public WebConfig(AuthInterceptor authInterceptor) {
this.authInterceptor = authInterceptor;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
registry.addInterceptor(authInterceptor)
.order(1)
.addPathPatterns("/admin/**");
.addPathPatterns("/orders/**"); // 주문 관련 기능은 회원만 가능하도록
}
}
54 changes: 54 additions & 0 deletions src/main/java/com/hcommerce/heecommerce/common/utils/JwtUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.hcommerce.heecommerce.common.utils;

import com.hcommerce.heecommerce.auth.InvalidTokenException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import java.security.Key;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class JwtUtils {

private Key key;

public JwtUtils(@Value("${jwt.secret}") String secret) {
if(secret == null || secret.isBlank()) { // isEmpty가 아닌 isBlank를 사용한 이유 : " " 값을 가지는 경우도 예외로 처리해야 하므로,
// TODO : 클라이언트 - 내부 서버 예러 입니다. / 서버 로그 - jwt secret 값을 확인해주세요.
}

this.key = Keys.hmacShaKeyFor(secret.getBytes());
}

public String encode(int userId) {
if(userId < 0) {
// TODO : 유효하지 않은 사용자입니다.
}

return Jwts.builder()
.claim("userId", userId)
.signWith(key)
.compact();
}

public Claims decode(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
} catch (SignatureException e) { // token이 우리가 발행한 토큰이 아닌 경우
throw new InvalidTokenException("token(value = "+token+") is invalid");
} catch (IllegalArgumentException e) { // token이 Null 또는 "", " " 일 경우, IllegalArgumentException: JWT String argument cannot be null or empty. 발생
throw new InvalidTokenException("token(value = null or empty) is invalid");
} catch (MalformedJwtException e) {
throw new InvalidTokenException("token(value = "+token+") is invalid");
} catch (Exception e) {
throw new RuntimeException("JwtUtils : decode 예외"); // TODO : 임시로 만들어 놓음
}
}
}
Loading

0 comments on commit 11a382d

Please sign in to comment.