Skip to content
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
50 changes: 50 additions & 0 deletions api-test.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
### ์ „์—ญ ๋ณ€์ˆ˜ ์„ค์ • (ํ•„์š”์‹œ ๋ณ€๊ฒฝ)
@auth_host = http://localhost:8080
@trip_host = http://localhost:8081
@email = test@naver.com
@password = password1234

### 1. [Auth] ํšŒ์›๊ฐ€์ž…
POST {{auth_host}}/users
Content-Type: application/json

{
"email": "{{email}}",
"password": "{{password}}",
"name": "ํ…Œ์Šคํ„ฐ"
}

### 2. [Auth] ๋กœ๊ทธ์ธ ๋ฐ ํ† ํฐ ๋ฐœ๊ธ‰
# ์ฃผ์˜: LoginAuthenticationFilter ๊ตฌํ˜„์— ๋”ฐ๋ผ id, password๋ฅผ ํ—ค๋”๋กœ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.
POST {{auth_host}}/login
Content-Type: application/json
id: {{email}}
password: {{password}}

> {%
// ์‘๋‹ต์—์„œ accessToken์„ ์ถ”์ถœํ•˜์—ฌ ์ „์—ญ ๋ณ€์ˆ˜ 'auth_token'์— ์ €์žฅ
client.global.set("auth_token", response.body.data.accessToken);
client.log("Acquired Token: " + response.body.data.accessToken);
%}

### 3. [Trip] ์—ฌํ–‰ ์ƒ์„ฑ (ํ† ํฐ ์‚ฌ์šฉ)
POST {{trip_host}}/trips
Content-Type: application/json
Authorization: Bearer {{auth_token}}

{
"locationId": "550e8400-e29b-41d4-a716-446655440001",
"title": "์ œ์ฃผ๋„ ์šฐ์ • ์—ฌํ–‰",
"description": "์นœ๊ตฌ๋“ค๊ณผ ํ•จ๊ป˜ํ•˜๋Š” ์ฆ๊ฑฐ์šด ์ œ์ฃผ๋„ ์—ฌํ–‰์ž…๋‹ˆ๋‹ค.",
"start": "2026-07-01",
"end": "2026-07-05",
"open": true,
"maxParticipants": 4,
"category": "DOMESTIC",
"hashTags": ["์ œ์ฃผ๋„", "์šฐ์ •์—ฌํ–‰", "๋ง›์ง‘ํƒ๋ฐฉ"]
}

### 4. [Trip] ์ƒ์„ฑ๋œ ์—ฌํ–‰ ๋ชฉ๋ก ์กฐํšŒ (๊ฒ€์ฆ)
GET {{trip_host}}/trips
Content-Type: application/json
Authorization: Bearer {{auth_token}}
9 changes: 7 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.0'
id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.7'
}

Expand Down Expand Up @@ -28,6 +28,11 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'

// [์‹ ๊ทœ ์ถ”๊ฐ€] OAuth2 ํด๋ผ์ด์–ธํŠธ ๋ฐ WebClient ์˜์กด์„ฑ
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-webflux'

implementation "org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.5"
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.5"
compileOnly 'org.projectlombok:lombok'
Expand All @@ -47,4 +52,4 @@ dependencies {

tasks.named('test') {
useJUnitPlatform()
}
}
3 changes: 3 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}
rootProject.name = 'auth'
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
package com.retrip.auth.application.config;


import com.retrip.auth.domain.entity.Member;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;


