Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] 공공 api키 관리 기능 구현 #148

Merged
merged 13 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import com.talkka.server.admin.service.AdminService;
import com.talkka.server.admin.service.CollectBusRouteService;
import com.talkka.server.admin.service.PublicApiKeyService;
import com.talkka.server.user.service.UserService;

import lombok.RequiredArgsConstructor;
Expand All @@ -18,8 +19,7 @@ public class AdminController {
private final AdminService adminService;
private final UserService userService;
private final CollectBusRouteService collectBusRouteService;
// private final PublicApiKeyService publicApiKeyService;
// private final PublicApiKeyService publicApiKeyService;
private final PublicApiKeyService publicApiKeyService;
// private final DynamicSchedulingConfig dynamicSchedulingConfig;

@GetMapping("")
Expand Down Expand Up @@ -51,11 +51,11 @@ public String collectRoute(Model model) {
return "admin/collect";
}

// @GetMapping("/key")
// public String publicApiKey(Model model) {
// model.addAttribute("apiKeys", publicApiKeyService.getKeyList());
// return "admin/key";
// }
@GetMapping("/key")
public String publicApiKey(Model model) {
model.addAttribute("apiKeys", publicApiKeyService.getKeyList());
return "admin/key";
}
//
// @GetMapping("/scheduler")
// public String scheduler(Model model) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.talkka.server.admin.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.talkka.server.admin.exception.PublicApiKeyAlreadyExistsException;
import com.talkka.server.admin.exception.PublicApiKeyNotFoundException;
import com.talkka.server.admin.service.PublicApiKeyService;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/admin/api/key")
public class PublicApiKeyController {
private final PublicApiKeyService publicApiKeyService;

@PostMapping("")
public ResponseEntity<?> createKey(@RequestBody String secret) {
try {
publicApiKeyService.createKey(secret);
} catch (PublicApiKeyAlreadyExistsException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
return ResponseEntity.ok().build();
}

@DeleteMapping("")
public ResponseEntity<?> deleteKey(@RequestBody String secret) {
try {
publicApiKeyService.deleteKey(secret);
} catch (PublicApiKeyNotFoundException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.talkka.server.admin.dao;

import java.time.LocalDateTime;

import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity(name = "public_apikey")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class PublicApiKeyEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "secret", nullable = false)
private String secret;

@Column(name = "key_usage", nullable = false)
private Integer keyUsage;

@Column(name = "created_at", nullable = false)
@CreatedDate
private LocalDateTime createdAt;

public void use() {
this.keyUsage++;
}

public void reset() {
this.keyUsage = 0;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.talkka.server.admin.dao;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PublicApiKeyRepository extends JpaRepository<PublicApiKeyEntity, Long> {
boolean existsBySecret(String secret);

void deleteBySecret(String secret);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.talkka.server.admin.dto;

import java.time.LocalDateTime;

import com.talkka.server.admin.dao.PublicApiKeyEntity;

public record PublicApiKeyRespDto(
Long id,
String secret,
Integer keyUsage,
LocalDateTime createdAt
) {
public static PublicApiKeyRespDto of(PublicApiKeyEntity key) {
return new PublicApiKeyRespDto(
key.getId(),
key.getSecret(),
key.getKeyUsage(),
key.getCreatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.talkka.server.admin.exception;

public class PublicApiKeyAlreadyExistsException extends RuntimeException {
private static final String MESSAGE = "이미 등록된 api key 입니다.";

public PublicApiKeyAlreadyExistsException() {
super(MESSAGE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.talkka.server.admin.exception;

public class PublicApiKeyNotFoundException extends RuntimeException {
private static final String MESSAGE = "존재하지 않는 api key 입니다.";

public PublicApiKeyNotFoundException() {
super(MESSAGE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.talkka.server.admin.service;

import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.talkka.server.admin.dao.PublicApiKeyEntity;
import com.talkka.server.admin.dao.PublicApiKeyRepository;
import com.talkka.server.admin.dto.PublicApiKeyRespDto;
import com.talkka.server.admin.exception.PublicApiKeyAlreadyExistsException;
import com.talkka.server.admin.exception.PublicApiKeyNotFoundException;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class PublicApiKeyService {
private final PublicApiKeyRepository publicApiKeyRepository;

@Transactional
public PublicApiKeyRespDto createKey(String secret) throws PublicApiKeyAlreadyExistsException {
if (publicApiKeyRepository.existsBySecret(secret)) {
throw new PublicApiKeyAlreadyExistsException();
}
var key = publicApiKeyRepository.save(
PublicApiKeyEntity.builder()
.secret(secret)
.keyUsage(0)
.build()
);
return PublicApiKeyRespDto.of(key);
}

@Transactional
public void deleteKey(String secret) throws PublicApiKeyNotFoundException {
if (!publicApiKeyRepository.existsBySecret(secret)) {
throw new PublicApiKeyNotFoundException();
}
publicApiKeyRepository.deleteBySecret(secret);
}

public List<PublicApiKeyRespDto> getKeyList() {
return publicApiKeyRepository.findAll().stream()
.map(PublicApiKeyRespDto::of)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.talkka.server.api.core.config;

public interface ApiKeyProperty {
public interface ApiKeyProvider {
String getApiKey();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.talkka.server.api.datagg.config;

import java.util.ArrayList;
import java.util.List;

import org.springframework.context.annotation.Primary;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import com.talkka.server.admin.dao.PublicApiKeyEntity;
import com.talkka.server.admin.dao.PublicApiKeyRepository;
import com.talkka.server.api.core.config.ApiKeyProvider;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
@Primary
public class PersistenceApiKeyProvider implements ApiKeyProvider {

private final PublicApiKeyRepository publicApiKeyRepository;
private final List<PublicApiKeyEntity> keys = new ArrayList<>();
private int rollingKeyIndex = 0;
private final int MAX_USAGE = 950; // 일일 최대 사용량

@Override
public String getApiKey() {
rollingKeyIndex = (rollingKeyIndex + 1) % keys.size();
updateUsage(keys.get(rollingKeyIndex));
return keys.get(rollingKeyIndex).getSecret();
}

// 매일 자정에 키 사용량 리셋
@PostConstruct
@Scheduled(cron = "0 0 0 * * *")
public void init() {
rollingKeyIndex = 0;
keys.clear();
keys.addAll(publicApiKeyRepository.findAll());
// 사용량 초기화
keys.forEach(PublicApiKeyEntity::reset);
publicApiKeyRepository.saveAll(keys);
}

// 20분마다 사용량 DB에 반영
@Scheduled(cron = "0 */20 * * * *")
private void persist() {
publicApiKeyRepository.saveAll(keys);
}

private void updateUsage(PublicApiKeyEntity key) {
key.use();
// 일일 사용량이 초과되면 리스트에서 삭제 후 DB에 반영
if (key.getKeyUsage() >= MAX_USAGE) {
keys.remove(key);
publicApiKeyRepository.save(key);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import com.talkka.server.api.core.config.ApiKeyProperty;
import com.talkka.server.api.core.config.ApiKeyProvider;

import lombok.Getter;
import lombok.Setter;

@Getter
@Component
@ConfigurationProperties(prefix = "openapi.public.bus.service-key")
public class BusApiKeyProperty implements ApiKeyProperty {
public class PropertyApiKeyProvider implements ApiKeyProvider {
@Setter
private List<String> keys;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.DefaultUriBuilderFactory;

import com.talkka.server.api.core.config.ApiKeyProvider;
import com.talkka.server.api.core.exception.ApiClientException;
import com.talkka.server.api.datagg.config.BusApiKeyProperty;
import com.talkka.server.api.datagg.dto.BusArrivalBodyDto;
import com.talkka.server.api.datagg.dto.BusArrivalRespDto;
import com.talkka.server.api.datagg.dto.BusLocationBodyDto;
Expand All @@ -36,7 +36,7 @@
@RequiredArgsConstructor
public class SimpleBusApiService implements BusApiService {
private static final Logger log = LoggerFactory.getLogger(SimpleBusApiService.class);
private final BusApiKeyProperty busApiKeyProperty;
private final ApiKeyProvider apiKeyProvider;
private final RestTemplate restTemplate = new RestTemplate();
private static final String host = "apis.data.go.kr";

Expand Down Expand Up @@ -119,7 +119,7 @@ private URI getOpenApiUri(String path, MultiValueMap<String, String> params) {
.scheme("https")
.host(host)
.path(path)
.queryParam("serviceKey", this.busApiKeyProperty.getApiKey())
.queryParam("serviceKey", this.apiKeyProvider.getApiKey())
.queryParams(params)
.build();
}
Expand Down
31 changes: 0 additions & 31 deletions server/src/main/resources/templates/admin/collect-route-form.html

This file was deleted.

Loading
Loading