Skip to content

Commit 39bca0f

Browse files
authored
[인증(Authentication)] 김선호 미션 제출합니다. 🚀 (#40)
* refactor: 폼 로그인 인증을 컨트롤러가 아닌 폼 로그인 인터셉터에서 담당하도록 변경 * refactor: Basic 인증을 컨트롤러가 아닌 Basic 인증 인터셉터가 처리하도록 변경 * refactor: 서비스 코드와 인증 코드를 각각 app, security 패키지로 분리 - 인증 로직이 서비스 로직에 의존하지 않도록 중간 객체를 이요하여 의존성 분리 * refactor(step1): 폼 로그인 인증을 인터셉터가 아닌 필터로 처리하도록 변경 - 인터셉터는 Spring MVC에 속하고 이것은 app로직으로 볼 수 있으므로 비즈니스 로직과 인증/인가를 분리하기 위해 인터셉터가 아닌 필터에서 처리하도록 변경 * test: mockMvc request, response 콘솔 출력 추가 * refactor(step1): Basic 인증을 인터셉터가 아닌 필터로 처리하도록 변경 * chore(step1): 미사용 RequestHeader 파라미터 제거 * chore(step1): 미사용 AuthenticationException 삭제 - 필터의 예외처리는 Spring MVC 예외처리에 포함되지 않으므로 ResponseStatus는 동작 안 함 * test: 테스트 수행 시 로그 레벨 설정 (디버깅 용도) * feat(step1): HTTP 요청 처리 여부를 결정하는 RequestMatcher 인터페이스 정의 * feat(step1): HTTP 요청에 대한 인증/인가 필터를 보관하는 SecurityFilterChain 정의 및 구현체 DefaultSecurityFilterChain 구현 * feat(step1): DelegatingFilterProxy로 부터 인증/인가 책임을 위임받아 그 책임을 수행할 FilterChainProxy 구현 * feat(step1): DelegatingFilterProxy를 필터로 등록하는 SecurityFilterAutoConfiguration 구현 * feat(step1): FilterChainProxy를 빈으로 등록하는 WebSecurityConfiguration 구현 * feat(step1): Security 관련 설정을 자동으로 로드하는 SecurityAutoConfiguration 구현 * refactor(step1): springSecurityFilterChain라는 빈이 있을 때만 DelegatingFilterProxy를 필터로 등록하도록 수정 - springSecurityFilterChain 이름의 빈(=filterChainProxy)이 없으면 DelegatingFilterProxy를 필터로 등록할 필요가 없다. * refactor(step1): 폼 로그인 인증, Basic 인증 방식을 SecurityFilterChain을 통해 진행하도록 변경 * chore(step1): 폼 로그인 인증에서 filterChain.doFilter를 처리하지 않는 이유 주석 추가 * refactor(step1): security의 필터 관련 클래스를 security.filter 패키지로 응집 * refactor(step1): app에서 security를 사용하는 부분을 app.security로 이동
1 parent ad6fa9b commit 39bca0f

22 files changed

+423
-67
lines changed

src/main/java/nextstep/app/SecurityAuthenticationApplication.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package nextstep.app;
22

3+
import nextstep.security.config.SecurityAutoConfiguration;
34
import org.springframework.boot.SpringApplication;
45
import org.springframework.boot.autoconfigure.SpringBootApplication;
6+
import org.springframework.context.annotation.Import;
57

68
@SpringBootApplication
9+
@Import(SecurityAutoConfiguration.class)
710
public class SecurityAuthenticationApplication {
811

912
public static void main(String[] args) {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package nextstep.app.security;
2+
3+
import nextstep.security.filter.*;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
7+
import java.util.List;
8+
9+
@Configuration
10+
public class AppSecurityConfiguration {
11+
12+
private final UserDetailsService userDetailsService;
13+
14+
public AppSecurityConfiguration(UserDetailsService userDetailsService) {
15+
this.userDetailsService = userDetailsService;
16+
}
17+
18+
@Bean
19+
public SecurityFilterChain formLoginSecurityFilterChain() {
20+
return new DefaultSecurityFilterChain(
21+
(httpServletRequest) -> httpServletRequest.getRequestURI().equals("/login"),
22+
List.of(
23+
new FormLoginFilter(userDetailsService)
24+
)
25+
);
26+
}
27+
28+
@Bean
29+
public SecurityFilterChain basicAuthenticationSecurityFilterChain() {
30+
return new DefaultSecurityFilterChain(
31+
(httpServletRequest) -> httpServletRequest.getRequestURI().equals("/members"),
32+
List.of(
33+
new BasicAuthenticationFilter(userDetailsService)
34+
)
35+
);
36+
}
37+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package nextstep.app.security;
2+
3+
import nextstep.app.domain.MemberRepository;
4+
import nextstep.security.filter.UserDetails;
5+
import nextstep.security.filter.UserDetailsService;
6+
import org.springframework.stereotype.Service;
7+
8+
import java.util.Optional;
9+
10+
@Service
11+
public class UserDetailsServiceImpl implements UserDetailsService {
12+
13+
private final MemberRepository memberRepository;
14+
15+
public UserDetailsServiceImpl(MemberRepository memberRepository) {
16+
this.memberRepository = memberRepository;
17+
}
18+
19+
@Override
20+
public Optional<UserDetails> findUserDetailsByUsername(String username) {
21+
return memberRepository.findByEmail(username)
22+
.map(member -> new UserDetails(member.getEmail(), member.getPassword()));
23+
}
24+
}

src/main/java/nextstep/app/ui/AuthenticationException.java

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/main/java/nextstep/app/ui/LoginController.java

Lines changed: 0 additions & 37 deletions
This file was deleted.

src/main/java/nextstep/app/ui/MemberController.java

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22

33
import nextstep.app.domain.Member;
44
import nextstep.app.domain.MemberRepository;
5-
import nextstep.app.util.Base64Convertor;
65
import org.springframework.http.ResponseEntity;
76
import org.springframework.web.bind.annotation.GetMapping;
8-
import org.springframework.web.bind.annotation.RequestHeader;
97
import org.springframework.web.bind.annotation.RestController;
108

119
import java.util.List;
@@ -20,22 +18,8 @@ public MemberController(MemberRepository memberRepository) {
2018
}
2119

2220
@GetMapping("/members")
23-
public ResponseEntity<List<Member>> list(@RequestHeader("Authorization") String authorization) {
24-
try {
25-
String credentials = authorization.split(" ")[1];
26-
String decodedString = Base64Convertor.decode(credentials);
27-
String[] usernameAndPassword = decodedString.split(":");
28-
String username = usernameAndPassword[0];
29-
String password = usernameAndPassword[1];
30-
31-
memberRepository.findByEmail(username)
32-
.filter(it -> it.matchPassword(password))
33-
.orElseThrow(AuthenticationException::new);
34-
35-
List<Member> members = memberRepository.findAll();
36-
return ResponseEntity.ok(members);
37-
} catch (Exception e) {
38-
throw new AuthenticationException();
39-
}
21+
public ResponseEntity<List<Member>> list() {
22+
List<Member> members = memberRepository.findAll();
23+
return ResponseEntity.ok(members);
4024
}
4125
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package nextstep.security.config;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
import org.springframework.context.annotation.Import;
5+
6+
@Configuration
7+
@Import({
8+
WebSecurityConfiguration.class,
9+
SecurityFilterAutoConfiguration.class,
10+
})
11+
public class SecurityAutoConfiguration {
12+
13+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package nextstep.security.config;
2+
3+
import jakarta.servlet.Filter;
4+
import nextstep.security.filter.FilterChainProxy;
5+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
6+
import org.springframework.boot.web.servlet.FilterRegistrationBean;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.context.annotation.Configuration;
9+
import org.springframework.web.filter.DelegatingFilterProxy;
10+
11+
/**
12+
* DelegatingFilterProxy를 FilterChainProxy를 참조하게 한 후 빈으로 등록한다.
13+
*/
14+
@Configuration
15+
public class SecurityFilterAutoConfiguration {
16+
17+
@Bean
18+
@ConditionalOnBean(name = FilterChainProxy.FILTER_CHAIN_PROXY_BEAN_NAME)
19+
public FilterRegistrationBean<Filter> springSecurityFilterChainRegistration() {
20+
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
21+
22+
registration.setFilter(new DelegatingFilterProxy(FilterChainProxy.FILTER_CHAIN_PROXY_BEAN_NAME));
23+
24+
return registration;
25+
}
26+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package nextstep.security.config;
2+
3+
import nextstep.security.filter.FilterChainProxy;
4+
import nextstep.security.filter.SecurityFilterChain;
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
8+
import java.util.List;
9+
10+
/**
11+
* "springSecurityFilterChain"이라는 이름으로 FilterChainProxy를 빈으로 등록한다.
12+
*/
13+
@Configuration
14+
public class WebSecurityConfiguration {
15+
16+
@Bean(name = FilterChainProxy.FILTER_CHAIN_PROXY_BEAN_NAME)
17+
public FilterChainProxy filterChainProxy(List<SecurityFilterChain> securityFilterChains) {
18+
return new FilterChainProxy(securityFilterChains);
19+
}
20+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package nextstep.security.filter;
2+
3+
import jakarta.servlet.*;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import jakarta.servlet.http.HttpServletResponse;
6+
import nextstep.security.util.Base64Convertor;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
10+
import java.io.IOException;
11+
import java.util.Optional;
12+
13+
public class BasicAuthenticationFilter implements Filter {
14+
15+
private static final Logger log = LoggerFactory.getLogger(BasicAuthenticationFilter.class);
16+
17+
private final UserDetailsService userDetailsService;
18+
19+
public BasicAuthenticationFilter(UserDetailsService userDetailsService) {
20+
this.userDetailsService = userDetailsService;
21+
}
22+
23+
@Override
24+
public void doFilter(
25+
ServletRequest servletRequest,
26+
ServletResponse servletResponse,
27+
FilterChain filterChain
28+
) throws IOException, ServletException {
29+
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
30+
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
31+
32+
try {
33+
String authorization = httpRequest.getHeader("Authorization");
34+
String credentials = authorization.split(" ")[1]; // "Basic " 뒤의 문자열
35+
String decodedString = Base64Convertor.decode(credentials);
36+
String[] usernameAndPassword = decodedString.split(":");
37+
String username = usernameAndPassword[0];
38+
String password = usernameAndPassword[1];
39+
40+
Optional<UserDetails> userDetailsOpt = userDetailsService.findUserDetailsByUsername(username);
41+
if (userDetailsOpt.isEmpty()) {
42+
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "User not found");
43+
return;
44+
}
45+
46+
UserDetails userDetails = userDetailsOpt.get();
47+
if (!userDetails.matchesPassword(password)) {
48+
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Password mismatch");
49+
return;
50+
}
51+
52+
httpRequest.setAttribute("userDetails", userDetails); // 이후 필터에서 사용 가능하도록 인증된 사용자 정보를 저장
53+
54+
filterChain.doFilter(servletRequest, servletResponse);
55+
} catch (RuntimeException e) {
56+
log.debug("Authentication failed", e);
57+
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed");
58+
}
59+
}
60+
}

0 commit comments

Comments
 (0)