Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e32e8f8
add: Jwt 인증 보안 설정 추가
se0hui Feb 26, 2025
8c55622
add: 토큰 생성 및 검증을 위한 JWT 설정 추가
se0hui Feb 26, 2025
7b96405
add: JWT 생성 및 검증 서비스 구현
se0hui Feb 26, 2025
de4594d
add: JWT 인증 필터 추가
se0hui Feb 26, 2025
fb0ca8b
add: 토큰 모델 정의
se0hui Feb 26, 2025
13ab13e
add: 인증 로직을 위한 AuthPort 인터페이스 및 어댑터 구현
se0hui Feb 26, 2025
c5cc646
add: AuthService 구현
se0hui Feb 26, 2025
65ccf5d
add: usecase 추가
se0hui Feb 26, 2025
f1b06e5
add: 로그인, 회원가입, 토큰 재발급을 위한 엔드포인트 추가
se0hui Feb 26, 2025
85ff69a
add: 커스텀 Exception 추가
se0hui Feb 26, 2025
02aec47
add: CustomUserDetailsService 구현
se0hui Feb 26, 2025
b8e323f
add: API 요청 및 응답 모델 정의
se0hui Feb 26, 2025
2ef57ab
add: application-test.yml에 JWT 설정 추가
se0hui Feb 26, 2025
04b4cb4
add: AuthUseCase 테스트 추가
se0hui Feb 26, 2025
7408657
chore: RefreshTokenRequestFormatInvalidException
se0hui Feb 28, 2025
df766e2
update: accessToken 만료 시간 주입 방식 수정
se0hui Feb 28, 2025
7c25d64
chore: 클래스명 변경에 따른 수정
se0hui Feb 28, 2025
d845873
update: 기존 토큰 제거 로직 추가
se0hui Mar 1, 2025
44479f3
chore:
se0hui Mar 1, 2025
6427d35
chore: package 네임 소문자로 수정
se0hui Mar 2, 2025
24ad717
chore: ``accessTokenExpiresAt``으로 일관성 있게 수정
se0hui Mar 2, 2025
d911405
update: NPE 방지 및 예외 처리
se0hui Mar 2, 2025
de60b66
update: email 키 사용하여 토큰 삭제
se0hui Mar 2, 2025
0a2e410
update: 값 검증 관련 어노테이션으로 대체
se0hui Mar 2, 2025
9c77f31
update: findMembersByCriteria 사용
se0hui Mar 4, 2025
7fc465d
chore:
se0hui Mar 4, 2025
e6635da
delete: AuthUseCase 삭제
se0hui Mar 5, 2025
9bfaa03
update: 리프레시 토큰 TTL 적용
se0hui Mar 5, 2025
fb9968f
update: AuthServiceTest 작성
se0hui Mar 5, 2025
8dcd2b0
chore:
se0hui Mar 5, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
- name: 🐋 Docker Run
run: docker-compose -f docker-compose.test.yml up -d
- name: ⌛ Wait for Application
run: sleep 30
run: sleep 120
- name: 🧪 Test Application
run: |
RESPONSE=$(curl -s "http://127.0.0.1:8080${{ secrets.HEALTH_CHECK_PATH }}")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.ampersand.groom.domain.auth.application.port;

import com.ampersand.groom.domain.member.persistence.entity.MemberJpaEntity;

import java.util.Optional;

