Skip to content

Commit ebff044

Browse files
committed
feat: Admin 로그인 API 및 JWT 권한 체계 구현
1 parent e4f33cd commit ebff044

15 files changed

Lines changed: 199 additions & 8 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.semosan.api.common.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
6+
import org.springframework.security.crypto.password.PasswordEncoder;
7+
8+
@Configuration
9+
public class PasswordEncoderConfig {
10+
11+
@Bean
12+
public PasswordEncoder passwordEncoder() {
13+
return new BCryptPasswordEncoder();
14+
}
15+
}

src/main/java/com/semosan/api/common/config/SecurityConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
7575
.requestMatchers(AUTH_URIS).permitAll()
7676
.requestMatchers(WEBSOCKET_URIS).permitAll()
7777
.requestMatchers(HttpMethod.GET, "/api/app-version").permitAll()
78+
.requestMatchers("/api/admin/login").permitAll()
79+
.requestMatchers("/api/admin/**").hasRole("ADMIN")
80+
.requestMatchers(HttpMethod.PUT, "/api/app-version").hasRole("ADMIN")
7881
.anyRequest().authenticated()
7982
)
8083
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

src/main/java/com/semosan/api/common/jwt/JwtFilter.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.slf4j.MDC;
1616
import org.springframework.http.MediaType;
1717
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
18+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
1819
import org.springframework.security.core.context.SecurityContextHolder;
1920
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
2021
import org.springframework.stereotype.Component;
@@ -72,8 +73,14 @@ protected void doFilterInternal(
7273
Long userId = jwtService.getUserIdFromClaims(claims);
7374
MDC.put("userId", String.valueOf(userId));
7475

76+
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
77+
String tokenType = claims.get("tokenType", String.class);
78+
if ("ADMIN".equals(tokenType)) {
79+
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
80+
}
81+
7582
UsernamePasswordAuthenticationToken authentication =
76-
new UsernamePasswordAuthenticationToken(userId, null, new ArrayList<>());
83+
new UsernamePasswordAuthenticationToken(userId, null, authorities);
7784
SecurityContextHolder.getContext().setAuthentication(authentication);
7885
}
7986
filterChain.doFilter(request, response);

src/main/java/com/semosan/api/common/jwt/JwtService.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.semosan.api.common.exception.GeneralException;
44
import com.semosan.api.common.status.ErrorStatus;
5+
import com.semosan.api.domain.admin.entity.Admin;
56
import com.semosan.api.domain.user.entity.User;
67
import io.jsonwebtoken.*;
78
import io.jsonwebtoken.security.Keys;
@@ -84,6 +85,19 @@ public String hashToken(String token) {
8485
}
8586
}
8687

