[SpringCloud MSA]Users MicroService(로그인 제외)
이 글은 인프런 강의 "Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA)"를 정리한 글입니다. 문제/오류가 있을 시 댓글로 알려주시면 감사드리겠습니다
Ecommerce 어플리케이션 microservice로 만들기
- Catalog, User, Order 3개의 서비스를 만든다
- Eureka, API Gateway Server 사용
- 마이크로서비스간 메시지 발행 및 구독을 위해 Kafka 사용(Queueing)
- Configuration 도 외부에서 관리할 것이다(Config Server)
User Microservice
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.8-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.zzarbttoo</groupId>
<artifactId>user-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>user-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
<spring-cloud.version>2020.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.3.176</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>2.3.8</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
... 생략
- Eureka Discovery Client 설정
- DB는 H2를 이용했으며 1.3 대를 이용해 테이블이 자동생성되도록 했다(상위 버전에서는 자동생성이 되지 않는다)
- JPA를 이용하기 위해 jpa 관련 dependecy 추가
- 데이터 변환을 편리하게 하기 위해 model mapper 추가
application.yml
server:
port: 0
spring:
application:
name: user-service
h2:
console:
enabled: true
settings:
web-allow-others: true #dashboard 설정
path: /h2-console #dashboard path
datasource:
driver-class-name: org.h2.Driver
url : jdbc:h2:mem:testdb
#username: sa
#password : 1234
eureka:
instance:
instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}}
client:
register-with-eureka: true
fetch-registry: true #외부 검색 가능하도록
service-url:
defaltZone: http://192.168.0.20:8761/eureka #server의 위치 지정
greeting:
message: Welcome to the Simple E-commerce
- /h2-console로 요청을 하면 h2 dashboard를 볼 수 있도록 설정
@SpringbootApplication 부분
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
- @EnableDiscoveryClient 를 통해 유레카 클라이언트 등록
UserController
package com.zzarbttoo.userservice.controller;
import com.netflix.discovery.converters.Auto;
import com.zzarbttoo.userservice.dto.UserDto;
import com.zzarbttoo.userservice.service.UserService;
import com.zzarbttoo.userservice.vo.Greeting;
import com.zzarbttoo.userservice.vo.RequestUser;
import com.zzarbttoo.userservice.vo.ResponseUser;
import org.dom4j.util.UserDataAttribute;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/")
public class UserController {
private Environment env;
private UserService userService;
@Autowired
private Greeting greeting;
@Autowired //bean이 자동 주입
public UserController(Environment env, UserService userService) {
this.env = env;
this.userService = userService;
}
@GetMapping("/health_check")
public String status(){
return "It's Working in User Service";
}
@GetMapping("/welcome")
public String welcome(){
//return env.getProperty("greeting.message");
return greeting.getMessage();
}
// 회원가입
@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);
//created = 201로 반환한다
return ResponseEntity.status(HttpStatus.CREATED).body(responseUser);
}
}
- Environment, UserService를 직접 @Autowired 하는 것 보다는 생성자를 이용해 주입하는 것이 권장된다
- 혹은 직접 주입하는 파일을 따로 만들어서 이용하는 것이 좋다
@Component @Data //@AllArgsConstructor //모든 생성자 //@NoArgsConstructor //default 생성자 public class Greeting { @Value("${greeting.message}") private String message; }
- /welcome을 이용하면 Environment 객체를 이용해 application.yml 파일의
- @RequestBody를 이용해 body 값을 RequestUser 객체로 받음
- userService로 추출한 UserDto를 전송해 DB에 insert를 하도록 함
- userDto 객체는 다시 ResponseUser 객체로 바꾸어 client측에 전송함
- HttpStatus 값은 단순히 Success(200)으로 하는 것이 아니라 HttpStatus.CREATED(201)로 하는 것이 정확하다
- Request 값(UserRequest), 통신할 때 사용하는 DTO(UserDTO), DB 엔티티 값(UserEntity), Response 값(ResponseUser) 값은 안의 내용이 같더라 하더라도 분리해서 사용해야한다
- ModelMapper 객체를 이용해 값이 같은 객체를 빠르게 변환하도록 함
[RequestUser -> UserDTO -> UserEntity] -> ResponseUser
RequestUser
package com.zzarbttoo.userservice.vo;
import lombok.Data;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@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 chracters")
private String name;
@NotNull(message = "Password cannot be null")
@Size(min = 8, message = "Password must be equal or greater than 8 letters")
private String pwd;
}
- post 요청을 할 때 body 값들을 RequestUser로 받는다
- 최초로 받는 값이기 때문에 Validation 체크를 해준다
UserDTO
package com.zzarbttoo.userservice.dto;
import lombok.Data;
import java.util.Date;
@Data
public class UserDto {
private String email;
private String name;
private String pwd;
private String userId;
private Date createAt;
private String encryptedPwd;
}
- 프로그램 내부에서 이동을 할 때 사용이되는 값
UserEntity
package com.zzarbttoo.userservice.jpa;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
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;
}
- DB에 실질적으로 저장되는 Entity이며 Table과 매칭된다
ResponseUser
package com.zzarbttoo.userservice.vo;
import lombok.Data;
@Data
public class ResponseUser {
private String email;
private String name;
private String userId;
}
- Response 할 때 UserDto를 전송하는 것이 아니라 필요한 값만 전송하기 위해 ResponseUser 객체를 따로 만들었다
Service
package com.zzarbttoo.userservice.service;
import com.zzarbttoo.userservice.dto.UserDto;
public interface UserService {
UserDto createUser(UserDto userDto);
}
- Service interface 먼저 선언
ServiceImpl
package com.zzarbttoo.userservice.service;
import com.netflix.discovery.converters.Auto;
import com.zzarbttoo.userservice.dto.UserDto;
import com.zzarbttoo.userservice.jpa.UserEntity;
import com.zzarbttoo.userservice.jpa.UserRepository;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.modelmapper.spi.MatchingStrategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class UserServiceImpl implements UserService{
@Autowired
UserRepository userRepository;
@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("encrypted_password");
userRepository.save(userEntity);
//DB에 저장하기 위해서는 UserEntity 가 필요
//Mapper : DTO Class -> Entity Class
UserDto returnUserDto = mapper.map(userEntity, UserDto.class);
return returnUserDto;
}
}
- Service를 상속받은 ServiceImpl
- @Service 를 이용해 서비스 로직을 처리하는 것을 알려준다
- 여기에서 비즈니스 로직 처리를 한다
- UserDto에 랜덤 ID를 추가하고, UserEntity로 변환한다
- 이후 이를 DB에 저장하도록 Repository로 전달
- return 할 때는 UserDto로 값을 다시 변환해서 보내도록 한다
Repository
package com.zzarbttoo.userservice.jpa;
import org.springframework.data.repository.CrudRepository;
//CRUD 작업을 하기 때문에 CrudRepository 상속
//Entity 정보 입력, 기본키 classType 입력
public interface UserRepository extends CrudRepository<UserEntity, Long> {
}
- CrudRepository<Entity 객체, 기본키의 데이터형태>를 상속해서 CRUD, JPA를 사용할 수 있도록 한다
- save 등 기본 명령어는 선언을 하지 않고 사용할 수 있다
- 조금 더 복잡할 경우 선언해서 사용하면 된다
| 실행
- 유레카 서버를 실행하고, user-service를 실행해 클라이언트로 등록
- user-service가 실행된 포트에서 /h2-console로 들어가면 h2 대시보드 접속 가능
- 회원가입 요청 후 select 실행을 하면 데이터가 생성된 것을 확인할 수 있다
User Microservice - Security
Authentication(인증) + Authorization(권한)
패스워드를 해싱해서 저장할 것이다
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- spring security 의존성 추가
WebSecurity
package com.zzarbttoo.userservice.security;
import org.springframework.context.annotation.Configuration;
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;
@Configuration //configuration은 우선순위가 높다
@EnableWebSecurity //webSecurity로 등록한다는 얘기
public class WebSecurity extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
// /users/로 시작하는 모든 것은 permit
http.authorizeRequests().antMatchers("/users/**").permitAll();
http.headers().frameOptions().disable(); //h2가 frame별로 구분되어있기 때문에 이것을 설정하지 않으면 에러 발생
}
}
- @Configuration은 우선순위가 높다
- @EnableWebSecurity 를 이용해 WebSecurity로 등록을 해준다
- csrf 방지
- /users/~~ 로 들어오는 요청은 모두 허용
- frameOptions 를 추가해서 h2 작동할 수 있도록 한다(h2가 frame 별로 쪼개져있기 때문에 이를 허용)
UserServiceImpl
package com.zzarbttoo.userservice.service;
import com.netflix.discovery.converters.Auto;
import com.zzarbttoo.userservice.dto.UserDto;
import com.zzarbttoo.userservice.jpa.UserEntity;
import com.zzarbttoo.userservice.jpa.UserRepository;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.modelmapper.spi.MatchingStrategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class UserServiceImpl implements UserService{
UserRepository userRepository;
BCryptPasswordEncoder passwordEncoder;
//인자들도 Bean 등록이 되어야 메모리에 적재를 할 수 있음
//userRepository는 Bean 등록이 되어있는데, BCryptPasswordEncoder은 등록이 되어있지 않음
//가장 먼저 등록이 진행되는 곳에 BCryptPasswordEncoder Bean 등록
@Autowired
public UserServiceImpl(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder){
this.userRepository = userRepository;
this.passwordEncoder = 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("encrypted_password");
userEntity.setEncryptedPwd(passwordEncoder.encode(userDto.getPwd()));
userRepository.save(userEntity);
//DB에 저장하기 위해서는 UserEntity 가 필요
//Mapper : DTO Class -> Entity Class
UserDto returnUserDto = mapper.map(userEntity, UserDto.class);
return returnUserDto;
}
}
- 비밀번호 암호화를 위해 ServiceImple 부분 수정
- password를 해싱하기 위해 Bycrpt 알고리즘 사용(랜덤 salt를 부여해 여러번 Hash를 적용한 암호화 방식)
- BCryptPassword를 @Autowired 해야하는데 Bean 등록이 안되어있다(Repository의 경우 CRUDRepository를 상속받아 자동으로 bean 등록이 되어있는 듯 하다)
- 그래서 가장 먼저 Bean 등록이 되는 곳에 BCryptPassword Bean을 등록시킨다
@SpringBootApplication 부분
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
//초기화를 위해 인자의 bean도 초기화되어 메모리에 적재되어야 한다
//하지만 선언할 곳이 없다(legacy에서는 xml로 bean 등록)
// springboot에서는 가장 먼저 실행되는 springbootApplication에 Bean 등록을 해줘야한다
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
- 가장 먼저 등록이 되는 @SpringBootApplication 부분에 @Bean 등록을 진행
| 실행
- security 설정을 하면 실행 중간 console 창에 security password가 뜨는 것을 확인할 수 있다
- 비밀번호가 Hashing 되어있는 것을 확인할 수 있다
Author And Source
이 문제에 관하여([SpringCloud MSA]Users MicroService(로그인 제외)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@zzarbttoo/SpringCloud-MSAUsers-MicroService로그인-제외저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)