public interface AuthPort {

Optional<MemberJpaEntity> findMembersByCriteria(String email);

void save(MemberJpaEntity member);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.ampersand.groom.domain.auth.application.service;

import com.ampersand.groom.domain.auth.application.port.AuthPort;
import com.ampersand.groom.domain.auth.expection.*;
import com.ampersand.groom.domain.auth.domain.JwtToken;
import com.ampersand.groom.domain.auth.presentation.data.request.SignupRequest;
import com.ampersand.groom.domain.member.persistence.entity.MemberJpaEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.time.Instant;

@Service
@RequiredArgsConstructor
public class AuthService {

private final JwtService jwtService;
private final PasswordEncoder passwordEncoder;
private final AuthPort authPort;

@Value("${spring.jwt.token.access-expiration}")
private long accessTokenExpiration;

@Value("${spring.jwt.token.refresh-expiration}")
private long refreshTokenExpiration;

public JwtToken signIn(String email, String password) {

MemberJpaEntity user = authPort.findMembersByCriteria(email)
.orElseThrow(()->new UserNotFoundException());

if(!user.getIsAvailable()) {
throw new UserForbiddenException();
}

if (!passwordEncoder.matches(password, user.getPassword())) {
throw new PasswordInvalidException();
}

String accessToken = jwtService.createAccessToken(email);
String refreshToken = jwtService.createRefreshToken(email);

return JwtToken.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.accessTokenExpiration(Instant.now().plusMillis(accessTokenExpiration))
.refreshTokenExpiration(Instant.now().plusMillis(refreshTokenExpiration))
.role(user.getRole())
.build();
}

public JwtToken refreshToken(String refreshToken) {
if (refreshToken == null || refreshToken.isEmpty()) {
throw new RefreshTokenRequestFormatInvalidException();
}

String email = jwtService.getEmailFromToken(refreshToken);
boolean isTokenValid = jwtService.refreshToken(email, refreshToken);
if (!isTokenValid) {
throw new RefreshTokenExpiredOrInvalidException();
}

if (!jwtService.validateToken(refreshToken)) {
throw new RefreshTokenExpiredOrInvalidException();
}

MemberJpaEntity user = authPort.findMembersByCriteria(email)
.orElseThrow(()->new UserNotFoundException());

String newAccessToken = jwtService.createAccessToken(email);
String newRefreshToken = jwtService.createRefreshToken(email);

return JwtToken.builder()
.accessToken(newAccessToken)
.refreshToken(newRefreshToken)
.accessTokenExpiration(Instant.now().plusMillis(accessTokenExpiration))
.refreshTokenExpiration(Instant.now().plusMillis(refreshTokenExpiration))
.role(user.getRole())
.build();
}

public void signup(SignupRequest request) {
authPort.findMembersByCriteria(request.getEmail())
.ifPresent(emailVerification -> {
throw new UserExistException();
});

MemberJpaEntity newUser = MemberJpaEntity.builder()
.name(request.getName())
.email(request.getEmail())
.password(passwordEncoder.encode(request.getPassword()))
.generation(1)
.isAvailable(true)
.build();

authPort.save(newUser);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.ampersand.groom.domain.auth.application.service;

import com.ampersand.groom.domain.auth.application.port.AuthPort;
import com.ampersand.groom.domain.auth.expection.UserNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

import java.util.Collections;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final AuthPort authPort;

@Override
public UserDetails loadUserByUsername(String email) {
return authPort.findMembersByCriteria(email)
.map(member -> {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(member.getRole().name());
return new User(member.getEmail(), member.getPassword(), Collections.singletonList(authority));
})
.orElseThrow(() -> new UserNotFoundException());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.ampersand.groom.domain.auth.application.service;

import io.jsonwebtoken.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class JwtService {

private final RedisTemplate<String, String> redisTemplate;
private final SecretKey secretKey;
@Getter
private final long accessTokenExpiration;
private final long refreshTokenExpiration;

public String createAccessToken(String email) {
return generateToken(email, accessTokenExpiration);
}

public String createRefreshToken(String email) {
String refreshToken = generateToken(email, refreshTokenExpiration);

redisTemplate.opsForValue().set("refresh_token:" + email, refreshToken, refreshTokenExpiration, TimeUnit.SECONDS);

return refreshToken;
}

public boolean refreshToken(String email, String refreshToken) {
String storedToken = redisTemplate.opsForValue().get("refresh_token:" + email);

if (storedToken == null || !storedToken.equals(refreshToken)) {
return false;
}
return true;
}

private String generateToken(String subject, long expirationMs) {
Date now = new Date();
return Jwts.builder()
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + expirationMs))
.signWith(secretKey)
.compact();
}

public String getEmailFromToken(String token) {
return parseClaims(token).getSubject();
}

public boolean validateToken(String token) {
try {
parseClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}

private Claims parseClaims(String token) {
return Jwts.parser()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
}

public String resolveToken(HttpServletRequest request) {
String token = request.getHeader("Authorization");
return (token != null && token.startsWith("Bearer ")) ? token.substring(7) : null;
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/ampersand/groom/domain/auth/domain/JwtToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ampersand.groom.domain.auth.domain;

import com.ampersand.groom.domain.member.domain.constant.MemberRole;
import lombok.Builder;
import lombok.Getter;

import java.time.Instant;

@Getter
@Builder
public class JwtToken {

private String accessToken;
private String refreshToken;
private Instant accessTokenExpiration;
private Instant refreshTokenExpiration;
private MemberRole role;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ampersand.groom.domain.auth.expection;

import com.ampersand.groom.global.error.ErrorCode;
import com.ampersand.groom.global.error.exception.GroomException;

public class EmailOrPasswordEmptyException extends GroomException {
public EmailOrPasswordEmptyException() {
super(ErrorCode.EMAIL_OR_PASSWORD_EMPTY);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ampersand.groom.domain.auth.expection;

import com.ampersand.groom.global.error.ErrorCode;
import com.ampersand.groom.global.error.exception.GroomException;

public class PasswordInvalidException extends GroomException {
public PasswordInvalidException() {
super(ErrorCode.PASSWORD_INVALID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ampersand.groom.domain.auth.expection;

import com.ampersand.groom.global.error.ErrorCode;
import com.ampersand.groom.global.error.exception.GroomException;

public class RefreshTokenExpiredOrInvalidException extends GroomException {
public RefreshTokenExpiredOrInvalidException() {
super(ErrorCode.REFRESH_TOKEN_EXPIRED_OR_INVALID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ampersand.groom.domain.auth.expection;

import com.ampersand.groom.global.error.ErrorCode;
import com.ampersand.groom.global.error.exception.GroomException;

public class RefreshTokenRequestFormatInvalidException extends GroomException {
public RefreshTokenRequestFormatInvalidException() {
super(ErrorCode.REFRESH_TOKEN_REQUEST_FORMAT_INVALID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ampersand.groom.domain.auth.expection;

import com.ampersand.groom.global.error.ErrorCode;
import com.ampersand.groom.global.error.exception.GroomException;

public class UserExistException extends GroomException {
public UserExistException() {
super(ErrorCode.USER_ALREADY_EXISTS);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ampersand.groom.domain.auth.expection;

import com.ampersand.groom.global.error.ErrorCode;
import com.ampersand.groom.global.error.exception.GroomException;

public class UserForbiddenException extends GroomException {
public UserForbiddenException() {
super(ErrorCode.USER_FORBIDDEN);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ampersand.groom.domain.auth.expection;

import com.ampersand.groom.global.error.ErrorCode;
import com.ampersand.groom.global.error.exception.GroomException;

public class UserNotFoundException extends GroomException {
public UserNotFoundException() {
super(ErrorCode.USER_NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.ampersand.groom.domain.auth.persistence.adapter.auth;

import com.ampersand.groom.domain.auth.application.port.AuthPort;
import com.ampersand.groom.domain.member.persistence.entity.MemberJpaEntity;
import com.ampersand.groom.domain.member.persistence.repository.MemberJpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
@RequiredArgsConstructor
public class AuthPortAdapter implements AuthPort {

private final MemberJpaRepository memberJpaRepository;

@Override
public Optional<MemberJpaEntity> findMembersByCriteria(String email) {
return memberJpaRepository.findMembersByCriteria(null, null, null, email, null, null)
.stream().findFirst();
}


@Override
public void save(MemberJpaEntity member) {
memberJpaRepository.save(member);
}
}
Loading
Loading