본 게시물은 스스로의 공부를 위한 글입니다.
틀린 내용이 있을 수 있습니다.
POST /login
: 로그인 기능. 인증이 필요 없음POST /users
: 회원가입 기능. 인증이 필요 없음GET /health_check
: 서비스 작동 여부 확인. 인증(로그인)이 필요함.BCryptPasswordEncoder
로 암호화 후 내장 h2 db에 저장@EnableEurekaServer
추가@SpringBootApplication
@EnableEurekaServer //추가!
public class DiscoveryServiceApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryserviceApplication.class, args);
}
}
server:
port: 8761
spring:
application:
name: discoveryService
eureka:
client:
register-with-eureka: false
fetch-registry: false
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
server:
port: 8000
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
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
token:
secret: user_token
eureka.client.service-url.defaultZone: http://localhost:8761/eureka
POST /login
: 로그인 기능. 인증이 필요 없음POST /users
: 회원가입 기능. 인증이 필요 없음GET /health_check
: 서비스 작동 여부 확인. 인증(로그인)이 필요함.spring.cloud.gateway.routes
id
: 해당 라우터의 고유 식별자uri
: 포워딩될 주소. lb://USER-SERVICE
처럼 등록하면 eureka에 USER-SERVICE
란 이름의 Application으로 연결된다.predicates
: 조건식path
, Method
: 설정한 주소와 메서드로 요청이 들어올경우에 적용filters
: 기본 제공 필터를 사용하거나 커스텀 필터를 적용할 수 있다.RemoveRequestHeader
: 설정한 이름의 RequestHeader 제거RewritePath
: 요청 path를 재작성 후 해당 서비스에게 전달해준다./user-service/health_check
로 들어온 요청은 /health_check
로 서비스에게 요청한다.AuthorizationHeaderFilter
: 아래에서 작성할 커스텀 필터. 인증 관련 필터이다.AuthorizationHeaderFilter
)@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
Environment env;
public AuthorizationHeaderFilter(Environment env) {
super(Config.class);
this.env = env;
}
public static class Config {}
@Override
public GatewayFilter apply(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 boolean isJwtValid(String jwt) {
boolean returnValue = true;
String subject = null;
try {
subject = Jwts.parser().setSigningKey(env.getProperty("token.secret"))
.parseClaimsJws(jwt)
.getBody()
.getSubject();
} catch (Exception ex) {
log.info("jwt 변환 중 예외 발생");
returnValue = false;
}
if (subject == null || subject.isEmpty()) returnValue = false;
return returnValue;
}
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
log.error(err);
return response.setComplete();
}
}
bearer ~~~
형식으로 들어오기 때문에, 앞에 bearer
문자를 빼버려야한다.requst
에서 관련 헤더가 있는지 확인 HttpStatus.UNAUTHORIZED
(401) 을 보냄<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.3.176</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>2.3.8</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
@Data @Entity
@Table(name="users")
public class UserEntity {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false, length = 50, unique = true)
private String email;
@Column(nullable = false, length = 50)
private String name;
@Column(nullable = false, unique = true)
private String userId;
@Column(nullable = false, unique = true)
private String encryptedPwd;
}
@Data
public class UserDto {
private String email;
private String name;
private String pwd;
private String userId;
private Date createdAt;
private String encryptedPwd;
private List<ResponseOrder> orders;
}
@Data
@JsonInclude(JsonInclude.Include.NON_NULL) //null인 데이터는 리턴하지 Json 결과에 포함되지 않는다.
public class ResponseUser {
private String email;
private String name;
private String userId;
private List<ResponseOrder> orders;
}
@Data
public class RequestUser {
@NotNull(message = "Email cannot be null")
@Size(min = 2, message = "Email not be less than two characters")
@Email
private String email;
@NotNull(message = "Name cannot be null")
@Size(min = 2, message = "Name not be less than two characters")
private String name;
@NotNull(message = "Password cannot be null")
@Size(min = 8, message = "Password must be equal or grater than 9 characters")
private String pwd;
}
public interface UserRepository extends CrudRepository<UserEntity, Long> {
UserEntity findByUserId(String userId);
UserEntity findByEmail(String username);
}
/login
은 spring security에서 기본 제공하므로 따로 만들 필요없다.@RestController
@RequestMapping("/")
@RequiredArgsConstructor
public class UserController {
private final Environment env;
private final Greeting greeting;
private final UserService userService;
@GetMapping("/health_check")
public String status(){
return String.format("It's Working in User Service on PORT %S",
env.getProperty("local.server.port"));
}
@PostMapping("/users")
public ResponseEntity<ResponseUser> createUser(@RequestBody RequestUser user){
ModelMapper mapper=new ModelMapper();
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
UserDto userDto=mapper.map(user, UserDto.class);
userService.createUser(userDto);
ResponseUser responseUser=mapper.map(userDto, ResponseUser.class);
return ResponseEntity.status(HttpStatus.CREATED).body(responseUser);
}
}
public interface UserService extends UserDetailsService {
UserDto createUser(UserDto userDto);
UserDto getUserDetailByEmail(String username);
}
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
@Override
public UserDto createUser(UserDto userDto) {
userDto.setUserId(UUID.randomUUID().toString());
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
UserEntity userEntity = mapper.map(userDto, UserEntity.class); //변환 작업
userEntity.setEncryptedPwd(passwordEncoder.encode(userDto.getPwd()));
userRepository.save(userEntity);
UserDto returnUserDto = mapper.map(userEntity, UserDto.class);
return returnUserDto;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userEntity = userRepository.findByEmail(username);
if(userEntity==null) throw new UsernameNotFoundException(username);
return new User(userEntity.getEmail(), userEntity.getEncryptedPwd(),
true, true, true,
true, new ArrayList<>());
}
@Override
public UserDto getUserDetailByEmail(String email) {
UserEntity userEntity = userRepository.findByEmail(email);
if(userEntity==null) new UsernameNotFoundException(email);
UserDto userDto=new ModelMapper().map(userEntity, UserDto.class);
return userDto;
}
}
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
@Configuration
@EnableWebSecurity //WebSecurity 사용
@RequiredArgsConstructor
public class WebSecurity extends WebSecurityConfigurerAdapter {
private final UserService userService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final Environment env;
@Override //권한에 관련된 설정
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().antMatchers("/**")
.hasIpAddress("192.168.43.84") // 로컬 ip
.and()
.addFilter(getAuthenticationFilter());
http.headers().frameOptions().disable(); //h2 오류 방지
}
private AuthenticationFilter getAuthenticationFilter() throws Exception {
AuthenticationFilter authenticationFilter =
new AuthenticationFilter(authenticationManager(),userService, env);
return authenticationFilter;
}
@Override //인증에 관련된 설정
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService) //userDetailService 가 구현된 클래스 등록
.passwordEncoder(bCryptPasswordEncoder); //사용자가 입력한 패스워드는 자동으로 암호화됨
}
}
WebSecurityConfigurerAdapter
을 extends해야함configure(HttpSecurity http)
에서 요청 주소, ip에 따른 설정을 할 수 있다. 위의 경우 필터 적용 getAuthenticationFilter()
필터의 경우 AuthenticationFilter
객체(아래에서 작성) 생성 후 리턴 configure(AuthenticationManagerBuilder auth)
: 인증과 관련된 설정AuthenticationFilter
작성@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private UserService userService;
private Environment env;
public AuthenticationFilter(AuthenticationManager authenticationManager,
UserService userService, Environment env) {
super.setAuthenticationManager(authenticationManager);
this.userService = userService;
this.env = env;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
try {
RequestLogin creds = new ObjectMapper().readValue(request.getInputStream(), RequestLogin.class);
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(creds.getEmail(), creds.getPassword(), new ArrayList<>());
return getAuthenticationManager().authenticate(token);
} 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 userDetails = userService.getUserDetailByEmail(userName);
String token = Jwts.builder()
.setSubject(userDetails.getUserId())
.setExpiration(new Date(System.currentTimeMillis()+Long.parseLong(env.getProperty("token.expiration_time"))))
.signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret"))
.compact();
response.addHeader("token", token);
response.addHeader("userId", userDetails.getUserId());
}
}
attemptAuthentication
으로 들어온다.attemptAuthentication()
과 successfulAuthentication()
구현 (인증과 관련된 작업)attemptAuthentication()
: 전달받은 데이터를 바탕으로 Token 생성 (인증)UserDetailService
의 loadUserByUsername
에서 repository에서 userEntity 를 찾은 후 인증에 성공하면 successfulAuthentication
이 실행된다.successfulAuthentication()
: 인증 성공후 로직server:
port: 0
spring:
application:
name: user-service
h2:
console:
enabled: true
settings:
web-allow-others: true
path: /h2-console
datasource:
dirver-class-name: org.h2.Driver
url: jdbc:h2:mem:testdb
eureka:
instance:
instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}}
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://127.0.0.1:8761/eureka
token:
expiration_time: 8640000 # 하루
secret: user_token
http://localhost:8761
에서 Service Discovery에 API Gateway와 User-service 등록 확인/health_check
요청인프런의 'Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA)(Dowon Lee)'을 스스로 정리한 글입니다.
자세한 내용은 해당 강의를 참고해주세요.