From 95ecd296730e0336cff5a1624b455ad67c51a4ac Mon Sep 17 00:00:00 2001 From: roy-zz Date: Fri, 22 Apr 2022 00:47:19 +0900 Subject: [PATCH] =?UTF-8?q?Spring=20Security=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EC=A4=91=EA=B0=84=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- catalog-service/build.gradle | 2 +- discovery/build.gradle | 2 +- gateway/build.gradle | 4 +- .../filter/AuthorizationHeaderFilter.java | 70 ++++++++++++++++++ .../gateway/filter/GlobalFilter.java | 2 +- gateway/src/main/resources/application.yml | 72 ++++++++++++------ order-service/build.gradle | 2 +- test-server1/build.gradle | 2 +- test-server2/build.gradle | 2 +- user-service/build.gradle | 3 +- .../controller/UserController.java | 2 +- .../repository/UserRepository.java | 1 + .../security/AuthenticationFilter.java | 73 +++++++++++++++++++ .../userservice/security/WebSecurity.java | 29 +++++++- .../userservice/service/UserService.java | 4 +- .../service/impl/UserServiceImpl.java | 20 +++++ .../userservice/vo/request/LoginRequest.java | 20 +++++ .../src/main/resources/application.yml | 10 ++- 19 files changed, 287 insertions(+), 35 deletions(-) create mode 100644 gateway/src/main/java/com/roy/springcloud/gateway/filter/AuthorizationHeaderFilter.java create mode 100644 user-service/src/main/java/com/roy/springcloud/userservice/security/AuthenticationFilter.java create mode 100644 user-service/src/main/java/com/roy/springcloud/userservice/vo/request/LoginRequest.java diff --git a/build.gradle b/build.gradle index 1b1a84e..04eee54 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.springframework.boot' version '2.6.6' + id 'org.springframework.boot' version '2.4.2' id 'io.spring.dependency-management' version '1.0.11.RELEASE' } diff --git a/catalog-service/build.gradle b/catalog-service/build.gradle index 2504ecd..503145a 100644 --- a/catalog-service/build.gradle +++ b/catalog-service/build.gradle @@ -1,5 +1,5 @@ ext { - set('springCloudVersion', "2021.0.1") + set('springCloudVersion', "2020.0.5") } dependencies { diff --git a/discovery/build.gradle b/discovery/build.gradle index 1def927..97ffc41 100644 --- a/discovery/build.gradle +++ b/discovery/build.gradle @@ -1,5 +1,5 @@ ext { - set('springCloudVersion', "2021.0.1") + set('springCloudVersion', "2020.0.5") } dependencies { diff --git a/gateway/build.gradle b/gateway/build.gradle index 209b3c1..73d0369 100644 --- a/gateway/build.gradle +++ b/gateway/build.gradle @@ -1,10 +1,12 @@ ext { - set('springCloudVersion', "2021.0.1") + set('springCloudVersion', "2020.0.5") } dependencies { + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.cloud:spring-cloud-starter-gateway' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'io.jsonwebtoken:jjwt:0.9.1' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/gateway/src/main/java/com/roy/springcloud/gateway/filter/AuthorizationHeaderFilter.java b/gateway/src/main/java/com/roy/springcloud/gateway/filter/AuthorizationHeaderFilter.java new file mode 100644 index 0000000..c9ad898 --- /dev/null +++ b/gateway/src/main/java/com/roy/springcloud/gateway/filter/AuthorizationHeaderFilter.java @@ -0,0 +1,70 @@ +package com.roy.springcloud.gateway.filter; + +import io.jsonwebtoken.Jwts; +import lombok.extern.slf4j.Slf4j; +import org.apache.logging.log4j.util.Strings; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.core.env.Environment; +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; + +@Slf4j +@Component +public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory { + private final Environment environment; + + public AuthorizationHeaderFilter(Environment environment) { + super(Config.class); + this.environment = environment; + } + + @Override + public GatewayFilter apply(AuthorizationHeaderFilter.Config config) { + return (exchange, chain) -> { + ServerHttpRequest request = exchange.getRequest(); + if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) { + return onError(exchange, "No authorization header", HttpStatus.UNAUTHORIZED); + } + + String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0); + String jwt = authorizationHeader.replace("Bearer", ""); + + if (!isJwtValid(jwt)) { + return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED); + } + return chain.filter(exchange); + }; + } + + private Mono onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) { + ServerHttpResponse response = exchange.getResponse(); + response.setStatusCode(httpStatus); + log.error(err); + return response.setComplete(); + } + + private boolean isJwtValid(String jwt) { + boolean returnValue = true; + + String subject = null; + try { + subject = Jwts.parser().setSigningKey(environment.getProperty("token.secret")) + .parseClaimsJws(jwt).getBody() + .getSubject(); + } catch (Exception ex) { + returnValue = false; + } + if (Strings.isBlank(subject)) { + returnValue = false; + } + return returnValue; + } + + public static class Config {} +} diff --git a/gateway/src/main/java/com/roy/springcloud/gateway/filter/GlobalFilter.java b/gateway/src/main/java/com/roy/springcloud/gateway/filter/GlobalFilter.java index a844ca4..372851e 100644 --- a/gateway/src/main/java/com/roy/springcloud/gateway/filter/GlobalFilter.java +++ b/gateway/src/main/java/com/roy/springcloud/gateway/filter/GlobalFilter.java @@ -23,7 +23,7 @@ public class GlobalFilter extends AbstractGatewayFilterFactory {}", request.getId()); + log.info("Global Filter Start: request uri -> {}", request.getLocalAddress()); } return chain.filter(exchange).then(Mono.fromRunnable(() -> { if (config.isShowPostLogger()) { diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml index e9f863b..a5f1282 100644 --- a/gateway/src/main/resources/application.yml +++ b/gateway/src/main/resources/application.yml @@ -10,6 +10,9 @@ eureka: service-url: defaultZone: http://localhost:8761/eureka +token: + secret: user_token + spring: application: name: gateway-service @@ -22,10 +25,31 @@ spring: showPreLogger: true showPostLogger: true routes: + - id: user-service + uri: lb://USER-SERVICE + predicates: + - Path=/user-service/login + - Method=POST + filters: + - RemoveRequestHeader=Cookie + - RewritePath=/user-service/(?.*), /$\{segment} + - id: user-service + uri: lb://USER-SERVICE + predicates: + - Path=/user-service/users + - Method=POST + filters: + - RemoveRequestHeader=Cookie + - RewritePath=/user-service/(?.*), /$\{segment} - id: user-service uri: lb://USER-SERVICE predicates: - Path=/user-service/** + - Method=GET + filters: + - RemoveRequestHeader=Cookie + - RewritePath=/user-service/(?.*), /$\{segment} + - AuthorizationHeaderFilter - id: catalog-service uri: lb://CATALOG-SERVICE predicates: @@ -34,25 +58,29 @@ spring: uri: lb://ORDER-SERVICE predicates: - Path=/order-service/** - - id: test-server-1 - uri: lb://TEST-SERVER-1 - predicates: - - Path=/test-server-1/** - filters: - - name: CustomFilter - - name: LoggingFilter - args: - message: TEST-SERVER-1 - showPreLogger: true - showPostLogger: true - - id: test-server-2 - uri: lb://TEST-SERVER-2 - predicates: - - Path=/test-server-2/** - filters: - - name: CustomFilter - - name: LoggingFilter - args: - message: TEST-SERVER-2 - showPreLogger: true - showPostLogger: true \ No newline at end of file +# - id: test-server-1 +# uri: lb://TEST-SERVER-1 +# predicates: +# - Path=/test-server-1/** +# filters: +# - name: CustomFilter +# - name: LoggingFilter +# args: +# message: TEST-SERVER-1 +# showPreLogger: true +# showPostLogger: true +# - id: test-server-2 +# uri: lb://TEST-SERVER-2 +# predicates: +# - Path=/test-server-2/** +# filters: +# - name: CustomFilter +# - name: LoggingFilter +# args: +# message: TEST-SERVER-2 +# showPreLogger: true +# showPostLogger: true + +logging: + lelvel: + com.roy.springcloud.gateway: DEBUG \ No newline at end of file diff --git a/order-service/build.gradle b/order-service/build.gradle index ba804b8..224d4e2 100644 --- a/order-service/build.gradle +++ b/order-service/build.gradle @@ -1,5 +1,5 @@ ext { - set('springCloudVersion', "2021.0.1") + set('springCloudVersion', "2020.0.5") } dependencies { diff --git a/test-server1/build.gradle b/test-server1/build.gradle index 06f5774..06aa4af 100644 --- a/test-server1/build.gradle +++ b/test-server1/build.gradle @@ -1,5 +1,5 @@ ext { - set('springCloudVersion', "2021.0.1") + set('springCloudVersion', "2020.0.5") } dependencies { diff --git a/test-server2/build.gradle b/test-server2/build.gradle index 06f5774..06aa4af 100644 --- a/test-server2/build.gradle +++ b/test-server2/build.gradle @@ -1,5 +1,5 @@ ext { - set('springCloudVersion', "2021.0.1") + set('springCloudVersion', "2020.0.5") } dependencies { diff --git a/user-service/build.gradle b/user-service/build.gradle index a9bbe9d..7a09da8 100644 --- a/user-service/build.gradle +++ b/user-service/build.gradle @@ -1,5 +1,5 @@ ext { - set('springCloudVersion', "2021.0.1") + set('springCloudVersion', "2020.0.5") } dependencies { @@ -10,6 +10,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'io.jsonwebtoken:jjwt:0.9.1' compileOnly 'org.projectlombok:lombok' diff --git a/user-service/src/main/java/com/roy/springcloud/userservice/controller/UserController.java b/user-service/src/main/java/com/roy/springcloud/userservice/controller/UserController.java index 6cf02b8..23991e3 100644 --- a/user-service/src/main/java/com/roy/springcloud/userservice/controller/UserController.java +++ b/user-service/src/main/java/com/roy/springcloud/userservice/controller/UserController.java @@ -16,7 +16,7 @@ import java.util.List; import static com.roy.springcloud.util.mapper.MapperUtil.toObject; @RestController -@RequestMapping("/user-service") +@RequestMapping("/") @RequiredArgsConstructor public class UserController { private final Environment environment; diff --git a/user-service/src/main/java/com/roy/springcloud/userservice/repository/UserRepository.java b/user-service/src/main/java/com/roy/springcloud/userservice/repository/UserRepository.java index 7c9a62d..79c643c 100644 --- a/user-service/src/main/java/com/roy/springcloud/userservice/repository/UserRepository.java +++ b/user-service/src/main/java/com/roy/springcloud/userservice/repository/UserRepository.java @@ -7,5 +7,6 @@ import java.util.Optional; public interface UserRepository extends CrudRepository { Optional findByUserId(String userId); + Optional findByEmail(String email); Iterable findAll(); } diff --git a/user-service/src/main/java/com/roy/springcloud/userservice/security/AuthenticationFilter.java b/user-service/src/main/java/com/roy/springcloud/userservice/security/AuthenticationFilter.java new file mode 100644 index 0000000..f315c3a --- /dev/null +++ b/user-service/src/main/java/com/roy/springcloud/userservice/security/AuthenticationFilter.java @@ -0,0 +1,73 @@ +package com.roy.springcloud.userservice.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.roy.springcloud.userservice.dto.UserDto; +import com.roy.springcloud.userservice.service.UserService; +import com.roy.springcloud.userservice.vo.request.LoginRequest; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.env.Environment; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.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.Collections; +import java.util.Date; +import java.util.Objects; + +@SuppressWarnings("UastIncorrectHttpHeaderInspection") +@Slf4j +public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter { + private final UserService userService; + private final Environment environment; + + public AuthenticationFilter(AuthenticationManager authenticationManager, + UserService userService, + Environment environment) { + super(authenticationManager); + this.userService = userService; + this.environment = environment; + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + try { + LoginRequest loginRequest = new ObjectMapper().readValue(request.getInputStream(), LoginRequest.class); + return getAuthenticationManager().authenticate( + new UsernamePasswordAuthenticationToken( + loginRequest.getEmail(), + loginRequest.getPassword(), + Collections.emptyList() + ) + ); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { + String userName = ((User) authResult.getPrincipal()).getUsername(); + UserDto userDto = userService.getUserDetailsByEmail(userName); + + String token = Jwts.builder() + .setSubject(userDto.getUserId()) + .setExpiration(new Date(System.currentTimeMillis() + Long.parseLong(Objects.requireNonNull(environment.getProperty("token.expiration_time"))))) + .signWith(SignatureAlgorithm.HS512, environment.getProperty("token.secret")) + .compact(); + + response.addHeader("token", token); + response.addHeader("userId", userDto.getUserId()); + } + +} diff --git a/user-service/src/main/java/com/roy/springcloud/userservice/security/WebSecurity.java b/user-service/src/main/java/com/roy/springcloud/userservice/security/WebSecurity.java index 5bf68b0..51fea1a 100644 --- a/user-service/src/main/java/com/roy/springcloud/userservice/security/WebSecurity.java +++ b/user-service/src/main/java/com/roy/springcloud/userservice/security/WebSecurity.java @@ -1,18 +1,45 @@ package com.roy.springcloud.userservice.security; +import com.roy.springcloud.userservice.service.UserService; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +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.crypto.bcrypt.BCryptPasswordEncoder; @Configuration @EnableWebSecurity +@RequiredArgsConstructor public class WebSecurity extends WebSecurityConfigurerAdapter { + private final UserService userService; + private final BCryptPasswordEncoder passwordEncoder; + private final Environment environment; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); - http.authorizeHttpRequests().antMatchers("/users/**").permitAll(); + + http.authorizeRequests().antMatchers("/actuator/**").permitAll(); + http.authorizeRequests().antMatchers("/health-check/**").permitAll(); + http.authorizeRequests().antMatchers("/**") + .hasIpAddress("192.168.0.2") + .and() + .addFilter(getAuthenticationFilter()); + http.headers().frameOptions().disable(); } + + private AuthenticationFilter getAuthenticationFilter() throws Exception { + AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager(), userService, environment); + authenticationFilter.setAuthenticationManager(authenticationManager()); + return authenticationFilter; + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userService).passwordEncoder(passwordEncoder); + } } diff --git a/user-service/src/main/java/com/roy/springcloud/userservice/service/UserService.java b/user-service/src/main/java/com/roy/springcloud/userservice/service/UserService.java index 52de389..0a1165b 100644 --- a/user-service/src/main/java/com/roy/springcloud/userservice/service/UserService.java +++ b/user-service/src/main/java/com/roy/springcloud/userservice/service/UserService.java @@ -1,11 +1,13 @@ package com.roy.springcloud.userservice.service; import com.roy.springcloud.userservice.dto.UserDto; +import org.springframework.security.core.userdetails.UserDetailsService; import java.util.List; -public interface UserService { +public interface UserService extends UserDetailsService { void createUser(UserDto userDTO); UserDto getUserByUserId(String userId); + UserDto getUserDetailsByEmail(String email); List getAllUser(); } diff --git a/user-service/src/main/java/com/roy/springcloud/userservice/service/impl/UserServiceImpl.java b/user-service/src/main/java/com/roy/springcloud/userservice/service/impl/UserServiceImpl.java index 5598339..3c651eb 100644 --- a/user-service/src/main/java/com/roy/springcloud/userservice/service/impl/UserServiceImpl.java +++ b/user-service/src/main/java/com/roy/springcloud/userservice/service/impl/UserServiceImpl.java @@ -5,11 +5,13 @@ import com.roy.springcloud.userservice.dto.UserDto; import com.roy.springcloud.userservice.repository.UserRepository; import com.roy.springcloud.userservice.service.UserService; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.UUID; @@ -36,6 +38,13 @@ public class UserServiceImpl implements UserService { return toObject(savedUser, UserDto.class); } + @Override + public UserDto getUserDetailsByEmail(String email) { + User savedUser = userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + return toObject(savedUser, UserDto.class); + } + @Override public List getAllUser() { Iterable savedUsers = userRepository.findAll(); @@ -45,4 +54,15 @@ public class UserServiceImpl implements UserService { }); return response; } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + User savedUser = userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + return new org.springframework.security.core.userdetails.User( + savedUser.getEmail(), savedUser.getEncryptedPassword(), + true, true, true, true, + Collections.emptyList() + ); + } } diff --git a/user-service/src/main/java/com/roy/springcloud/userservice/vo/request/LoginRequest.java b/user-service/src/main/java/com/roy/springcloud/userservice/vo/request/LoginRequest.java new file mode 100644 index 0000000..27ea447 --- /dev/null +++ b/user-service/src/main/java/com/roy/springcloud/userservice/vo/request/LoginRequest.java @@ -0,0 +1,20 @@ +package com.roy.springcloud.userservice.vo.request; + +import lombok.Data; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@Data +public class LoginRequest { + @Email + @NotBlank(message = "Email cannot be blank") + @Size(min = 2, message = "Email not be less than two characters") + private String email; + + @NotNull(message = "Password cannot be null") + @Size(min = 8, message = "Password must be equal or grater than 8 characters and less than 16 characters") + private String password; +} diff --git a/user-service/src/main/resources/application.yml b/user-service/src/main/resources/application.yml index bd64ab9..f16e4c6 100644 --- a/user-service/src/main/resources/application.yml +++ b/user-service/src/main/resources/application.yml @@ -23,5 +23,13 @@ eureka: service-url: defaultZone: http://localhost:8761/eureka +token: + expiration_time: 864000000 + secret: user_token + greeting: - message: Welcome to the Simple E-Commerce(User Service). \ No newline at end of file + message: Welcome to the Simple E-Commerce(User Service). + +logging: + lelvel: + com.roy.springcloud.userservice: DEBUG \ No newline at end of file