@Data
@AllArgsConstructor
@NoArgsConstructor
public class ReadDto {
public static final int MODEL_LENGTH = 35;
private Integer dcv;
private Integer dca;
private Integer dcw;
}
여기서 만약, 필드 별 필요한 데이터의 타입 등이 있을 경우 아래와 같이 어노테이션을 만들어서 Dto 필드에 넣어준다.
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ModbusValue {
int offset();
ModbusValueType type() default UINT_16;
}
바뀐 Dto
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ReadDto {
public static final int MODEL_LENGTH = 35;
@ModbusValue(offset = 0, type = INT_16)
private Integer dcv;
@ModbusValue(offset = 1, type = INT_16)
private Integer dca;
@ModbusValue(offset = 2, type = INT_16)
private Integer dcw;
}
특정한 Dto가 아닌, 제네릭으로 받아서 Dto마다의 결과값을 반환하기 위해 제네릭 메소드로 생성한다.
public <T> T getDataObject(T object) {
try {
// Dto의 static 필드값이 필요한 경우
int quantity = object.getClass().getField(MODEL_LENGTH)
.getInt(object);
Class<?> modbusEntity = object.getClass();
Field[] columns = modbusEntity.getDeclaredFields();
// 필드 별 로직 구성
Arrays.stream(columns)
.parallel()
.forEach(column -> {
try {
ModbusValue annotation = column.getAnnotation(ModbusValue.class);
if (annotation == null) {
return;
}
int offset = annotation.offset();
var type = annotation.type();
Object value = getModbusValue(type, offset);
column.setAccessible(true);
column.set(object, castType(value, column.getType().getSimpleName()));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
});
return object; // 최종 DTO 객체 반환
} catch (Exception e) {
log.error("[ getDtoWithModbusRawData Error ] message: {}", e.getMessage(), e);
throw new RuntimeException(e);
}
}
이렇게 하면, 원하는 Dto의 필드들에 값을 주입하고 다시 Dto를 반환하게 할 수 있다.
emailCertNumberMap: 이메일 검증 시에 쓰이며, Dto에는 code와 createdAt이 있다. 검증 로직에서 아래 상수로 시간이 지나면 검증이 실패하게끔 만들어준다. 이 포스트에서는 인메모리로 다루어봤다.
VERIFY_TIMEOUT: 서버에서의 이메일 검증 만료 시간
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class UserService {
public static final int VERIFY_TIMEOUT = 35;
private final UserRepository repository;
private final EmailService emailService;
private final UserGroupRepository userGroupRepository;
private final PasswordEncoder passwordEncoder;
@Getter
public Map<String, MfaEmailVerifyMapDto> emailCertNumberMap = new HashMap<>();
...
public void registerMfaTypes(MfaRegisterDto mfaRegisterDto) {
User user = repository.findByUsernameAndDeletedAtIsNull(mfaRegisterDto.getId())
.orElseThrow();
List<MfaTypesDto> mfaTypes = user.getMfaTypes();
List<MfaType> mfaTypeList = mfaTypes
.stream()
.map(MfaTypesDto::getMfaType)
.toList();
if (mfaTypeList.contains(mfaRegisterDto.getMfaType())) {
throw new IllegalArgumentException("이미 등록된 인증 입니다.");
}
mfaTypes.add(new MfaTypesDto(mfaRegisterDto.getMfaType(), LocalDateTime.now()));
user.updateIsUsingMfa(true);
user.updateMfaTypes(mfaTypes);
}
public void deleteMfaTypes(MfaDeleteDto deleteDto) {
User user = repository.findByUsernameAndDeletedAtIsNull(deleteDto.getId())
.orElseThrow();
List<MfaTypesDto> mfaTypes = user.getMfaTypes();
List<MfaType> mfaTypeList = mfaTypes
.stream()
.map(MfaTypesDto::getMfaType)
.toList();
if (!mfaTypeList.contains(deleteDto.getMfaType())) {
throw new IllegalArgumentException("등록되지 않은 인증 입니다.");
}
mfaTypes.removeIf(dto -> dto.getMfaType() == deleteDto.getMfaType());
if (mfaTypes.size() == 0) {
user.updateIsUsingMfa(false);
}
user.updateMfaTypes(mfaTypes);
}
public String registerAuthenticator(MfaOtpRegisterDto dto) {
return repository
.findByUsernameAndDeletedAtIsNull(dto.getId())
.orElseThrow()
.getSecret();
}
public void verifyAuthenticatorCode(MfaOtpVerifyDto dto) {
String name = dto.getEmail();
User user = repository.findByUsernameAndDeletedAtIsNull(name).orElseThrow();
Totp totp = new Totp(user.getSecret());
if (!isValidLong(dto.getCode()) || !totp.verify(dto.getCode())) {
throw new AuthorizationException("Code Invalid");
}
}
private boolean isValidLong(String code) {
try {
Long.parseLong(code);
} catch (NumberFormatException e) {
return false;
}
return true;
}
public void sendAuthenticationCodeByEmail(String userId) {
repository.findByUsernameAndDeletedAtIsNull(userId).orElseThrow();
Random num = new Random();
StringBuilder randomNumber = new StringBuilder();
for (int i = 0; i < 6; i++) {
randomNumber.append(num.nextInt(10));
}
EmailSendDto emailSendDto = new EmailSendDto(
AUTH,
"이메일 서버",
포트,
"이메일 주소",
userId,
"",
"",
true,
TLS_V_1_2,
"인증번호",
"인증번호는 " + randomNumber + " 입니다.",
null
);
emailService.sendMail(emailSendDto);
this.emailCertNumberMap.put(
userId,
new MfaEmailVerifyMapDto(
randomNumber.toString(),
LocalDateTime.now()
)
);
}
public void certEmail(MfaEmailVerifyRequestDto mfaEmailVerifyRequestDto) {
String name = mfaEmailVerifyRequestDto.getEmail();
MfaEmailVerifyMapDto mapDto = this.emailCertNumberMap.get(name);
Duration between = Duration.between(LocalDateTime.now(), mapDto.getCreatedAt());
if (Math.abs(between.getSeconds()) > VERIFY_TIMEOUT) {
throw new AuthorizationException("Code Expired");
}
if (!Objects.equals(mapDto.getCode(), mfaEmailVerifyRequestDto.getCode())) {
throw new AuthorizationException("Code Invalid");
}
}
}
11. AuthService
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class AuthService {
private final UserRepository userRepository;
private final AuthenticationManager authenticationManager;
private final PasswordEncoder passwordEncoder;
@Getter
public Map<String, MfaEmailVerifyMapDto> emailCertNumberMap = new HashMap<>();
public ResponseLoginDto login(RequestLoginDto loginDto) {
User user = userRepository.findByUsernameAndDeletedAtIsNull(loginDto.getId()).orElseThrow();
if (!passwordEncoder.matches(loginDto.getPassword(), user.getPassword())) {
log.error("[Password does not match] Username: {}", user.getUsername());
throw new TokenProblemException("Password does not match");
}
if (user.getIsBlocked()) {
log.error("[User login is blocked] Username: {}", user.getUsername());
throw new AccessDeniedException("Blocked user.");
}
Authentication authentication = this.authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginDto.getId(), loginDto.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
List<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList();
List<String> groupNames = new ArrayList<>();
for (UserGroup group : user.getGroups()) {
groupNames.add(group.getName());
}
return new ResponseLoginDto(
user.getIsUsingMfa(),
user.getMfaTypes(),
user.getId(),
user.getUsername(),
user.getFirstName(),
user.getLastName(),
groupNames,
roles,
null
);
}
@Retry(value = 2)
public ResponseLoginDto refreshToken(String username) {
User user = userRepository.findByUsernameAndDeletedAtIsNull(username).orElseThrow();
Set<String> roles = new HashSet<>();
List<String> groupNames = new ArrayList<>();
for (UserGroup group : user.getGroups()) {
roles.addAll(List.of(group.getRolesList()));
groupNames.add(group.getName());
}
return new ResponseLoginDto(
user.getIsUsingMfa(),
user.getMfaTypes(),
user.getId(),
user.getUsername(),
user.getFirstName(),
user.getLastName(),
groupNames,
roles.stream().toList(),
null);
}
public long getExpiredTime(Cookie jwtCookie) {
return Timestamp.valueOf(LocalDateTime.now().plusSeconds(jwtCookie.getMaxAge()))
.getTime();
}
}
최종적으로 API를 만들기 위한 단계이다. API는 로그인 시 필요한 AuthController, 사용자 별 복합 인증 관리에 필요한 UserController 두 곳에서 사용할 것이다.
12. AuthController
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/auth")
public class AuthController {
private static final int CLIENT_VERIFY_TIMEOUT = 30;
private final AuthService service;
private final UserService userService;
private final JwtUtil jwtUtil;
@PostMapping("/login")
@EventLogging(code = SecurityEventCode.USER_LOGIN)
public CommonResponse<ResponseLoginDto> login(
HttpServletResponse response,
HttpSession session,
@Valid @RequestBody RequestLoginDto loginDto
) {
ResponseLoginDto responseLoginDto = service.login(loginDto);
if (!responseLoginDto.isUsingMfa()) {
long exp = addJwtCookie(response, loginDto.getId());
responseLoginDto.setExp(exp);
return new CommonResponse<>(responseLoginDto);
}
return new CommonResponse<>(responseLoginDto);
}
@PostMapping("/mfa/authenticator/verify")
public CommonResponse<Long> verifyAuthenticatorCode(
HttpServletResponse response,
HttpSession session,
@Valid @RequestBody MfaOtpVerifyDto dto
) {
verifySessionMfaType(session, OTP);
userService.verifyAuthenticatorCode(dto);
long exp = addJwtCookie(
response,
dto.getEmail()
);
return new CommonResponse<>(exp);
}
@PostMapping("/mfa/email/send")
public CommonResponse<Integer> sendAuthenticationCodeByEmail(
@RequestBody MfaEmailSendRequestDto mfaEmailSendRequestDto
) {
userService.sendAuthenticationCodeByEmail(mfaEmailSendRequestDto.getEmail());
return new CommonResponse<>(CLIENT_VERIFY_TIMEOUT);
}
@PostMapping("/mfa/email/verify")
public CommonResponse<Long> certEmail(
HttpServletResponse response,
HttpSession session,
@RequestBody MfaEmailVerifyRequestDto dto
) {
verifySessionMfaType(session, EMAIL);
userService.certEmail(dto);
long exp = addJwtCookie(
response,
dto.getEmail()
);
return new CommonResponse<>(exp);
}
@PostMapping("/logout")
@EventLogging(code = SecurityEventCode.USER_LOGOUT)
public CommonResponse<Boolean> logout(
HttpServletResponse response,
HttpSession session
) {
ResponseCookie jwtCookie = jwtUtil.getCleanJwtCookie();
response.addHeader(HttpHeaders.SET_COOKIE, jwtCookie.toString());
return new CommonResponse<>(true);
}
@PostMapping("/refresh")
public CommonResponse<ResponseLoginDto> refresh(
HttpServletResponse response,
HttpServletRequest request
) {
String token = this.jwtUtil.getJwtFromCookies(request);
if ((token == null) || (token.length() < 1)) {
log.error("[Invalid token] token: {}", token);
throw new TokenProblemException("Invalid token");
}
if (!jwtUtil.validateJwtToken(token)) {
log.error("[The token has expired] token: {}", token);
throw new TokenProblemException("The token has expired.");
}
String username = jwtUtil.getUsernameFromJwtToken(token);
Cookie jwtCookie = jwtUtil.generateJwtCookie(username);
response.addCookie(jwtCookie);
var exp = service.getExpiredTime(jwtCookie);
ResponseLoginDto responseLoginDto = service.refreshToken(username);
responseLoginDto.setExp(exp);
return new CommonResponse<>(responseLoginDto);
}
private void verifySessionMfaType(HttpSession session, MfaType mfaType) {
MfaType isUsingMfa = sessionUtil.getIsUsingMfa(session);
if (isUsingMfa != null && isUsingMfa != mfaType) {
throw new IllegalArgumentException("이미 다른 복합 인증이 사용중입니다.");
}
sessionUtil.setIsUsingMfa(session, mfaType);
}
private long addJwtCookie(HttpServletResponse response, String name) {
Cookie jwtCookie = jwtUtil.generateJwtCookie(name);
response.addCookie(jwtCookie);
return service.getExpiredTime(jwtCookie);
}
}
13. UserController
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/user")
@PreAuthorize("hasRole('ROLE_USER_READ') || hasRole('ROLE_USER_ADMIN')")
public class UserController {
private final UserService service;
@GetMapping
public CommonResponse<List<ResponseUserDto>> findAll() {
return new CommonResponse<>(service.findAll());
}
@GetMapping("/{id}")
public CommonResponse<ResponseUserDto> findByIndexId(
@PathVariable(value = "id") long userId) {
return new CommonResponse<>(service.findByIndexId(userId));
}
@GetMapping("/name/{username}")
public CommonResponse<Boolean> findByUsername(
@PathVariable(value = "username") String username) {
return new CommonResponse<>(service.isUserEmptyByUsername(username));
}
@PreAuthorize("hasRole('ROLE_USER_ADMIN')")
@PostMapping
@EventLogging(code = SecurityEventCode.USER_USER_CREATE)
public CommonResponse<Long> create(
@RequestBody @Valid UserCreateDto createDto) {
Long id = service.create(createDto);
createDto.setPassword("");
return new CommonResponse<>(id);
}
@PreAuthorize("hasRole('ROLE_USER_ADMIN')")
@PutMapping
@EventLogging(code = SecurityEventCode.USER_USER_UPDATE)
public CommonResponse<Boolean> updateUser(
@RequestBody @Valid UserUpdateDto updateDto) {
service.updateUser(updateDto);
return new CommonResponse<>(true);
}
@PreAuthorize("hasRole('ROLE_USER_ADMIN')")
@PutMapping("/password")
@EventLogging(code = SecurityEventCode.USER_USER_PASSWORD_CHANGE)
public CommonResponse<Boolean> updatePasswordByUser(
@RequestBody @Valid UserUpdatePasswordDto updateDto) {
service.updatePasswordByUser(updateDto);
return new CommonResponse<>(true);
}
@PreAuthorize("hasRole('ROLE_USER_ADMIN')")
@PutMapping("/me")
@EventLogging(code = SecurityEventCode.USER_USER_UPDATE_OWN_USER)
public CommonResponse<Boolean> updateOwnUser(
@RequestBody @Valid OwnUserUpdateDto updateDto) {
service.updateOwnUser(updateDto);
return new CommonResponse<>(true);
}
@PostMapping("/mfa/register")
public CommonResponse<Boolean> registerMfaTypes(
@Valid @RequestBody MfaRegisterDto registerDto
) {
service.registerMfaTypes(registerDto);
return new CommonResponse<>(true);
}
@PostMapping("/mfa/authenticator/verify")
public CommonResponse<Boolean> verifyAuthenticatorCode(
@Valid @RequestBody MfaOtpVerifyDto dto
) {
service.verifyAuthenticatorCode(dto);
return new CommonResponse<>(true);
}
@PostMapping("/mfa/delete")
public CommonResponse<Boolean> deleteMfaTypes(
@Valid @RequestBody MfaDeleteDto deleteDto
) {
service.deleteMfaTypes(deleteDto);
return new CommonResponse<>(true);
}
@PostMapping("/mfa/authenticator/register")
public CommonResponse<String> registerAuthenticator(
@Valid @RequestBody MfaOtpRegisterDto otpRegisterDto
) {
return new CommonResponse<>(service.registerAuthenticator(otpRegisterDto));
}
@PostMapping("/mfa/email/send")
public CommonResponse<Boolean> sendAuthenticationCodeByEmail(
@Valid @RequestBody MfaEmailSendRequestDto mfaEmailSendRequestDto
) {
service.sendAuthenticationCodeByEmail(mfaEmailSendRequestDto.getEmail());
return new CommonResponse<>(true);
}
@PostMapping("/mfa/email/verify")
public CommonResponse<Boolean> certEmail(
@Valid @RequestBody MfaEmailVerifyRequestDto dto
) {
service.certEmail(dto);
return new CommonResponse<>(true);
}
@PreAuthorize("hasRole('ROLE_USER_ADMIN')")
@PutMapping("/admin")
@EventLogging(code = SecurityEventCode.USER_USER_PASSWORD_CHANGE_BY_ADMIN)
public CommonResponse<Boolean> updatePasswordByAdmin(
@RequestBody @Valid UserUpdatePasswordDto updateDto) {
service.updatePasswordByAdmin(updateDto);
return new CommonResponse<>(true);
}
@PreAuthorize("hasRole('ROLE_USER_ADMIN')")
@DeleteMapping
@EventLogging(code = SecurityEventCode.USER_USER_DELETE)
public CommonResponse<Boolean> deleteUsers(
@Valid @RequestBody CommonIdListDto listDto) {
service.deleteUsers(listDto);
return new CommonResponse<>(true);
}
}
@ToString
@Getter
@NoArgsConstructor
@Builder
public class ApiResponseDto<T> {
private T data;
private ApiResponseDto(T data){
this.data=data;
}
public static <T> ApiResponseDto<T> of(T data) {
return new ApiResponseDto<>(data);
}
}
utils/consts/EnumDocs
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EnumDocs {
// 문서화하고 싶은 모든 enum값을 명시
Map<String,String> Sex;
Map<String,String> memberStatus;
}
utils/consts/CommonDocController
@ToString
@Getter
@NoArgsConstructor
@Builder
public class ApiResponseDto<T> {
private T data;
private ApiResponseDto(T data){
this.data=data;
}
public static <T> ApiResponseDto<T> of(T data) {
return new ApiResponseDto<>(data);
}
}
utils/consts/CommonDocControllerTest
// restdocs의 get 이 아님을 주의!!
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
class CommonDocControllerTest extends RestDocsTestSupport {
@Test
public void enums() throws Exception {
// 요청
ResultActions result = this.mockMvc.perform(
get("/test/enums")
.contentType(MediaType.APPLICATION_JSON)
);
// 결과값
MvcResult mvcResult = result.andReturn();
// 데이터 파싱
EnumDocs enumDocs = getData(mvcResult);
// 문서화 진행
result.andExpect(status().isOk())
.andDo(restDocs.document(
customResponseFields("custom-response", beneathPath("data.memberStatus").withSubsectionId("memberStatus"), // (1)
attributes(key("title").value("memberStatus")),
enumConvertFieldDescriptor((enumDocs.getMemberStatus()))
),
customResponseFields("custom-response", beneathPath("data.sex").withSubsectionId("sex"),
attributes(key("title").value("sex")),
enumConvertFieldDescriptor((enumDocs.getSex()))
)
));
}
// 커스텀 템플릿 사용을 위한 함수
public static CustomResponseFieldsSnippet customResponseFields
(String type,
PayloadSubsectionExtractor<?> subsectionExtractor,
Map<String, Object> attributes, FieldDescriptor... descriptors) {
return new CustomResponseFieldsSnippet(type, subsectionExtractor, Arrays.asList(descriptors), attributes
, true);
}
// Map으로 넘어온 enumValue를 fieldWithPath로 변경하여 리턴
private static FieldDescriptor[] enumConvertFieldDescriptor(Map<String, String> enumValues) {
return enumValues.entrySet().stream()
.map(x -> fieldWithPath(x.getKey()).description(x.getValue()))
.toArray(FieldDescriptor[]::new);
}
// mvc result 데이터 파싱
private EnumDocs getData(MvcResult result) throws IOException {
ApiResponseDto<EnumDocs> apiResponseDto = objectMapper
.readValue(result.getResponse().getContentAsByteArray(),
new TypeReference<ApiResponseDto<EnumDocs>>() {}
);
return apiResponseDto.getData();
}
}
@Repository
public interface ApiRouteRepository extends R2dbcRepository<ApiRoute, String> {
}
7. ApiRouteService 생성
Service Interface는 아래의 Override 한 메소드만 추가해주면 된다.
@Service
@RequiredArgsConstructor
public class ApiRouteServiceImpl implements ApiRouteService {
private final ApiRouteRepository apiRouteRepository;
@Override
public Flux<ApiRoute> getAll() {
return this.apiRouteRepository.findAll();
}
public Mono<ApiRoute> create(ApiRoute apiRoute) {
return this.apiRouteRepository.save(apiRoute);
}
public Mono<ApiRoute> getById(String id) {
return this.apiRouteRepository.findById(id);
}
}
8. ApiPathRouteLocatorImpl 생성
@AllArgsConstructor
public class ApiPathRouteLocatorImpl implements RouteLocator {
private final ApiRouteService apiRouteService;
private final RouteLocatorBuilder routeLocatorBuilder;
@Override
public Flux<Route> getRoutes() {
RouteLocatorBuilder.Builder routesBuilder = routeLocatorBuilder.routes();
return apiRouteService.getAll()
.map(apiRoute -> routesBuilder.route(String.valueOf(apiRoute.getRouteIdentifier()),
predicateSpec -> setPredicateSpec(apiRoute, predicateSpec)))
.collectList()
.flatMapMany(builders -> routesBuilder.build()
.getRoutes());
}
private Buildable<Route> setPredicateSpec(ApiRoute apiRoute, PredicateSpec predicateSpec) {
BooleanSpec booleanSpec = predicateSpec.path(apiRoute.getPath());
if (!StringUtils.isEmpty(apiRoute.getMethod())) {
booleanSpec.and()
.method(apiRoute.getMethod());
}
return booleanSpec.uri(apiRoute.getUri());
}
@Override
public Flux<Route> getRoutesByMetadata(Map<String, Object> metadata) {
return RouteLocator.super.getRoutesByMetadata(metadata);
}
}
9. GatewayConfig 생성
@Configuration
@Slf4j
public class GatewayConfig {
@Bean
public RouteLocator routeLocator(ApiRouteService routeService,
RouteLocatorBuilder routeLocationBuilder) {
return new ApiPathRouteLocatorImpl(routeService, routeLocationBuilder);
}
}
여기까지가 기본적인 프록시를 위한 라우터이다.
다음 부터는 API로 route를 CRUD 하기 위한 작업이다.
10. ApiRouteRouter Configuration 생성
@Configuration
public class ApiRouteRouter {
@Bean
public RouterFunction<ServerResponse> route(ApiRouteHandler apiRouteHandler) {
return RouterFunctions.route(POST("/routes")
.and(accept(MediaType.APPLICATION_JSON)), apiRouteHandler::create)
.andRoute(GET("/routes/{routeId}")
.and(accept(MediaType.APPLICATION_JSON)), apiRouteHandler::getById)
.andRoute(GET("/routes/refresh-routes")
.and(accept(MediaType.APPLICATION_JSON)), apiRouteHandler::refreshRoutes);
}
}
11. ApiROuteHandler 생성
@RequiredArgsConstructor
@Component
@Slf4j
public class ApiRouteHandler {
private final ApiRouteService routeService;
private final GatewayRoutesRefresher gatewayRoutesRefresher;
public Mono<ServerResponse> create(ServerRequest serverRequest) {
Mono<ApiRoute> apiRoute = serverRequest.bodyToMono(ApiRoute.class);
return apiRoute.flatMap(route ->
ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(routeService.create(route), ApiRoute.class));
}
public Mono<ServerResponse> getById(ServerRequest serverRequest) {
log.info("serverRequest.pathVariable(\"routeId\") = {}",
serverRequest.pathVariable("routeId"));
final String apiId = serverRequest.pathVariable("routeId");
Mono<ApiRoute> apiRoute = routeService.getById(apiId);
return apiRoute.flatMap(route -> ServerResponse.ok()
.body(fromValue(route)))
.switchIfEmpty(ServerResponse.notFound()
.build());
}
public Mono<ServerResponse> refreshRoutes(ServerRequest serverRequest) {
gatewayRoutesRefresher.refreshRoutes();
return ServerResponse.ok().body(BodyInserters.fromObject("Routes reloaded successfully"));
}
}
12. GatewayRoutesRefresher 생성
@Component
public class GatewayRoutesRefresher implements ApplicationEventPublisherAware {
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* Refresh the routes to load from data store
*/
public void refreshRoutes() {
applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
}
}
만약, domain Hostname에 따라 Proxy되는 서버의 주소를 변경하고자 하면 다음과 같은 방법을 쓸 수 있다.
@Override
public GatewayFilter apply(HostNameFilterDto hostNameFilterdto) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
request.getHeaders();
String[] split = request.getURI().getHost().split("\\.");
String siteId = split[0];
Mono<Site> siteMono = siteService.findById(siteId);
Site site = siteMono.share().block();
String uri = Objects.requireNonNull(site).getConnectHost();
int port = site.getConnectPort();
if (port != 80) {
uri += ":" + port;
}
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
Route newRoute = Route.async()
.id(site.getId())
.uri(uri)
.predicate(serverWebExchange -> false)
.order(Objects.requireNonNull(route).getOrder())
.filters(route.getFilters())
.build();
exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, newRoute);
return chain.filter(exchange);
};
}
@Configuration
@RequiredArgsConstructor
public class MqttConfig {
private static final String MQTT_CLIENT_ID = MqttAsyncClient.generateClientId();
private final MqttProperties properties;
/**
* DefaultMqttPahoClientFactory를 통해 MQTT 클라이언트를 등록
*/
@Bean
public DefaultMqttPahoClientFactory defaultMqttPahoClientFactory() {
DefaultMqttPahoClientFactory clientFactory = new DefaultMqttPahoClientFactory();
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(true);
options.setServerURIs(new String[]{properties.getUrl()});
options.setUserName(properties.getName());
options.setPassword(properties.getPassword().toCharArray());
clientFactory.setConnectionOptions(options);
return clientFactory;
}
/**
* MQTT 클라이언트를 통해 메시지를 구독하기 위하여 MqttPahoMessageDrivenChannelAdapter를 통해 메시지 수신을 위한 채널을 구성
*/
// @Bean
// public MessageChannel mqttInputChannel() {
// return new DirectChannel();
// }
//
// @Bean
// public MessageProducer inboundChannel() {
// MqttPahoMessageDrivenChannelAdapter adapter =
// new MqttPahoMessageDrivenChannelAdapter(
// properties.getUrl(),
// MQTT_CLIENT_ID,
// properties.getTopic());
// adapter.setCompletionTimeout(5000);
// adapter.setConverter(new DefaultPahoMessageConverter());
// adapter.setQos(1);
// adapter.setOutputChannel(mqttInputChannel());
// return adapter;
// }
//
// @Bean
// @ServiceActivator(inputChannel = "mqttInputChannel")
// public MessageHandler inboundMessageHandler() {
// return message -> {
// String topic = (String) message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC);
// System.out.println("Topic:" + topic);
// System.out.println("Payload" + message.getPayload());
// };
// }
/**
* Message outbound를 위한 채널 구성
*/
@Bean
public MessageChannel mqttOutboundChannel() {
return new DirectChannel();
}
@Bean
@ServiceActivator(inputChannel = "mqttOutboundChannel")
public MessageHandler mqttOutbound(DefaultMqttPahoClientFactory clientFactory) {
MqttPahoMessageHandler messageHandler =
new MqttPahoMessageHandler(MQTT_CLIENT_ID, clientFactory);
messageHandler.setAsync(true);
messageHandler.setDefaultQos(1);
return messageHandler;
}
@MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
public interface MyGateway {
void sendToMqtt(String data, @Header(MqttHeaders.TOPIC) String topic);
}
}
5. MqttService 클래스 생성
@Service
@RequiredArgsConstructor
public class MqttService {
private final MyGateway myGateway;
public void send() {
myGateway.sendToMqtt("12345", "/a/b/q");
}
}