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

자체 로그인 API 구현 #27

Merged
merged 7 commits into from
Feb 17, 2024
Merged
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
9 changes: 7 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ dependencies {
// Spring Configuration Processor
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'

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

// AWS S3
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.658'

Expand Down Expand Up @@ -126,8 +131,8 @@ jacocoTestCoverageVerification {
}

includes = [
'*.*Service*',
'*.*Controller*'
'*service.*Service*',
'*controller.*Controller*'
]
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ajou.hertz.common.auth;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetailsService;

import com.ajou.hertz.domain.user.service.UserQueryService;

@Configuration
public class CustomUserDetailsService {

@Bean
public UserDetailsService userDetailsService(UserQueryService userQueryService) {
return username -> new UserPrincipal(
userQueryService.getDtoById(Long.parseLong(username))
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.ajou.hertz.common.auth;

import static org.springframework.http.HttpStatus.*;
import static org.springframework.http.MediaType.*;

import java.io.IOException;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import com.ajou.hertz.common.exception.constant.CustomExceptionType;
import com.ajou.hertz.common.exception.dto.response.ErrorResponse;
import com.ajou.hertz.common.exception.util.ExceptionUtils;
import com.ajou.hertz.common.logger.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

/**
* Endpoint에 대해 접근 권한이 존재하지 않을 때 동작하는 handler.
*
* @param request that resulted in an <code>AccessDeniedException</code>
* @param response so that the user agent can be advised of the failure
* @param accessDeniedException that caused the invocation
* @throws IOException if an input or output exception occurred
*/
@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException
) throws IOException {
Logger.warn(String.format(
"JwtAccessDeniedHandler.handle() ex=%s",
ExceptionUtils.getExceptionStackTrace(accessDeniedException)
));

response.setStatus(FORBIDDEN.value());
response.setContentType(APPLICATION_JSON_VALUE);
response.setCharacterEncoding("utf-8");
response.getWriter().write(
new ObjectMapper().writeValueAsString(
new ErrorResponse(
CustomExceptionType.ACCESS_DENIED.getCode(),
CustomExceptionType.ACCESS_DENIED.getMessage()
)
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.ajou.hertz.common.auth;

import static org.springframework.http.HttpStatus.*;
import static org.springframework.http.MediaType.*;

import java.io.IOException;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.ajou.hertz.common.exception.constant.CustomExceptionType;
import com.ajou.hertz.common.exception.dto.response.ErrorResponse;
import com.ajou.hertz.common.exception.util.ExceptionUtils;
import com.ajou.hertz.common.logger.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

/**
* 인증이 필요한 endpoint에 대해 인증되지 않았을 때 동작하는 handler.
*
* @param request that resulted in an <code>AuthenticationException</code>
* @param response so that the user agent can begin authentication
* @param authenticationException that caused the invocation
* @throws IOException if an input or output exception occurred
*/
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authenticationException
) throws IOException {
Logger.warn(String.format(
"JwtAuthenticationEntryPoint.commence() ex=%s",
ExceptionUtils.getExceptionStackTrace(authenticationException)
));

response.setStatus(UNAUTHORIZED.value());
response.setContentType(APPLICATION_JSON_VALUE);
response.setCharacterEncoding("utf-8");
response.getWriter().write(
new ObjectMapper().writeValueAsString(
new ErrorResponse(
CustomExceptionType.ACCESS_DENIED.getCode(),
CustomExceptionType.ACCESS_DENIED.getMessage()
)
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.ajou.hertz.common.auth;

import java.io.IOException;

import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private static final String TOKEN_TYPE_BEARER_PREFIX = "Bearer ";

private final JwtTokenProvider jwtTokenProvider;

/**
* 모든 요청마다 작동하여, jwt access token을 확인한다.
* 유효한 token이 있는 경우 token을 parsing해서 사용자 정보를 읽고 SecurityContext에 사용자 정보를 저장한다.
*
* @param request request 객체
* @param response response 객체
* @param filterChain FilterChain 객체
*/
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String accessToken = getAccessToken(request);

if (StringUtils.hasText(accessToken)) {
try {
jwtTokenProvider.validateToken(accessToken);
Authentication authentication = jwtTokenProvider.getAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception ignored) {
// 인증 권한 설정 중 에러가 발생하면 권한을 부여하지 않고 다음 단계로 진행
}
}
filterChain.doFilter(request, response);
}

/**
* Request의 header에서 token을 읽어온다.
*
* @param request Request 객체
* @return Header에서 추출한 token
*/
public String getAccessToken(HttpServletRequest request) {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null || !authorizationHeader.startsWith(TOKEN_TYPE_BEARER_PREFIX)) {
return null;
}
return authorizationHeader.substring(TOKEN_TYPE_BEARER_PREFIX.length());
}
}
52 changes: 52 additions & 0 deletions src/main/java/com/ajou/hertz/common/auth/JwtExceptionFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.ajou.hertz.common.auth;

import java.io.IOException;

import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.ajou.hertz.common.auth.exception.TokenValidateException;
import com.ajou.hertz.common.exception.constant.CustomExceptionType;
import com.ajou.hertz.common.exception.dto.response.ErrorResponse;
import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
* <code>JwtAuthenticationFilter</code>에서 발생하는 에러를 처리하기 위한 filter
*
* @see JwtAuthenticationFilter
*/
@Component
public class JwtExceptionFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (TokenValidateException ex) {
setErrorResponse(CustomExceptionType.TOKEN_VALIDATE, response);
}
}

/**
* Exception 정보를 입력받아 응답할 error response를 설정한다.
*
* @param exceptionType exception type
* @param response HttpServletResponse 객체
*/
private void setErrorResponse(
CustomExceptionType exceptionType,
HttpServletResponse response
) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=UTF-8");
ErrorResponse errorResponse = new ErrorResponse(exceptionType.getCode(), exceptionType.getMessage());
new ObjectMapper().writeValue(response.getOutputStream(), errorResponse);
}
}
Loading
Loading