Skip to content

Commit e6fab22

Browse files
authored
Merge pull request #27 from Ajou-Hertz/feature/#22-login
자체 로그인 API 구현
2 parents cf62982 + ea3479b commit e6fab22

34 files changed

+1240
-16
lines changed

build.gradle

+7-2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ dependencies {
5656
// Spring Configuration Processor
5757
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
5858

59+
// JWT
60+
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
61+
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
62+
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
63+
5964
// AWS S3
6065
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.658'
6166

@@ -126,8 +131,8 @@ jacocoTestCoverageVerification {
126131
}
127132

128133
includes = [
129-
'*.*Service*',
130-
'*.*Controller*'
134+
'*service.*Service*',
135+
'*controller.*Controller*'
131136
]
132137
}
133138
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.ajou.hertz.common.auth;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.security.core.userdetails.UserDetailsService;
6+
7+
import com.ajou.hertz.domain.user.service.UserQueryService;
8+
9+
@Configuration
10+
public class CustomUserDetailsService {
11+
12+
@Bean
13+
public UserDetailsService userDetailsService(UserQueryService userQueryService) {
14+
return username -> new UserPrincipal(
15+
userQueryService.getDtoById(Long.parseLong(username))
16+
);
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.ajou.hertz.common.auth;
2+
3+
import static org.springframework.http.HttpStatus.*;
4+
import static org.springframework.http.MediaType.*;
5+
6+
import java.io.IOException;
7+
8+
import org.springframework.security.access.AccessDeniedException;
9+
import org.springframework.security.web.access.AccessDeniedHandler;
10+
import org.springframework.stereotype.Component;
11+
12+
import com.ajou.hertz.common.exception.constant.CustomExceptionType;
13+
import com.ajou.hertz.common.exception.dto.response.ErrorResponse;
14+
import com.ajou.hertz.common.exception.util.ExceptionUtils;
15+
import com.ajou.hertz.common.logger.Logger;
16+
import com.fasterxml.jackson.databind.ObjectMapper;
17+
18+
import jakarta.servlet.http.HttpServletRequest;
19+
import jakarta.servlet.http.HttpServletResponse;
20+
21+
@Component
22+
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
23+
24+
/**
25+
* Endpoint에 대해 접근 권한이 존재하지 않을 때 동작하는 handler.
26+
*
27+
* @param request that resulted in an <code>AccessDeniedException</code>
28+
* @param response so that the user agent can be advised of the failure
29+
* @param accessDeniedException that caused the invocation
30+
* @throws IOException if an input or output exception occurred
31+
*/
32+
@Override
33+
public void handle(
34+
HttpServletRequest request,
35+
HttpServletResponse response,
36+
AccessDeniedException accessDeniedException
37+
) throws IOException {
38+
Logger.warn(String.format(
39+
"JwtAccessDeniedHandler.handle() ex=%s",
40+
ExceptionUtils.getExceptionStackTrace(accessDeniedException)
41+
));
42+
43+
response.setStatus(FORBIDDEN.value());
44+
response.setContentType(APPLICATION_JSON_VALUE);
45+
response.setCharacterEncoding("utf-8");
46+
response.getWriter().write(
47+
new ObjectMapper().writeValueAsString(
48+
new ErrorResponse(
49+
CustomExceptionType.ACCESS_DENIED.getCode(),
50+
CustomExceptionType.ACCESS_DENIED.getMessage()
51+
)
52+
)
53+
);
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.ajou.hertz.common.auth;
2+
3+
import static org.springframework.http.HttpStatus.*;
4+
import static org.springframework.http.MediaType.*;
5+
6+
import java.io.IOException;
7+
8+
import org.springframework.security.core.AuthenticationException;
9+
import org.springframework.security.web.AuthenticationEntryPoint;
10+
import org.springframework.stereotype.Component;
11+
12+
import com.ajou.hertz.common.exception.constant.CustomExceptionType;
13+
import com.ajou.hertz.common.exception.dto.response.ErrorResponse;
14+
import com.ajou.hertz.common.exception.util.ExceptionUtils;
15+
import com.ajou.hertz.common.logger.Logger;
16+
import com.fasterxml.jackson.databind.ObjectMapper;
17+
18+
import jakarta.servlet.http.HttpServletRequest;
19+
import jakarta.servlet.http.HttpServletResponse;
20+
21+
@Component
22+
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
23+
24+
/**
25+
* 인증이 필요한 endpoint에 대해 인증되지 않았을 때 동작하는 handler.
26+
*
27+
* @param request that resulted in an <code>AuthenticationException</code>
28+
* @param response so that the user agent can begin authentication
29+
* @param authenticationException that caused the invocation
30+
* @throws IOException if an input or output exception occurred
31+
*/
32+
@Override
33+
public void commence(
34+
HttpServletRequest request,
35+
HttpServletResponse response,
36+
AuthenticationException authenticationException
37+
) throws IOException {
38+
Logger.warn(String.format(
39+
"JwtAuthenticationEntryPoint.commence() ex=%s",
40+
ExceptionUtils.getExceptionStackTrace(authenticationException)
41+
));
42+
43+
response.setStatus(UNAUTHORIZED.value());
44+
response.setContentType(APPLICATION_JSON_VALUE);
45+
response.setCharacterEncoding("utf-8");
46+
response.getWriter().write(
47+
new ObjectMapper().writeValueAsString(
48+
new ErrorResponse(
49+
CustomExceptionType.ACCESS_DENIED.getCode(),
50+
CustomExceptionType.ACCESS_DENIED.getMessage()
51+
)
52+
)
53+
);
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.ajou.hertz.common.auth;
2+
3+
import java.io.IOException;
4+
5+
import org.springframework.http.HttpHeaders;
6+
import org.springframework.security.core.Authentication;
7+
import org.springframework.security.core.context.SecurityContextHolder;
8+
import org.springframework.stereotype.Component;
9+
import org.springframework.util.StringUtils;
10+
import org.springframework.web.filter.OncePerRequestFilter;
11+
12+
import jakarta.servlet.FilterChain;
13+
import jakarta.servlet.ServletException;
14+
import jakarta.servlet.http.HttpServletRequest;
15+
import jakarta.servlet.http.HttpServletResponse;
16+
import lombok.RequiredArgsConstructor;
17+
18+
@RequiredArgsConstructor
19+
@Component
20+
public class JwtAuthenticationFilter extends OncePerRequestFilter {
21+
22+
private static final String TOKEN_TYPE_BEARER_PREFIX = "Bearer ";
23+
24+
private final JwtTokenProvider jwtTokenProvider;
25+
26+
/**
27+
* 모든 요청마다 작동하여, jwt access token을 확인한다.
28+
* 유효한 token이 있는 경우 token을 parsing해서 사용자 정보를 읽고 SecurityContext에 사용자 정보를 저장한다.
29+
*
30+
* @param request request 객체
31+
* @param response response 객체
32+
* @param filterChain FilterChain 객체
33+
*/
34+
@Override
35+
protected void doFilterInternal(
36+
HttpServletRequest request,
37+
HttpServletResponse response,
38+
FilterChain filterChain
39+
) throws ServletException, IOException {
40+
String accessToken = getAccessToken(request);
41+
42+
if (StringUtils.hasText(accessToken)) {
43+
try {
44+
jwtTokenProvider.validateToken(accessToken);
45+
Authentication authentication = jwtTokenProvider.getAuthentication(accessToken);
46+
SecurityContextHolder.getContext().setAuthentication(authentication);
47+
} catch (Exception ignored) {
48+
// 인증 권한 설정 중 에러가 발생하면 권한을 부여하지 않고 다음 단계로 진행
49+
}
50+
}
51+
filterChain.doFilter(request, response);
52+
}
53+
54+
/**
55+
* Request의 header에서 token을 읽어온다.
56+
*
57+
* @param request Request 객체
58+
* @return Header에서 추출한 token
59+
*/
60+
public String getAccessToken(HttpServletRequest request) {
61+
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
62+
if (authorizationHeader == null || !authorizationHeader.startsWith(TOKEN_TYPE_BEARER_PREFIX)) {
63+
return null;
64+
}
65+
return authorizationHeader.substring(TOKEN_TYPE_BEARER_PREFIX.length());
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.ajou.hertz.common.auth;
2+
3+
import java.io.IOException;
4+
5+
import org.springframework.stereotype.Component;
6+
import org.springframework.web.filter.OncePerRequestFilter;
7+
8+
import com.ajou.hertz.common.auth.exception.TokenValidateException;
9+
import com.ajou.hertz.common.exception.constant.CustomExceptionType;
10+
import com.ajou.hertz.common.exception.dto.response.ErrorResponse;
11+
import com.fasterxml.jackson.databind.ObjectMapper;
12+
13+
import jakarta.servlet.FilterChain;
14+
import jakarta.servlet.ServletException;
15+
import jakarta.servlet.http.HttpServletRequest;
16+
import jakarta.servlet.http.HttpServletResponse;
17+
18+
/**
19+
* <code>JwtAuthenticationFilter</code>에서 발생하는 에러를 처리하기 위한 filter
20+
*
21+
* @see JwtAuthenticationFilter
22+
*/
23+
@Component
24+
public class JwtExceptionFilter extends OncePerRequestFilter {
25+
26+
@Override
27+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
28+
FilterChain filterChain) throws ServletException, IOException {
29+
try {
30+
filterChain.doFilter(request, response);
31+
} catch (TokenValidateException ex) {
32+
setErrorResponse(CustomExceptionType.TOKEN_VALIDATE, response);
33+
}
34+
}
35+
36+
/**
37+
* Exception 정보를 입력받아 응답할 error response를 설정한다.
38+
*
39+
* @param exceptionType exception type
40+
* @param response HttpServletResponse 객체
41+
*/
42+
private void setErrorResponse(
43+
CustomExceptionType exceptionType,
44+
HttpServletResponse response
45+
) throws IOException {
46+
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
47+
response.setCharacterEncoding("utf-8");
48+
response.setContentType("application/json; charset=UTF-8");
49+
ErrorResponse errorResponse = new ErrorResponse(exceptionType.getCode(), exceptionType.getMessage());
50+
new ObjectMapper().writeValue(response.getOutputStream(), errorResponse);
51+
}
52+
}

0 commit comments

Comments
 (0)