diff --git a/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/filter/AuthorizationHeaderFilter.java b/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/filter/AuthorizationHeaderFilter.java new file mode 100644 index 0000000..a8af87b --- /dev/null +++ b/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/filter/AuthorizationHeaderFilter.java @@ -0,0 +1,73 @@ +package com.justpickup.customerapigatewayservice.filter; + +import com.justpickup.customerapigatewayservice.security.JwtTokenProvider; +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.validateJwtToken(token)) { + return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED); + } + + String subject = jwtTokenProvider.getUserId(token); + if (false == jwtTokenProvider.getRoles(token).contains("Customer")) { + return onError(exchange, "권한 없음", HttpStatus.UNAUTHORIZED); + } + + ServerHttpRequest newRequest = request.mutate() + .header("user-id", subject) + .build(); + + return chain.filter(exchange.mutate().request(newRequest).build()); + }; + } + + // Mono(단일 값), Flux(다중 값) -> Spring WebFlux + private Mono onError(ServerWebExchange exchange, String errorMsg, HttpStatus httpStatus) { + log.error(errorMsg); + + ServerHttpResponse response = exchange.getResponse(); + response.setStatusCode(httpStatus); + + return response.setComplete(); + } +} diff --git a/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/security/JwtTokenProvider.java b/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/security/JwtTokenProvider.java new file mode 100644 index 0000000..af5fc77 --- /dev/null +++ b/customer-apigateway-service/src/main/java/com/justpickup/customerapigatewayservice/security/JwtTokenProvider.java @@ -0,0 +1,103 @@ +package com.justpickup.customerapigatewayservice.security; + +import io.jsonwebtoken.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.List; +import java.util.UUID; + +@Component +@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 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 String getUserId(String token) { + return getClaimsFromJwtToken(token).getSubject(); + } + + private Claims getClaimsFromJwtToken(String token) { + try { + return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } + + public String getRefreshTokenId(String token) { + return getClaimsFromJwtToken(token).get("value").toString(); + } + + public List getRoles(String token) { + return (List) getClaimsFromJwtToken(token).get("roles"); + } + + 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/customer-apigateway-service/src/main/resources/application.yml b/customer-apigateway-service/src/main/resources/application.yml index 8662617..31f9c15 100644 --- a/customer-apigateway-service/src/main/resources/application.yml +++ b/customer-apigateway-service/src/main/resources/application.yml @@ -34,9 +34,50 @@ 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/oauth2/authorization/* + filters: + - RewritePath=/user-service/(?.*),/$\{segment} + - id: user-service + uri: lb://USER-SERVICE + predicates: + - Path=/user-service/*/oauth2/code/* + 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 + refresh-token-name: refresh-token + access-token-name: access-token \ No newline at end of file diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/OAuthService.java b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/OAuthService.java new file mode 100644 index 0000000..e0bf2bb --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/domain/jwt/service/OAuthService.java @@ -0,0 +1,94 @@ +package com.justpickup.userservice.domain.jwt.service; + +import com.justpickup.userservice.domain.user.dto.CustomerDto; +import com.justpickup.userservice.domain.user.dto.OAuthAttributeDto; +import com.justpickup.userservice.domain.user.entity.Customer; +import com.justpickup.userservice.domain.user.repository.CustomerRepository; +import com.justpickup.userservice.domain.user.repository.UserRepository; +import com.justpickup.userservice.domain.user.service.UserService; +import com.justpickup.userservice.domain.user.service.UserServiceImpl; +import com.justpickup.userservice.global.utils.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@RequiredArgsConstructor +@Service +public class OAuthService implements OAuth2UserService { + + private final CustomerRepository customerRepository; + private final HttpServletResponse response; + private final HttpServletRequest request; + private final JwtTokenProvider jwtTokenProvider; + private final RefreshTokenService refreshTokenService; + private final UserServiceImpl userServiceImpl; + + @Override + @Transactional + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2UserService delegate = new DefaultOAuth2UserService(); + OAuth2User oAuth2User = delegate.loadUser(userRequest); + //OAuth 서비스 id + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + //OAuth 로그인 진행시 키가 되는 필드값 + String userNameAttributeName = userRequest.getClientRegistration() + .getProviderDetails() + .getUserInfoEndpoint() + .getUserNameAttributeName(); + + // OAuth2UserService + OAuthAttributeDto attributeDto = OAuthAttributeDto.of(registrationId, userNameAttributeName,oAuth2User.getAttributes()); + + + Customer customer = saveCustomer(attributeDto); + + + // TODO: 2022/02/16 Response에 token 담아 보내기 + + String userEmail = customer.getEmail(); + + + Collection authorities = userServiceImpl.loadUserByUsername(userEmail).getAuthorities(); + List roles = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()); + + String accessToken = jwtTokenProvider.createJwtAccessToken(userEmail, request.getRequestURI(), roles); + String refreshToken = jwtTokenProvider.createJwtRefreshToken(); + + refreshTokenService.updateRefreshToken(customer.getId(), jwtTokenProvider.getRefreshTokenId(refreshToken)); + + response.setHeader("Access-token",accessToken); + response.setHeader("refresh-token",refreshToken); + + + + return new DefaultOAuth2User( + authorities + , attributeDto.getAttributes() + , attributeDto.getNameAttributeKey()); + + } + + @Transactional + public Customer saveCustomer(OAuthAttributeDto attributeDto){ + return customerRepository.save( + customerRepository.findByEmail(attributeDto.getEmail()) + .orElse(attributeDto.toEntity(attributeDto)) + ); + } + +} 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 index 2889f3e..6fdc481 100644 --- 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 @@ -3,6 +3,7 @@ package com.justpickup.userservice.domain.jwt.service; import com.justpickup.userservice.domain.user.dto.JwtTokenDto; public interface RefreshTokenService { + void updateRefreshToken(Long id, String uuid); JwtTokenDto refreshJwtToken(String accessToken, String refreshToken); void logoutToken(String accessToken); } 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 index 8df6285..fcd9981 100644 --- 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 @@ -16,7 +16,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; 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; diff --git a/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/Customer.java b/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/Customer.java index d22c522..bdaea52 100644 --- a/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/Customer.java +++ b/user-service/src/main/java/com/justpickup/userservice/domain/user/entity/Customer.java @@ -17,7 +17,7 @@ public class Customer extends User { private AuthType oauthType; public Customer(String email, String password, String name, String phoneNumber, AuthType oauthType) { - super(email, password, name, phoneNumber,null); + super(email, password, name, phoneNumber); this.dtype = Customer.class.getSimpleName(); this.oauthType = oauthType; } 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 7bc4133..757013b 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,9 +26,6 @@ public abstract class User extends BaseEntity { private String phoneNumber; - @Enumerated(EnumType.STRING) - private Role role; - @Column(insertable = false,updatable = false) protected String dtype; 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 fd6136d..a6e5a93 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,16 +1,13 @@ package com.justpickup.userservice.domain.user.service; import com.justpickup.userservice.domain.user.dto.CustomerDto; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.core.user.OAuth2User; import com.justpickup.userservice.domain.user.dto.StoreOwnerDto; import com.justpickup.userservice.domain.user.entity.StoreOwner; -public interface UserService extends OAuth2UserService { +public interface UserService { + - void updateRefreshToken(Long id, String refreshToken); 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 c39d4d0..f3b01a4 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,8 +1,6 @@ package com.justpickup.userservice.domain.user.service; -import com.justpickup.userservice.domain.jwt.utils.JwtTokenProvider; import com.justpickup.userservice.domain.user.dto.CustomerDto; -import com.justpickup.userservice.domain.user.dto.OAuthAttributeDto; import com.justpickup.userservice.domain.user.dto.StoreOwnerDto; import com.justpickup.userservice.domain.user.entity.Customer; import com.justpickup.userservice.domain.user.entity.StoreOwner; @@ -12,28 +10,17 @@ 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.GrantedAuthority; 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.security.oauth2.client.userinfo.DefaultOAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.user.DefaultOAuth2User; -import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -44,9 +31,6 @@ public class UserServiceImpl implements UserService, UserDetailsService { private final CustomerRepository customerRepository; private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; - private final HttpServletResponse response; - private final HttpServletRequest request; - private final JwtTokenProvider jwtTokenProvider; @@ -74,77 +58,8 @@ 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.getRefreshTokenId()); + storeOwnerDto.getPhoneNumber(), storeOwnerDto.getBusinessNumber()); return userRepository.save(storeOwner); } - - - - - @Override - @Transactional - public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - OAuth2UserService delegate = new DefaultOAuth2UserService(); - OAuth2User oAuth2User = delegate.loadUser(userRequest); - //OAuth 서비스 id - String registrationId = userRequest.getClientRegistration().getRegistrationId(); - //OAuth 로그인 진행시 키가 되는 필드값 - String userNameAttributeName = userRequest.getClientRegistration() - .getProviderDetails() - .getUserInfoEndpoint() - .getUserNameAttributeName(); - - // OAuth2UserService - OAuthAttributeDto attributeDto = OAuthAttributeDto.of(registrationId, userNameAttributeName,oAuth2User.getAttributes()); - - - Customer customer = saveCustomer(attributeDto); - - - // TODO: 2022/02/16 Response에 token 담아 보내기 - - String userEmail = customer.getEmail(); - - - Collection authorities = loadUserByUsername(userEmail).getAuthorities(); - List roles = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()); - - String accessToken = jwtTokenProvider.createJwtAccessToken(userEmail, request.getRequestURI(), roles); - String refreshToken = jwtTokenProvider.createJwtRefreshToken(); - - updateRefreshToken(customer.getId(), jwtTokenProvider.getRefreshTokenId(refreshToken)); - - customer.changeRefreshToken(refreshToken); - - response.setHeader("Access-token",accessToken); - response.setHeader("refresh-token",refreshToken); - - - - return new DefaultOAuth2User( - authorities - , attributeDto.getAttributes() - , attributeDto.getNameAttributeKey()); - - } - - @Transactional - public Customer saveCustomer(OAuthAttributeDto attributeDto){ - return customerRepository.save( - customerRepository.findByEmail(attributeDto.getEmail()) - .orElse(attributeDto.toEntity(attributeDto)) - ); - } - - - @Transactional - @Override - public void updateRefreshToken(Long id, String refreshToken) { - User user = userRepository.findById(id) - .orElseThrow(() -> new NotExistUserException("사용자 고유번호 : " + id + "는 없는 사용자입니다.")); - - user.changeRefreshToken(refreshToken); - } - } 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 f876491..d9863c3 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,6 @@ package com.justpickup.userservice.global.security; +import com.justpickup.userservice.domain.jwt.service.OAuthService; import com.justpickup.userservice.domain.jwt.service.RefreshTokenServiceImpl; import com.justpickup.userservice.global.utils.JwtTokenProvider; import com.justpickup.userservice.domain.user.service.UserService; @@ -14,6 +15,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur 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.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @@ -26,7 +28,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { private final RefreshTokenServiceImpl refreshTokenServiceImpl; private final CookieProvider cookieProvider; - private final UserService userService; + private final OAuthService oAuthService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { @@ -52,7 +54,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { http.oauth2Login() .defaultSuccessUrl("http://just-pickup.com:8000/customer-frontend-service/") .userInfoEndpoint() - .userService(userService); + .userService(oAuthService); http.addFilter(loginAuthenticationFilter); http.addFilterBefore(new HeaderAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);