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
6 changes: 0 additions & 6 deletions .env

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ docker-compose.override.yml

node_modules/
dist/
.env
11 changes: 11 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ dependencies {

// Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'


// Jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
implementation 'org.springframework.boot:spring-boot-configuration-processor'
}

tasks.named('test') {
Expand Down
11 changes: 0 additions & 11 deletions src/main/java/com/umc/umc9th/HelloController.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.umc.umc9th.domain.auth.controller;

import com.umc.umc9th.domain.auth.jwt.dto.JWTResponseDTO;
import com.umc.umc9th.domain.auth.jwt.dto.RefreshRequestDTO;
import com.umc.umc9th.domain.auth.jwt.service.JwtService;
import com.umc.umc9th.domain.auth.service.LoginService;
import com.umc.umc9th.domain.user.dto.request.LoginRequest;
import com.umc.umc9th.domain.user.dto.request.SignUpRequest;
import com.umc.umc9th.domain.user.dto.response.AuthResponse;
import com.umc.umc9th.domain.user.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Tag(name = "인증 API", description = "회원가입, 로그인, 토큰 관리 API")
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;
private final LoginService loginService;
private final JwtService jwtService;

@Operation(
summary = "회원가입",
description = "새로운 사용자를 등록합니다. 이메일은 중복될 수 없습니다."
)
@PostMapping("/signup")
public ResponseEntity<AuthResponse> signUp(@Valid @RequestBody SignUpRequest request) {
AuthResponse response = authService.signUp(request);
return ResponseEntity.ok(response);
}

@Operation(
summary = "로그인",
description = "이메일과 비밀번호로 로그인하여 JWT 토큰을 발급받습니다."
)
@PostMapping("/login")
public ResponseEntity<JWTResponseDTO> login(@Valid @RequestBody LoginRequest request) {
JWTResponseDTO response = loginService.login(request);
return ResponseEntity.ok(response);
}

@Operation(
summary = "액세스 토큰 갱신",
description = "리프레시 토큰을 사용하여 새로운 액세스 토큰과 리프레시 토큰을 발급받습니다. (Refresh Token Rotation)"
)
@PostMapping("/refresh")
public ResponseEntity<JWTResponseDTO> refresh(@RequestBody RefreshRequestDTO request) {
JWTResponseDTO response = jwtService.refreshRotate(request);
return ResponseEntity.ok(response);
}
}
55 changes: 55 additions & 0 deletions src/main/java/com/umc/umc9th/domain/auth/filter/JWTFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.umc.umc9th.domain.auth.filter;

import com.umc.umc9th.domain.auth.jwt.JWTProvider;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Collections;
import java.util.List;

public class JWTFilter extends OncePerRequestFilter {

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

String authorization = request.getHeader("Authorization");
if (authorization == null) {
filterChain.doFilter(request, response);
return;
}

if (!authorization.startsWith("Bearer ")) {
throw new ServletException("Invalid JWT token");
}

// 토큰 파싱
String accessToken = authorization.split(" ")[1];

if (JWTProvider.isValid(accessToken, true)) {

String userEmail = JWTProvider.getUserEmail(accessToken);
String role = JWTProvider.getRole(accessToken);

List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority(role));

Authentication auth = new UsernamePasswordAuthenticationToken(userEmail, null, authorities);
SecurityContextHolder.getContext().setAuthentication(auth);

filterChain.doFilter(request, response);

} else {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"error\":\"토큰 만료 또는 유효하지 않은 토큰\"}");
}
}
}
68 changes: 68 additions & 0 deletions src/main/java/com/umc/umc9th/domain/auth/filter/LoginFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.umc.umc9th.domain.auth.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.umc.umc9th.domain.user.dto.request.LoginRequest;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;

public class LoginFilter extends UsernamePasswordAuthenticationFilter {

private final AuthenticationManager authenticationManager;
private final ObjectMapper objectMapper;

public LoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
this.objectMapper = new ObjectMapper();
setFilterProcessesUrl("/api/auth/login");
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {

// POST 메소드가 아니면 예외 발생
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}

try {
LoginRequest loginRequest = objectMapper.readValue(request.getInputStream(), LoginRequest.class);

UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
loginRequest.userEmail(),
loginRequest.password()
);

return authenticationManager.authenticate(authToken);

} catch (IOException e) {
throw new RuntimeException("로그인 요청 파싱 실패", e);
}
}

@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult) throws IOException {

// LoginSuccessHandler에서 처리
}

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException {

response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"error\":\"로그인 실패\"}");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.umc.umc9th.domain.auth.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.umc.umc9th.domain.auth.jwt.JWTProvider;
import com.umc.umc9th.domain.auth.jwt.dto.JWTResponseDTO;
import com.umc.umc9th.domain.auth.jwt.service.JwtService;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;

@Component
@RequiredArgsConstructor
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

private final JwtService jwtService;
private final ObjectMapper objectMapper;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {

String userEmail = authentication.getName();

Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();
String role = auth.getAuthority();

// JWT 토큰 생성
String accessToken = JWTProvider.createJWT(userEmail, role, true);
String refreshToken = JWTProvider.createJWT(userEmail, role, false);

// Refresh 토큰 DB 저장
jwtService.addRefresh(userEmail, refreshToken);

// 응답
JWTResponseDTO jwtResponse = new JWTResponseDTO(accessToken, refreshToken);

response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(jwtResponse));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.umc.umc9th.domain.auth.handler;

import com.umc.umc9th.domain.auth.jwt.service.JwtService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class RefreshTokenLogoutHandler implements LogoutHandler {

private final JwtService jwtService;

@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {

String refreshToken = request.getHeader("RefreshToken");

if (refreshToken != null && jwtService.existsRefresh(refreshToken)) {
jwtService.removeRefresh(refreshToken);
}
}
}
Loading