Skip to content
Open
23 changes: 15 additions & 8 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.6"
id "org.springframework.boot" version "3.5.6" // 사용자 버전 유지
id "io.spring.dependency-management" version "1.1.7"
}

Expand All @@ -19,19 +19,26 @@ repositories {
}

dependencies {
// 기본 웹 라이브러리
// 1. 기본 웹 라이브러리
implementation 'org.springframework.boot:spring-boot-starter-web'

// 데이터베이스 연동(JPA) 라이브러리
// 2. 데이터베이스 연동(JPA) 라이브러리

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// 테스트용 H2 인메모리 데이터베이스
runtimeOnly 'com.h2database:h2'
// Lombok 라이브러리 (코드 자동 생성)

// 3. 보안 및 JWT 관련 라이브러리 (추가됨)
implementation 'org.springframework.boot:spring-boot-starter-security' // Spring Security 코어
implementation 'io.jsonwebtoken:jjwt-api:0.11.5' // JWT 인터페이스
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' // JWT 구현체
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' // JSON 처리를 위한 Jackson 연동
testImplementation 'org.springframework.security:spring-security-test' // 보안 테스트용

// 4. 유틸리티 및 DB 드라이버
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
// --- ▲▲▲▲▲ 여기까지 추가 ▲▲▲▲▲ ---

// 기본 테스트 라이브러리
// 5. 테스트 라이브러리
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
Expand Down
52 changes: 52 additions & 0 deletions src/main/java/com/example/devSns/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.example.devSns.config;

import com.example.devSns.security.JwtFilter;
import com.example.devSns.security.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
Comment on lines +16 to +19
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spring Security를 사용하셨네요! 모듈이 복잡한 편이라서 나중에 인증/인가 전체 흐름 관련해서 파트별로 기능을 정리해두면 좋을 것 같아요~ 해당 부분 템플릿화해두면 초기 프로젝트 진행 시 인증/인가 부분을 빠르게 처리해서 넘겨줄 수 있어서 좋습니다


private final JwtTokenProvider jwtTokenProvider;

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// CSRF 설정 Disable
.csrf((csrf) -> csrf.disable())

// 세션 사용 안 함 (STATELESS)
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

// 요청 권한 설정
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/api/auth/**", "/h2-console/**").permitAll() // 로그인, 회원가입은 누구나 접근 가능
.anyRequest().authenticated() // 그 외 모든 요청은 인증 필요
)

// H2 Console iframe 허용
.headers((headers) -> headers.frameOptions(frameOptions -> frameOptions.disable()))

// JwtFilter 를 UsernamePasswordAuthenticationFilter 앞에 등록
.addFilterBefore(new JwtFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}
36 changes: 36 additions & 0 deletions src/main/java/com/example/devSns/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.example.devSns.controller;

import com.example.devSns.dto.MemberLoginRequestDto;
import com.example.devSns.dto.MemberResponseDto;
import com.example.devSns.dto.MemberSignUpRequestDto;
import com.example.devSns.dto.TokenRequestDto;
import com.example.devSns.dto.TokenDto;
import com.example.devSns.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;

@PostMapping("/signup")
public ResponseEntity<MemberResponseDto> signup(@RequestBody MemberSignUpRequestDto requestDto) {
return ResponseEntity.ok(authService.signup(requestDto));
}

@PostMapping("/login")
public ResponseEntity<TokenDto> login(@RequestBody MemberLoginRequestDto requestDto) {
return ResponseEntity.ok(authService.login(requestDto));
}

@PostMapping("/reissue")
public ResponseEntity<TokenDto> reissue(@RequestBody TokenRequestDto requestDto) {
return ResponseEntity.ok(authService.reissue(requestDto));
}
Comment on lines +32 to +35
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

액세스 토큰 재발급로직까지 구현해 주셨네요! 좋습니다

}
5 changes: 5 additions & 0 deletions src/main/java/com/example/devSns/domain/Authority.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.devSns.domain;

public enum Authority {
ROLE_USER, ROLE_ADMIN
}
21 changes: 16 additions & 5 deletions src/main/java/com/example/devSns/domain/Member.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package com.example.devSns.domain;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@NoArgsConstructor
@Table(name = "members") // 'user'는 H2 DB 등에서 예약어일 수 있으므로 'members' 사용

@AllArgsConstructor // Builder 패턴 사용 시 필요
@Builder // ★ 클래스 레벨에 추가하여 builder() 메서드 자동 생성 ★
@Table(name = "members")

public class Member {

@Id
Expand All @@ -21,19 +26,25 @@ public class Member {
private String username; // 로그인 ID

@Column(nullable = false)
private String password; // 실제로는 해싱(Hashing) 필요

private String password;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 코드 리뷰 사항은 아니긴 한데 해싱과 암호화의 차이에 대해서도 알아두시면 좋을 것 같습니다!

해싱 - 단방향
암호화 - 양방향 (암호화 - 복호화)

비밀번호 저장 시 암호화는 키 값 노출 시 복호화가 가능한데, 복호화가 불가능한 해싱 기법을 사용하면 해싱에는 복호화 키 개념이 없기에 상대적으로 안전해서 보통 해싱을 사용하는 것 같아요


@Column(nullable = false, unique = true)
private String nickname; // 사용자가 표시할 이름
private String nickname;

@Enumerated(EnumType.STRING)
private Authority authority; // 권한

// Member가 삭제되면, 관련 Post도 모두 삭제 (Cascade)
@OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE, orphanRemoval = true)
@Builder.Default // Builder 사용 시 초기화 값을 유지하기 위해 필요
private List<Post> posts = new ArrayList<>();

@OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE, orphanRemoval = true)
@Builder.Default
private List<Comment> comments = new ArrayList<>();

@OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE, orphanRemoval = true)
@Builder.Default
private List<PostLike> likes = new ArrayList<>();

public Member(String username, String password, String nickname) {
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/com/example/devSns/domain/RefreshToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.example.devSns.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@Table(name = "refresh_token")
@Entity
public class RefreshToken {

@Id
@Column(name = "rt_key")
private String key; // Member ID (username)

@Column(name = "rt_value")
private String value; // Refresh Token 값

@Builder
public RefreshToken(String key, String value) {
this.key = key;
this.value = value;
}

public RefreshToken updateValue(String token) {
this.value = token;
return this;
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/example/devSns/dto/MemberLoginRequestDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.devSns.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class MemberLoginRequestDto {
private String username;
private String password;
Comment on lines +8 to +13
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO의 경우는 Record를 사용하시는 걸 추천드립니다! 그러면 @Getter, @AllArgsConstructor를 사용하지 않아도 되기에 Lombok에 의존하지 않고도 깔끔하게 개발할 수 있습니다. 그리고 불변 객체라 동시에 여러 쓰레드를 사용하더라도 안전하다는 장점이 있습니다.


// 아이디/비번을 인증 토큰 형태로 변환하는 메서드
public UsernamePasswordAuthenticationToken toAuthentication() {
return new UsernamePasswordAuthenticationToken(username, password);
}
}
18 changes: 14 additions & 4 deletions src/main/java/com/example/devSns/dto/MemberResponseDto.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
package com.example.devSns.dto;

import com.example.devSns.domain.Member;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MemberResponseDto {
private Long id;
private String username;
private String nickname;
Comment on lines 8 to 15
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

username도 담아주는 게 활용하기 좋겠네요! 그런데 id의 경우 나중에 프론트와 협업할 때는 응답에 필요한 경우가 종종 생깁니다. 예를 들어 목록 조회 -> 상세 페이지 흐름에서 상세 페이지 요청을 위해 대상 memberId를 프론트 단에서 알아야 하는 상황 등이 있겠네요. 비즈니스 로직 흐름 고려하셔서 진행하시면 될 것 같습니다


public MemberResponseDto(Member member) {
this.id = member.getId();
this.nickname = member.getNickname();
// Member 엔티티를 받아서 DTO로 변환해주는 정적 메서드
public static MemberResponseDto of(Member member) {
return MemberResponseDto.builder()
.username(member.getUsername())
.nickname(member.getNickname())
.build();

}
}
17 changes: 17 additions & 0 deletions src/main/java/com/example/devSns/dto/MemberSignUpRequestDto.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
package com.example.devSns.dto;

import com.example.devSns.domain.Authority;
import com.example.devSns.domain.Member;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.security.crypto.password.PasswordEncoder;

@Getter
@Setter
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO의 경우 말 그대로 데이터 전송용 객체다 보니 처음 만들 때 완전하게 만들고 setter를 지양하는 게 좋을 것 같습니다!

@NoArgsConstructor

public class MemberSignUpRequestDto {
private String username;
private String password;
private String nickname;

// DTO 정보를 바탕으로 Member 엔티티를 만드는 메서드
public Member toMember(PasswordEncoder passwordEncoder) {
return Member.builder()
.username(username)
.password(passwordEncoder.encode(password))
.nickname(nickname)
.authority(Authority.ROLE_USER)
.build();
}

}
15 changes: 15 additions & 0 deletions src/main/java/com/example/devSns/dto/TokenDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.devSns.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

@Data
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Data의 경우 기능이 너무 많아서 객체의 책임 관점에서 조금 위험할 수도 있을 것 같아요!

@Builder
@AllArgsConstructor
public class TokenDto {
private String grantType;
private String accessToken;
private String refreshToken;
private Long accessTokenExpiresIn;
}
11 changes: 11 additions & 0 deletions src/main/java/com/example/devSns/dto/TokenRequestDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.devSns.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class TokenRequestDto {
private String accessToken;
private String refreshToken;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
// 닉네임으로 회원 검색 (검색 기능용)
// 닉네임으로 회원 검색
Optional<Member> findByNickname(String nickname);

// 로그인 ID(username)로 회원 검색 (★ 이 부분이 빠져있어서 에러가 났습니다 ★)
Optional<Member> findByUsername(String username);

// 중복 가입 방지용 존재 여부 확인 (★ 이 부분도 같이 추가해주세요 ★)
boolean existsByUsername(String username);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.devSns.repository;

import com.example.devSns.domain.RefreshToken;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, String> {
Optional<RefreshToken> findByKey(String key);
}
Loading