diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..a61084c
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,22 @@
+## ๐ ๋ช ์ฃผ์ฐจ ์ํฌ๋ถ์ธ๊ฐ์?
+- ์: Week01
+
+---
+
+## โจ ์ด๋ฒ ์ฃผ์ ์์
ํ ๋ด์ฉ
+- ๊ตฌํ/์์ ํ ๊ธฐ๋ฅ ์์ฝ
+ - ์: ํ์๊ฐ์
API ๊ตฌํ
+ - ์: UI ๋ ์ด์์ ์ ๋ฆฌ
+
+---
+
+## ๐ ๋ฆฌ๋ทฐ ์์ฒญ/ํ์ธ ๋ฐ๊ณ ์ถ์ ๋ถ๋ถ
+- ์: ๋น๋ฐ๋ฒํธ ์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง์ด ์ ์ ํ์ง ํ์ธ ๋ถํ๋๋ฆฝ๋๋ค.
+- ์: Controller ๋จ์ ์ฝ๋ ๊ตฌ์กฐ ํผ๋๋ฐฑ ์ํฉ๋๋ค.
+
+---
+
+## โ
์ฒดํฌ๋ฆฌ์คํธ
+- [ ] `weekN/` ํด๋ ์์ ๊ณผ์ ์ ๋ฆฌ ์๋ฃ
+- [ ] PR ์์ฑ ์ **base = ์กฐ์ง ๋ด ๋ณธ์ธ ๋ธ๋์น**, **compare = ๋ด Fork main ๋ธ๋์น**๋ก ์ค์ ํ๋์ง ํ์ธ
+- [ ] PR ์ ๋ชฉ์ **`[WeekN] ๋๋ค์/์ด๋ฆ ๋ฏธ์
์ ์ถ`** ๊ท์น ๋ง๊ฒ ์์ฑ
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b157827
--- /dev/null
+++ b/README.md
@@ -0,0 +1,40 @@
+# ๐ฑ 9th-Springboot
+> 9th ์๊ฒฝ๋ **UMC Springboot ํํธ Repository** ์
๋๋ค.
+
+
+
+
+
+
+
+
+## ๐ GitHub ๋ฏธ์
์ ์ถ ๋ฐฉ๋ฒ
+1. ๋ณธ์ธ์ ๋๋ค์์ผ๋ก ์ ๋ธ๋์น๋ฅผ ๋ง๋ค์ด ์ฃผ์ธ์.
+2. ์ด ๋ ํฌ์งํ ๋ฆฌ๋ฅผ ๋ณธ์ธ ๊ณ์ ์ผ๋ก Fork ํฉ๋๋ค.
+3. Forkํ ๋ ํฌ์งํ ๋ฆฌ์ ๋ฏธ์
๊ฒฐ๊ณผ๋ฌผ์ ์ฌ๋ฆฝ๋๋ค.
+4. ๋ณธ์ธ ๋ ํฌ์งํ ๋ฆฌ์์ SKU-UMC ๋ ํฌ์งํ ๋ฆฌ์ ๋๋ค์ ๋ธ๋์น๋ก Pull Request(PR)๋ฅผ ๋ณด๋ด๋ฉด ๋!
+- [SKU UMC GitHub ๋ฏธ์
์ ์ถ ๊ฐ์ด๋](https://acoustic-daffodil-00e.notion.site/GitHub-26731d49a1348042878ff024dcbde258)
+
+
+
+## ๐ PR ๊ท์น
+- **์ ๋ชฉ ํ์**
+ - [WeekN] ๋๋ค์/์ด๋ฆ ๋ฏธ์
์ ์ถ
+ - ex) `[Week01] ํ๋กฌ/์ ๋ค์ธ ๋ฏธ์
์ ์ถ`
+
+- **์ฃผ์์ฌํญ**
+ - main ๋ธ๋์น๋ก๋ ์ ๋ PR โ
+ - ๋ฐ๋์ fork๋ ๋ณธ์ธ ๋ ํฌ โ original ๋ ํฌ ๋ณธ์ธ ๋ธ๋์น๋ก PR ๋ณด๋ด๊ธฐ โ
+
+- **๋ฆฌ๋ทฐ & ๋จธ์ง**
+ - ํํธ์ฅ์ด PR ๋ด์ฉ์ ํ์ธ ํ ํผ๋๋ฐฑ์ ๋จ๊น๋๋ค.
+ - ํ์ ์ ์์ /๋ณด์์ ์งํํ ๋ค ๋ค์ ํ์ธ์ ๋ฐ์ต๋๋ค.
+ - ์ต์ข
์ ์ผ๋ก ํ์ธ๋๋ฉด approve ํ ๋จธ์งํฉ๋๋ค.
+
+
+
+## ๐ข ์๋ด์ฌํญ
+- PR ๊ธฐ์ค์ผ๋ก ์ํฌ๋ถ ์ํ ์ฌ๋ถ๋ฅผ ์ฒดํฌํฉ๋๋ค.
+ โ ๋ชจ๋ ๋ฏธ์
์ ์๋ฃํ์ง ๋ชปํ๋๋ผ๋ ์งํํ ๋งํผ ์ ์ถ ๋ถํ๋๋ฆฝ๋๋ค!
+- ๋ฆฌ๋ทฐ๋ ๋จ์ํ ํ๊ฐ๊ฐ ์๋๋ผ ํจ๊ป ๋ฐฐ์ฐ๋ ๊ณผ์ ์ด๋, ํผ๋๋ฐฑ์ ์ฐธ๊ณ ํด ๋ถ์กฑํ๋ ๋ถ๋ถ์ ์ฑ์๊ฐ๋ฉด ๋ฉ๋๋ค.
+- ๋ชจ๋ฅด๋ ๋ถ๋ถ์ด ์์ผ๋ฉด ์ธ์ ๋ ์ด์์ง์๊ฒ ์ง๋ฌธํด ์ฃผ์ธ์!
diff --git a/build.gradle b/build.gradle
index d36aabf..8ac929f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -46,6 +46,16 @@ 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') {
diff --git a/src/main/java/com/example/umc_9th_springboot/domain/user/controller/UserController.java b/src/main/java/com/example/umc_9th_springboot/domain/user/controller/UserController.java
new file mode 100644
index 0000000..5294b58
--- /dev/null
+++ b/src/main/java/com/example/umc_9th_springboot/domain/user/controller/UserController.java
@@ -0,0 +1,61 @@
+package com.example.umc_9th_springboot.domain.user.controller;
+
+import com.example.umc_9th_springboot.domain.user.dto.req.UserReqDTO;
+import com.example.umc_9th_springboot.domain.user.dto.res.UserResDTO;
+import com.example.umc_9th_springboot.domain.user.service.UserCommandService;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpSession;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/users")
+@RequiredArgsConstructor
+public class UserController {
+
+ private final UserCommandService userCommandService;
+
+ //ํ์๊ฐ์
์ธ์
API
+ @PostMapping("/sign-up/session")
+ public UserResDTO.SignUpDTO signUp(@RequestBody UserReqDTO.SignUpDTO request) {
+ return userCommandService.signup(request);
+ }
+
+ // ๋ก๊ทธ์ธ ์ธ์
API
+ @PostMapping("/login/session")
+ public UserResDTO.LoginSessionDTO loginSession(
+ @RequestBody UserReqDTO.LoginSessionDTO request,
+ HttpServletRequest httpRequest
+ ) {
+ UserResDTO.LoginSessionDTO response = userCommandService.login(request);
+
+ // ์ธ์
์์ฑ ๋ฐ ์ฌ์ฉ์ ์ ๋ณด ์ ์ฅ
+ HttpSession session = httpRequest.getSession(true);
+ session.setAttribute("loginUserId", response.getUserId());
+ session.setAttribute("loginUserEmail", response.getEmail());
+
+ return response;
+ }
+ // ๋ก๊ทธ์์ ์ธ์
๋ฐฉ์
+ @PostMapping("/logout/session")
+ public void logoutSession(HttpServletRequest httpRequest) {
+ HttpSession session = httpRequest.getSession(false);
+ if (session != null) {
+ session.invalidate();
+ }
+ }
+
+ // ๋ก๊ทธ์ธ JWT ๋ฐฉ์ API
+ @PostMapping("/login/jwt")
+ public UserResDTO.LoginJwtDTO loginJwt(
+ @RequestBody UserReqDTO.LoginJwtDTO request
+ ) {
+ return userCommandService.loginJwt(request);
+ }
+
+ // jwt ํ์๊ฐ์
API
+ @PostMapping("/sign-up/jwt")
+ public UserResDTO.SignUpJwtDTO signUpJwt(@RequestBody UserReqDTO.SignUpDTO request) {
+ return userCommandService.signupJwt(request);
+ }
+}
diff --git a/src/main/java/com/example/umc_9th_springboot/domain/user/converter/UserConverter.java b/src/main/java/com/example/umc_9th_springboot/domain/user/converter/UserConverter.java
new file mode 100644
index 0000000..2764cc9
--- /dev/null
+++ b/src/main/java/com/example/umc_9th_springboot/domain/user/converter/UserConverter.java
@@ -0,0 +1,26 @@
+package com.example.umc_9th_springboot.domain.user.converter;
+
+import com.example.umc_9th_springboot.domain.user.dto.req.UserReqDTO;
+import com.example.umc_9th_springboot.domain.user.entity.User;
+import com.example.umc_9th_springboot.global.auth.enums.Role;
+
+import java.time.LocalDate;
+
+public class UserConverter {
+ public static User toUser(
+ UserReqDTO.SignUpDTO request,
+ String encodedPassword,
+ Role role
+ ) {
+ return User.builder()
+ .email(request.getEmail())
+ .password(encodedPassword)
+ .name(request.getName())
+ .gender(request.getGender())
+ .birth(LocalDate.parse(request.getBirth()))
+ .address(request.getAddress())
+ .role(role)
+ .point(0)
+ .build();
+ }
+}
diff --git a/src/main/java/com/example/umc_9th_springboot/domain/user/dto/req/UserReqDTO.java b/src/main/java/com/example/umc_9th_springboot/domain/user/dto/req/UserReqDTO.java
new file mode 100644
index 0000000..05af230
--- /dev/null
+++ b/src/main/java/com/example/umc_9th_springboot/domain/user/dto/req/UserReqDTO.java
@@ -0,0 +1,43 @@
+package com.example.umc_9th_springboot.domain.user.dto.req;
+
+import com.example.umc_9th_springboot.domain.user.enums.Gender;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+
+public class UserReqDTO {
+
+ //ํ์๊ฐ์
์ธ์
๋ฐฉ์ ์์ฒญ DTO
+ @Getter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Builder
+ public static class SignUpDTO {
+ private String email;
+ private String password;
+ private String name;
+ private Gender gender;
+ private String birth;
+ private String address;
+ }
+
+ //๋ก๊ทธ์ธ ์ธ์
๋ฐฉ์ ์์ฒญ DTO
+ @Getter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Builder
+ public static class LoginSessionDTO {
+ private String email;
+ private String password;
+ }
+
+ // ๋ก๊ทธ์ธ jwt ๋ฐฉ์ ์์ฒญ DTO
+ public record LoginJwtDTO(
+ @NotBlank
+ String email,
+ @NotBlank
+ String password
+ ) {}
+}
diff --git a/src/main/java/com/example/umc_9th_springboot/domain/user/dto/res/UserResDTO.java b/src/main/java/com/example/umc_9th_springboot/domain/user/dto/res/UserResDTO.java
new file mode 100644
index 0000000..6a9fe23
--- /dev/null
+++ b/src/main/java/com/example/umc_9th_springboot/domain/user/dto/res/UserResDTO.java
@@ -0,0 +1,53 @@
+package com.example.umc_9th_springboot.domain.user.dto.res;
+import com.example.umc_9th_springboot.global.auth.enums.Role;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+public class UserResDTO {
+
+ //ํ์๊ฐ์
์ธ์
๋ฐฉ์ ์๋ต DTO
+ @Getter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Builder
+ public static class SignUpDTO {
+ private Long userId;
+ private String email;
+ private String name;
+ private Role role;
+ }
+
+ //๋ก๊ทธ์ธ ์ธ์
๋ฐฉ์ ์๋ต DTO
+ @Getter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Builder
+ public static class LoginSessionDTO {
+ private Long userId;
+ private String email;
+ private String name;
+ private Role role;
+ }
+
+ // ๋ก๊ทธ์ธ jwt ๋ฐฉ์ ์๋ต DTO
+ @Builder
+ public record LoginJwtDTO(
+ Long userId,
+ String accessToken
+ ) {}
+
+ // jwt ํ์๊ฐ์
์๋ต DTO
+ @Getter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Builder
+ public static class SignUpJwtDTO {
+ private Long userId;
+ private String email;
+ private String name;
+ private Role role;
+ private String accessToken;
+ }
+}
diff --git a/src/main/java/com/example/umc_9th_springboot/domain/user/entity/User.java b/src/main/java/com/example/umc_9th_springboot/domain/user/entity/User.java
index 02d25fc..b113a27 100644
--- a/src/main/java/com/example/umc_9th_springboot/domain/user/entity/User.java
+++ b/src/main/java/com/example/umc_9th_springboot/domain/user/entity/User.java
@@ -7,6 +7,7 @@
import com.example.umc_9th_springboot.domain.shop.entity.UserRegion;
import com.example.umc_9th_springboot.domain.review.entity.Review;
import com.example.umc_9th_springboot.domain.review.entity.ReviewComment;
+import com.example.umc_9th_springboot.global.auth.enums.Role;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@@ -31,6 +32,15 @@ public class User extends BaseEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
+ @Column(nullable = false, unique = true)
+ private String email;
+
+ @Column(nullable = false)
+ private String password;
+
+ @Enumerated(EnumType.STRING)
+ private Role role;
+
@Column(nullable = false, length = 20)
private String name;
diff --git a/src/main/java/com/example/umc_9th_springboot/domain/user/repository/UserRepository.java b/src/main/java/com/example/umc_9th_springboot/domain/user/repository/UserRepository.java
index 6a26a5d..6414849 100644
--- a/src/main/java/com/example/umc_9th_springboot/domain/user/repository/UserRepository.java
+++ b/src/main/java/com/example/umc_9th_springboot/domain/user/repository/UserRepository.java
@@ -9,6 +9,9 @@
public interface UserRepository extends JpaRepository {
+ // ์ด๋ฉ์ผ๋ก ์กฐํ
+ Optional findByEmail(String email);
+
//๋ง์ดํ์ด์ง ํ์ ์ ๋ณด ์กฐํ
Optional findById(Long id);
}
diff --git a/src/main/java/com/example/umc_9th_springboot/domain/user/service/UserCommandService.java b/src/main/java/com/example/umc_9th_springboot/domain/user/service/UserCommandService.java
new file mode 100644
index 0000000..296bce9
--- /dev/null
+++ b/src/main/java/com/example/umc_9th_springboot/domain/user/service/UserCommandService.java
@@ -0,0 +1,18 @@
+package com.example.umc_9th_springboot.domain.user.service;
+
+import com.example.umc_9th_springboot.domain.user.dto.req.UserReqDTO;
+import com.example.umc_9th_springboot.domain.user.dto.res.UserResDTO;
+
+public interface UserCommandService {
+
+ // Session ํ์๊ฐ์
+ UserResDTO.SignUpDTO signup(UserReqDTO.SignUpDTO dto);
+ // Session ๋ก๊ทธ์ธ
+ UserResDTO.LoginSessionDTO login(UserReqDTO.LoginSessionDTO dto);
+
+ // jwt ๋ก๊ทธ์ธ
+ UserResDTO.LoginJwtDTO loginJwt(UserReqDTO.LoginJwtDTO dto);
+
+ // jwt ํ์๊ฐ์
+ UserResDTO.SignUpJwtDTO signupJwt(UserReqDTO.SignUpDTO dto);
+}
diff --git a/src/main/java/com/example/umc_9th_springboot/domain/user/service/impl/UserCommandServiceImpl.java b/src/main/java/com/example/umc_9th_springboot/domain/user/service/impl/UserCommandServiceImpl.java
new file mode 100644
index 0000000..11cda96
--- /dev/null
+++ b/src/main/java/com/example/umc_9th_springboot/domain/user/service/impl/UserCommandServiceImpl.java
@@ -0,0 +1,115 @@
+package com.example.umc_9th_springboot.domain.user.service.impl;
+
+import com.example.umc_9th_springboot.domain.user.converter.UserConverter;
+import com.example.umc_9th_springboot.domain.user.dto.req.UserReqDTO;
+import com.example.umc_9th_springboot.domain.user.dto.res.UserResDTO;
+import com.example.umc_9th_springboot.domain.user.entity.User;
+import com.example.umc_9th_springboot.domain.user.repository.UserRepository;
+import com.example.umc_9th_springboot.domain.user.service.UserCommandService;
+import com.example.umc_9th_springboot.global.auth.enums.Role;
+import com.example.umc_9th_springboot.global.auth.util.JwtUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import java.util.NoSuchElementException;
+
+
+@Service
+@RequiredArgsConstructor
+public class UserCommandServiceImpl implements UserCommandService {
+
+ private final UserRepository userRepository;
+
+ private final PasswordEncoder passwordEncoder;
+
+ private final JwtUtil jwtUtil;
+
+ //ํ์๊ฐ์
+ @Override
+ public UserResDTO.SignUpDTO signup(UserReqDTO.SignUpDTO dto) {
+
+ String encodedPassword = passwordEncoder.encode(dto.getPassword());
+
+ User user = UserConverter.toUser(dto, encodedPassword, Role.USER);
+
+ User savedUser = userRepository.save(user);
+
+ return UserResDTO.SignUpDTO.builder()
+ .userId(savedUser.getId())
+ .email(savedUser.getEmail())
+ .name(savedUser.getName())
+ .role(savedUser.getRole())
+ .build();
+ }
+
+ // Session ๋ก๊ทธ์ธ
+ @Override
+ public UserResDTO.LoginSessionDTO login(UserReqDTO.LoginSessionDTO dto) {
+
+ User user = userRepository.findByEmail(dto.getEmail())
+ .orElseThrow(() -> new NoSuchElementException("์กด์ฌํ์ง ์๋ ํ์์
๋๋ค."));
+
+ if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) {
+ throw new IllegalArgumentException("๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.");
+ }
+
+ return UserResDTO.LoginSessionDTO.builder()
+ .userId(user.getId())
+ .email(user.getEmail())
+ .name(user.getName())
+ .role(user.getRole())
+ .build();
+ }
+
+ // jwt ๋ก๊ทธ์ธ
+ @Override
+ public UserResDTO.LoginJwtDTO loginJwt(UserReqDTO.LoginJwtDTO dto) {
+
+ UserResDTO.LoginSessionDTO loginResult = login(
+ new UserReqDTO.LoginSessionDTO(
+ dto.email(),
+ dto.password()
+ )
+ );
+
+ String accessToken = jwtUtil.createAccessToken(
+ loginResult.getUserId(),
+ loginResult.getEmail(),
+ loginResult.getRole().name()
+ );
+
+ return UserResDTO.LoginJwtDTO.builder()
+ .userId(loginResult.getUserId())
+ .accessToken(accessToken)
+ .build();
+ }
+
+ // jwt ํ์๊ฐ์
+ @Override
+ public UserResDTO.SignUpJwtDTO signupJwt(UserReqDTO.SignUpDTO dto) {
+
+ String encodedPassword = passwordEncoder.encode(dto.getPassword());
+
+ User user = UserConverter.toUser(dto, encodedPassword, Role.USER);
+
+ User savedUser = userRepository.save(user);
+
+
+ String accessToken = jwtUtil.createAccessToken(
+ savedUser.getId(),
+ savedUser.getEmail(),
+ savedUser.getRole().name()
+ );
+
+
+ return UserResDTO.SignUpJwtDTO.builder()
+ .userId(savedUser.getId())
+ .email(savedUser.getEmail())
+ .name(savedUser.getName())
+ .role(savedUser.getRole())
+ .accessToken(accessToken)
+ .build();
+ }
+
+}
diff --git a/src/main/java/com/example/umc_9th_springboot/global/auth/enums/Role.java b/src/main/java/com/example/umc_9th_springboot/global/auth/enums/Role.java
new file mode 100644
index 0000000..4ab46fb
--- /dev/null
+++ b/src/main/java/com/example/umc_9th_springboot/global/auth/enums/Role.java
@@ -0,0 +1,5 @@
+package com.example.umc_9th_springboot.global.auth.enums;
+
+public enum Role {
+ ROLE_ADMIN, USER, ROLE_USER
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/umc_9th_springboot/global/auth/service/CustomUserDetailsService.java b/src/main/java/com/example/umc_9th_springboot/global/auth/service/CustomUserDetailsService.java
new file mode 100644
index 0000000..cef0057
--- /dev/null
+++ b/src/main/java/com/example/umc_9th_springboot/global/auth/service/CustomUserDetailsService.java
@@ -0,0 +1,29 @@
+package com.example.umc_9th_springboot.global.auth.service;
+
+import com.example.umc_9th_springboot.domain.user.entity.User;
+import com.example.umc_9th_springboot.domain.user.repository.UserRepository;
+import com.example.umc_9th_springboot.global.auth.util.CustomUserDetails;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class CustomUserDetailsService implements UserDetailsService {
+
+ private final UserRepository userRepository;
+
+ @Override
+ public UserDetails loadUserByUsername(String email)
+ throws UsernameNotFoundException {
+
+ User user = userRepository.findByEmail(email)
+ .orElseThrow(() ->
+ new UsernameNotFoundException("ํด๋น ์ด๋ฉ์ผ์ ๊ฐ์ง ์ ์ ๊ฐ ์์ต๋๋ค: " + email)
+ );
+
+ return new CustomUserDetails(user);
+ }
+}
diff --git a/src/main/java/com/example/umc_9th_springboot/global/auth/util/CustomUserDetails.java b/src/main/java/com/example/umc_9th_springboot/global/auth/util/CustomUserDetails.java
new file mode 100644
index 0000000..ea60e40
--- /dev/null
+++ b/src/main/java/com/example/umc_9th_springboot/global/auth/util/CustomUserDetails.java
@@ -0,0 +1,43 @@
+package com.example.umc_9th_springboot.global.auth.util;
+
+import com.example.umc_9th_springboot.domain.user.entity.User;
+import lombok.Getter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.List;
+
+@Getter
+public class CustomUserDetails implements UserDetails {
+
+ private final User user;
+
+ public CustomUserDetails(User user){
+ this.user = user;
+ }
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name()));
+ }
+
+ @Override
+ public String getPassword() { return user.getPassword(); }
+
+ @Override
+ public String getUsername() { return user.getEmail(); }
+
+ @Override
+ public boolean isAccountNonExpired() { return true; }
+
+ @Override
+ public boolean isAccountNonLocked() { return true; }
+
+ @Override
+ public boolean isCredentialsNonExpired() { return true; }
+
+ @Override
+ public boolean isEnabled() { return true; }
+}
diff --git a/src/main/java/com/example/umc_9th_springboot/global/auth/util/JwtAuthFilter.java b/src/main/java/com/example/umc_9th_springboot/global/auth/util/JwtAuthFilter.java
new file mode 100644
index 0000000..e769e6d
--- /dev/null
+++ b/src/main/java/com/example/umc_9th_springboot/global/auth/util/JwtAuthFilter.java
@@ -0,0 +1,80 @@
+package com.example.umc_9th_springboot.global.auth.util;
+
+import com.example.umc_9th_springboot.global.apiPayload.ApiResponse;
+import com.example.umc_9th_springboot.global.apiPayload.code.GeneralErrorCode;
+import com.example.umc_9th_springboot.global.auth.service.CustomUserDetailsService;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+public class JwtAuthFilter extends OncePerRequestFilter {
+
+ private final JwtUtil jwtUtil;
+ private final CustomUserDetailsService customUserDetailsService;
+
+ @Override
+ protected void doFilterInternal(
+ @NonNull HttpServletRequest request,
+ @NonNull HttpServletResponse response,
+ @NonNull FilterChain filterChain
+ ) throws ServletException, IOException {
+
+ try {
+ // ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
+ String token = request.getHeader("Authorization");
+
+ // token์ด ์๊ฑฐ๋ Bearer๊ฐ ์๋๋ฉด ๋๊ธฐ๊ธฐ
+ if (token == null || !token.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ // Bearer์ด๋ฉด ์ถ์ถ
+ token = token.replace("Bearer ", "");
+
+ // AccessToken ๊ฒ์ฆํ๊ธฐ
+ if (jwtUtil.isValid(token)) {
+ // ํ ํฐ์์ ์ด๋ฉ์ผ ์ถ์ถ
+ String email = jwtUtil.getEmail(token);
+
+ // ์ธ์ฆ ๊ฐ์ฒด ์์ฑ
+ UserDetails user = customUserDetailsService.loadUserByUsername(email);
+ Authentication auth = new UsernamePasswordAuthenticationToken(
+ user,
+ null,
+ user.getAuthorities()
+ );
+
+ // SecurityContext์ ์ธ์ฆ ์ ์ฅ
+ SecurityContextHolder.getContext().setAuthentication(auth);
+ }
+
+ filterChain.doFilter(request, response);
+ } catch (Exception e) {
+ response.setContentType("application/json;charset=UTF-8");
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+
+ ApiResponse errorResponse = ApiResponse.onFailure(
+ GeneralErrorCode.UNAUTHORIZED,
+ null
+ );
+
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.writeValue(response.getOutputStream(), errorResponse);
+ }
+ }
+}
diff --git a/src/main/java/com/example/umc_9th_springboot/global/auth/util/JwtUtil.java b/src/main/java/com/example/umc_9th_springboot/global/auth/util/JwtUtil.java
new file mode 100644
index 0000000..82c46a4
--- /dev/null
+++ b/src/main/java/com/example/umc_9th_springboot/global/auth/util/JwtUtil.java
@@ -0,0 +1,82 @@
+package com.example.umc_9th_springboot.global.auth.util;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+
+@Component
+public class JwtUtil {
+
+ private SecretKey secretKey;
+ private final String secret;
+ private final Duration accessExpiration;
+
+ // application.yml ์ jwt.token.* ๊ฐ ์ฃผ์
+ public JwtUtil(
+ @Value("${jwt.token.secretKey}") String secret,
+ @Value("${jwt.token.expiration.access}") Long accessExpiration
+ ) {
+ this.secret = secret;
+ this.accessExpiration = Duration.ofMillis(accessExpiration);
+ }
+
+ @PostConstruct
+ public void init() {
+ this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
+ }
+
+ // AccessToken ์์ฑ
+ public String createAccessToken(Long userId, String email, String role) {
+ return createToken(userId, email, role, accessExpiration);
+ }
+
+ public String getEmail(String token) {
+ try {
+ return getClaims(token).getPayload().get("email", String.class);
+ } catch (JwtException e) {
+ return null;
+ }
+ }
+
+ public boolean isValid(String token) {
+ try {
+ getClaims(token);
+ return true;
+ } catch (JwtException e) {
+ return false;
+ }
+ }
+
+
+ private String createToken(Long userId, String email, String role, Duration expiration) {
+ Instant now = Instant.now();
+
+ return Jwts.builder()
+ .subject(String.valueOf(userId))
+ .claim("email", email)
+ .claim("role", role)
+ .issuedAt(Date.from(now))
+ .expiration(Date.from(now.plus(expiration)))
+ .signWith(secretKey)
+ .compact();
+ }
+
+ private Jws getClaims(String token) throws JwtException {
+ return Jwts.parser()
+ .verifyWith(secretKey)
+ .clockSkewSeconds(60)
+ .build()
+ .parseSignedClaims(token);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/umc_9th_springboot/global/config/SecurityConfig.java b/src/main/java/com/example/umc_9th_springboot/global/config/SecurityConfig.java
new file mode 100644
index 0000000..b01194c
--- /dev/null
+++ b/src/main/java/com/example/umc_9th_springboot/global/config/SecurityConfig.java
@@ -0,0 +1,68 @@
+package com.example.umc_9th_springboot.global.config;
+
+import com.example.umc_9th_springboot.global.auth.util.JwtAuthFilter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+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;
+
+
+@EnableWebSecurity
+@Configuration
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+ private final JwtAuthFilter jwtAuthFilter;
+
+ private final String[] allowUris = {
+ "/swagger-ui/**",
+ "/swagger-resources/**",
+ "/v3/api-docs/**",
+ "/webjars/**",
+ "/api/users/sign-up/session",
+ "/api/users/login/session",
+ "/api/users/logout/session",
+ "/api/users/login/jwt",
+ "/api/users/sign-up/jwt"
+ };
+
+ // PasswordEncoder ๋น ๋ฑ๋ก
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ // Security ํํฐ ์ฒด์ธ ์ค์
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+
+ .authorizeHttpRequests(requests -> requests
+ .requestMatchers(allowUris).permitAll()
+ .requestMatchers("/admin/**").hasRole("ADMIN")
+ .anyRequest().authenticated()
+ )
+
+ .formLogin(form -> form
+ .defaultSuccessUrl("/swagger-ui/index.html", true)
+ .permitAll()
+ )
+
+ .logout(logout -> logout
+ .logoutUrl("/logout")
+ .logoutSuccessUrl("/login?logout")
+ .permitAll()
+ )
+
+ .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
+
+ return http.build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 4df69a7..25a5c57 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -23,3 +23,9 @@ spring:
properties:
hibernate:
format_sql: true
+
+jwt:
+ token:
+ secretKey: ${JWT_SECRET_KEY}
+ expiration:
+ access: ${JWT_ACCESS_EXPIRE}