Skip to content

Commit

Permalink
Merge pull request #9 from YAPP-Github/feature/TNT-28
Browse files Browse the repository at this point in the history
[TNT-28] feat: Authentication Filter ๊ตฌํ˜„
  • Loading branch information
fakerdeft authored Jan 7, 2025
2 parents 598a775 + 33fb5aa commit 9d4e746
Show file tree
Hide file tree
Showing 21 changed files with 937 additions and 35 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:

- name: MySQL Docker container for tests
run: |
sudo docker run -d -p 3306:3306 --env MYSQL_DATABASE={} --env MYSQL_ROOT_PASSWORD=root mysql:latest
sudo docker run -d -p 3306:3306 --env MYSQL_DATABASE=tnt_dev --env MYSQL_ROOT_PASSWORD=root mysql:latest
- name: Build
env:
Expand Down
7 changes: 5 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ jacocoTestCoverageVerification {
excludes = [
'*.*Application',
'*.*Config',
'*.*.*GlobalExceptionHandler'
'*.error.*'
]
}
}
Expand All @@ -126,7 +126,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-webflux'

// ์• ํ”Œ ๋กœ๊ทธ์ธ์„ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
// TSID
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.9.0'

// ์• ํ”Œ ๋กœ๊ทธ์ธ ๊ด€๋ จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
implementation 'com.auth0:jwks-rsa:0.22.1'
implementation 'org.json:json:20231013'
implementation 'org.bouncycastle:bcprov-jdk18on:1.79'
Expand Down
75 changes: 75 additions & 0 deletions config/naver-intellij-formatter-custom.xml
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="&#10;"/>
<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>
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
63 changes: 63 additions & 0 deletions src/main/java/com/tnt/application/auth/SessionService.java
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)) {
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);
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/tnt/domain/auth/SessionValue.java
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;
}
62 changes: 62 additions & 0 deletions src/main/java/com/tnt/domain/member/Member.java
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)
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;
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/tnt/domain/member/SocialType.java
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> {

}
106 changes: 106 additions & 0 deletions src/main/java/com/tnt/global/auth/SessionAuthenticationFilter.java
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());

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));
}

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);

log.info("์‹œํ๋ฆฌํ‹ฐ ์ปจํ…์ŠคํŠธ์— ์ธ์ฆ ์ •๋ณด ์ €์žฅ ์™„๋ฃŒ - SessionId: {}", sessionId);
}
}
Loading

0 comments on commit 9d4e746

Please sign in to comment.