Spring Security 적용 중간 커밋

This commit is contained in:
roy-zz
2022-04-22 00:47:19 +09:00
parent c328697e65
commit 95ecd29673
19 changed files with 287 additions and 35 deletions

View File

@@ -1,5 +1,5 @@
plugins { 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' id 'io.spring.dependency-management' version '1.0.11.RELEASE'
} }

View File

@@ -1,5 +1,5 @@
ext { ext {
set('springCloudVersion', "2021.0.1") set('springCloudVersion', "2020.0.5")
} }
dependencies { dependencies {

View File

@@ -1,5 +1,5 @@
ext { ext {
set('springCloudVersion', "2021.0.1") set('springCloudVersion', "2020.0.5")
} }
dependencies { dependencies {

View File

@@ -1,10 +1,12 @@
ext { ext {
set('springCloudVersion', "2021.0.1") set('springCloudVersion', "2020.0.5")
} }
dependencies { dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway' implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-test'

View File

@@ -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<AuthorizationHeaderFilter.Config> {
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<Void> 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 {}
}

View File

@@ -23,7 +23,7 @@ public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Conf
ServerHttpResponse response = exchange.getResponse(); ServerHttpResponse response = exchange.getResponse();
log.info("Global Filter Message: {}", config.getMessage()); log.info("Global Filter Message: {}", config.getMessage());
if (config.isShowPreLogger()) { if (config.isShowPreLogger()) {
log.info("Global Filter Start: request id -> {}", request.getId()); log.info("Global Filter Start: request uri -> {}", request.getLocalAddress());
} }
return chain.filter(exchange).then(Mono.fromRunnable(() -> { return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isShowPostLogger()) { if (config.isShowPostLogger()) {

View File

@@ -10,6 +10,9 @@ eureka:
service-url: service-url:
defaultZone: http://localhost:8761/eureka defaultZone: http://localhost:8761/eureka
token:
secret: user_token
spring: spring:
application: application:
name: gateway-service name: gateway-service
@@ -22,10 +25,31 @@ spring:
showPreLogger: true showPreLogger: true
showPostLogger: true showPostLogger: true
routes: routes:
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/login
- Method=POST
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*), /$\{segment}
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/users
- Method=POST
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*), /$\{segment}
- id: user-service - id: user-service
uri: lb://USER-SERVICE uri: lb://USER-SERVICE
predicates: predicates:
- Path=/user-service/** - Path=/user-service/**
- Method=GET
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*), /$\{segment}
- AuthorizationHeaderFilter
- id: catalog-service - id: catalog-service
uri: lb://CATALOG-SERVICE uri: lb://CATALOG-SERVICE
predicates: predicates:
@@ -34,25 +58,29 @@ spring:
uri: lb://ORDER-SERVICE uri: lb://ORDER-SERVICE
predicates: predicates:
- Path=/order-service/** - Path=/order-service/**
- id: test-server-1 # - id: test-server-1
uri: lb://TEST-SERVER-1 # uri: lb://TEST-SERVER-1
predicates: # predicates:
- Path=/test-server-1/** # - Path=/test-server-1/**
filters: # filters:
- name: CustomFilter # - name: CustomFilter
- name: LoggingFilter # - name: LoggingFilter
args: # args:
message: TEST-SERVER-1 # message: TEST-SERVER-1
showPreLogger: true # showPreLogger: true
showPostLogger: true # showPostLogger: true
- id: test-server-2 # - id: test-server-2
uri: lb://TEST-SERVER-2 # uri: lb://TEST-SERVER-2
predicates: # predicates:
- Path=/test-server-2/** # - Path=/test-server-2/**
filters: # filters:
- name: CustomFilter # - name: CustomFilter
- name: LoggingFilter # - name: LoggingFilter
args: # args:
message: TEST-SERVER-2 # message: TEST-SERVER-2
showPreLogger: true # showPreLogger: true
showPostLogger: true # showPostLogger: true
logging:
lelvel:
com.roy.springcloud.gateway: DEBUG

View File

@@ -1,5 +1,5 @@
ext { ext {
set('springCloudVersion', "2021.0.1") set('springCloudVersion', "2020.0.5")
} }
dependencies { dependencies {

View File

@@ -1,5 +1,5 @@
ext { ext {
set('springCloudVersion', "2021.0.1") set('springCloudVersion', "2020.0.5")
} }
dependencies { dependencies {

View File

@@ -1,5 +1,5 @@
ext { ext {
set('springCloudVersion', "2021.0.1") set('springCloudVersion', "2020.0.5")
} }
dependencies { dependencies {

View File

@@ -1,5 +1,5 @@
ext { ext {
set('springCloudVersion', "2021.0.1") set('springCloudVersion', "2020.0.5")
} }
dependencies { dependencies {
@@ -10,6 +10,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'

View File

@@ -16,7 +16,7 @@ import java.util.List;
import static com.roy.springcloud.util.mapper.MapperUtil.toObject; import static com.roy.springcloud.util.mapper.MapperUtil.toObject;
@RestController @RestController
@RequestMapping("/user-service") @RequestMapping("/")
@RequiredArgsConstructor @RequiredArgsConstructor
public class UserController { public class UserController {
private final Environment environment; private final Environment environment;

View File

@@ -7,5 +7,6 @@ import java.util.Optional;
public interface UserRepository extends CrudRepository<User, Long> { public interface UserRepository extends CrudRepository<User, Long> {
Optional<User> findByUserId(String userId); Optional<User> findByUserId(String userId);
Optional<User> findByEmail(String email);
Iterable<User> findAll(); Iterable<User> findAll();
} }

View File

@@ -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());
}
}

View File

@@ -1,18 +1,45 @@
package com.roy.springcloud.userservice.security; 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.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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurity extends WebSecurityConfigurerAdapter { public class WebSecurity extends WebSecurityConfigurerAdapter {
private final UserService userService;
private final BCryptPasswordEncoder passwordEncoder;
private final Environment environment;
@Override @Override
protected void configure(HttpSecurity http) throws Exception { protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable(); 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(); 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);
}
} }

View File

@@ -1,11 +1,13 @@
package com.roy.springcloud.userservice.service; package com.roy.springcloud.userservice.service;
import com.roy.springcloud.userservice.dto.UserDto; import com.roy.springcloud.userservice.dto.UserDto;
import org.springframework.security.core.userdetails.UserDetailsService;
import java.util.List; import java.util.List;
public interface UserService { public interface UserService extends UserDetailsService {
void createUser(UserDto userDTO); void createUser(UserDto userDTO);
UserDto getUserByUserId(String userId); UserDto getUserByUserId(String userId);
UserDto getUserDetailsByEmail(String email);
List<UserDto> getAllUser(); List<UserDto> getAllUser();
} }

View File

@@ -5,11 +5,13 @@ import com.roy.springcloud.userservice.dto.UserDto;
import com.roy.springcloud.userservice.repository.UserRepository; import com.roy.springcloud.userservice.repository.UserRepository;
import com.roy.springcloud.userservice.service.UserService; import com.roy.springcloud.userservice.service.UserService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -36,6 +38,13 @@ public class UserServiceImpl implements UserService {
return toObject(savedUser, UserDto.class); 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 @Override
public List<UserDto> getAllUser() { public List<UserDto> getAllUser() {
Iterable<User> savedUsers = userRepository.findAll(); Iterable<User> savedUsers = userRepository.findAll();
@@ -45,4 +54,15 @@ public class UserServiceImpl implements UserService {
}); });
return response; 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()
);
}
} }

View File

@@ -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;
}

View File

@@ -23,5 +23,13 @@ eureka:
service-url: service-url:
defaultZone: http://localhost:8761/eureka defaultZone: http://localhost:8761/eureka
token:
expiration_time: 864000000
secret: user_token
greeting: greeting:
message: Welcome to the Simple E-Commerce(User Service). message: Welcome to the Simple E-Commerce(User Service).
logging:
lelvel:
com.roy.springcloud.userservice: DEBUG