From 85ed714791ca53fa1e87eceb3c202ea19791ddde Mon Sep 17 00:00:00 2001 From: LeeJaeHyeok97 Date: Sun, 27 Aug 2023 12:57:02 +0900 Subject: [PATCH] =?UTF-8?q?[FEAT]#13=20JWT=20access-token,=20=EC=8A=A4?= =?UTF-8?q?=ED=94=84=EB=A7=81=20=EC=8B=9C=ED=81=90=EB=A6=AC=ED=8B=B0=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 9 +++ .../com/example/rcp1/WantedApplication.java | 1 + .../domain/user/application/UserService.java | 24 +++++++ .../example/rcp1/domain/user/domain/User.java | 18 +++-- .../domain/repository/UserRepository.java | 4 ++ .../rcp1/domain/user/dto/SignInReq.java | 15 ++++ .../user/presentation/UserController.java | 27 +++++++- .../com/example/rcp1/global/SuccessCode.java | 4 +- .../global/config/security/JwtFilter.java | 69 +++++++++++++++++++ .../config/security/SecurityConfig.java | 48 +++++++++++++ .../global/config/security/util/JwtUtil.java | 32 +++++++++ 11 files changed, 239 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/example/rcp1/domain/user/dto/SignInReq.java create mode 100644 src/main/java/com/example/rcp1/global/config/security/JwtFilter.java create mode 100644 src/main/java/com/example/rcp1/global/config/security/SecurityConfig.java create mode 100644 src/main/java/com/example/rcp1/global/config/security/util/JwtUtil.java diff --git a/build.gradle b/build.gradle index f02e70c..0d6420f 100644 --- a/build.gradle +++ b/build.gradle @@ -34,9 +34,18 @@ dependencies { implementation 'io.springfox:springfox-boot-starter:3.0.0' // Security, Authentication + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.security:spring-security-test' implementation(group: 'io.jsonwebtoken', name: 'jjwt', version: '0.11.5') implementation('io.jsonwebtoken:jjwt:0.9.1') + //java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter 에러 날 때 추가해야 하는 라이브러리 + implementation('javax.xml.bind:jaxb-api:2.3.0') + + + // https://mvnrepository.com/artifact/com.auth0/java-jwt + implementation group: 'com.auth0', name: 'java-jwt', version: '4.3.0' + implementation('org.springframework.boot:spring-boot-starter') diff --git a/src/main/java/com/example/rcp1/WantedApplication.java b/src/main/java/com/example/rcp1/WantedApplication.java index 70f248d..9f5557b 100644 --- a/src/main/java/com/example/rcp1/WantedApplication.java +++ b/src/main/java/com/example/rcp1/WantedApplication.java @@ -2,6 +2,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; @SpringBootApplication public class WantedApplication { diff --git a/src/main/java/com/example/rcp1/domain/user/application/UserService.java b/src/main/java/com/example/rcp1/domain/user/application/UserService.java index 70a327b..bbae7af 100644 --- a/src/main/java/com/example/rcp1/domain/user/application/UserService.java +++ b/src/main/java/com/example/rcp1/domain/user/application/UserService.java @@ -5,11 +5,22 @@ import com.example.rcp1.domain.user.dto.SignUpReq; import com.example.rcp1.global.BaseResponse; import com.example.rcp1.global.SuccessCode; +import com.example.rcp1.global.config.security.util.JwtUtil; import lombok.RequiredArgsConstructor; import org.mindrot.jbcrypt.BCrypt; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +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; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + @Service @RequiredArgsConstructor @@ -17,6 +28,11 @@ public class UserService { private final UserRepository userRepository; + @Value("${SECRET_KEY}") + private String secret_key; + + // 1시간 + private Long expiredMs = 1000 * 60 * 60L; // 회원가입 @Transactional @@ -39,4 +55,12 @@ public User signUp(SignUpReq signUpReq) { return user; } + + + public String login(String email, String password) { + // 인증 과정 + return JwtUtil.createJwt(email, secret_key, expiredMs); + } + + } diff --git a/src/main/java/com/example/rcp1/domain/user/domain/User.java b/src/main/java/com/example/rcp1/domain/user/domain/User.java index ca51e7a..65d1c50 100644 --- a/src/main/java/com/example/rcp1/domain/user/domain/User.java +++ b/src/main/java/com/example/rcp1/domain/user/domain/User.java @@ -2,16 +2,19 @@ import com.example.rcp1.domain.common.BaseEntity; import jakarta.persistence.*; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; @Entity -@Getter -@Setter +@Data @NoArgsConstructor @Table(name = "USER") public class User extends BaseEntity { @@ -58,7 +61,6 @@ public class User extends BaseEntity { @Builder - public User(Long id, String email, String password, String name, String phoneNumber, String specializedField, Long career, String position, String school, String job, LocalDateTime created, LocalDateTime updated, String status) { this.id = id; this.email = email; @@ -74,4 +76,6 @@ public User(Long id, String email, String password, String name, String phoneNum this.updated = updated; this.status = status; } + + } diff --git a/src/main/java/com/example/rcp1/domain/user/domain/repository/UserRepository.java b/src/main/java/com/example/rcp1/domain/user/domain/repository/UserRepository.java index 64fb99a..74d8977 100644 --- a/src/main/java/com/example/rcp1/domain/user/domain/repository/UserRepository.java +++ b/src/main/java/com/example/rcp1/domain/user/domain/repository/UserRepository.java @@ -2,7 +2,11 @@ import com.example.rcp1.domain.user.domain.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Optional; public interface UserRepository extends JpaRepository { + } diff --git a/src/main/java/com/example/rcp1/domain/user/dto/SignInReq.java b/src/main/java/com/example/rcp1/domain/user/dto/SignInReq.java new file mode 100644 index 0000000..69bdd4c --- /dev/null +++ b/src/main/java/com/example/rcp1/domain/user/dto/SignInReq.java @@ -0,0 +1,15 @@ +package com.example.rcp1.domain.user.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class SignInReq { + @NotBlank + @Email + private String email; + + @NotBlank + private String password; +} diff --git a/src/main/java/com/example/rcp1/domain/user/presentation/UserController.java b/src/main/java/com/example/rcp1/domain/user/presentation/UserController.java index 5bde0a0..9459f60 100644 --- a/src/main/java/com/example/rcp1/domain/user/presentation/UserController.java +++ b/src/main/java/com/example/rcp1/domain/user/presentation/UserController.java @@ -2,16 +2,20 @@ import com.example.rcp1.domain.user.application.UserService; import com.example.rcp1.domain.user.domain.User; +import com.example.rcp1.domain.user.dto.SignInReq; import com.example.rcp1.domain.user.dto.SignUpReq; import com.example.rcp1.global.BaseResponse; import com.example.rcp1.global.ErrorCode; import com.example.rcp1.global.SuccessCode; +import io.swagger.models.Response; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; + @RestController @RequiredArgsConstructor @RequestMapping("/user") @@ -31,11 +35,28 @@ public ResponseEntity> signUp(@Valid @RequestBody SignUpReq s } } - @GetMapping("/hello") - public ResponseEntity hello() { - return ResponseEntity.ok("Hello, Postman!"); + + // 로그인 - access-token 발급 성공 + @PostMapping("/signIn") + public ResponseEntity> signIn(@Valid @RequestBody SignInReq signInReq) { + try { + String token = userService.login(signInReq.getEmail(), signInReq.getPassword()); + return ResponseEntity.ok(BaseResponse.success(SuccessCode.SIGNIN_SUCCESS, token)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(BaseResponse.error(ErrorCode.REQUEST_VALIDATION_EXCEPTION, "로그인에 실패했습니다.")); + } +// return ResponseEntity.ok(BaseResponse.success(SuccessCode.SIGNIN_SUCCESS, userService.login("이재혁", ""))); + } + + // 테스트용 인가 글쓰기 (삭제 예정) + @PostMapping("/write") + public ResponseEntity writeReview(Authentication authentication) { + return ResponseEntity.ok().body(authentication.getName() + "님의 글작성이 완료되었습니다."); } + + } diff --git a/src/main/java/com/example/rcp1/global/SuccessCode.java b/src/main/java/com/example/rcp1/global/SuccessCode.java index 7020b36..57c051b 100644 --- a/src/main/java/com/example/rcp1/global/SuccessCode.java +++ b/src/main/java/com/example/rcp1/global/SuccessCode.java @@ -13,8 +13,8 @@ public enum SuccessCode { // api 만들고 수정하기 // CUSTOM_SUCCESS(OK, "~ 조회에 성공했습니다."), // CUSTOM_CREATED_SUCCESS(CREATED, "~ 생성에 성공했습니다."); - SIGNUP_SUCCESS(OK, "회원가입에 성공했습니다."); - + SIGNUP_SUCCESS(OK, "회원가입에 성공했습니다."), + SIGNIN_SUCCESS(OK, "로그인에 성공했습니다."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/example/rcp1/global/config/security/JwtFilter.java b/src/main/java/com/example/rcp1/global/config/security/JwtFilter.java new file mode 100644 index 0000000..57436f7 --- /dev/null +++ b/src/main/java/com/example/rcp1/global/config/security/JwtFilter.java @@ -0,0 +1,69 @@ +package com.example.rcp1.global.config.security; + +import com.example.rcp1.domain.user.application.UserService; +import com.example.rcp1.global.config.security.util.JwtUtil; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; + +@RequiredArgsConstructor +@Slf4j +public class JwtFilter extends OncePerRequestFilter { + + + private final UserService userService; + @Value("${SECRET_KEY}") + private final String secretKey; + + // 필터 관문 + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + log.info("authorization : {}", authorization); + + // 토큰 안보내면 블락 + if (authorization == null || !authorization.startsWith("Bearer ")) { + log.error("authentication을 잘못 보냈습니다."); + filterChain.doFilter(request, response); + return; + } + + // 토큰 꺼내기 + String token = authorization.split(" ")[1]; + + // 토큰 만료 여부 확인 + if (JwtUtil.isExpired(token, secretKey)) { + log.error("토큰이 만료되었습니다."); + filterChain.doFilter(request, response); + return; + } + + // UserName Token에서 꺼내기 + String email = JwtUtil.getUserEmail(token, secretKey); + log.info("email:{}", email); + + // 권한 부여 + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(email, null, List.of(new SimpleGrantedAuthority("USER"))); + + // Detail + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/example/rcp1/global/config/security/SecurityConfig.java b/src/main/java/com/example/rcp1/global/config/security/SecurityConfig.java new file mode 100644 index 0000000..8804c65 --- /dev/null +++ b/src/main/java/com/example/rcp1/global/config/security/SecurityConfig.java @@ -0,0 +1,48 @@ +package com.example.rcp1.global.config.security; + +import com.example.rcp1.domain.user.application.UserService; +import com.example.rcp1.domain.user.domain.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +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.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final UserService userService; + + @Value("${SECRET_KEY}") + private String secretKey; + + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + .httpBasic().disable() + .csrf().disable() + .cors().and() + .authorizeHttpRequests() + .requestMatchers("/user/signUp", "/user/signIn").permitAll() + .requestMatchers(HttpMethod.POST, "/user/**").authenticated() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .addFilterBefore(new JwtFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class) + .build(); + } + + + +} diff --git a/src/main/java/com/example/rcp1/global/config/security/util/JwtUtil.java b/src/main/java/com/example/rcp1/global/config/security/util/JwtUtil.java new file mode 100644 index 0000000..67d105e --- /dev/null +++ b/src/main/java/com/example/rcp1/global/config/security/util/JwtUtil.java @@ -0,0 +1,32 @@ +package com.example.rcp1.global.config.security.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +import java.util.Date; + +public class JwtUtil { + + public static String getUserEmail(String token, String secretkey) { + return Jwts.parser().setSigningKey(secretkey).parseClaimsJws(token) + .getBody().get("email", String.class); + } + + public static boolean isExpired(String token, String secretkey) { + System.out.println("token = " + token); + return Jwts.parser().setSigningKey(secretkey).parseClaimsJws(token).getBody().getExpiration().before(new Date()); + } + + public static String createJwt(String email, String secretKey, Long expiredMs) { + Claims claims = Jwts.claims(); + claims.put("email", email); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + expiredMs)) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + } +}