Repository: prgrms-be-devcourse/NBE5-7-1-Team06
Pull request #55: refactor: 관리자 로그인 수정
Author: login: kwang2134
Created: 2025-04-25T08:03:54Z | Merged: 2025-04-26T15:23:52Z

<!--
  템플릿은 아직 PR 작성이 익숙하지 않으신 분들을 위해서 제공하는 가이드입니다!
  리뷰어 또는 이 PR을 보게 될 다른 사람들이 이 PR을 보는데 참고할 수 있는 내용이 있다면 포함해서 작성해주시면 됩니다.
-->

## 📌 과제 설명 <!-- 어떤 걸 만들었는지 대략적으로 설명해주세요 -->
### 관리자 로그인 수정
- 로그아웃 후 재로그인 시 로그인이 안되던 문제 수정
- CustomUserDetailsService의 loadUserByusername에서 매번 새로운 UserDetails 객체를 반환 

### yml 파일 관리자 정보를 담는 클래스 추가
- AdminProperties 클래스 추가
- yml에 정의된 관리자 정보를 담는 클래스
- CustomUserDetailsService 관리자 정보 필드 AdminProperties 클래스 사용으로 변경
- AdminProperties 클래스 사용으로 인한 구조 변경 -> UserDetails 필드 제거

### 관리자 로그인 페이지 수정
- 오류 출력이 누락되던 문제 수정
- 오류 파라미터로 넘어오던 String 값 불일치 수정 (login-failed -> login_failed)

--- 
더미 데이터가 깨져서 수정했었는데 수정 코드가 pr에 올라와 있길래 revert 하여 더미데이터 관련 커밋은 무시하셔도 됩니다.



