Skip to content
Open
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
36 changes: 18 additions & 18 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.6'
id 'io.spring.dependency-management' version '1.1.7'
id 'org.springframework.boot' version '3.5.6'
id 'io.spring.dependency-management' version '1.1.6'
id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
description = 'Demo project for Spring Boot'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
java { sourceCompatibility = '21' }

repositories {
mavenCentral()
}
repositories { mavenCentral() }

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
runtimeOnly 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
useJUnitPlatform()
useJUnitPlatform()
enabled = false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.example.devSns.config;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;

public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {

String token = resolveToken(request);

if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}

private String resolveToken(HttpServletRequest request) {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
return header.substring(7);
}
return null;
}
}
80 changes: 80 additions & 0 deletions src/main/java/com/example/devSns/config/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.example.devSns.config;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Collections;
import java.util.Date;

@Component
public class JwtTokenProvider {

@Value("${jwt.secret}")
private String secretKey;

@Value("${jwt.access-token-validity-in-seconds}")
private long validityInSeconds;

private Key key;

@PostConstruct
public void init() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}

public String createToken(String username, Long memberId) {
Date now = new Date();
Date validity = new Date(now.getTime() + validityInSeconds * 1000);

return Jwts.builder()
.setSubject(username)
.claim("memberId", memberId)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}

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

private Claims getClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}

public Authentication getAuthentication(String token) {
Claims claims = getClaims(token);
String username = claims.getSubject();

User principal = new User(username, "", Collections.emptyList());
return new UsernamePasswordAuthenticationToken(
principal,
token,
principal.getAuthorities()
);
}

public Long getMemberId(String token) {
Claims claims = getClaims(token);
return claims.get("memberId", Long.class);
}
}
58 changes: 58 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,58 @@
package com.example.devSns.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;

public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.httpBasic(basic -> basic.disable())
.formLogin(form -> form.disable())
.sessionManagement(sm ->
sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
// 로그인/회원가입, H2 콘솔은 누구나 허용
.requestMatchers("/auth/**", "/h2-console/**").permitAll()
// 읽기(GET)은 일단 공개
.requestMatchers(HttpMethod.GET, "/**").permitAll()
// 작성/수정/삭제는 인증 필요
.requestMatchers(HttpMethod.POST, "/**").authenticated()
.requestMatchers(HttpMethod.PUT, "/**").authenticated()
.requestMatchers(HttpMethod.DELETE, "/**").authenticated()
.anyRequest().authenticated()
);

// H2 콘솔용 frame 옵션
http.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()));

// JWT 필터 등록
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
46 changes: 46 additions & 0 deletions src/main/java/com/example/devSns/domain/Comment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.example.devSns.domain;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import lombok.*;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotBlank
@Column(nullable = false)
private String content;

/** 어느 Post 에 달린 댓글인지 */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;

/** 누가 쓴 댓글인지 */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member author;

@Builder
private Comment(String content, Post post, Member author) {
this.content = content;
this.post = post;
this.author = author;
}

public void update(String content) {
this.content = content;
}

void setPostInternal(Post post) {
this.post = post;
}

public void changeAuthor(Member author) {
this.author = author;
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/example/devSns/domain/CommentRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.devSns.domain;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;

public interface CommentRepository extends JpaRepository<Comment, Long> {

List<Comment> findByPostIdOrderByIdAsc(Long postId);
Optional<Comment> findByIdAndPostId(Long id, Long postId);

List<Comment> findByAuthorIdOrderByIdDesc(Long memberId);
}
33 changes: 33 additions & 0 deletions src/main/java/com/example/devSns/domain/Follow.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.devSns.domain;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(
name = "follow",
uniqueConstraints = @UniqueConstraint(columnNames = {"follower_id", "following_id"})
)
public class Follow {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

// 나를 기준으로: 내가 팔로우 하는 사람
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "follower_id", nullable = false)
private Member follower;

// 내가 팔로우 당하는 사람
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "following_id", nullable = false)
private Member following;

@Builder
private Follow(Member follower, Member following) {
this.follower = follower;
this.following = following;
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/example/devSns/domain/FollowRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.devSns.domain;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface FollowRepository extends JpaRepository<Follow, Long> {

boolean existsByFollowerIdAndFollowingId(Long followerId, Long followingId);

void deleteByFollowerIdAndFollowingId(Long followerId, Long followingId);

List<Follow> findByFollowerId(Long followerId); // 내가 팔로우하는 사람들
List<Follow> findByFollowingId(Long followingId); // 나를 팔로우하는 사람들
}
45 changes: 45 additions & 0 deletions src/main/java/com/example/devSns/domain/Member.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.example.devSns.domain;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import lombok.*;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotBlank
@Column(nullable = false, unique = true, length = 50)
private String username; // 로그인 아이디

@NotBlank
@Column(nullable = false, length = 100)
private String password; // 인코딩된 비밀번호

@Column(length = 50)
private String nickname; // 화면용 이름

@Column(length = 255)
private String bio; // 자기소개 (옵션)

@Builder
private Member(String username, String password, String nickname, String bio) {
this.username = username;
this.password = password;
this.nickname = nickname;
this.bio = bio;
}

public void updateProfile(String nickname, String bio) {
this.nickname = nickname;
this.bio = bio;
}

public void changePassword(String encodedPassword) {
this.password = encodedPassword;
}
}
Loading