Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Comment thread
snowykte0426 marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.gsm._8th.class4.backend.task12.domain.auth.Controller;

import com.gsm._8th.class4.backend.task12.domain.auth.Repository.UserRepository;
import com.gsm._8th.class4.backend.task12.domain.auth.Service.newsignService;
import com.gsm._8th.class4.backend.task12.domain.auth.dto.UserLoginRequest;
import com.gsm._8th.class4.backend.task12.domain.auth.dto.UserSignupRequest;
import com.gsm._8th.class4.backend.task12.global.security.TokenResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class newsignController {

private final newsignService authService;
private final UserRepository userRepository;
@PostMapping("/signup")
public ResponseEntity<String> signup(@RequestBody UserSignupRequest request) {
authService.signup(request);
return ResponseEntity.ok("회원가입 성공");
}
Comment thread
snowykte0426 marked this conversation as resolved.
Outdated

@PostMapping("/login")
Comment thread
snowykte0426 marked this conversation as resolved.
Outdated
public ResponseEntity<?> login(@RequestBody UserLoginRequest request) {
TokenResponse tokenResponse = authService.login(request.getUsername(), request.getPassword());

if (tokenResponse != null) {
return ResponseEntity.ok(tokenResponse);
} else {
return ResponseEntity.status(401).body("로그인 실패: 아이디 또는 비밀번호가 잘못되었습니다.");
}
}

@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestHeader("Refresh-Token") String refreshToken) {
TokenResponse newTokens = authService.refreshToken(refreshToken);

if (newTokens != null) {
return ResponseEntity.ok(newTokens);
} else {
return ResponseEntity.status(403).body("유효하지 않은 리프레시 토큰입니다.");
}
}
@DeleteMapping("/delete")
public ResponseEntity<?> deleteUser(@RequestBody UserLoginRequest request) {
Comment thread
snowykte0426 marked this conversation as resolved.
Outdated
TokenResponse tokenResponse = authService.login(request.getUsername(), request.getPassword());

if (tokenResponse != null) {
return userRepository.findByUsername(request.getUsername())
.map(user -> {
userRepository.delete(user);
return ResponseEntity.ok("계정 삭제 완료");
})
.orElse(ResponseEntity.status(404).body("사용자를 찾을 수 없습니다."));
} else {
return ResponseEntity.status(401).body("로그인 실패: 아이디 또는 비밀번호가 잘못되었습니다.");
Comment thread
snowykte0426 marked this conversation as resolved.
Outdated
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.gsm._8th.class4.backend.task12.domain.auth.Entity;

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

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

@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Table(name = "new_sign")
public class NewSign {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String role;
private String email;
}
Comment on lines +1 to +28
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.

Entity 이름이 new_sign인 것은 현재 비즈니스 요구사항에는 적합하지 않은 것 같습니다

만약 회원가입이 승인제로 이뤄지는 서비스여서 새 계정 정보를 승인 전까지 임시로 저장하는 테이블이라면 나름 적합할 수도 있겠지만...
현재 비즈니스 요구에서는 단순한 사용자 정보 저장이 목적이니만큼 UserMember 라는 이름이 적합한 것일것 같습니다

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>로그인</title>
</head>
<body>
<h2>로그인</h2>
<form action="/login" method="post">
<label>사용자 이름:</label>
<input type="text" name="username" required>
<br>
<label>비밀번호:</label>
<input type="password" name="password" required>
<br>
<button type="submit">로그인</button>
</form>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>회원가입</title>
</head>
<body>
<h2>회원가입</h2>
<form action="/user/signup" method="post">
<label>사용자 이름:</label>
<input type="text" name="username" required>
<br>
<label>비밀번호:</label>
<input type="password" name="password" required>
<br>
<button type="submit">회원가입</button>
</form>
<a href="/user/login">로그인</a>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.gsm._8th.class4.backend.task12.domain.auth.Repository;

import com.gsm._8th.class4.backend.task12.domain.auth.Entity.NewSign;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<NewSign, Long> {
Optional<NewSign> findByUsername(String username);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.gsm._8th.class4.backend.task12.domain.auth.Service;

import com.gsm._8th.class4.backend.task12.domain.auth.dto.UserSignupRequest;
import com.gsm._8th.class4.backend.task12.global.security.TokenResponse;
import com.gsm._8th.class4.backend.task12.domain.auth.Entity.NewSign;
import com.gsm._8th.class4.backend.task12.domain.auth.Repository.UserRepository;
import com.gsm._8th.class4.backend.task12.global.security.JwtTokenService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@RequiredArgsConstructor
public class newsignService {

private final UserRepository userRepository;
private final JwtTokenService jwtTokenService;
private final PasswordEncoder passwordEncoder;

@Transactional
public void signup(UserSignupRequest request) {
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
throw new RuntimeException("이미 존재하는 사용자입니다.");
}
NewSign newUser = NewSign.builder()
.username(request.getUsername())
.password(passwordEncoder.encode(request.getPassword()))
.email(request.getEmail())
.role("ROLE_USER")
.build();
userRepository.save(newUser);
}

// 로그인
public TokenResponse login(String username, String rawPassword) {
Optional<NewSign> userOptional = userRepository.findByUsername(username);
if (userOptional.isPresent()) {
NewSign user = userOptional.get();
if (passwordEncoder.matches(rawPassword, user.getPassword())) {
String accessToken = jwtTokenService.createAccessToken(username);
String refreshToken = jwtTokenService.createRefreshToken(username);
return new TokenResponse(accessToken, refreshToken);
}
}
return null; // 로그인 실패
Comment thread
snowykte0426 marked this conversation as resolved.
Outdated
}
// 리프레시 토큰을 통한 JWT 재발급
public TokenResponse refreshToken(String refreshToken) {
String username = jwtTokenService.getUsernameFromToken(refreshToken);

if (username != null && jwtTokenService.validateRefreshToken(username, refreshToken)) {
jwtTokenService.revokeRefreshToken(username); // 기존 리프레시 토큰 폐기 Refresh Token Rotation 적용

String newAccessToken = jwtTokenService.createAccessToken(username);
String newRefreshToken = jwtTokenService.createRefreshToken(username);

return new TokenResponse(newAccessToken, newRefreshToken);
}
return null;
Comment thread
snowykte0426 marked this conversation as resolved.
Outdated
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.gsm._8th.class4.backend.task12.domain.auth.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter

public class UserLoginRequest {
private String username;
private String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.gsm._8th.class4.backend.task12.domain.auth.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class UserSignupRequest {

@NotBlank(message = "사용자 이름은 필수 입력값입니다.")
@Size(min = 3, max = 20, message = "사용자 이름은 3~20자로 입력해야 합니다.")
private String username;

@NotBlank(message = "비밀번호는 필수 입력값입니다.")
@Size(min = 6, message = "비밀번호는 최소 6자 이상이어야 합니다.")
private String password;

@NotBlank(message = "이메일은 필수 입력값입니다.")
@Email(message = "올바른 이메일 형식을 입력해주세요.")
private String email;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.gsm._8th.class4.backend.task12.global.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenService jwtTokenService;

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

String token = request.getHeader("Authorization");

if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
if (jwtTokenService.validateToken(token)) {
String username = jwtTokenService.getUsernameFromToken(token);

// UserDetails 객체 생성
UserDetails userDetails = User.withUsername(username)
.password("") // JWT는 비밀번호를 검증하지 않음
.authorities("ROLE_USER") // 기본 역할 부여
.build();

// 인증 객체 생성
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

// Spring Security 컨텍스트에 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.gsm._8th.class4.backend.task12.global.security;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Service
public class JwtTokenService {

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

private final long accessTokenValidity = 60 * 60 * 1000L;
private final long refreshTokenValidity = 120 * 60 * 1000L;
private final ConcurrentHashMap<String, String> refreshTokenStore = new ConcurrentHashMap<>();
private Key getSigningKey() {
return Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
}
public String createAccessToken(String username) {
return createToken(username, accessTokenValidity);
}
public String createRefreshToken(String username) {
String refreshToken = createToken(username, refreshTokenValidity);
refreshTokenStore.put(username, refreshToken);
return refreshToken;
}
private String createToken(String subject, long validity) {
Date now = new Date();
return Jwts.builder()
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + validity))
.signWith(getSigningKey(), SignatureAlgorithm.HS256) // 변경됨
.compact();
}
public boolean validateToken(String token) {
try {
Jws<Claims> claims = Jwts.parser()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token);

return !claims.getBody().getExpiration().before(new Date()); // 만료 확인
} catch (JwtException | IllegalArgumentException e) {
log.error("유효하지 않은 토큰: {}", e.getMessage());
return false;
}
}
public String getUsernameFromToken(String token) {
return Jwts.parser() // 변경됨
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateRefreshToken(String username, String refreshToken) {
return refreshToken.equals(refreshTokenStore.get(username));
}
public void revokeRefreshToken(String username) {
refreshTokenStore.remove(username);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.gsm._8th.class4.backend.task12.global.security;

public class SecretKey {
public static String JWT_SECRET_KEY = "236979CB6F1AD6B6A6184A31E6BE37DB3818CC36871E26235DD67DCFE4041492";
Comment thread
snowykte0426 marked this conversation as resolved.
Outdated
}
Loading