-
Notifications
You must be signed in to change notification settings - Fork 0
[TNT-28] feat: Authentication Filter 구현 #9
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
Changes from all commits
2765da8
bbd6f6d
397688b
348aa14
4881e56
57e3db9
19cbf55
7df0f63
4a22cd0
287a966
a98cdbc
a5090c1
3ec4f7a
ddcebf1
88df790
436f484
f60cdda
2dd0d6a
8523fdb
2ac8730
7e22fab
fff07b4
38ee34f
f71c593
f1668bb
de025d6
b3f7b7a
1ac7bde
0c56e0e
8144fe5
446702c
4356b0b
aace3c2
f0ac237
0786625
11e8c66
531186d
e16edc5
7266c03
d83e7f8
aa9bca4
4bba1c1
c4b227c
1a170ed
be6a353
b517e65
7a7dd5d
91c6d12
a8a26e2
71ce996
f80c5bd
6b2b7a0
5a33c7e
6e15931
a9efc0b
ee987e0
86e8605
b8f65dc
08dbcf8
9c176e4
36e3566
8495903
733b374
281858d
c88469f
f35949e
3335217
6f330ff
11aed26
ccf892e
94ee88b
9bf6614
b759144
1028f01
0e248b4
39dd95b
b7c75a7
2590e9b
33fb5aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| <code_scheme name="Naver-coding-convention-v1.2 custom" version="173"> | ||
| <option name="LINE_SEPARATOR" value=" "/> | ||
| <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="5"/> | ||
| <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="3"/> | ||
| <option name="IMPORT_LAYOUT_TABLE"> | ||
| <value> | ||
| <package name="" withSubpackages="true" static="false"/> | ||
| <emptyLine/> | ||
| <package name="javax" withSubpackages="true" static="false"/> | ||
| <package name="java" withSubpackages="true" static="false"/> | ||
| <emptyLine/> | ||
| <package name="" withSubpackages="true" static="true"/> | ||
| </value> | ||
| </option> | ||
| <option name="RIGHT_MARGIN" value="120"/> | ||
| <option name="ENABLE_JAVADOC_FORMATTING" value="true"/> | ||
| <option name="FORMATTER_TAGS_ENABLED" value="true"/> | ||
| <JavaCodeStyleSettings> | ||
| <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99"/> | ||
| <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="1"/> | ||
| <option name="IMPORT_LAYOUT_TABLE"> | ||
| <value> | ||
| <emptyLine/> | ||
| <package name="" withSubpackages="true" static="true"/> | ||
| <emptyLine/> | ||
| <package name="java" withSubpackages="true" static="false"/> | ||
| <emptyLine/> | ||
| <package name="javax" withSubpackages="true" static="false"/> | ||
| <emptyLine/> | ||
| <package name="org" withSubpackages="true" static="false"/> | ||
| <emptyLine/> | ||
| <package name="net" withSubpackages="true" static="false"/> | ||
| <emptyLine/> | ||
| <package name="com" withSubpackages="true" static="false"/> | ||
| <emptyLine/> | ||
| <package name="" withSubpackages="true" static="false"/> | ||
| <emptyLine/> | ||
| <package name="com.nhncorp" withSubpackages="true" static="false"/> | ||
| <emptyLine/> | ||
| <package name="com.navercorp" withSubpackages="true" static="false"/> | ||
| <emptyLine/> | ||
| <package name="com.naver" withSubpackages="true" static="false"/> | ||
| <emptyLine/> | ||
| </value> | ||
| </option> | ||
| <option name="ENABLE_JAVADOC_FORMATTING" value="false"/> | ||
| </JavaCodeStyleSettings> | ||
| <codeStyleSettings language="JAVA"> | ||
| <option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false"/> | ||
| <option name="LINE_COMMENT_ADD_SPACE" value="true"/> | ||
| <option name="KEEP_FIRST_COLUMN_COMMENT" value="false"/> | ||
| <option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false"/> | ||
| <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1"/> | ||
| <option name="KEEP_BLANK_LINES_IN_CODE" value="1"/> | ||
| <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1"/> | ||
| <option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1"/> | ||
| <option name="ALIGN_MULTILINE_PARAMETERS" value="false"/> | ||
| <option name="SPACE_AFTER_TYPE_CAST" value="false"/> | ||
| <option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true"/> | ||
| <option name="CALL_PARAMETERS_WRAP" value="1"/> | ||
| <option name="METHOD_PARAMETERS_WRAP" value="1"/> | ||
| <option name="EXTENDS_LIST_WRAP" value="1"/> | ||
| <option name="THROWS_LIST_WRAP" value="5"/> | ||
| <option name="EXTENDS_KEYWORD_WRAP" value="1"/> | ||
| <option name="METHOD_CALL_CHAIN_WRAP" value="5"/> | ||
| <option name="BINARY_OPERATION_WRAP" value="1"/> | ||
| <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true"/> | ||
| <option name="TERNARY_OPERATION_WRAP" value="1"/> | ||
| <option name="ARRAY_INITIALIZER_WRAP" value="1"/> | ||
| <indentOptions> | ||
| <option name="CONTINUATION_INDENT_SIZE" value="4"/> | ||
| <option name="USE_TAB_CHARACTER" value="true"/> | ||
| </indentOptions> | ||
| </codeStyleSettings> | ||
| </code_scheme> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| package com.tnt.application.auth; | ||
|
|
||
| import static io.micrometer.common.util.StringUtils.*; | ||
| import static java.util.Objects.*; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| import org.springframework.data.redis.core.RedisTemplate; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import com.tnt.domain.auth.SessionValue; | ||
| import com.tnt.global.error.exception.UnauthorizedException; | ||
|
|
||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| @Slf4j | ||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class SessionService { | ||
|
|
||
| static final long SESSION_DURATION = 2L * 24 * 60 * 60; // 48시간 | ||
| private static final String AUTHORIZATION_HEADER = "Authorization"; | ||
| private static final String SESSION_ID_PREFIX = "SESSION-ID "; | ||
| private final RedisTemplate<String, SessionValue> redisTemplate; | ||
|
|
||
| public String authenticate(HttpServletRequest request) { | ||
| String authHeader = request.getHeader(AUTHORIZATION_HEADER); | ||
|
|
||
| if (isBlank(authHeader) || !authHeader.startsWith(SESSION_ID_PREFIX)) { | ||
ymkim97 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| log.error("Authorization 헤더가 존재하지 않거나 올바르지 않은 형식입니다."); | ||
|
|
||
| throw new UnauthorizedException("인가 세션이 존재하지 않습니다."); | ||
| } | ||
|
|
||
| String sessionId = authHeader.substring(SESSION_ID_PREFIX.length()); | ||
|
|
||
| requireNonNull(redisTemplate.opsForValue().get(sessionId), "세션 스토리지에 세션이 존재하지 않습니다."); | ||
|
|
||
| return sessionId; | ||
| } | ||
|
|
||
| public void createSession(String memberId, HttpServletRequest request) { | ||
| SessionValue sessionValue = SessionValue.builder() | ||
| .lastAccessTime(LocalDateTime.now()) | ||
| .userAgent(request.getHeader("User-Agent")) | ||
| .clientIp(request.getRemoteAddr()) | ||
| .build(); | ||
|
|
||
| redisTemplate.opsForValue().set( | ||
| memberId, | ||
| sessionValue, | ||
| SESSION_DURATION, | ||
| TimeUnit.SECONDS | ||
| ); | ||
| } | ||
|
|
||
| public void removeSession(String sessionId) { | ||
| redisTemplate.delete(sessionId); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.tnt.domain.auth; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| public class SessionValue { | ||
|
|
||
| private LocalDateTime lastAccessTime; | ||
| private String userAgent; | ||
| private String clientIp; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| package com.tnt.domain.member; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| import com.tnt.global.entity.BaseTimeEntity; | ||
|
|
||
| import io.hypersistence.utils.hibernate.id.Tsid; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.EnumType; | ||
| import jakarta.persistence.Enumerated; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.Table; | ||
| import lombok.AccessLevel; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @Table(name = "member") | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class Member extends BaseTimeEntity { | ||
|
|
||
| @Id | ||
| @Tsid | ||
| @Column(name = "id", nullable = false, unique = true) | ||
| private Long id; | ||
|
|
||
| @Column(name = "social_id", nullable = false, unique = true) | ||
ymkim97 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| private String socialId; | ||
|
|
||
| @Column(name = "email", nullable = false, length = 100) | ||
| private String email; | ||
|
|
||
| @Column(name = "name", nullable = false, length = 50) | ||
| private String name; | ||
|
|
||
| @Column(name = "age", nullable = false) | ||
| private int age; | ||
|
|
||
| @Column(name = "profile", nullable = false) | ||
| private String profile; | ||
|
|
||
| @Column(name = "deleted_at") | ||
| private LocalDateTime deletedAt; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(name = "social_type", nullable = false) | ||
| private SocialType socialType; | ||
|
|
||
| @Builder | ||
| public Member(Long id, String socialId, String email, String name, int age, SocialType socialType) { | ||
| this.id = id; | ||
| this.socialId = socialId; | ||
| this.email = email; | ||
| this.name = name; | ||
| this.age = age; | ||
| this.profile = ""; | ||
| this.socialType = socialType; | ||
|
Comment on lines
+55
to
+60
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. id(pk)나 int 등 primitive는 제외하고 객체들은
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
아하 좋습니다 ! null 체크나 유효성 검사 컨벤션은 조만간 상의해봐요 !! |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.tnt.domain.member; | ||
|
|
||
| public enum SocialType { | ||
| KAKAO, | ||
| GOOGLE, | ||
| APPLE | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.tnt.domain.member.repository; | ||
|
|
||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import com.tnt.domain.member.Member; | ||
|
|
||
| @Repository | ||
| public interface MemberRepository extends JpaRepository<Member, Long> { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| package com.tnt.global.auth; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.List; | ||
|
|
||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
| import org.springframework.security.core.Authentication; | ||
| import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; | ||
| import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; | ||
| import org.springframework.security.core.context.SecurityContextHolder; | ||
| import org.springframework.security.core.userdetails.User; | ||
| import org.springframework.security.core.userdetails.UserDetails; | ||
| import org.springframework.util.AntPathMatcher; | ||
| import org.springframework.web.filter.OncePerRequestFilter; | ||
|
|
||
| import com.tnt.application.auth.SessionService; | ||
|
|
||
| import jakarta.servlet.FilterChain; | ||
| import jakarta.servlet.ServletException; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| @Slf4j | ||
| @RequiredArgsConstructor | ||
| public class SessionAuthenticationFilter extends OncePerRequestFilter { | ||
|
|
||
| private final GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); | ||
| private final AntPathMatcher pathMatcher = new AntPathMatcher(); | ||
| private final List<String> allowedUris; | ||
| private final SessionService sessionService; | ||
|
|
||
| @Override | ||
| protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, | ||
| FilterChain filterChain) throws ServletException, IOException { | ||
| String requestUri = request.getRequestURI(); | ||
| String queryString = request.getQueryString(); | ||
|
|
||
| log.info("들어온 요청 - URI: {}, Query: {}, Method: {}", requestUri, queryString != null ? queryString : "쿼리 스트링 없음", | ||
| request.getMethod()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. log들은 로직에 섞여 보이지 않게 개행 처리로 구분해주는건 어떨까요!? 다른쪽들도 다 마찬가지 입니다!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아하 좋습니다 바로 수정 들어가겠습니다 !! |
||
|
|
||
| if (isAllowedUri(requestUri)) { | ||
| log.info("{} 허용 URI. 세션 유효성 검사 스킵.", requestUri); | ||
|
|
||
| filterChain.doFilter(request, response); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| checkSessionAndAuthentication(request); | ||
| } catch (RuntimeException e) { | ||
| log.error("인증 처리 중 에러 발생: ", e); | ||
|
|
||
| handleUnauthorizedException(response, e); | ||
| return; | ||
| } | ||
|
|
||
| filterChain.doFilter(request, response); | ||
| } | ||
|
|
||
| private boolean isAllowedUri(String requestUri) { | ||
| boolean allowed = false; | ||
|
|
||
| for (String pattern : allowedUris) { | ||
| if (pathMatcher.match(pattern, requestUri)) { | ||
| allowed = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| log.info("URI {} is {}allowed", requestUri, allowed ? "" : "not "); | ||
|
|
||
| return allowed; | ||
| } | ||
|
|
||
| private void checkSessionAndAuthentication(HttpServletRequest request) { | ||
| String sessionId = sessionService.authenticate(request); | ||
|
|
||
| saveAuthentication(Long.parseLong(sessionId)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기에
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
createSession은 추후 개발할 회원 로그인 로직 쪽에서 사용하려고 했습니다 !! |
||
| } | ||
|
|
||
| private void handleUnauthorizedException(HttpServletResponse response, RuntimeException exception) throws | ||
| IOException { | ||
| log.error("인증 실패: ", exception); | ||
|
|
||
| response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); | ||
| response.setContentType("application/json;charset=UTF-8"); | ||
| response.getWriter().write(exception.getMessage()); | ||
| } | ||
|
|
||
| private void saveAuthentication(Long sessionId) { | ||
| UserDetails userDetails = User.builder() | ||
| .username(String.valueOf(sessionId)) | ||
| .password("") | ||
| .roles("USER") | ||
| .build(); | ||
|
|
||
| Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, | ||
| authoritiesMapper.mapAuthorities(userDetails.getAuthorities())); | ||
|
|
||
| SecurityContextHolder.getContext().setAuthentication(authentication); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 좋습니다~
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
비동기 상황이 보통 그럴까요?? |
||
|
|
||
| log.info("시큐리티 컨텍스트에 인증 정보 저장 완료 - SessionId: {}", sessionId); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.