public class CustomUserDetails implements UserDetails {
@Getter
public class CustomUserDetails implements UserDetails, OAuth2User {

private final Member member;
private Map<String, Object> attributes;


public CustomUserDetails(Member member) {
this.member = member;
}


public CustomUserDetails(Member member, Map<String, Object> attributes) {
this.member = member;
this.attributes = attributes;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return member.getAuthorities().getValues().stream()
Expand All @@ -27,11 +37,23 @@ public Collection<? extends GrantedAuthority> getAuthorities() {

@Override
public String getPassword() {

return member.getPassword().getValue();
}

@Override
public String getUsername() {
return member.getEmail().getValue();
}
}


@Override
public Map<String, Object> getAttributes() {
return attributes;
}

@Override
public String getName() {
return member.getId().toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;


@Getter
@RequiredArgsConstructor
@ConfigurationProperties("token.jwt")
public class JwtConfig {
private final String secret;

private final String privateKey;
private final String publicKey;

private final String header;
private final String prefix;
private final AccessConfig access;
Expand All @@ -26,4 +28,4 @@ public static class AccessConfig {
public static class RefreshConfig {
private final int expireMin;
}
}
}
185 changes: 185 additions & 0 deletions src/main/java/com/retrip/auth/application/config/JwtProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package com.retrip.auth.application.config;

import com.retrip.auth.application.in.response.LoginResponse;
import io.jsonwebtoken.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;

import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
import com.retrip.auth.application.config.CustomUserDetails;

/**
* JWT ํ† ํฐ์˜ ์ƒ์„ฑ(Sign) ๋ฐ ๊ฒ€์ฆ(Verify)์„ ๋‹ด๋‹นํ•˜๋Š” ํด๋ž˜์Šค (RSA ๋ฐฉ์‹)
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtProvider {

private final JwtConfig jwtConfig;

/**
* [์ƒ์„ฑ] ์ธ์ฆ ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ RSA ์„œ๋ช…๋œ Access/Refresh Token ์ƒ์„ฑ
*/
public LoginResponse.TokenResponse generateTokens(Authentication authentication) {
Instant now = Instant.now();
String authorities = String.join(",", getAuthorities(authentication));

String memberId = authentication.getName();
String email = authentication.getName();

Object principal = authentication.getPrincipal();
if (principal instanceof CustomUserDetails userDetails) {
memberId = userDetails.getName(); // CustomUserDetails.getName()์€ UUID(String) ๋ฐ˜ํ™˜
email = userDetails.getUsername(); // CustomUserDetails.getUsername()์€ ์ด๋ฉ”์ผ ๋ฐ˜ํ™˜
}

String accessToken = createToken(
memberId, // sub (UUID)
email, // claim: username (Email)
authorities,
now,
jwtConfig.getAccess().getExpireMin()
);

String refreshToken = createToken(
memberId, // sub (UUID)
email, // claim: username (Email)
authorities,
now,
jwtConfig.getRefresh().getExpireMin()
);

return new LoginResponse.TokenResponse(accessToken, refreshToken);
}

private String createToken(String subject, String username, String authorities, Instant issuedAt, long expirationMinutes) {
try {
PrivateKey privateKey = getPrivateKey(jwtConfig.getPrivateKey());
Instant expiration = issuedAt.plus(expirationMinutes, ChronoUnit.MINUTES);

return Jwts.builder()
.subject(subject)
.claims(
Map.of(
"username", username,
"authorities", authorities
)
)
.issuedAt(Date.from(issuedAt))
.expiration(Date.from(expiration))
.signWith(privateKey, Jwts.SIG.RS256)
.compact();
} catch (Exception e) {
throw new RuntimeException("ํ† ํฐ ์ƒ์„ฑ ์‹คํŒจ", e);
}
}

/**
* [๊ฒ€์ฆ] ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ (RSA Public Key ์‚ฌ์šฉ)
*/
public boolean validateToken(String token) {
try {
PublicKey publicKey = getPublicKey(jwtConfig.getPublicKey());
Jwts.parser()
.verifyWith(publicKey)
.build()
.parseSignedClaims(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT Token", e);
} catch (ExpiredJwtException e) {
log.info("Expired JWT Token", e);
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT Token", e);
} catch (IllegalArgumentException e) {
log.info("JWT claims string is empty.", e);
} catch (Exception e) {
log.error("JWT validation error", e);
}
return false;
}

/**
* [ํŒŒ์‹ฑ] ํ† ํฐ์—์„œ ์ธ์ฆ ๊ฐ์ฒด ์ถ”์ถœ
*/
public Authentication getAuthentication(String token) {
try {
PublicKey publicKey = getPublicKey(jwtConfig.getPublicKey());
Claims claims = Jwts.parser()
.verifyWith(publicKey)
.build()
.parseSignedClaims(token)
.getPayload();

String username = claims.get("username", String.class);
String authoritiesStr = claims.get("authorities", String.class);

List<GrantedAuthority> authorities = Arrays.stream(authoritiesStr.split(","))
.map(String::trim)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());

return new UsernamePasswordAuthenticationToken(username, null, authorities);

} catch (Exception e) {
throw new RuntimeException("์ธ์ฆ ์ •๋ณด ์ถ”์ถœ ์‹คํŒจ", e);
}
}

public Claims parseClaims(String token) {
try {
PublicKey publicKey = getPublicKey(jwtConfig.getPublicKey());
return Jwts.parser()
.verifyWith(publicKey)
.build()
.parseSignedClaims(token)
.getPayload();
} catch (ExpiredJwtException e) {
// ๋งŒ๋ฃŒ๋œ ํ† ํฐ์ด์–ด๋„ ์ •๋ณด๋ฅผ ๊บผ๋‚ด๊ธฐ ์œ„ํ•ด Claims ๋ฐ˜ํ™˜
return e.getClaims();
} catch (Exception e) {
throw new RuntimeException("ํ† ํฐ ํŒŒ์‹ฑ ์‹คํŒจ", e);
}
}

//ํ‚ค ํŒŒ์‹ฑ ํ—ฌํผ
private PrivateKey getPrivateKey(String key) throws Exception {
String sanitizedKey = key
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] keyBytes = Base64.getDecoder().decode(sanitizedKey);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
return KeyFactory.getInstance("RSA").generatePrivate(spec);
}

private PublicKey getPublicKey(String key) throws Exception {
String sanitizedKey = key
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] keyBytes = Base64.getDecoder().decode(sanitizedKey);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
return KeyFactory.getInstance("RSA").generatePublic(spec);
}

private List<String> getAuthorities(Authentication authentication) {
return authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList();
}
}
Loading