Diff:
diff --git a/backend/src/main/java/kr/co/programmers/cafe/domain/admin/AdminController.java b/backend/src/main/java/kr/co/programmers/cafe/domain/admin/AdminController.java
index cfc0ae3..5b4ed08 100644
--- a/backend/src/main/java/kr/co/programmers/cafe/domain/admin/AdminController.java
+++ b/backend/src/main/java/kr/co/programmers/cafe/domain/admin/AdminController.java
@@ -41,10 +41,11 @@ public class AdminController {
     @GetMapping("/login")
     public String adminLogin(@RequestParam(required = false) String error, Model model) {
         log.info("로그인 페이지");
+        log.error("error: {}", error);
         if (error != null) {
             switch (error) {
                 case "ip_blocked" -> model.addAttribute("errorMessage", "외부 IP 에선 접속이 불가능합니다!");
-                case "login-failed" -> model.addAttribute("errorMessage", "올바르지 않은 계정입니다!");
+                case "login_failed" -> model.addAttribute("errorMessage", "올바르지 않은 계정입니다!");
             }
             log.error("로그인 실패");
         }
diff --git a/backend/src/main/java/kr/co/programmers/cafe/global/config/AdminProperties.java b/backend/src/main/java/kr/co/programmers/cafe/global/config/AdminProperties.java
new file mode 100644
index 0000000..34d0086
--- /dev/null
+++ b/backend/src/main/java/kr/co/programmers/cafe/global/config/AdminProperties.java
@@ -0,0 +1,16 @@
+package kr.co.programmers.cafe.global.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Component
+@ConfigurationProperties(prefix = "spring.security.user")
+@Getter
+@Setter
+public class AdminProperties {
+    private String name;
+    private String password;
+    private String roles;
+}
diff --git a/backend/src/main/java/kr/co/programmers/cafe/global/config/CustomUserDetailsService.java b/backend/src/main/java/kr/co/programmers/cafe/global/config/CustomUserDetailsService.java
index 4f4fa10..99dc916 100644
--- a/backend/src/main/java/kr/co/programmers/cafe/global/config/CustomUserDetailsService.java
+++ b/backend/src/main/java/kr/co/programmers/cafe/global/config/CustomUserDetailsService.java
@@ -7,41 +7,33 @@
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 
 @Slf4j
 @Service
 public class CustomUserDetailsService implements UserDetailsService {
 
-    private final UserDetails userDetails;
+    private final PasswordEncoder encoder;
+    private final AdminProperties adminProperties;
 
     // yml 파일 기반 인증 정보를 담은 UserDetails 생성
-    public CustomUserDetailsService(@Value("${spring.security.user.name}") String username,
-                                    @Value("${spring.security.user.password}") String password,
-                                    @Value("${spring.security.user.roles}") String roles) {
-        log.info("roles = {}", roles);
-        this.userDetails = User.builder()
-                .username(username)
-                .password(new BCryptPasswordEncoder().encode(password))
-                .roles(roles)
-                .build();
+    public CustomUserDetailsService(AdminProperties adminProperties, PasswordEncoder encoder) {
+        this.encoder = encoder;
+        this.adminProperties = adminProperties;
     }
 
     // yml 파일 기반 인증 정보를 만들기 위해 Override
     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
-        if (!userDetails.getUsername().equals(username)) {
+        if (!username.equals(adminProperties.getName())) {
             throw new UsernameNotFoundException("사용자를 찾을 수 없습니다.");
         }
 
-        boolean isAdmin = userDetails.getAuthorities().stream()
-                .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
-
-        if (!isAdmin) {
-            log.warn("ADMIN 권한 부족");
-            throw new UsernameNotFoundException("권한이 부족합니다.");  // 로그인 실패 처리됨
-        }
-        log.info("Authorities = {}", userDetails.getAuthorities());
-        return userDetails;
+        return User.builder()
+                .username(adminProperties.getName())
+                .password(encoder.encode(adminProperties.getPassword()))
+                .roles(adminProperties.getRoles())
+                .build();
     }
 }
\ No newline at end of file


Reviews:
user: login: KEEKE132
body: 확인했습니다.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-25T08:15:55Z
user: login: Shs0160
body: 확인했습니다!
궁금한 부분이 있는데  관리자 정보를 필드말고 AdminProperties 클래스를 따로 만들어서 받아오도록 수정하신 이유가 있나요?? (효율성이나 관리 등,,)
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-25T12:58:03Z
user: login: sjsk3232
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-26T05:52:53Z
user: login: sjsk3232
body: 확인했습니다!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-26T05:53:08Z
user: login: kwang2134
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-26T06:01:53Z
user: login: LimKangHyun
body: 확인했습니다!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-26T14:59:32Z

Review comments:
diff_hunk: @@ -7,41 +7,33 @@
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 
 @Slf4j
 @Service
 public class CustomUserDetailsService implements UserDetailsService {
 
-    private final UserDetails userDetails;
+    private final PasswordEncoder encoder;
+    private final AdminProperties adminProperties;
 
     // yml 파일 기반 인증 정보를 담은 UserDetails 생성
-    public CustomUserDetailsService(@Value("${spring.security.user.name}") String username,
-                                    @Value("${spring.security.user.password}") String password,
-                                    @Value("${spring.security.user.roles}") String roles) {
-        log.info("roles = {}", roles);
-        this.userDetails = User.builder()
-                .username(username)
-                .password(new BCryptPasswordEncoder().encode(password))
-                .roles(roles)
-                .build();
+    public CustomUserDetailsService(AdminProperties adminProperties, PasswordEncoder encoder) {
path: backend/src/main/java/kr/co/programmers/cafe/global/config/CustomUserDetailsService.java
user: login: sjsk3232
body: 이전처럼 application.yml에서 값을 가져와 초기화하는 게 아니라면, @RequiredArgsConstructor 사용해도 되는 부분일까요?
created_at: 2025-04-26T05:52:53Z
author_association: COLLABORATOR
diff_hunk: @@ -7,41 +7,33 @@
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 
 @Slf4j
 @Service
 public class CustomUserDetailsService implements UserDetailsService {
 
-    private final UserDetails userDetails;
+    private final PasswordEncoder encoder;
+    private final AdminProperties adminProperties;
 
     // yml 파일 기반 인증 정보를 담은 UserDetails 생성
-    public CustomUserDetailsService(@Value("${spring.security.user.name}") String username,
-                                    @Value("${spring.security.user.password}") String password,
-                                    @Value("${spring.security.user.roles}") String roles) {
-        log.info("roles = {}", roles);
-        this.userDetails = User.builder()
-                .username(username)
-                .password(new BCryptPasswordEncoder().encode(password))
-                .roles(roles)
-                .build();
+    public CustomUserDetailsService(AdminProperties adminProperties, PasswordEncoder encoder) {
path: backend/src/main/java/kr/co/programmers/cafe/global/config/CustomUserDetailsService.java
user: login: kwang2134
body: AdminProperties와 PasswordEncoder 둘 다 빈으로 등록되어 있어 @requiredargsconstructor 사용해도 무관합니다!
created_at: 2025-04-26T06:01:53Z
author_association: COLLABORATOR

PR comments:
user: login: kwang2134
created_at: 2025-04-25T15:43:35Z
body: @Shs0160 
특별한 큰 이유는 없고 오류를 수정하다가 깔끔한 거 같아 변경했긴 합니다!

처음에 사실 AdminProperties 클래스를 따로 만들지 말지 고민은 했었습니다.
관리자 정보는 하나뿐이고 값이 변할 일도 없으니 UserDetails 객체를 CustomUserDetailsService의 내부 필드로 두고 계속 재사용해도 괜찮지 않을까 생각했습니다. 그래서 처음엔 CustomUserDetailsService 생성자에서 yml 파일의 값을 받아와 UserDetails 객체를 채워주는 방식으로 구현했었는데 로그아웃을 하며 인증 정보를 날릴 때 CustomUserDetailsService 내부 UserDetails 객체의 비밀번호가 비워지면서 오류가 발생하더라구요. 

그래서 매번 새로운 UserDetails를 반환해주기 위해 yml 파일 값들을 내부 필드에 저장해둬야 했는데 필드가 많아지니 지저분한 느낌이 들어서 클래스로 따로 뽑았습니다. 만약 UserDetails도 상속 받아 추가 필드를 구성하고 관리자의 추가 정보가 생긴다면 클래스를 따로 뽑은 지금이 좀 더 깔끔하게 유지할 수 있지 않을까 싶네요! 

그런데 사실상 yml 파일에 저장해 쓸 일이 거의 없을 거 같긴 해서 큰 의미는 또 없을 것 같긴 합니다.

author_association: COLLABORATOR
user: login: Shs0160
created_at: 2025-04-26T04:21:19Z
body: @kwang2134 
그러네요! 저희는 지금 yml에 정보를 직접 넣어놓고 사용하니까,,
이해했습니다! 감사합니다👍

author_association: COLLABORATOR