diff --git a/README.md b/README.md new file mode 100644 index 0000000..15ac609 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Spring-JPA-study +D-Coding 백엔드 스터디 + +## 1주차 과제 제출(23-01-21) +스프링 입문 강의 정리 + +## 2~4주차 과제 +- 간단한 ‘당근마켓’ 벡엔드 구현해보기 +### 요구 사항 + +- 회원가입/로그인 기능 +- 상품등록 기능 +- 상품페이지 기능 +- 마이 페이지(나의 당근 페이지) 기능 + +### 제한 사항 + +- Spring 또는 Spring Boot Framework를 사용해야합니다. +- JPA를 사용해야합니다. (Spring data jpa 활용 가능) +- 인증/인가 방식은 JWT & Spring Security 를 활용해야합니다. + +### 2주차 + +- 요구사항대로 당근마켓 ERD 그려보기 +- 요구사항 API 구현 (가능한 만큼) +- jwt, security 이용하여 로그인, 회원가입 기능 구현 + + +![dcoding-erd](https://user-images.githubusercontent.com/85729858/215239770-a2ac0b3a-cd5b-443d-b7a3-d026ce9ae5c5.png) + + +### 3주차 + +- 피드백 반영하여 카테고리를 enum으로 처리하도록 하고, createdDate와 updatedDate추가 +- 로그인 기능 리펙토링 +- 회원가입 기능 추가 +- 상품 관련 엔티티, 레포지토리, 서비스 추가 +- 상태, 카테고리 enum추가 + +![image](https://user-images.githubusercontent.com/85729858/216071539-42dc3af6-452e-464f-9a68-c52364c5fc3f.png) diff --git a/build.gradle b/build.gradle index 15b77ef..ee53417 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.0.1' + id 'org.springframework.boot' version '2.7.8' id 'io.spring.dependency-management' version '1.1.0' } group = 'com.dku' version = '0.0.1-SNAPSHOT' -sourceCompatibility = '17' +sourceCompatibility = '11' configurations { compileOnly { @@ -27,6 +27,15 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + + runtimeOnly 'com.h2database:h2' + //implementation 'com.auth0:java-jwt:3.18.2' + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + implementation 'org.springframework.boot:spring-boot-starter-validation' } tasks.named('test') { diff --git a/src/main/java/com/dku/springstudy/SpringStudyApplication.java b/src/main/java/com/dku/springstudy/SpringStudyApplication.java index ef164c9..1bef475 100644 --- a/src/main/java/com/dku/springstudy/SpringStudyApplication.java +++ b/src/main/java/com/dku/springstudy/SpringStudyApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class SpringStudyApplication { diff --git a/src/main/java/com/dku/springstudy/config/SecurityConfig.java b/src/main/java/com/dku/springstudy/config/SecurityConfig.java new file mode 100644 index 0000000..65e9db0 --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/SecurityConfig.java @@ -0,0 +1,46 @@ +package com.dku.springstudy.config; + +import com.dku.springstudy.jwt.JwtAuthenticationFilter; +import com.dku.springstudy.jwt.JwtTokenProvider; +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.http.SessionCreationPolicy; + +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + private final JwtTokenProvider jwtTokenProvider; + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .httpBasic().disable() + .formLogin().disable() + .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/api/v1/**").permitAll() + //.antMatchers("/api/v1/").hasRole("USER") + //.anyRequest().authenticated() + .anyRequest().permitAll() + .and() + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } +} diff --git a/src/main/java/com/dku/springstudy/domain/BaseTimeEntity.java b/src/main/java/com/dku/springstudy/domain/BaseTimeEntity.java new file mode 100644 index 0000000..673b74c --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/BaseTimeEntity.java @@ -0,0 +1,23 @@ +package com.dku.springstudy.domain; + +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@EntityListeners(AuditingEntityListener.class) +@MappedSuperclass +@Getter +public class BaseTimeEntity { + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdDate; + + @LastModifiedDate + private LocalDateTime lastModifiedDate; +} diff --git a/src/main/java/com/dku/springstudy/domain/like/like.java b/src/main/java/com/dku/springstudy/domain/like/like.java new file mode 100644 index 0000000..47f557b --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/like/like.java @@ -0,0 +1,4 @@ +package com.dku.springstudy.domain.like; + +public class like { +} diff --git a/src/main/java/com/dku/springstudy/domain/product/Category.java b/src/main/java/com/dku/springstudy/domain/product/Category.java new file mode 100644 index 0000000..7d1abcd --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/product/Category.java @@ -0,0 +1,36 @@ +package com.dku.springstudy.domain.product; + +import lombok.Getter; + +@Getter +public enum Category { + /* + ‘디지털기기’, ‘생활가전’, ‘가구/인테리어’, ‘유아동’, ‘생활/가공식품’, + ‘유아도서’, ‘스포츠/레저’, ‘여성잡화’, ‘여성의류’, ‘남성패션/잡화’, + ‘게임/취미’, ‘뷰티/미용’, ‘반려동물용품’, ‘도서/티켓/음반’, ‘식물’, + ‘기타 중고물품’, ‘중고차’ + */ + DIGITAL_DEVICE("디지털기기"), + HOME_ELECTRONIC("생활가전"), + FURNITURE_INTERIOR("가구/인테리어"), + CHILD("유아동"), + LIVING_PROCESSED_FOODS("생활/가공식품"), + CHILD_BOOK("유아도서"), + SPORTS_LEISURE("스포츠/레저"), + WOMAN_GOODS("여성잡화"), + WOMAN_CLOTHES("여성의류"), + MAN_FASHION_GOODS("남성패션/잡화"), + GAME_HOBBY("게임/취미"), + BEAUTY("뷰티/미용"), + PET_GOODS("반려동물용품"), + BOOK_TICKET_RECORD("도서/티켓/음반"), + PLANT("식물"), + USED_ETC("기타 중고물품"), + USED_CAR("중고차"); + + private final String korCategory; + + Category(String korCategory) { + this.korCategory = korCategory; + } +} diff --git a/src/main/java/com/dku/springstudy/domain/product/Product.java b/src/main/java/com/dku/springstudy/domain/product/Product.java new file mode 100644 index 0000000..fd9edaa --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/product/Product.java @@ -0,0 +1,35 @@ +package com.dku.springstudy.domain.product; + +import com.dku.springstudy.domain.BaseTimeEntity; +import com.dku.springstudy.domain.user.User; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Product extends BaseTimeEntity { + @Id + @GeneratedValue + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + private String productName; + private String productImgUrl; + private Integer cost; + private String contents; + + @Enumerated(EnumType.STRING) + private ProductStatus status; + + @Enumerated(EnumType.STRING) + private Category category; + + +} diff --git a/src/main/java/com/dku/springstudy/domain/product/ProductStatus.java b/src/main/java/com/dku/springstudy/domain/product/ProductStatus.java new file mode 100644 index 0000000..81dd814 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/product/ProductStatus.java @@ -0,0 +1,15 @@ +package com.dku.springstudy.domain.product; + +import lombok.Getter; + +@Getter +public enum ProductStatus { + ON_SALE("판매중"), SOLD_OUT("판매완료"), RESERVED("예약중"); + + private final String korStatus; + + ProductStatus(String korStatus) { + this.korStatus = korStatus; + } + +} diff --git a/src/main/java/com/dku/springstudy/domain/product/repository/ProductRepository.java b/src/main/java/com/dku/springstudy/domain/product/repository/ProductRepository.java new file mode 100644 index 0000000..fda69c6 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/product/repository/ProductRepository.java @@ -0,0 +1,7 @@ +package com.dku.springstudy.domain.product.repository; + +import com.dku.springstudy.domain.product.Product; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProductRepository extends JpaRepository { +} diff --git a/src/main/java/com/dku/springstudy/domain/product/service/ProductService.java b/src/main/java/com/dku/springstudy/domain/product/service/ProductService.java new file mode 100644 index 0000000..decbdb7 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/product/service/ProductService.java @@ -0,0 +1,13 @@ +package com.dku.springstudy.domain.product.service; + +import com.dku.springstudy.domain.product.repository.ProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ProductService { + private final ProductRepository productRepository; +} diff --git a/src/main/java/com/dku/springstudy/domain/user/Role.java b/src/main/java/com/dku/springstudy/domain/user/Role.java new file mode 100644 index 0000000..a7c98a7 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/Role.java @@ -0,0 +1,5 @@ +package com.dku.springstudy.domain.user; + +public enum Role { + USER, ADMIN +} diff --git a/src/main/java/com/dku/springstudy/domain/user/User.java b/src/main/java/com/dku/springstudy/domain/user/User.java new file mode 100644 index 0000000..0869383 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/User.java @@ -0,0 +1,43 @@ +package com.dku.springstudy.domain.user; + +import com.dku.springstudy.domain.BaseTimeEntity; +import com.dku.springstudy.domain.product.Product; +import lombok.*; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class User extends BaseTimeEntity { + + @Id + @GeneratedValue + private Long id; + + @OneToMany(mappedBy = "user") + private List products = new ArrayList<>(); + + private String email; + private String password; + private String username; + private String phoneNumber; + private String nickname; + private String profileImgUrl; + + @Enumerated(EnumType.STRING) + private Role role; + + @Builder + public User(String email, String password, String username, String phoneNumber, String nickname, String profileImgUrl) { + this.email = email; + this.password = password; + this.username = username; + this.phoneNumber = phoneNumber; + this.nickname = nickname; + this.profileImgUrl = profileImgUrl; + this.role = Role.USER; + } +} diff --git a/src/main/java/com/dku/springstudy/domain/user/controller/UserController.java b/src/main/java/com/dku/springstudy/domain/user/controller/UserController.java new file mode 100644 index 0000000..538898a --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/controller/UserController.java @@ -0,0 +1,37 @@ +package com.dku.springstudy.domain.user.controller; + + +import com.dku.springstudy.domain.user.dto.LoginRequestDto; +import com.dku.springstudy.domain.user.dto.SignUpRequestDTO; +import com.dku.springstudy.domain.user.service.UserService; +import com.dku.springstudy.jwt.TokenDto; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1") +public class UserController { + private final UserService userService; + + @PostMapping("/signup") + public ResponseEntity signUp(@Valid @RequestBody SignUpRequestDTO requestDTO){ + userService.signUp(requestDTO); + + return ResponseEntity.status(HttpStatus.CREATED).body(null); + } + + @PostMapping("/login") + public TokenDto login(@Valid @RequestBody LoginRequestDto loginRequestDto) { + String email = loginRequestDto.getEmail(); + String password = loginRequestDto.getPassword(); + return userService.login(email, password); + } +} diff --git a/src/main/java/com/dku/springstudy/domain/user/dto/LoginRequestDto.java b/src/main/java/com/dku/springstudy/domain/user/dto/LoginRequestDto.java new file mode 100644 index 0000000..2a5b8ec --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/dto/LoginRequestDto.java @@ -0,0 +1,9 @@ +package com.dku.springstudy.domain.user.dto; + +import lombok.Data; + +@Data +public class LoginRequestDto { + private String email; + private String password; +} diff --git a/src/main/java/com/dku/springstudy/domain/user/dto/SignUpRequestDTO.java b/src/main/java/com/dku/springstudy/domain/user/dto/SignUpRequestDTO.java new file mode 100644 index 0000000..78c44ba --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/dto/SignUpRequestDTO.java @@ -0,0 +1,31 @@ +package com.dku.springstudy.domain.user.dto; + +import com.dku.springstudy.domain.user.User; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Data +@NoArgsConstructor +public class SignUpRequestDTO { + @NonNull + private String email; + @NonNull + private String password; + @NonNull + private String username; + @NonNull + private String phoneNumber; + @NonNull + private String nickname; + + public User toEntity() { + return User.builder() + .email(email) + .password(password) + .username(username) + .phoneNumber(phoneNumber) + .nickname(nickname) + .build(); + } +} diff --git a/src/main/java/com/dku/springstudy/domain/user/repository/UserRepository.java b/src/main/java/com/dku/springstudy/domain/user/repository/UserRepository.java new file mode 100644 index 0000000..304784f --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/repository/UserRepository.java @@ -0,0 +1,10 @@ +package com.dku.springstudy.domain.user.repository; + +import com.dku.springstudy.domain.user.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/dku/springstudy/domain/user/service/CustomUserDetailsService.java b/src/main/java/com/dku/springstudy/domain/user/service/CustomUserDetailsService.java new file mode 100644 index 0000000..6cb7d4a --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/service/CustomUserDetailsService.java @@ -0,0 +1,25 @@ +package com.dku.springstudy.domain.user.service; + +import com.dku.springstudy.jwt.SecurityUserDetails; +import com.dku.springstudy.domain.user.User; + +import com.dku.springstudy.domain.user.repository.UserRepository; +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("해당하는 유저를 찾을 수 없습니다.")); + return new SecurityUserDetails(user); + } + +} diff --git a/src/main/java/com/dku/springstudy/domain/user/service/UserService.java b/src/main/java/com/dku/springstudy/domain/user/service/UserService.java new file mode 100644 index 0000000..e356a85 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/user/service/UserService.java @@ -0,0 +1,50 @@ +package com.dku.springstudy.domain.user.service; + +import com.dku.springstudy.domain.user.User; +import com.dku.springstudy.domain.user.dto.SignUpRequestDTO; +import com.dku.springstudy.domain.user.repository.UserRepository; +import com.dku.springstudy.jwt.JwtTokenProvider; +import com.dku.springstudy.jwt.TokenDto; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.validation.Valid; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class UserService { + private final AuthenticationManagerBuilder authenticationManagerBuilder; + private final JwtTokenProvider jwtTokenProvider; + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + @Transactional + public TokenDto login(String email, String password) { + // 1. Login ID/PW 를 기반으로 Authentication 객체 생성 + // 이때 authentication 는 인증 여부를 확인하는 authenticated 값이 false + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, password); + + // 2. 실제 검증 (사용자 비밀번호 체크)이 이루어지는 부분 + // authenticate 매서드가 실행될 때 CustomUserDetailsService 에서 만든 loadUserByUsername 메서드가 실행 + Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); + + // 3. 인증 정보를 기반으로 JWT 토큰 생성 + + return jwtTokenProvider.generateToken(authentication); + } + + @Transactional + public Long signUp(@Valid SignUpRequestDTO requestDTO){ + String bcryptPassword = passwordEncoder.encode(requestDTO.getPassword()); + requestDTO.setPassword(bcryptPassword); + User user = requestDTO.toEntity(); + userRepository.save(user); + + return user.getId(); + } +} diff --git a/src/main/java/com/dku/springstudy/jwt/JwtAuthenticationFilter.java b/src/main/java/com/dku/springstudy/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..9942825 --- /dev/null +++ b/src/main/java/com/dku/springstudy/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,42 @@ +package com.dku.springstudy.jwt; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends GenericFilterBean { + private final JwtTokenProvider jwtTokenProvider; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + + // 1. Request Header 에서 JWT 토큰 추출 + String token = resolveToken((HttpServletRequest) request); + + // 2. validateToken 으로 토큰 유효성 검사 + if (token != null && jwtTokenProvider.validateToken(token)) { + // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장 + Authentication authentication = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + chain.doFilter(request, response); + } + // Request Header 에서 토큰 정보 추출 + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/src/main/java/com/dku/springstudy/jwt/JwtTokenProvider.java b/src/main/java/com/dku/springstudy/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..16f4a44 --- /dev/null +++ b/src/main/java/com/dku/springstudy/jwt/JwtTokenProvider.java @@ -0,0 +1,110 @@ +package com.dku.springstudy.jwt; + + +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +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.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.stream.Collectors; + + +@Slf4j +@Component +public class JwtTokenProvider { + + private final Key key; + + public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + public TokenDto generateToken(Authentication authentication) { + // 권한 가져오기 + String authorities = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + long now = (new Date()).getTime(); + // Access Token 생성 + Date accessTokenExpiresIn = new Date(now + 86400000); + String accessToken = Jwts.builder() + .setSubject(authentication.getName()) + .claim("auth", authorities) + .setExpiration(accessTokenExpiresIn) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + // Refresh Token 생성 + //1일: 24 * 60 * 60 * 1000 = 86400000 + String refreshToken = Jwts.builder() + .setExpiration(new Date(now + 86400000)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + return TokenDto.builder() + .grantType("Bearer") + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + // JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 메서드 + public Authentication getAuthentication(String accessToken) { + // 토큰 복호화 + Claims claims = parseClaims(accessToken); + + if (claims.get("auth") == null) { + throw new RuntimeException("권한 정보가 없는 토큰입니다."); + } + + // 클레임에서 권한 정보 가져오기 + Collection authorities = + Arrays.stream(claims.get("auth").toString().split(",")) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + // UserDetails 객체를 만들어서 Authentication 리턴 + UserDetails principal = new User(claims.getSubject(), "", authorities); + return new UsernamePasswordAuthenticationToken(principal, "", authorities); + } + + // 토큰 정보를 검증하는 메서드 + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return true; + } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { + log.info("Invalid JWT Token", e); + } catch (ExpiredJwtException e) { + log.info("Expired JWT Token", e); + } catch (UnsupportedJwtException e) { + log.info("Unsupported JWT Token", e); + } catch (IllegalArgumentException e) { + log.info("JWT claims string is empty.", e); + } + return false; + } + + private Claims parseClaims(String accessToken) { + try { + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } +} diff --git a/src/main/java/com/dku/springstudy/jwt/SecurityUserDetails.java b/src/main/java/com/dku/springstudy/jwt/SecurityUserDetails.java new file mode 100644 index 0000000..6ec4fa7 --- /dev/null +++ b/src/main/java/com/dku/springstudy/jwt/SecurityUserDetails.java @@ -0,0 +1,62 @@ +package com.dku.springstudy.jwt; + + +import com.dku.springstudy.domain.user.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.ArrayList; +import java.util.Collection; + + +public class SecurityUserDetails implements UserDetails { + private User user; + + public SecurityUserDetails(User user) { + this.user = user; + } + + public User getUser() { + return user; + } + + @Override + public Collection getAuthorities() { + Collection authorities = new ArrayList<>(); + String roles = user.getRole().toString(); + for (String role : roles.split(",")) { + authorities.add(() -> role); + } + return authorities; + } + + @Override + public String getUsername() { + return user.getEmail(); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @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/dku/springstudy/jwt/TokenDto.java b/src/main/java/com/dku/springstudy/jwt/TokenDto.java new file mode 100644 index 0000000..0737d70 --- /dev/null +++ b/src/main/java/com/dku/springstudy/jwt/TokenDto.java @@ -0,0 +1,15 @@ +package com.dku.springstudy.jwt; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +@AllArgsConstructor +public class TokenDto { + //grantType은 JWT 대한 인증 타입 + private String grantType; + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/com/dku/springstudy/testInit.java b/src/main/java/com/dku/springstudy/testInit.java new file mode 100644 index 0000000..1b44c75 --- /dev/null +++ b/src/main/java/com/dku/springstudy/testInit.java @@ -0,0 +1,56 @@ +package com.dku.springstudy; + +import com.dku.springstudy.domain.user.User; +import com.dku.springstudy.domain.user.dto.SignUpRequestDTO; +import com.dku.springstudy.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.PostConstruct; +import javax.persistence.EntityManager; + +@Component +@RequiredArgsConstructor +public class testInit { + private final InitService initService; + + @PostConstruct + public void init() { + initService.dbInit1(); + + } + + @Component + @Transactional + @RequiredArgsConstructor + static class InitService { + private final UserRepository userRepository; + + private final EntityManager em; + private final PasswordEncoder passwordEncoder; + + public void dbInit1() { + + User user = createUser("qwe", "123", "username", "01012341234", "usernick"); + em.persist(user); + + } + + private User createUser(String email, String password, String username, String phoneNumber, String nickname) { + SignUpRequestDTO requestDTO = new SignUpRequestDTO(); + String bcryptPassword = passwordEncoder.encode(password); + + requestDTO.setEmail(email); + requestDTO.setPassword(bcryptPassword); + requestDTO.setUsername(username); + requestDTO.setPhoneNumber(phoneNumber); + requestDTO.setNickname(nickname); + User user = requestDTO.toEntity(); + + return user; + } + + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..c822a97 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:h2:tcp://localhost/~/test + username: sa + password: + driver-class-name: org.h2.Driver + + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + #show_sql: true + format_sql: true +logging.level: + org.hibernate.SQL: debug + org.hibernate.type: trace + +jwt: + secret: VlwEyVBsYt9V7zq57TejMnVUyzblYcfPQye08f7MGVA9XkHa