Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'com.fasterxml.jackson.core:jackson-annotations'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'

// JWT (JJWT) 라이브러리
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' // JSON 직렬화용

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

}

tasks.named('test') {
Expand Down
90 changes: 90 additions & 0 deletions src/main/java/dev/admin/admin/controller/AdminAuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package dev.admin.admin.controller;

import dev.admin.admin.dto.request.LoginRequestDto;
import dev.admin.admin.dto.response.AdminInfoResponse;
import dev.admin.admin.dto.response.JwtDto;
import dev.admin.admin.dto.response.JwtPayload;
import dev.admin.admin.service.command.AdminAuthCommandService;
import dev.admin.admin.utils.JwtUtil;
import dev.admin.global.apiPayload.ApiResponse;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/admin")
@RequiredArgsConstructor
public class AdminAuthController {

private final AdminAuthCommandService authCommandService;
private final JwtUtil jwtUtil;

@PostMapping("/login")
public ApiResponse<JwtDto> login(@RequestBody LoginRequestDto requestDto, HttpServletResponse response) {
JwtDto tokenDto = authCommandService.login(requestDto);

ResponseCookie accessTokenCookie = ResponseCookie.from("accessToken", tokenDto.getAccessToken())
.httpOnly(true)
.secure(false)
.path("/")
.sameSite("Lax")
.maxAge(60 * 60)
.build();

ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", tokenDto.getRefreshToken())
.httpOnly(true)
.secure(false)
.path("/")
.sameSite("Lax")
.maxAge(7 * 24 * 60 * 60)
.build();

response.addHeader("Set-Cookie", accessTokenCookie.toString());
response.addHeader("Set-Cookie", refreshTokenCookie.toString());

return ApiResponse.onSuccess(tokenDto);
}

@PostMapping("/logout")
public ApiResponse<Void> logout(@CookieValue("refreshToken") String refreshToken,
HttpServletResponse response) {
authCommandService.logout(refreshToken);

ResponseCookie clearAccessToken = ResponseCookie.from("accessToken", "")
.httpOnly(true)
.secure(false)
.path("/")
.maxAge(0)
.sameSite("Lax")
.build();

ResponseCookie clearRefreshToken = ResponseCookie.from("refreshToken", "")
.httpOnly(true)
.secure(false)
.path("/")
.maxAge(0)
.sameSite("Lax")
.build();

response.addHeader("Set-Cookie", clearAccessToken.toString());
response.addHeader("Set-Cookie", clearRefreshToken.toString());

return ApiResponse.onSuccess(null);
}

@GetMapping("/me")
public ApiResponse<AdminInfoResponse> me(@CookieValue("accessToken") String accessToken) {
JwtPayload payload = jwtUtil.parseToken(accessToken);

AdminInfoResponse info = new AdminInfoResponse(
payload.getSub(),
payload.getEmail(),
payload.getName(),
payload.getRole()
);

return ApiResponse.onSuccess(info);
}
}
10 changes: 10 additions & 0 deletions src/main/java/dev/admin/admin/dto/request/LoginRequestDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dev.admin.admin.dto.request;

import lombok.Getter;

@Getter
public class LoginRequestDto {

private String email;
private String password;
}
31 changes: 31 additions & 0 deletions src/main/java/dev/admin/admin/dto/response/AdminInfoResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package dev.admin.admin.dto.response;

public class AdminInfoResponse {
private String id;
private String email;
private String name;
private String role;

public AdminInfoResponse(String id, String email, String name, String role) {
this.id = id;
this.email = email;
this.name = name;
this.role = role;
}

public String getId() {
return id;
}

public String getEmail() {
return email;
}

public String getName() {
return name;
}

public String getRole() {
return role;
}
}
12 changes: 12 additions & 0 deletions src/main/java/dev/admin/admin/dto/response/JwtDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dev.admin.admin.dto.response;


import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class JwtDto {
private String accessToken;
private String refreshToken;
}
17 changes: 17 additions & 0 deletions src/main/java/dev/admin/admin/dto/response/JwtPayload.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dev.admin.admin.dto.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class JwtPayload {
private String sub; // subject (adminId)
private String email;
private String name;
private String role;
}
10 changes: 10 additions & 0 deletions src/main/java/dev/admin/admin/entity/Admin.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import dev.admin.global.entity.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.*;
import dev.admin.admin.entity.Role;


@Entity
@Getter
Expand All @@ -20,4 +22,12 @@ public class Admin extends BaseTimeEntity {

@Column(nullable = false)
private String password;

@Column(nullable = false)
private String name;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;

}
6 changes: 6 additions & 0 deletions src/main/java/dev/admin/admin/entity/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dev.admin.admin.entity;

public enum Role {
ADMIN, // 관리자
// 필요하면 다른 역할도 추가 가능
}
68 changes: 68 additions & 0 deletions src/main/java/dev/admin/admin/filter/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package dev.admin.admin.filter;

import dev.admin.admin.security.AdminDetailsService;
import dev.admin.admin.utils.JwtUtil;
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.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtUtil jwtUtil;
private final AdminDetailsService adminDetailsService;

/**
* 요청 헤더에서 JWT 꺼내서 → 유효하면 SecurityContext에 인증 정보 저장
*/
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

String token = null;

// 1. Authorization 헤더에서 Bearer 토큰 추출
String authHeader = request.getHeader("Authorization");
if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
}

// 2. Authorization 헤더가 없으면 쿠키에서 accessToken 추출
if (token == null && request.getCookies() != null) {
for (var cookie : request.getCookies()) {
if (cookie.getName().equals("accessToken")) {
token = cookie.getValue();
break;
}
}
}

// 3. 토큰이 있고, 유효한 경우
if (token != null && jwtUtil.validateToken(token)) {

String email = jwtUtil.getEmailFromToken(token);
var adminDetails = adminDetailsService.loadUserByUsername(email);

UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(adminDetails, null, adminDetails.getAuthorities());

authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}
}
7 changes: 7 additions & 0 deletions src/main/java/dev/admin/admin/repository/AdminRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,12 @@
import dev.admin.admin.entity.Admin;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface AdminRepository extends JpaRepository<Admin, Long> {

/**
* 이메일로 관리자 정보 조회 (로그인용)
*/
Optional<Admin> findByEmail(String email);
}
39 changes: 39 additions & 0 deletions src/main/java/dev/admin/admin/repository/RedisTokenRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package dev.admin.admin.repository;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;

import java.time.Duration;
import java.util.Optional;

@Slf4j
@RequiredArgsConstructor
@Repository
public class RedisTokenRepository implements TokenRepository {

private final StringRedisTemplate redis;

@Override
public void save(String key, String value) {
redis.opsForValue().set(key, value, Duration.ofMinutes(30));
}

@Override
public Optional<String> find(String key) {
return Optional.ofNullable(redis.opsForValue().get(key));
}

@Override
public void delete(String key) {
redis.delete(key);
}

@Override
public boolean exists(String key) {
Boolean result = redis.hasKey(key);
return result != null && result;
}
}

10 changes: 10 additions & 0 deletions src/main/java/dev/admin/admin/repository/TokenRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dev.admin.admin.repository;

import java.util.Optional;

public interface TokenRepository {
void save(String key, String value);
Optional<String> find(String key);
void delete(String key);
boolean exists(String key);
}
Loading