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 {
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'
}

View File

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

View File

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

View File

@@ -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'

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();
log.info("Global Filter Message: {}", config.getMessage());
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(() -> {
if (config.isShowPostLogger()) {

View File

@@ -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>.*), /$\{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
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/**
- Method=GET
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*), /$\{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
# - 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

View File

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

View File

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

View File

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

View File

@@ -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'

View File

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

View File

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

View File

@@ -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<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.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<UserDto> getAllUser() {
Iterable<User> 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()
);
}
}

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:
defaultZone: http://localhost:8761/eureka
token:
expiration_time: 864000000
secret: user_token
greeting:
message: Welcome to the Simple E-Commerce(User Service).
logging:
lelvel:
com.roy.springcloud.userservice: DEBUG