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 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}