-
Notifications
You must be signed in to change notification settings - Fork 42
[인증(Authentication) - 2, 3, 4단계] 김선호 미션 제출합니다. 🚀 #42
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
Changes from all commits
200fb25
dfbd707
470703c
2975d27
523128f
13bf0b7
6c7ae54
f708698
951231b
57bc51c
4244c33
3122529
02080d2
b8b7894
ff9812e
4de355e
302079c
424470a
4ae90bc
e2bd956
ec51df0
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 |
---|---|---|
@@ -1,5 +1,8 @@ | ||
package nextstep.app.security; | ||
|
||
import nextstep.security.context.HttpSessionSecurityContextRepository; | ||
import nextstep.security.context.SecurityContextHolderFilter; | ||
import nextstep.security.context.SecurityContextRepository; | ||
import nextstep.security.filter.*; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
|
@@ -17,20 +20,13 @@ public AppSecurityConfiguration(UserDetailsService userDetailsService) { | |
|
||
@Bean | ||
public SecurityFilterChain formLoginSecurityFilterChain() { | ||
return new DefaultSecurityFilterChain( | ||
(httpServletRequest) -> httpServletRequest.getRequestURI().equals("/login"), | ||
List.of( | ||
new FormLoginFilter(userDetailsService) | ||
) | ||
); | ||
} | ||
SecurityContextRepository contextRepository = new HttpSessionSecurityContextRepository(); | ||
|
||
@Bean | ||
public SecurityFilterChain basicAuthenticationSecurityFilterChain() { | ||
return new DefaultSecurityFilterChain( | ||
(httpServletRequest) -> httpServletRequest.getRequestURI().equals("/members"), | ||
List.of( | ||
new BasicAuthenticationFilter(userDetailsService) | ||
new SecurityContextHolderFilter(contextRepository), | ||
new UsernamePasswordAuthenticationFilter(userDetailsService, contextRepository), | ||
new BasicAuthenticationFilter(userDetailsService, contextRepository) | ||
Comment on lines
+28
to
+29
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. userDetailsService를 인자로 넘기는 것과 authenticationManager를 넘기는 방법도 있을 것 같은데 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.
이 정도 차이가 있을 것 같은데 혹시 의도하신게 이 부분일까요..! 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. 네, userDetailsService 대신 AuthenticationManager를 주입해주면 어떨까 하는 코멘트였습니다 :) |
||
) | ||
); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package nextstep.security; | ||
|
||
public class AuthenticationException extends Exception{ | ||
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. Exception을 상속한 이유가 있나요? 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. 여기서는 메서드를 호출하는 쪽에서 명시적으로 예외를 핸들링하게 하기 위해서 사용했습니다. AuthenticationFilter에게는 AuthenticationException까지 핸들링하는 책임이 있다고 생각해서, AuthenticationFilter 하위 레이어인 AuthenticationManager, AuthenticationProvider 에서 예외를 체크 예외로 던지는게 낫다고 생각했어요. 언체크 예외를 던지면 AuthenticationException이 발생했는지 눈치 채기가 어렵다 보니까, 일부러 이런 방식을 택했습니다. 시큐리티 코드를 보니 실제로는 ProviderManager에서 예외를 핸들링 해주고 있는데, 미션에서는 별도의 예외 처리 레이어를 두기가 애매하더라고요. 그래서 HttpServletRequest를 알고 있는 AuthenticationFilter 선에서 예외 핸들링을 마치려고 했던 것도 있습니다..! (질문 주신 의도에 맞는 답변일지 궁금하네요..!) 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. 체크드 익셉션을 사용할 경우 의도하지 않은 곳에서도 throw를 통해 넘겨주어야 하기 때문에 가급적이면 언체크드 익셉션을 사용하기를 권장합니다. 우선 실제 시큐리티 코드에서는 예외 처리를 별도의 필터에서 진행하기 때문에 중간 단계에서는 예외 처리에 대해 고민하지 않도록 하기 위해서라도 언체크드 익셉션을 활용한다고 볼 수 있습니다! |
||
|
||
public AuthenticationException() { | ||
super(); | ||
} | ||
|
||
public AuthenticationException(String message) { | ||
super(message); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package nextstep.security.authentication; | ||
|
||
public interface Authentication { | ||
|
||
Object getCredentials(); | ||
|
||
Object getPrincipal(); | ||
|
||
boolean isAuthenticated(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package nextstep.security.authentication; | ||
|
||
import nextstep.security.AuthenticationException; | ||
|
||
@FunctionalInterface | ||
public interface AuthenticationManager { | ||
|
||
/** | ||
* Attempts to authenticate the passed {@link Authentication} object, returning a | ||
* fully populated <code>Authentication</code> object if successful. | ||
* @param authentication the authentication request object | ||
* @return a fully authenticated object including credentials | ||
* @throws AuthenticationException if authentication fails | ||
*/ | ||
Comment on lines
+8
to
+14
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. 주석도 가져오신건가요?ㅋㅋㅋ 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. 맞습니다 ㅎㅎ 시큐리티 코드 분석을 하다 보니 복잡한 구현으로 인해 제가 컨텍스트를 종종 놓치더라고요. |
||
Authentication authenticate(Authentication authentication) throws AuthenticationException; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package nextstep.security.authentication; | ||
|
||
import nextstep.security.AuthenticationException; | ||
|
||
public interface AuthenticationProvider { | ||
|
||
/** | ||
* Performs authentication with the same contract as AuthenticationManager.authenticate(Authentication). | ||
* @param authentication the authentication request object. | ||
* @return a fully authenticated object including credentials. | ||
* @throws AuthenticationException if authentication fails. | ||
*/ | ||
Authentication authenticate(Authentication authentication) throws AuthenticationException; | ||
|
||
boolean supports(Class<?> authentication); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package nextstep.security.authentication; | ||
|
||
import nextstep.security.AuthenticationException; | ||
import nextstep.security.filter.UserDetails; | ||
import nextstep.security.filter.UserDetailsService; | ||
import org.springframework.util.Assert; | ||
import org.springframework.util.StringUtils; | ||
|
||
import java.util.Optional; | ||
|
||
public class DaoAuthenticationProvider implements AuthenticationProvider { | ||
|
||
private final UserDetailsService userDetailsService; | ||
|
||
public DaoAuthenticationProvider(UserDetailsService userDetailsService) { | ||
this.userDetailsService = userDetailsService; | ||
} | ||
|
||
@Override | ||
public Authentication authenticate(Authentication authentication) throws AuthenticationException { | ||
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, "Only UsernamePasswordAuthenticationToken is supported"); | ||
|
||
String username = (String) authentication.getPrincipal(); | ||
String password = (String) authentication.getCredentials(); | ||
|
||
if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) { | ||
throw new AuthenticationException("username or password is empty"); | ||
} | ||
|
||
Optional<UserDetails> userDetailsOpt = userDetailsService.findUserDetailsByUsername(username); | ||
if (userDetailsOpt.isEmpty()) { | ||
throw new AuthenticationException("User not found"); | ||
} | ||
|
||
UserDetails userDetails = userDetailsOpt.get(); | ||
if (!userDetails.matchesPassword(password)) { | ||
throw new AuthenticationException("username or password invalid"); | ||
} | ||
|
||
// matches password. authenticated. | ||
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. 이 주석은 어떤 의미인가요? 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. if (!userDetails.matchesPassword(password)) {
throw new AuthenticationException("username or password invalid");
}
// matches password. authenticated. '비밀번호에 대한 검증을 끝냈으니, 이제 비밀번호는 일치하고 인증이 된 것이다. 그러니까 인증된 Authentication을 리턴하면 된다.' 라는 의미를 남기고 싶었는데, 지금 보니 컨텍스트가 생략되었네요. 주석을 달더라도 조금 더 컨텍스트를 전달하는, 의미 있는 주석을 달 수 있도록 해봐야겠습니다! |
||
return UsernamePasswordAuthenticationToken.authenticated(authentication); | ||
} | ||
|
||
@Override | ||
public boolean supports(Class<?> authentication) { | ||
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package nextstep.security.authentication; | ||
|
||
import nextstep.security.AuthenticationException; | ||
|
||
import java.util.List; | ||
|
||
public class ProviderManager implements AuthenticationManager { | ||
|
||
private final List<AuthenticationProvider> providers; | ||
|
||
public ProviderManager(List<AuthenticationProvider> providers) { | ||
this.providers = providers; | ||
} | ||
|
||
@Override | ||
public Authentication authenticate(Authentication authentication) throws AuthenticationException { | ||
for (AuthenticationProvider provider : providers) { | ||
if (provider.supports(authentication.getClass())) { | ||
return provider.authenticate(authentication); | ||
} | ||
} | ||
|
||
return authentication; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package nextstep.security.authentication; | ||
|
||
public class UsernamePasswordAuthenticationToken implements Authentication { | ||
|
||
private final String username; | ||
private final String password; | ||
private final boolean authenticated; | ||
|
||
private UsernamePasswordAuthenticationToken(String username, String password, boolean authenticated) { | ||
this.username = username; | ||
this.password = password; | ||
this.authenticated = authenticated; | ||
} | ||
|
||
public static UsernamePasswordAuthenticationToken unAuthenticated(String username, String password) { | ||
return new UsernamePasswordAuthenticationToken(username, password, false); | ||
} | ||
|
||
public static UsernamePasswordAuthenticationToken authenticated(Authentication authentication) { | ||
return new UsernamePasswordAuthenticationToken( | ||
(String) authentication.getPrincipal(), | ||
(String) authentication.getCredentials(), | ||
true | ||
); | ||
} | ||
|
||
@Override | ||
public Object getCredentials() { | ||
return this.password; | ||
} | ||
|
||
@Override | ||
public Object getPrincipal() { | ||
return this.username; | ||
} | ||
|
||
@Override | ||
public boolean isAuthenticated() { | ||
return this.authenticated; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package nextstep.security.context; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import jakarta.servlet.http.HttpSession; | ||
|
||
public class HttpSessionSecurityContextRepository implements SecurityContextRepository { | ||
|
||
public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; | ||
|
||
// todo: SecurityContextRepository의 loadContext와 saveContext 동작을 각각 테스트. | ||
@Override | ||
public SecurityContext loadContext(HttpServletRequest request) { | ||
SecurityContext securityContext = readSecurityContextFromSession(request.getSession()); | ||
|
||
if (securityContext == null) { | ||
return SecurityContextHolder.createEmptyContext(); | ||
} | ||
|
||
return securityContext; | ||
} | ||
|
||
@Override | ||
public void saveContext( | ||
SecurityContext context, | ||
HttpServletRequest request, | ||
HttpServletResponse response | ||
) { | ||
HttpSession session = request.getSession(); | ||
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context); | ||
} | ||
|
||
private SecurityContext readSecurityContextFromSession(HttpSession session) { | ||
if (session == null) { | ||
return null; | ||
} | ||
|
||
// Session Exists. | ||
Object securityContextFromSession = session.getAttribute(SPRING_SECURITY_CONTEXT_KEY); | ||
if (securityContextFromSession == null) { | ||
return null; | ||
} | ||
|
||
if (!(securityContextFromSession instanceof SecurityContext)) { | ||
return null; | ||
} | ||
|
||
// Everything Cool! SecurityContext Exists. | ||
return (SecurityContext) securityContextFromSession; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package nextstep.security.context; | ||
|
||
import nextstep.security.authentication.Authentication; | ||
|
||
import java.io.Serializable; | ||
|
||
public interface SecurityContext extends Serializable { | ||
|
||
Authentication getAuthentication(); | ||
|
||
void setAuthentication(Authentication authentication); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package nextstep.security.context; | ||
|
||
public class SecurityContextHolder { | ||
|
||
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>(); | ||
|
||
public static SecurityContext getContext() { | ||
SecurityContext context = contextHolder.get(); | ||
|
||
if (context == null) { | ||
context = createEmptyContext(); | ||
contextHolder.set(context); | ||
} | ||
|
||
return context; | ||
} | ||
|
||
public static void setContext(SecurityContext context) { | ||
contextHolder.set(context); | ||
} | ||
|
||
public static void clearContext() { | ||
contextHolder.remove(); | ||
} | ||
|
||
public static SecurityContext createEmptyContext() { | ||
return new SecurityContextImpl(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package nextstep.security.context; | ||
|
||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import org.springframework.util.Assert; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
|
||
import java.io.IOException; | ||
|
||
public class SecurityContextHolderFilter extends OncePerRequestFilter { | ||
|
||
private final SecurityContextRepository securityContextRepository; | ||
|
||
public SecurityContextHolderFilter(SecurityContextRepository securityContextRepository) { | ||
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null"); | ||
this.securityContextRepository = securityContextRepository; | ||
} | ||
|
||
@Override | ||
protected void doFilterInternal( | ||
HttpServletRequest request, | ||
HttpServletResponse response, | ||
FilterChain filterChain | ||
) throws ServletException, IOException { | ||
// contextRepository를 통해 SecurityContext를 가져온다. 없을 경우 empty context. | ||
SecurityContext context = this.securityContextRepository.loadContext(request); | ||
|
||
try { | ||
SecurityContextHolder.setContext(context); | ||
filterChain.doFilter(request, response); | ||
} finally { | ||
// 요청이 끝나고 SecurityContext를 clear. (HTTP Session에는 SecurityContext가 남아있을 수 있음) | ||
SecurityContextHolder.clearContext(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package nextstep.security.context; | ||
|
||
import nextstep.security.authentication.Authentication; | ||
|
||
public class SecurityContextImpl implements SecurityContext { | ||
|
||
private Authentication authentication; | ||
|
||
public SecurityContextImpl() { | ||
} | ||
|
||
public SecurityContextImpl(Authentication authentication) { | ||
this.authentication = authentication; | ||
} | ||
|
||
@Override | ||
public Authentication getAuthentication() { | ||
return this.authentication; | ||
} | ||
|
||
@Override | ||
public void setAuthentication(Authentication authentication) { | ||
this.authentication = authentication; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package nextstep.security.context; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
|
||
public interface SecurityContextRepository { | ||
|
||
SecurityContext loadContext(HttpServletRequest request); | ||
|
||
void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response); | ||
} |
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.
피드백 반영 👍