From 285ca1cd25c7c8f2feb07f16904bbcd76b67631c Mon Sep 17 00:00:00 2001 From: bum12ark Date: Tue, 15 Feb 2022 16:16:51 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(user-service):=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=96=91=EC=8B=9D=20=EB=B3=80=EA=B2=BD,=20userDeta?= =?UTF-8?q?ilService=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - User 엔티티 abstract로 변경 - UserService UserDetailService 구현 --- .../domain/user/dto/CustomerDto.java | 4 +- .../domain/user/dto/StoreOwnerDto.java | 16 ++++++++ .../userservice/domain/user/dto/UserDto.java | 9 +++-- .../domain/user/entity/StoreOwner.java | 6 +++ .../userservice/domain/user/entity/User.java | 14 ++++++- .../user/repository/UserRepository.java | 10 +++++ .../domain/user/service/UserService.java | 3 ++ .../domain/user/service/UserServiceImpl.java | 37 ++++++++++++++++++- 8 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 user-service/src/main/java/com/justpickup/userservice/domain/user/dto/StoreOwnerDto.java create mode 100644 user-service/src/main/java/com/justpickup/userservice/domain/user/repository/UserRepository.java diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/CustomerDto.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/CustomerDto.java index ac60aa5..b129153 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/CustomerDto.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/CustomerDto.java @@ -12,7 +12,7 @@ public class CustomerDto extends UserDto { } @Builder - public CustomerDto(Long id, String password, String name, String phoneNumber) { - super(id, password, name, phoneNumber); + public CustomerDto(Long id, String email, String password, String name, String phoneNumber, String dtype) { + super(id, email, password, name, phoneNumber, dtype); } } diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/StoreOwnerDto.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/StoreOwnerDto.java new file mode 100644 index 0000000..b5d2833 --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/StoreOwnerDto.java @@ -0,0 +1,16 @@ +package com.justpickup.userservice.domain.user.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class StoreOwnerDto extends UserDto { + private String businessNumber; + + @Builder + public StoreOwnerDto(Long id, String email, String password, String name, + String phoneNumber, String dtype, String businessNumber) { + super(id, email, password, name, phoneNumber, dtype); + this.businessNumber = businessNumber; + } +} diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/UserDto.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/UserDto.java index 3fad831..32df2aa 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/UserDto.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/UserDto.java @@ -1,15 +1,16 @@ package com.justpickup.userservice.domain.user.dto; import com.justpickup.userservice.domain.user.entity.Customer; -import lombok.Builder; import lombok.Getter; @Getter -public class UserDto { +public abstract class UserDto { private Long id; + private String email; private String password; private String name; private String phoneNumber; + private String dtype; // == 생성 메소드 == // public UserDto(Customer customer) { @@ -19,10 +20,12 @@ public class UserDto { this.phoneNumber = customer.getPhoneNumber(); } - public UserDto(Long id, String password, String name, String phoneNumber) { + public UserDto(Long id, String email, String password, String name, String phoneNumber, String dtype) { this.id = id; + this.email = email; this.password = password; this.name = name; this.phoneNumber = phoneNumber; + this.dtype = dtype; } } diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/StoreOwner.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/StoreOwner.java index 94510df..644d493 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/StoreOwner.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/StoreOwner.java @@ -1,5 +1,6 @@ package com.justpickup.userservice.domain.user.entity; +import com.justpickup.userservice.domain.user.dto.StoreOwnerDto; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -12,4 +13,9 @@ import javax.persistence.Table; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class StoreOwner extends User { private String businessNumber; + + public StoreOwner(String email, String password, String name, String phoneNumber, String businessNumber) { + super(email, password, name, phoneNumber); + this.businessNumber = businessNumber; + } } diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/User.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/User.java index 26b8a8b..a5f9b4c 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/User.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/User.java @@ -12,15 +12,27 @@ import javax.persistence.*; @Inheritance(strategy = InheritanceType.JOINED) @DiscriminatorColumn(name = "DTYPE") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class User extends BaseEntity { +public abstract class User extends BaseEntity { @Id @GeneratedValue @Column(name = "user_id") private Long id; + private String email; + private String password; private String name; private String phoneNumber; + + @Column(insertable = false, updatable = false) + private String dtype; + + public User(String email, String password, String name, String phoneNumber) { + this.email = email; + this.password = password; + this.name = name; + this.phoneNumber = phoneNumber; + } } diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/repository/UserRepository.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/repository/UserRepository.java new file mode 100644 index 0000000..4b8113c --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/repository/UserRepository.java @@ -0,0 +1,10 @@ +package com.justpickup.userservice.domain.user.repository; + +import com.justpickup.userservice.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String username); +} diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/service/UserService.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/service/UserService.java index b246962..1b41662 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/service/UserService.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/service/UserService.java @@ -1,7 +1,10 @@ package com.justpickup.userservice.domain.user.service; import com.justpickup.userservice.domain.user.dto.CustomerDto; +import com.justpickup.userservice.domain.user.dto.StoreOwnerDto; +import com.justpickup.userservice.domain.user.entity.StoreOwner; public interface UserService { CustomerDto findCustomerByUserId(Long userId); + StoreOwner saveStoreOwner(StoreOwnerDto storeOwnerDto); } diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/service/UserServiceImpl.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/service/UserServiceImpl.java index 9ddb80a..9e8f561 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/service/UserServiceImpl.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/service/UserServiceImpl.java @@ -1,21 +1,45 @@ package com.justpickup.userservice.domain.user.service; import com.justpickup.userservice.domain.user.dto.CustomerDto; +import com.justpickup.userservice.domain.user.dto.StoreOwnerDto; import com.justpickup.userservice.domain.user.entity.Customer; +import com.justpickup.userservice.domain.user.entity.StoreOwner; +import com.justpickup.userservice.domain.user.entity.User; import com.justpickup.userservice.domain.user.exception.NotExistUserException; import com.justpickup.userservice.domain.user.repository.CustomerRepository; +import com.justpickup.userservice.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.Collection; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @Slf4j -public class UserServiceImpl implements UserService { +public class UserServiceImpl implements UserService, UserDetailsService { private final CustomerRepository customerRepository; + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found in the database")); + + Collection authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority(user.getDtype())); + return new org.springframework.security.core.userdetails.User(user.getId().toString(), user.getPassword(), authorities); + } @Override public CustomerDto findCustomerByUserId(Long userId) { @@ -24,4 +48,15 @@ public class UserServiceImpl implements UserService { return new CustomerDto(customer); } + + @Override + @Transactional + public StoreOwner saveStoreOwner(StoreOwnerDto storeOwnerDto) { + String encode = passwordEncoder.encode(storeOwnerDto.getPassword()); + + StoreOwner storeOwner = new StoreOwner(storeOwnerDto.getEmail(), encode, storeOwnerDto.getName(), + storeOwnerDto.getPhoneNumber(), storeOwnerDto.getBusinessNumber()); + + return userRepository.save(storeOwner); + } } From 72c52e4b5838e9c24f1d4f332acc9f5f7eb14b69 Mon Sep 17 00:00:00 2001 From: bum12ark Date: Tue, 15 Feb 2022 16:19:10 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat(user-service):=20Spring=20Security=20?= =?UTF-8?q?=EB=B0=8F=20JWT=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D,=20=EC=9D=B8=EA=B0=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Spring Security 구현 - 로그인 인증 구현 - 인증 성공 시 JWT 발급 구현 --- .../userservice/global/config/AppConfig.java | 15 +++ .../userservice/global/dto/LoginRequest.java | 10 ++ .../security/HeaderAuthorizationFilter.java | 28 ++++++ .../security/LoginAuthenticationFilter.java | 91 +++++++++++++++++++ .../global/security/SecurityConfig.java | 43 +++++++++ 5 files changed, 187 insertions(+) create mode 100644 user-service/src/main/java/com/justpickup/userservice/global/config/AppConfig.java create mode 100644 user-service/src/main/java/com/justpickup/userservice/global/dto/LoginRequest.java create mode 100644 user-service/src/main/java/com/justpickup/userservice/global/security/HeaderAuthorizationFilter.java create mode 100644 user-service/src/main/java/com/justpickup/userservice/global/security/LoginAuthenticationFilter.java create mode 100644 user-service/src/main/java/com/justpickup/userservice/global/security/SecurityConfig.java diff --git a/user-service/src/main/java/com/justpickup/userservice/global/config/AppConfig.java b/user-service/src/main/java/com/justpickup/userservice/global/config/AppConfig.java new file mode 100644 index 0000000..1df114d --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/global/config/AppConfig.java @@ -0,0 +1,15 @@ +package com.justpickup.userservice.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class AppConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/user-service/src/main/java/com/justpickup/userservice/global/dto/LoginRequest.java b/user-service/src/main/java/com/justpickup/userservice/global/dto/LoginRequest.java new file mode 100644 index 0000000..4f6eda1 --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/global/dto/LoginRequest.java @@ -0,0 +1,10 @@ +package com.justpickup.userservice.global.dto; + +import lombok.Data; + +@Data +public class LoginRequest { + private String name; + private String email; + private String password; +} diff --git a/user-service/src/main/java/com/justpickup/userservice/global/security/HeaderAuthorizationFilter.java b/user-service/src/main/java/com/justpickup/userservice/global/security/HeaderAuthorizationFilter.java new file mode 100644 index 0000000..b26d6f2 --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/global/security/HeaderAuthorizationFilter.java @@ -0,0 +1,28 @@ +package com.justpickup.userservice.global.security; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +public class HeaderAuthorizationFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + if (request.getServletPath().equals("/login")) { + filterChain.doFilter(request, response); + return; + } + + String email = request.getHeader("jwt-sub"); + log.info("email jwt-sub = {}", email); + + filterChain.doFilter(request, response); + } +} diff --git a/user-service/src/main/java/com/justpickup/userservice/global/security/LoginAuthenticationFilter.java b/user-service/src/main/java/com/justpickup/userservice/global/security/LoginAuthenticationFilter.java new file mode 100644 index 0000000..66d5343 --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/global/security/LoginAuthenticationFilter.java @@ -0,0 +1,91 @@ +package com.justpickup.userservice.global.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.justpickup.userservice.global.dto.LoginRequest; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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.GrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Date; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@RequiredArgsConstructor +@Slf4j +public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter { + + private final AuthenticationManager authenticationManager; + + // login 리퀘스트 패스로 오는 요청을 판단 + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + Authentication authentication; + + try { + LoginRequest credential = new ObjectMapper().readValue(request.getInputStream(), LoginRequest.class); + + authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(credential.getEmail(), credential.getPassword()) + ); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + + return authentication; + } + + // 로그인 성공 이후 토큰 생성 + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException { + org.springframework.security.core.userdetails.User user = (User) authResult.getPrincipal(); + + String accessToken = Jwts.builder() + .setSubject(user.getUsername()) + .setExpiration( + new Date(System.currentTimeMillis() + 10 * 60 * 1000) + ) + .signWith(SignatureAlgorithm.HS512, "your-256-bit-secret") + .setIssuer(request.getRequestURI()) + .addClaims(Map.of("roles", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))) + .compact(); + + String refreshToken = Jwts.builder() + .setSubject(user.getUsername()) + .setExpiration( + new Date(System.currentTimeMillis() + 30 * 60 * 1000) + ) + .signWith(SignatureAlgorithm.HS512, "your-256-bit-secret") + .setIssuer(request.getRequestURI()) + .compact(); + + Map tokens = Map.of( + "access_token", accessToken, + "refresh_token", refreshToken + ); + response.setContentType(APPLICATION_JSON_VALUE); + new ObjectMapper().writeValue(response.getOutputStream(), tokens); + } + + @Override + protected void unsuccessfulAuthentication + (HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) + throws IOException, ServletException { + log.warn("로그인 실패!!"); + } +} diff --git a/user-service/src/main/java/com/justpickup/userservice/global/security/SecurityConfig.java b/user-service/src/main/java/com/justpickup/userservice/global/security/SecurityConfig.java new file mode 100644 index 0000000..cc1bd1e --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/global/security/SecurityConfig.java @@ -0,0 +1,43 @@ +package com.justpickup.userservice.global.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +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.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig extends WebSecurityConfigurerAdapter { + private final UserDetailsService userDetailsService; + private final BCryptPasswordEncoder bCryptPasswordEncoder; + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + LoginAuthenticationFilter loginAuthenticationFilter = new LoginAuthenticationFilter(authenticationManagerBean()); + loginAuthenticationFilter.setFilterProcessesUrl("/login"); + + http.csrf().disable(); + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + http.authorizeRequests().antMatchers("/login").permitAll(); + http.addFilter(loginAuthenticationFilter); + http.addFilterBefore(new HeaderAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class); + } + + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } +} From b6274466b64c488356ca97023a2e8ed932200726 Mon Sep 17 00:00:00 2001 From: bum12ark Date: Wed, 16 Feb 2022 11:42:25 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat(user-service):=20access=20token,=20ref?= =?UTF-8?q?resh=20token=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - jwt 패키지 추가 - refresh token을 통해 access token 생성 로직 추가 - jwt token provider 추가 --- .../userservice/UserServiceApplication.java | 13 ++ .../jwt/exception/TokenRefreshException.java | 11 ++ .../jwt/service/RefreshTokenService.java | 8 ++ .../jwt/service/RefreshTokenServiceImpl.java | 73 +++++++++++ .../domain/jwt/utils/JwtTokenProvider.java | 115 ++++++++++++++++++ .../domain/jwt/web/AuthController.java | 53 ++++++++ .../domain/user/dto/CustomerDto.java | 5 +- .../domain/user/dto/JwtTokenDto.java | 16 +++ .../domain/user/dto/StoreOwnerDto.java | 4 +- .../userservice/domain/user/dto/UserDto.java | 5 +- .../domain/user/entity/StoreOwner.java | 5 +- .../userservice/domain/user/entity/User.java | 13 +- .../domain/user/service/UserServiceImpl.java | 3 +- .../security/LoginAuthenticationFilter.java | 37 +++--- .../global/security/SecurityConfig.java | 16 ++- .../src/main/resources/application.yml | 9 +- 16 files changed, 353 insertions(+), 33 deletions(-) create mode 100644 user-service/src/main/java/com/justpickup/userservice/domain/jwt/exception/TokenRefreshException.java create mode 100644 user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/RefreshTokenService.java create mode 100644 user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/RefreshTokenServiceImpl.java create mode 100644 user-service/src/main/java/com/justpickup/userservice/domain/jwt/utils/JwtTokenProvider.java create mode 100644 user-service/src/main/java/com/justpickup/userservice/domain/jwt/web/AuthController.java create mode 100644 user-service/src/main/java/com/justpickup/userservice/domain/user/dto/JwtTokenDto.java diff --git a/user-service/src/main/java/com/justpickup/userservice/UserServiceApplication.java b/user-service/src/main/java/com/justpickup/userservice/UserServiceApplication.java index e7ad12a..9b26de3 100644 --- a/user-service/src/main/java/com/justpickup/userservice/UserServiceApplication.java +++ b/user-service/src/main/java/com/justpickup/userservice/UserServiceApplication.java @@ -1,8 +1,12 @@ package com.justpickup.userservice; +import com.justpickup.userservice.domain.user.dto.StoreOwnerDto; +import com.justpickup.userservice.domain.user.service.UserService; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; +import org.springframework.context.annotation.Bean; @SpringBootApplication @EnableEurekaClient @@ -12,4 +16,13 @@ public class UserServiceApplication { SpringApplication.run(UserServiceApplication.class, args); } + @Bean + CommandLineRunner run(UserService userService) { + return args -> { + StoreOwnerDto park = StoreOwnerDto.builder() + .email("test@gmail.com").password("1234").name("Park").phoneNumber("010-1234-5678") + .build(); + userService.saveStoreOwner(park); + }; + } } diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/jwt/exception/TokenRefreshException.java b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/exception/TokenRefreshException.java new file mode 100644 index 0000000..691c99f --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/exception/TokenRefreshException.java @@ -0,0 +1,11 @@ +package com.justpickup.userservice.domain.jwt.exception; + +import com.justpickup.userservice.global.exception.CustomException; +import org.springframework.http.HttpStatus; + +public class TokenRefreshException extends CustomException { + + public TokenRefreshException(String message) { + super(HttpStatus.FORBIDDEN, message); + } +} diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/RefreshTokenService.java b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/RefreshTokenService.java new file mode 100644 index 0000000..3475a20 --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/RefreshTokenService.java @@ -0,0 +1,8 @@ +package com.justpickup.userservice.domain.jwt.service; + +import com.justpickup.userservice.domain.user.dto.JwtTokenDto; + +public interface RefreshTokenService { + void updateRefreshToken(Long id, String refreshToken); + JwtTokenDto refreshJwtToken(String accessToken, String refreshToken); +} diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/RefreshTokenServiceImpl.java b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/RefreshTokenServiceImpl.java new file mode 100644 index 0000000..e5706cb --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/RefreshTokenServiceImpl.java @@ -0,0 +1,73 @@ +package com.justpickup.userservice.domain.jwt.service; + +import com.justpickup.userservice.domain.jwt.exception.TokenRefreshException; +import com.justpickup.userservice.domain.user.dto.JwtTokenDto; +import com.justpickup.userservice.domain.user.entity.User; +import com.justpickup.userservice.domain.user.exception.NotExistUserException; +import com.justpickup.userservice.domain.user.repository.UserRepository; +import com.justpickup.userservice.domain.jwt.utils.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Slf4j +public class RefreshTokenServiceImpl implements RefreshTokenService { + private final UserRepository userRepository; + private final JwtTokenProvider jwtTokenProvider; + + @Transactional + @Override + public void updateRefreshToken(Long id, String refreshToken) { + User user = userRepository.findById(id) + .orElseThrow(() -> new NotExistUserException("사용자 고유번호 : " + id + "는 없는 사용자입니다.")); + + user.changeRefreshToken(refreshToken); + } + + @Transactional + @Override + public JwtTokenDto refreshJwtToken(String accessToken, String refreshToken) { + String userId = jwtTokenProvider.getUserId(accessToken); + + User user = userRepository.findById(Long.valueOf(userId)) + .orElseThrow(() -> new NotExistUserException("사용자 고유번호 : " + userId + "는 없는 사용자입니다.")); + + // refresh token 검증 + if (!jwtTokenProvider.validateJwtToken(refreshToken)) { + // 익셉션 발생 - 로그 아웃 후 로그인 페이지로 이동 처리 + user.deleteRefreshToken(); + throw new TokenRefreshException("Not validate jwt token = " + refreshToken); + } + + String userRefreshTokenId = user.getRefreshTokenId(); + if (!jwtTokenProvider.equalRefreshTokenId(userRefreshTokenId, refreshToken)) { + // 익셉션 발생 - 로그인 아웃 후 로그인 페이지로 이동 처리 + user.deleteRefreshToken(); + throw new TokenRefreshException("Not equal jwt token! user = " + userRefreshTokenId + + ", refreshToken = " + refreshToken); + } + + Authentication authentication = jwtTokenProvider.getAuthentication(user.getEmail()); + List roles = authentication.getAuthorities() + .stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()); + + String newAccessToken = jwtTokenProvider.createJwtAccessToken(userId, "/refreshToken", roles); + String newRefreshToken = jwtTokenProvider.createJwtRefreshToken(); + + user.changeRefreshToken(newRefreshToken); + + return JwtTokenDto.builder() + .accessToken(newAccessToken) + .refreshToken(newRefreshToken) + .build(); + } +} diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/jwt/utils/JwtTokenProvider.java b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/utils/JwtTokenProvider.java new file mode 100644 index 0000000..ab6e357 --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/utils/JwtTokenProvider.java @@ -0,0 +1,115 @@ +package com.justpickup.userservice.domain.jwt.utils; + + +import io.jsonwebtoken.*; +import lombok.RequiredArgsConstructor; +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.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.List; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JwtTokenProvider { + + private final UserDetailsService userDetailsService; + + @Value("${token.access-expired-time}") + private long ACCESS_EXPIRED_TIME; + + @Value("${token.refresh-expired-time}") + private long REFRESH_EXPIRED_TIME; + + @Value("${token.secret}") + private String SECRET; + + public String createJwtAccessToken(String userId, String uri, List roles) { + Claims claims = Jwts.claims().setSubject(userId); + claims.put("roles", roles); + + return Jwts.builder() + .addClaims(claims) + .setExpiration( + new Date(System.currentTimeMillis() + ACCESS_EXPIRED_TIME) + ) + .setIssuedAt(new Date()) + .signWith(SignatureAlgorithm.HS512, SECRET) + .setIssuer(uri) + .compact(); + } + + public String createJwtRefreshToken() { + Claims claims = Jwts.claims(); + claims.put("value", UUID.randomUUID()); + + return Jwts.builder() + .addClaims(claims) + .setExpiration( + new Date(System.currentTimeMillis() + REFRESH_EXPIRED_TIME) + ) + .setIssuedAt(new Date()) + .signWith(SignatureAlgorithm.HS512, SECRET) + .compact(); + } + + public Authentication getAuthentication(String email) { + UserDetails userDetails = userDetailsService.loadUserByUsername(email); + return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities()); + } + + public String getUserId(String token) { + try { + return getClaimsFromJwtToken(token).getBody().getSubject(); + } catch (ExpiredJwtException expiredJwtException) { + return expiredJwtException.getClaims().getSubject(); + } + } + + private Jws getClaimsFromJwtToken(String token) { + return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token); + } + + public String getRefreshTokenId(String token) { + try { + return getClaimsFromJwtToken(token).getBody().get("value").toString(); + } catch (ExpiredJwtException expiredJwtException) { + return expiredJwtException.getClaims().get("value").toString(); + } + } + + public boolean validateJwtToken(String token) { + try { + Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token); + return true; + } catch (SignatureException e) { + log.error("Invalid JWT signature: {}", e.getMessage()); + return false; + } catch (MalformedJwtException e) { + log.error("Invalid JWT token: {}", e.getMessage()); + return false; + } catch (ExpiredJwtException e) { + log.error("JWT token is expired: {}", e.getMessage()); + return false; + } catch (UnsupportedJwtException e) { + log.error("JWT token is unsupported: {}", e.getMessage()); + return false; + } catch (IllegalArgumentException e) { + log.error("JWT claims string is empty: {}", e.getMessage()); + return false; + } + } + + public boolean equalRefreshTokenId(String refreshTokenId, String refreshToken) { + String compareToken = this.getRefreshTokenId(refreshToken); + return refreshTokenId.equals(compareToken); + } + +} diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/jwt/web/AuthController.java b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/web/AuthController.java new file mode 100644 index 0000000..9841b69 --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/web/AuthController.java @@ -0,0 +1,53 @@ +package com.justpickup.userservice.domain.jwt.web; + +import com.justpickup.userservice.domain.jwt.service.RefreshTokenServiceImpl; +import com.justpickup.userservice.domain.user.dto.JwtTokenDto; +import com.justpickup.userservice.global.dto.Result; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@Slf4j +public class AuthController { + + private final RefreshTokenServiceImpl refreshTokenServiceImpl; + + @GetMapping("/refreshToken") + public ResponseEntity refreshToken(@RequestHeader("X-AUTH-TOKEN") String accessToken, + @RequestHeader("REFRESH-TOKEN") String refreshToken) { + + JwtTokenDto jwtTokenDto = refreshTokenServiceImpl.refreshJwtToken(accessToken, refreshToken); + + return ResponseEntity.ok(Result.createSuccessResult(new RefreshTokenResponse(jwtTokenDto))); + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + static class RefreshTokenResponse { + private String accessToken; + private String refreshToken; + + public RefreshTokenResponse(JwtTokenDto jwtTokenDto) { + this.accessToken = jwtTokenDto.getAccessToken(); + this.refreshToken = jwtTokenDto.getRefreshToken(); + } + } + + @PostMapping("/logout") + public ResponseEntity logout(@RequestHeader("X-AUTH-TOKEN") String accessToken, + @RequestHeader("REFRESH-TOKEN") String refreshToken) { + log.info("########### logout!"); + // TODO: 2022/02/16 logout 구현 필요 + return ResponseEntity.ok(Result.createSuccessResult("success")); + } +} diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/CustomerDto.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/CustomerDto.java index b129153..3474449 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/CustomerDto.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/CustomerDto.java @@ -12,7 +12,8 @@ public class CustomerDto extends UserDto { } @Builder - public CustomerDto(Long id, String email, String password, String name, String phoneNumber, String dtype) { - super(id, email, password, name, phoneNumber, dtype); + public CustomerDto(Long id, String email, String password, String name, + String phoneNumber, String dtype, String refreshTokenId) { + super(id, email, password, name, phoneNumber, dtype, refreshTokenId); } } diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/JwtTokenDto.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/JwtTokenDto.java new file mode 100644 index 0000000..9e1f015 --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/JwtTokenDto.java @@ -0,0 +1,16 @@ +package com.justpickup.userservice.domain.user.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class JwtTokenDto { + private String accessToken; + private String refreshToken; + + @Builder + public JwtTokenDto(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/StoreOwnerDto.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/StoreOwnerDto.java index b5d2833..38004c2 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/StoreOwnerDto.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/StoreOwnerDto.java @@ -9,8 +9,8 @@ public class StoreOwnerDto extends UserDto { @Builder public StoreOwnerDto(Long id, String email, String password, String name, - String phoneNumber, String dtype, String businessNumber) { - super(id, email, password, name, phoneNumber, dtype); + String phoneNumber, String dtype, String businessNumber, String refreshTokenId) { + super(id, email, password, name, phoneNumber, dtype, refreshTokenId); this.businessNumber = businessNumber; } } diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/UserDto.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/UserDto.java index 32df2aa..f3aa62e 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/UserDto.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/dto/UserDto.java @@ -11,6 +11,7 @@ public abstract class UserDto { private String name; private String phoneNumber; private String dtype; + private String refreshTokenId; // == 생성 메소드 == // public UserDto(Customer customer) { @@ -20,12 +21,14 @@ public abstract class UserDto { this.phoneNumber = customer.getPhoneNumber(); } - public UserDto(Long id, String email, String password, String name, String phoneNumber, String dtype) { + public UserDto(Long id, String email, String password, String name, String phoneNumber, + String dtype, String refreshTokenId) { this.id = id; this.email = email; this.password = password; this.name = name; this.phoneNumber = phoneNumber; this.dtype = dtype; + this.refreshTokenId = refreshTokenId; } } diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/StoreOwner.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/StoreOwner.java index 644d493..25a58bb 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/StoreOwner.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/StoreOwner.java @@ -14,8 +14,9 @@ import javax.persistence.Table; public class StoreOwner extends User { private String businessNumber; - public StoreOwner(String email, String password, String name, String phoneNumber, String businessNumber) { - super(email, password, name, phoneNumber); + public StoreOwner(String email, String password, String name, String phoneNumber, + String businessNumber, String refreshTokenId) { + super(email, password, name, phoneNumber, refreshTokenId); this.businessNumber = businessNumber; } } diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/User.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/User.java index a5f9b4c..6eb5613 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/User.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/User.java @@ -26,13 +26,24 @@ public abstract class User extends BaseEntity { private String phoneNumber; + private String refreshTokenId; + @Column(insertable = false, updatable = false) private String dtype; - public User(String email, String password, String name, String phoneNumber) { + public User(String email, String password, String name, String phoneNumber, String refreshTokenId) { this.email = email; this.password = password; this.name = name; this.phoneNumber = phoneNumber; + this.refreshTokenId = refreshTokenId; + } + + public void changeRefreshToken(String refreshToken) { + this.refreshTokenId = refreshToken; + } + + public void deleteRefreshToken() { + this.refreshTokenId = null; } } diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/service/UserServiceImpl.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/service/UserServiceImpl.java index 9e8f561..e224093 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/service/UserServiceImpl.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/service/UserServiceImpl.java @@ -55,8 +55,9 @@ public class UserServiceImpl implements UserService, UserDetailsService { String encode = passwordEncoder.encode(storeOwnerDto.getPassword()); StoreOwner storeOwner = new StoreOwner(storeOwnerDto.getEmail(), encode, storeOwnerDto.getName(), - storeOwnerDto.getPhoneNumber(), storeOwnerDto.getBusinessNumber()); + storeOwnerDto.getPhoneNumber(), storeOwnerDto.getBusinessNumber(), storeOwnerDto.getRefreshTokenId()); return userRepository.save(storeOwner); } + } diff --git a/user-service/src/main/java/com/justpickup/userservice/global/security/LoginAuthenticationFilter.java b/user-service/src/main/java/com/justpickup/userservice/global/security/LoginAuthenticationFilter.java index 66d5343..311a6a1 100644 --- a/user-service/src/main/java/com/justpickup/userservice/global/security/LoginAuthenticationFilter.java +++ b/user-service/src/main/java/com/justpickup/userservice/global/security/LoginAuthenticationFilter.java @@ -1,9 +1,9 @@ package com.justpickup.userservice.global.security; import com.fasterxml.jackson.databind.ObjectMapper; +import com.justpickup.userservice.domain.jwt.service.RefreshTokenServiceImpl; +import com.justpickup.userservice.domain.jwt.utils.JwtTokenProvider; import com.justpickup.userservice.global.dto.LoginRequest; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.AuthenticationManager; @@ -19,7 +19,7 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.Date; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -30,6 +30,8 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private final AuthenticationManager authenticationManager; + private final JwtTokenProvider jwtTokenProvider; + private final RefreshTokenServiceImpl refreshTokenServiceImpl; // login 리퀘스트 패스로 오는 요청을 판단 @Override @@ -55,30 +57,25 @@ public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFil protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException { org.springframework.security.core.userdetails.User user = (User) authResult.getPrincipal(); - String accessToken = Jwts.builder() - .setSubject(user.getUsername()) - .setExpiration( - new Date(System.currentTimeMillis() + 10 * 60 * 1000) - ) - .signWith(SignatureAlgorithm.HS512, "your-256-bit-secret") - .setIssuer(request.getRequestURI()) - .addClaims(Map.of("roles", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))) - .compact(); + List roles = user.getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); - String refreshToken = Jwts.builder() - .setSubject(user.getUsername()) - .setExpiration( - new Date(System.currentTimeMillis() + 30 * 60 * 1000) - ) - .signWith(SignatureAlgorithm.HS512, "your-256-bit-secret") - .setIssuer(request.getRequestURI()) - .compact(); + String userId = user.getUsername(); + + String accessToken = jwtTokenProvider.createJwtAccessToken(userId, request.getRequestURI(), roles); + String refreshToken = jwtTokenProvider.createJwtRefreshToken(); + + refreshTokenServiceImpl.updateRefreshToken(Long.valueOf(userId), jwtTokenProvider.getRefreshTokenId(refreshToken)); Map tokens = Map.of( "access_token", accessToken, "refresh_token", refreshToken ); + response.setContentType(APPLICATION_JSON_VALUE); + new ObjectMapper().writeValue(response.getOutputStream(), tokens); } diff --git a/user-service/src/main/java/com/justpickup/userservice/global/security/SecurityConfig.java b/user-service/src/main/java/com/justpickup/userservice/global/security/SecurityConfig.java index cc1bd1e..7ba52d5 100644 --- a/user-service/src/main/java/com/justpickup/userservice/global/security/SecurityConfig.java +++ b/user-service/src/main/java/com/justpickup/userservice/global/security/SecurityConfig.java @@ -1,5 +1,7 @@ package com.justpickup.userservice.global.security; +import com.justpickup.userservice.domain.jwt.service.RefreshTokenServiceImpl; +import com.justpickup.userservice.domain.jwt.utils.JwtTokenProvider; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; @@ -18,6 +20,8 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserDetailsService userDetailsService; private final BCryptPasswordEncoder bCryptPasswordEncoder; + private final JwtTokenProvider jwtTokenProvider; + private final RefreshTokenServiceImpl refreshTokenServiceImpl; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { @@ -26,12 +30,20 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { - LoginAuthenticationFilter loginAuthenticationFilter = new LoginAuthenticationFilter(authenticationManagerBean()); + LoginAuthenticationFilter loginAuthenticationFilter = + new LoginAuthenticationFilter(authenticationManagerBean(), jwtTokenProvider, refreshTokenServiceImpl); loginAuthenticationFilter.setFilterProcessesUrl("/login"); http.csrf().disable(); + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); - http.authorizeRequests().antMatchers("/login").permitAll(); + + http.authorizeRequests().anyRequest().permitAll(); + + http.logout() + .logoutUrl("/logout") + .deleteCookies(""); + http.addFilter(loginAuthenticationFilter); http.addFilterBefore(new HeaderAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class); } diff --git a/user-service/src/main/resources/application.yml b/user-service/src/main/resources/application.yml index 363e73b..bfe5623 100644 --- a/user-service/src/main/resources/application.yml +++ b/user-service/src/main/resources/application.yml @@ -12,7 +12,7 @@ spring: jpa: hibernate: - ddl-auto: validate + ddl-auto: create generate-ddl: true open-in-view: false properties: @@ -34,4 +34,9 @@ logging: # jpa query, parameter 로그 (p6spy) decorator.datasource.p6spy: - enable-logging: true \ No newline at end of file + enable-logging: true + +token: + access-expired-time: 3600000 + refresh-expired-time: 604800000 + secret: my-secret \ No newline at end of file From 16f881a1b13b948225cb2c124e70c2b164a16327 Mon Sep 17 00:00:00 2001 From: bum12ark Date: Wed, 16 Feb 2022 11:44:21 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat(owner-apigateway-service):=20access=20?= =?UTF-8?q?token=20=EA=B2=80=EC=A6=9D=20=EB=B6=80=EB=B6=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - gatewayfilter 추가 --- owner-apigateway-service/build.gradle | 2 + .../filter/AuthorizationHeaderFilter.java | 74 +++++++++++++++++++ .../security/JwtTokenProvider.java | 42 +++++++++++ .../src/main/resources/application.yml | 29 +++++++- 4 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/filter/AuthorizationHeaderFilter.java create mode 100644 owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/security/JwtTokenProvider.java diff --git a/owner-apigateway-service/build.gradle b/owner-apigateway-service/build.gradle index 5094502..6986aa8 100644 --- a/owner-apigateway-service/build.gradle +++ b/owner-apigateway-service/build.gradle @@ -30,6 +30,8 @@ dependencies { implementation 'io.jsonwebtoken:jjwt:0.9.1' // https://mvnrepository.com/artifact/io.netty/netty-resolver-dns-native-macos implementation 'io.netty:netty-resolver-dns-native-macos:4.1.68.Final:osx-aarch_64' + // https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api + implementation 'javax.xml.bind:jaxb-api:2.3.1' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' diff --git a/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/filter/AuthorizationHeaderFilter.java b/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/filter/AuthorizationHeaderFilter.java new file mode 100644 index 0000000..3eb834f --- /dev/null +++ b/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/filter/AuthorizationHeaderFilter.java @@ -0,0 +1,74 @@ +package com.justpickup.ownerapigatewayservice.filter; + +import com.justpickup.ownerapigatewayservice.security.JwtTokenProvider; +import io.jsonwebtoken.Jwts; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +@Component +@Slf4j +public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory { + + private final JwtTokenProvider jwtTokenProvider; + + @Autowired + public AuthorizationHeaderFilter(JwtTokenProvider jwtTokenProvider) { + super(Config.class); + this.jwtTokenProvider = jwtTokenProvider; + } + + static class Config { + + } + + @Override + public GatewayFilter apply(Config config) { + return (exchange, chain) -> { + ServerHttpRequest request = exchange.getRequest(); + + HttpHeaders headers = request.getHeaders(); + if (!headers.containsKey(HttpHeaders.AUTHORIZATION)) { + return onError(exchange, "No authorization header", HttpStatus.UNAUTHORIZED); + } + + String authorizationHeader = headers.get(HttpHeaders.AUTHORIZATION).get(0); + + // JWT 토큰 판별 + String token = authorizationHeader.replace("Bearer", ""); + + if (jwtTokenProvider.isExpired(token)) { + return onError(exchange, "Access Token is Expired", HttpStatus.UNAUTHORIZED); + } + + String subject = jwtTokenProvider.getUserId(token); + if (subject == null) { + return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED); + } + + ServerHttpRequest request1 = request.mutate() + .header("jwt-sub", subject) + .build(); + + return chain.filter(exchange.mutate().request(request1).build()); + }; + } + + // Mono(단일 값), Flux(다중 값) -> Spring WebFlux + private Mono onError(ServerWebExchange exchange, String errorMsg, HttpStatus httpStatus) { + ServerHttpResponse response = exchange.getResponse(); + response.setStatusCode(httpStatus); + + log.error(errorMsg); + return response.setComplete(); + } +} diff --git a/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/security/JwtTokenProvider.java b/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/security/JwtTokenProvider.java new file mode 100644 index 0000000..d4fed6b --- /dev/null +++ b/owner-apigateway-service/src/main/java/com/justpickup/ownerapigatewayservice/security/JwtTokenProvider.java @@ -0,0 +1,42 @@ +package com.justpickup.ownerapigatewayservice.security; + +import io.jsonwebtoken.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JwtTokenProvider { + + @Value("${token.access-expired-time}") + private long ACCESS_EXPIRED_TIME; + + @Value("${token.refresh-expired-time}") + private long REFRESH_EXPIRED_TIME; + + @Value("${token.secret}") + private String SECRET; + + public String getUserId(String token) { + return getClaimsFromJwtToken(token).getBody().getSubject(); + } + + public boolean isExpired(String token) { + try { + return getClaimsFromJwtToken(token).getBody().getExpiration().before(new Date()); + } catch (ExpiredJwtException e) { + return true; + } catch (Exception e) { + return true; + } + } + + public Jws getClaimsFromJwtToken(String token) { + return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token); + } +} diff --git a/owner-apigateway-service/src/main/resources/application.yml b/owner-apigateway-service/src/main/resources/application.yml index 68321c9..7395f0e 100644 --- a/owner-apigateway-service/src/main/resources/application.yml +++ b/owner-apigateway-service/src/main/resources/application.yml @@ -33,9 +33,36 @@ spring: - Path=/store-service/** filters: - RewritePath=/store-service/(?.*),/$\{segment} + - id: user-service + uri: lb://USER-SERVICE + predicates: + - Path=/user-service/login + - Method=POST + filters: + - RewritePath=/user-service/(?.*),/$\{segment} + - id: user-service + uri: lb://USER-SERVICE + predicates: + - Path=/user-service/refreshToken + - Method=GET + filters: + - RewritePath=/user-service/(?.*),/$\{segment} + - id: user-service + uri: lb://USER-SERVICE + predicates: + - Path=/user-service/logout + - Method=POST + filters: + - RewritePath=/user-service/(?.*),/$\{segment} - id: user-service uri: lb://USER-SERVICE predicates: - Path=/user-service/** filters: - - RewritePath=/user-service/(?.*),/$\{segment} \ No newline at end of file + - AuthorizationHeaderFilter + - RewritePath=/user-service/(?.*),/$\{segment} + +token: + access-expired-time: 3600000 + refresh-expired-time: 604800000 + secret: my-secret \ No newline at end of file