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
7 changes: 6 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,17 @@ repositories {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.mindrot:jbcrypt:0.4'
}

tasks.named('test') {
Expand Down
9 changes: 9 additions & 0 deletions data/users.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"user123" : {
"id" : "user123",
"password" : "$2a$10$T5DRf1u9w4yABCuA9rbVFO3ig57iC/f5LdowvvoaNTXhlsADCoivW",
"email" : "[email protected]",
"nickname" : "사용자닉네임",
"createdAt" : 1742879450.367679500
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.sprint.mission.sbblogsystem.controller;

import com.sprint.mission.sbblogsystem.dto.LoginRequest;
import com.sprint.mission.sbblogsystem.dto.UserRegisterRequest;
import com.sprint.mission.sbblogsystem.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;

public UserController(UserService userService) {
this.userService = userService;
}

@PostMapping("/register")
public ResponseEntity<Map<String, Object>> register(@RequestBody UserRegisterRequest request){
userService.register(request);

Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "회원가입이 완료되었습니다.");

return ResponseEntity.ok(response);
}

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request){
String token = userService.login(request);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("token", token);
return ResponseEntity.ok(response);
}
}
23 changes: 23 additions & 0 deletions src/main/java/com/sprint/mission/sbblogsystem/domain/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.sprint.mission.sbblogsystem.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.Instant;


@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor

public class User {
private String id;
private String password;
private String email;
private String nickname;
private Instant createdAt;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.sprint.mission.sbblogsystem.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class LoginRequest {
private String id;
private String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.sprint.mission.sbblogsystem.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class UserRegisterRequest {
private String id;
private String password;
private String email;
private String nickname;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.sprint.mission.sbblogsystem.repository;

import com.sprint.mission.sbblogsystem.domain.User;

import java.util.Optional;

public interface UserRepository {

Optional<User> findById(String id);
Optional<User> findByNickname(String nickname);
boolean existsById(String id);
boolean existsByNickname(String nickname);
void save(User user);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.sprint.mission.sbblogsystem.repository.file;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sprint.mission.sbblogsystem.domain.User;
import com.sprint.mission.sbblogsystem.repository.UserRepository;
import org.springframework.stereotype.Repository;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

import java.io.File;
import java.io.IOException;
import java.util.*;

@Repository
public class FileUserRepository implements UserRepository {

private static final String FILE_PATH = "data/users.json";
private final Map<String, User> userMap = new HashMap<>();
private final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule());

public FileUserRepository() {
loadFromFile();
}

@Override
public Optional<User> findById(String id) {
return Optional.ofNullable(userMap.get(id));
}

@Override
public Optional<User> findByNickname(String nickname) {
return userMap.values().stream()
.filter(user -> user.getNickname().equals(nickname))
.findFirst();
}

@Override
public boolean existsById(String id) {
return userMap.containsKey(id);
}

@Override
public boolean existsByNickname(String nickname) {
return userMap.values().stream()
.anyMatch(user -> user.getNickname().equals(nickname));
}

@Override
public void save(User user) {
userMap.put(user.getId(), user);
saveToFile();
}

private void loadFromFile() {
File file = new File(FILE_PATH);
if (file.exists()){
try {
Map<String, User> loaded = objectMapper.readValue(file, new TypeReference<>() {
});
userMap.clear();
userMap.putAll(loaded);
} catch (IOException e) {
throw new RuntimeException("사용자 파일 로드 실패", e);
}
}
}

private void saveToFile() {
File file = new File(FILE_PATH);
file.getParentFile().mkdirs();
try {
objectMapper.writerWithDefaultPrettyPrinter().writeValue(file, userMap);
} catch (IOException e) {
throw new RuntimeException("사용자 파일 저장 실패", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.sprint.mission.sbblogsystem.service;

import com.sprint.mission.sbblogsystem.domain.User;
import com.sprint.mission.sbblogsystem.dto.LoginRequest;
import com.sprint.mission.sbblogsystem.dto.UserRegisterRequest;
import com.sprint.mission.sbblogsystem.repository.UserRepository;
import com.sprint.mission.sbblogsystem.util.ValidationUtil;
import com.sprint.mission.sbblogsystem.util.JwtUtil;
import org.apache.el.util.Validation;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.stereotype.Service;

import java.time.Instant;

@Service
public class UserService {
private final UserRepository userRepository;
private final JwtUtil jwtUtil;

public UserService(UserRepository userRepository, JwtUtil jwtUtil) {
this.userRepository = userRepository;
this.jwtUtil = jwtUtil;
}

public void register(UserRegisterRequest request) {
if (!ValidationUtil.isValidId(request.getId())) {
throw new IllegalArgumentException("ID는 6~30자여야 합니다.");
}
if (!ValidationUtil.isValidPassword(request.getPassword())){
throw new IllegalArgumentException("비밀번호는 12~50자이며, 영문/숫자/특수문자를 각각 2자 이상 포함해야 합니다.");
}
if (!ValidationUtil.isValidEmail(request.getEmail())){
throw new IllegalArgumentException("이메일 형식이 유효하지 않거나 100자 초과입니다.");
}
if (!ValidationUtil.isValidNickname(request.getNickname())){
throw new IllegalArgumentException("닉네임은 50자 이하여야 합니다.");
}

if (userRepository.existsById(request.getId())){
throw new IllegalArgumentException("이미 사용 중인 ID입니다.");
}

if (userRepository.existsByNickname(request.getNickname())){
throw new IllegalArgumentException("이미 사용 중인 닉네임입니다.");
}

String hashedPassword = BCrypt.hashpw(request.getPassword(), BCrypt.gensalt());

User user = new User(
request.getId(),
hashedPassword,
request.getEmail(),
request.getNickname(),
Instant.now()
);

userRepository.save(user);

}

public String login(LoginRequest request) {
User user = userRepository.findById(request.getId())
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));

if (!BCrypt.checkpw(request.getPassword(), user.getPassword())) {
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
}

return jwtUtil.generateToken(user.getId());
}

public boolean existsById(String userId) {
return userRepository.findById(userId).isPresent();
}
}
73 changes: 73 additions & 0 deletions src/main/java/com/sprint/mission/sbblogsystem/util/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.sprint.mission.sbblogsystem.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtUtil {
private final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

// 토큰에서 사용자 ID 추출
public String extractUserId(String token) {
return extractClaim(token, Claims::getSubject);
}

// 모든 클레임 추출
public Claims extractAllClaims(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}

// 특정 클레임 추출
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}

// 토큰 생성
public String generateToken(String userId) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userId);
}

// 토큰 생성 (내부 메서드)
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
// 무한 유효기간 대신 매우 긴 유효기간 설정 (예: 100년)
.setExpiration(new Date(System.currentTimeMillis() + 1000L * 60 * 60 * 24 * 365 * 100))
.signWith(key)
.compact();
}

// 토큰 유효성 검증
public Boolean validateToken(String token) {
try {
return !isTokenExpired(token);
} catch (Exception e) {
return false;
}
}

// 토큰 만료 확인
private Boolean isTokenExpired(String token) {
final Date expiration = extractExpiration(token);
return expiration.before(new Date());
}

// 만료일 추출
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
}

Loading