-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/api key issuance #4
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package com.tokenledgercloud.api.controller; | ||
|
|
||
| import com.tokenledgercloud.api.dto.ApiKeyCreateRequest; | ||
| import com.tokenledgercloud.api.dto.ApiKeyCreateResponse; | ||
| import com.tokenledgercloud.api.dto.ApiKeyResponse; | ||
| import com.tokenledgercloud.api.service.ApiKeyService; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.security.core.Authentication; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/api-keys") | ||
| @RequiredArgsConstructor | ||
| public class ApiKeyController { | ||
|
|
||
| private final ApiKeyService apiKeyService; | ||
|
|
||
| @PostMapping | ||
| public ApiKeyCreateResponse createApiKey(Authentication authentication, @Valid @RequestBody ApiKeyCreateRequest request) { | ||
| return apiKeyService.createApiKey(authentication, request); | ||
| } | ||
|
|
||
| @GetMapping | ||
| public List<ApiKeyResponse> getMyApiKeys(Authentication authentication) { | ||
| return apiKeyService.getMyApiKeys(authentication); | ||
| } | ||
|
|
||
| @PatchMapping("/{id}/deactivate") | ||
| public void deactivateApiKey(Authentication authentication, @PathVariable Long id) { | ||
| apiKeyService.deactivateApiKey(authentication, id); | ||
| } | ||
|
|
||
| @DeleteMapping("/{id}") | ||
| public void deleteApiKey(Authentication authentication, @PathVariable Long id) { | ||
| apiKeyService.deleteApiKey(authentication, id); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package com.tokenledgercloud.api.domain.apikey; | ||
|
|
||
| import com.tokenledgercloud.api.domain.member.Member; | ||
| import jakarta.persistence.*; | ||
| import lombok.*; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Entity | ||
| @Table(name = "api_keys") | ||
| @Getter | ||
| @Setter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| @Builder | ||
| public class ApiKey { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(name = "hashed_key", nullable = false, unique = true) | ||
| private String hashedKey; | ||
|
|
||
| @Column(name = "display_key", nullable = false) | ||
| private String displayKey; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "member_id", nullable = false) | ||
| private Member member; | ||
|
|
||
| @Column(name = "name") | ||
| private String name; | ||
|
|
||
| @Column(name = "created_at", nullable = false) | ||
| private LocalDateTime createdAt; | ||
|
|
||
| @Column(name = "last_used_at") | ||
| private LocalDateTime lastUsedAt; | ||
|
|
||
| @Column(name = "is_active") | ||
| @Builder.Default | ||
| private boolean isActive = true; | ||
|
|
||
| @PrePersist | ||
| public void prePersist() { | ||
| if (this.createdAt == null) { | ||
| this.createdAt = LocalDateTime.now(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.tokenledgercloud.api.domain.apikey; | ||
|
|
||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| public interface ApiKeyRepository extends JpaRepository<ApiKey, Long> { | ||
| Optional<ApiKey> findByHashedKey(String hashedKey); | ||
| Optional<ApiKey> findByHashedKeyAndIsActiveTrue(String hashedKey); | ||
| List<ApiKey> findByMemberId(Long memberId); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package com.tokenledgercloud.api.dto; | ||
|
|
||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.Size; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.Setter; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| @NoArgsConstructor | ||
| public class ApiKeyCreateRequest { | ||
| @NotBlank(message = "API Key name is required") | ||
| @Size(max = 50, message = "API Key name must be less than 50 characters") | ||
| private String name; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.tokenledgercloud.api.dto; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| @Builder | ||
| public class ApiKeyCreateResponse { | ||
| private Long id; | ||
| private String rawKey; | ||
| private String displayKey; | ||
| private String name; | ||
| private LocalDateTime createdAt; | ||
| private boolean isActive; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.tokenledgercloud.api.dto; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| @Builder | ||
| public class ApiKeyResponse { | ||
| private Long id; | ||
| private String displayKey; | ||
| private String name; | ||
| private LocalDateTime createdAt; | ||
| private boolean isActive; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package com.tokenledgercloud.api.global.security; | ||
|
|
||
| import com.tokenledgercloud.api.domain.member.Member; | ||
| import com.tokenledgercloud.api.service.ApiKeyService; | ||
| import jakarta.servlet.FilterChain; | ||
| import jakarta.servlet.ServletException; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
| import org.springframework.security.core.Authentication; | ||
| import org.springframework.security.core.authority.SimpleGrantedAuthority; | ||
| import org.springframework.security.core.context.SecurityContextHolder; | ||
| import org.springframework.security.core.userdetails.User; | ||
| import org.springframework.security.core.userdetails.UserDetails; | ||
| import org.springframework.util.StringUtils; | ||
| import org.springframework.web.filter.OncePerRequestFilter; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.Collections; | ||
|
|
||
| @RequiredArgsConstructor | ||
| public class ApiKeyAuthenticationFilter extends OncePerRequestFilter { | ||
|
|
||
| private final ApiKeyService apiKeyService; | ||
|
|
||
| @Override | ||
| protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||
| throws ServletException, IOException { | ||
|
|
||
| String apiKey = resolveApiKey(request); | ||
|
|
||
| if (StringUtils.hasText(apiKey)) { | ||
| try { | ||
| Member member = apiKeyService.verifyApiKey(apiKey); | ||
|
|
||
| UserDetails userDetails = new User( | ||
| member.getEmail(), | ||
| "", | ||
| Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + member.getRole().name())) | ||
| ); | ||
|
|
||
| Authentication authentication = new UsernamePasswordAuthenticationToken( | ||
| userDetails, null, userDetails.getAuthorities()); | ||
|
|
||
| SecurityContextHolder.getContext().setAuthentication(authentication); | ||
| } catch (Exception e) { | ||
| // Invalid API key - we can either throw error or just let it pass to be caught by security chain | ||
| // Here we just don't set the authentication | ||
| } | ||
|
Comment on lines
+47
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๋ชจ๋ ์์ธ( } catch (IllegalArgumentException e) {
logger.debug("Invalid API key provided: " + e.getMessage());
} catch (Exception e) {
logger.error("Unexpected error during API key authentication", e);
} |
||
| } | ||
|
|
||
| filterChain.doFilter(request, response); | ||
| } | ||
|
|
||
| private String resolveApiKey(HttpServletRequest request) { | ||
| return request.getHeader("X-API-KEY"); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.tokenledgercloud.api.global.util; | ||
|
|
||
| import java.nio.charset.StandardCharsets; | ||
| import java.security.MessageDigest; | ||
| import java.security.NoSuchAlgorithmException; | ||
| import java.util.HexFormat; | ||
|
|
||
| public class HashingUtil { | ||
|
|
||
| public static String hash(String input) { | ||
| try { | ||
| MessageDigest digest = MessageDigest.getInstance("SHA-256"); | ||
| byte[] encodedhash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); | ||
| return HexFormat.of().formatHex(encodedhash); | ||
| } catch (NoSuchAlgorithmException e) { | ||
| throw new RuntimeException("SHA-256 algorithm not found", e); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ํด๋์ค ๋ ๋ฒจ์
@Setter๋ ๊ฐ์ฒด์ ๋ฌด๋ถ๋ณํ ์ํ ๋ณ๊ฒฝ์ ํ์ฉํ์ฌ ์บก์ํ๋ฅผ ์ ํดํ๊ณ ์ ์ง๋ณด์์ฑ์ ๋จ์ด๋จ๋ฆด ์ ์์ต๋๋ค. ํ์ํ ํ๋์๋ง@Setter๋ฅผ ๋ถ์ด๊ฑฐ๋, ์๋ฏธ ์๋ ๋น์ฆ๋์ค ๋ฉ์๋(์:deactivate())๋ฅผ ํตํด ์ํ๋ฅผ ๋ณ๊ฒฝํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.