88+
public String generateAdminAccessToken(Admin admin) {
89+
Date now = new Date();
90+
Date expiration = new Date(now.getTime() + accessTokenExpiration);
91+
92+
return Jwts.builder()
93+
.subject(admin.getId().toString())
94+
.claim("tokenType", "ADMIN")
95+
.issuedAt(now)
96+
.expiration(expiration)
97+
.signWith(secretKey, Jwts.SIG.HS256)
98+
.compact();
99+
}
100+
87101
// Access Token 검증 후 Claims를 반환해 호출부에서 같은 토큰을 다시 파싱하지 않도록 한다.
88102
public Claims validateAccessTokenAndGetClaims(String accessToken) {
89103
if (accessToken == null || accessToken.isBlank()) {

src/main/java/com/semosan/api/common/status/ErrorStatus.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,12 @@ public enum ErrorStatus implements BaseStatus {
158158
*/
159159
APP_VERSION_NOT_FOUND(HttpStatus.NOT_FOUND, "APPV_404_1", "앱 버전 정보를 찾을 수 없습니다."),
160160
APP_VERSION_READ_FAILED(HttpStatus.BAD_GATEWAY, "APPV_502_1", "앱 버전 정보 조회에 실패했습니다."),
161-
APP_VERSION_UPDATE_FAILED(HttpStatus.BAD_GATEWAY, "APPV_502_2", "앱 버전 정보 저장에 실패했습니다.");
161+
APP_VERSION_UPDATE_FAILED(HttpStatus.BAD_GATEWAY, "APPV_502_2", "앱 버전 정보 저장에 실패했습니다."),
162+
163+
/**
164+
* Admin
165+
*/
166+
ADMIN_LOGIN_FAILED(HttpStatus.UNAUTHORIZED, "ADM_401_1", "아이디 또는 비밀번호가 올바르지 않습니다.");
162167

163168
private final HttpStatus httpStatus;
164169
private final String code;

src/main/java/com/semosan/api/common/status/SuccessStatus.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,12 @@ public enum SuccessStatus implements BaseStatus {
133133
* App Version
134134
*/
135135
APP_VERSION_GET_SUCCESS(HttpStatus.OK, "APPV_200_1", "앱 버전 정보 조회에 성공했습니다."),
136-
APP_VERSION_UPDATE_SUCCESS(HttpStatus.OK, "APPV_200_2", "앱 버전 정보 수정에 성공했습니다.");
136+
APP_VERSION_UPDATE_SUCCESS(HttpStatus.OK, "APPV_200_2", "앱 버전 정보 수정에 성공했습니다."),
137+
138+
/**
139+
* Admin
140+
*/
141+
ADMIN_LOGIN_SUCCESS(HttpStatus.OK, "ADM_200_1", "관리자 로그인에 성공했습니다.");
137142

138143
private final HttpStatus httpStatus;
139144
private final String code;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.semosan.api.domain.admin.controller;
2+
3+
import com.semosan.api.common.response.ApiResponse;
4+
import com.semosan.api.common.status.SuccessStatus;
5+
import com.semosan.api.domain.admin.controller.docs.AdminAuthControllerDocs;
6+
import com.semosan.api.domain.admin.dto.request.AdminLoginRequest;
7+
import com.semosan.api.domain.admin.dto.response.AdminLoginResponse;
8+
import com.semosan.api.domain.admin.service.AdminAuthService;
9+
import jakarta.servlet.http.HttpServletRequest;
10+
import jakarta.validation.Valid;
11+
import lombok.RequiredArgsConstructor;
12+
import org.springframework.http.ResponseEntity;
13+
import org.springframework.web.bind.annotation.PostMapping;
14+
import org.springframework.web.bind.annotation.RequestBody;
15+
import org.springframework.web.bind.annotation.RequestMapping;
16+
import org.springframework.web.bind.annotation.RestController;
17+
18+
@RestController
19+
@RequestMapping("/api/admin")
20+
@RequiredArgsConstructor
21+
public class AdminAuthController implements AdminAuthControllerDocs {
22+
23+
private final AdminAuthService adminAuthService;
24+
25+
@PostMapping("/login")
26+
@Override
27+
public ResponseEntity<ApiResponse<AdminLoginResponse>> login(
28+
@Valid @RequestBody AdminLoginRequest request,
29+
HttpServletRequest httpRequest
30+
) {
31+
String ipAddress = httpRequest.getRemoteAddr();
32+
String userAgent = httpRequest.getHeader("User-Agent");
33+
AdminLoginResponse response = adminAuthService.login(request, ipAddress, userAgent);
34+
return ApiResponse.success(SuccessStatus.ADMIN_LOGIN_SUCCESS, response);
35+
}
36+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.semosan.api.domain.admin.controller.docs;
2+
3+
import com.semosan.api.common.response.ApiResponse;
4+
import com.semosan.api.domain.admin.dto.request.AdminLoginRequest;
5+
import com.semosan.api.domain.admin.dto.response.AdminLoginResponse;
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
8+
import io.swagger.v3.oas.annotations.tags.Tag;
9+
import jakarta.servlet.http.HttpServletRequest;
10+
import jakarta.validation.Valid;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.web.bind.annotation.RequestBody;
13+
14+
@Tag(name = "Admin Auth", description = "관리자 인증 API")
15+
public interface AdminAuthControllerDocs {
16+
17+
@Operation(summary = "관리자 로그인", description = "관리자 아이디/비밀번호로 로그인하여 JWT를 발급받습니다.")
18+
@ApiResponses({
19+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "관리자 로그인 성공"),
20+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "아이디 또는 비밀번호 불일치")
21+
})
22+
ResponseEntity<ApiResponse<AdminLoginResponse>> login(
23+
@Valid @RequestBody AdminLoginRequest request,
24+
HttpServletRequest httpRequest
25+
);
26+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.semosan.api.domain.admin.dto.request;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
5+
public record AdminLoginRequest(
6+
7+
@NotBlank(message = "아이디는 필수입니다.")
8+
String username,
9+
10+
@NotBlank(message = "비밀번호는 필수입니다.")
11+
String password
12+
) {}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.semosan.api.domain.admin.dto.response;
2+
3+
public record AdminLoginResponse(
4+
Long adminId,
5+
String name,
6+
String accessToken
7+
) {}

0 commit comments

Comments
 (0)