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 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 649ea9d..a1dab8b 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 @@ -16,8 +16,9 @@ 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, 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 new file mode 100644 index 0000000..38004c2 --- /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, 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 3fad831..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 @@ -1,15 +1,17 @@ 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; + private String refreshTokenId; // == 생성 메소드 == // public UserDto(Customer customer) { @@ -19,10 +21,14 @@ 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, 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 94510df..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 @@ -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,10 @@ 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, 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 ad7694d..0580925 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,7 +12,7 @@ import javax.persistence.*; @Inheritance(strategy = InheritanceType.JOINED) @DiscriminatorColumn(name = "DTYPE") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public abstract class User extends BaseEntity { +public class User extends BaseEntity { @Id @GeneratedValue @Column(name = "user_id") @@ -26,6 +26,8 @@ public abstract class User extends BaseEntity { private String phoneNumber; + private String refreshTokenId; + @Enumerated(EnumType.STRING) @Column(nullable = false) private Role role; @@ -40,4 +42,20 @@ public abstract class User extends BaseEntity { this.phoneNumber = phoneNumber; this.role = role; } + + 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/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 5b57799..1e412b5 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 @@ -5,8 +5,10 @@ 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 { 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 541d7d2..517cc9b 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 @@ -2,11 +2,17 @@ package com.justpickup.userservice.domain.user.service; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.justpickup.userservice.domain.user.dto.CustomerDto; +import com.justpickup.userservice.domain.user.dto.StoreOwnerDto; +import com.justpickup.userservice.domain.user.dto.OAuthAttributeDto; import com.justpickup.userservice.domain.user.dto.OAuthAttributeDto; 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 lombok.*; +import com.justpickup.userservice.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.env.Environment; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -16,6 +22,13 @@ 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.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.core.env.Environment; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,16 +36,36 @@ import javax.servlet.http.HttpServletResponse; import java.io.Serializable; import java.util.Collections; +import javax.servlet.http.HttpServletResponse; +import java.io.Serializable; +import java.util.Collections; + +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; + private final HttpServletResponse response; private final Environment env; + @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) { Customer customer = customerRepository.findById(userId) @@ -41,6 +74,17 @@ 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(), storeOwnerDto.getRefreshTokenId()); + + return userRepository.save(storeOwner); + } + @Data @NoArgsConstructor @AllArgsConstructor @@ -82,7 +126,7 @@ public class UserServiceImpl implements UserService { Customer customer = customerRepository.save( customerRepository.findByEmail(attributeDto.getEmail()) - .orElse(attributeDto.toEntity(attributeDto)) + .orElse(attributeDto.toEntity(attributeDto)) ); // TODO: 2022/02/16 Response에 token 담아 보내기 @@ -104,9 +148,4 @@ public class UserServiceImpl implements UserService { this.email = user.getEmail(); } } - - - - - } 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..311a6a1 --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/global/security/LoginAuthenticationFilter.java @@ -0,0 +1,88 @@ +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 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.List; +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; + private final JwtTokenProvider jwtTokenProvider; + private final RefreshTokenServiceImpl refreshTokenServiceImpl; + + // 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(); + + List roles = user.getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + + 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); + } + + @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..7ba52d5 --- /dev/null +++ b/user-service/src/main/java/com/justpickup/userservice/global/security/SecurityConfig.java @@ -0,0 +1,55 @@ +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; +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; + private final JwtTokenProvider jwtTokenProvider; + private final RefreshTokenServiceImpl refreshTokenServiceImpl; + + @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(), jwtTokenProvider, refreshTokenServiceImpl); + loginAuthenticationFilter.setFilterProcessesUrl("/login"); + + http.csrf().disable(); + + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + http.authorizeRequests().anyRequest().permitAll(); + + http.logout() + .logoutUrl("/logout") + .deleteCookies(""); + + http.addFilter(loginAuthenticationFilter); + http.addFilterBefore(new HeaderAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class); + } + + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } +} diff --git a/user-service/src/main/resources/application.yml b/user-service/src/main/resources/application.yml index b30ea22..581c92a 100644 --- a/user-service/src/main/resources/application.yml +++ b/user-service/src/main/resources/application.yml @@ -43,4 +43,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