-
Notifications
You must be signed in to change notification settings - Fork 20
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
[step2] 리팩터링 #5
base: jin-jaylin
Are you sure you want to change the base?
[step2] 리팩터링 #5
Changes from all commits
9db8150
beac23b
96248b5
1362e11
db24c17
e0dbdf5
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 |
---|---|---|
|
@@ -2,8 +2,9 @@ | |
|
||
import nextstep.security.access.matcher.MvcRequestMatcher; | ||
import nextstep.security.authentication.*; | ||
import nextstep.security.authorization.LoginAuthorizationFilter; | ||
import nextstep.security.authorization.SessionAuthorizationFilter; | ||
import nextstep.security.authorization.RoleAuthorizationFilter; | ||
import nextstep.security.config.AuthorizeRequestMatcherRegistry; | ||
import nextstep.security.config.DefaultSecurityFilterChain; | ||
import nextstep.security.config.FilterChainProxy; | ||
import nextstep.security.config.SecurityFilterChain; | ||
|
@@ -29,6 +30,15 @@ public AuthConfig(UserDetailsService userDetailsService) { | |
this.userDetailsService = userDetailsService; | ||
} | ||
|
||
@Bean | ||
public AuthorizeRequestMatcherRegistry requestMatcherRegistryMapping() { | ||
AuthorizeRequestMatcherRegistry requestMatcherRegistry = new AuthorizeRequestMatcherRegistry(); | ||
requestMatcherRegistry | ||
.matcher(new MvcRequestMatcher(HttpMethod.GET, "/members")).hasAuthority("ADMIN") | ||
.matcher(new MvcRequestMatcher(HttpMethod.GET, "/members/me")).authenticated(); | ||
return requestMatcherRegistry; | ||
} | ||
|
||
@Bean | ||
public DelegatingFilterProxy securityFilterChainProxy() { | ||
return new DelegatingFilterProxy("filterChainProxy"); | ||
|
@@ -50,8 +60,8 @@ public SecurityFilterChain loginSecurityFilterChain() { | |
public SecurityFilterChain membersSecurityFilterChain() { | ||
List<Filter> filters = new ArrayList<>(); | ||
filters.add(new BasicAuthenticationFilter(authenticationManager())); | ||
filters.add(new LoginAuthorizationFilter(securityContextRepository())); | ||
filters.add(new RoleAuthorizationFilter()); | ||
filters.add(new SessionAuthorizationFilter(securityContextRepository(), requestMatcherRegistryMapping())); | ||
filters.add(new RoleAuthorizationFilter(requestMatcherRegistryMapping())); | ||
return new DefaultSecurityFilterChain(new MvcRequestMatcher(HttpMethod.GET, "/members"), filters); | ||
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. 요청에 따라 인가 방법을 결정할 수 있게 되면서, securityFilterChain도 도 하나로 통일 할 수 있습니다. (+) 왜 securityFilterChain을 하나로 관리하는 요구사항이 있을지도 고민해보셨으면 좋겠어요! 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. 네엥... SessionAuthorizationFilter이 위 계속 문의드린 로그인 관련된 기능이었는데 제외시키고 나머지도 하나로 통일 시켜보겠습네다. 👍 |
||
} | ||
|
||
|
@@ -64,5 +74,4 @@ public SecurityContextRepository securityContextRepository() { | |
public AuthenticationManager authenticationManager() { | ||
return new AuthenticationManager(new UsernamePasswordAuthenticationProvider(userDetailsService)); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,4 +25,11 @@ public ResponseEntity<List<Member>> list() { | |
return ResponseEntity.ok(members); | ||
} | ||
|
||
@GetMapping("/members/me") | ||
public ResponseEntity<Member> me() { | ||
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 authentication = SecurityContextHolder.getContext().getAuthentication(); | ||
String email = authentication.getPrincipal().toString(); | ||
Member member = memberRepository.findByEmail(email).orElseThrow(); | ||
return ResponseEntity.ok(member); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,9 @@ | ||
package nextstep.security.authorization; | ||
|
||
import nextstep.security.access.matcher.MvcRequestMatcher; | ||
import nextstep.security.authentication.Authentication; | ||
import nextstep.security.config.AuthorizeRequestMatcherRegistry; | ||
import nextstep.security.context.SecurityContextHolder; | ||
import nextstep.security.exception.AuthenticationException; | ||
import org.springframework.http.HttpMethod; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.web.filter.GenericFilterBean; | ||
|
||
|
@@ -17,22 +16,22 @@ | |
import java.io.IOException; | ||
|
||
public class RoleAuthorizationFilter extends GenericFilterBean { | ||
private static final MvcRequestMatcher DEFAULT_REQUEST_MATCHER = new MvcRequestMatcher(HttpMethod.GET, | ||
"/members"); | ||
|
||
private static final String ADMIN_ROLE = "ADMIN"; | ||
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. 이 값은 더이상 사용하지 않는 값이네요. |
||
|
||
private final AuthorizeRequestMatcherRegistry requestMatcherRegistry; | ||
|
||
public RoleAuthorizationFilter(AuthorizeRequestMatcherRegistry requestMatcherRegistry) { | ||
this.requestMatcherRegistry = requestMatcherRegistry; | ||
} | ||
|
||
@Override | ||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { | ||
try { | ||
if (!DEFAULT_REQUEST_MATCHER.matches((HttpServletRequest) request)) { | ||
chain.doFilter(request, response); | ||
return; | ||
} | ||
|
||
String attribute = requestMatcherRegistry.getAttribute((HttpServletRequest) request); | ||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); | ||
boolean isAuthorized = requestMatcherRegistry.isAuthorized(attribute, authentication); | ||
|
||
if (!authentication.getAuthorities().contains(ADMIN_ROLE)) { | ||
if (!isAuthorized) { | ||
Comment on lines
+30
to
+34
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. 이와같이 사용한다면 굳이 attribute를 registry에서 꺼내오지 않고 registry에 메시지를 보내도 괜찮지 않을까요?
|
||
((HttpServletResponse) response).sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase()); | ||
return; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package nextstep.security.config; | ||
|
||
import nextstep.security.access.matcher.RequestMatcher; | ||
import nextstep.security.authentication.Authentication; | ||
|
||
import javax.servlet.http.HttpServletRequest; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
|
||
public class AuthorizeRequestMatcherRegistry { | ||
private final Map<RequestMatcher, String> mappings = new HashMap<>(); | ||
|
||
public AuthorizedUrl matcher(RequestMatcher requestMatcher) { | ||
return new AuthorizedUrl(requestMatcher); | ||
} | ||
|
||
AuthorizeRequestMatcherRegistry addMapping(RequestMatcher requestMatcher, String attributes) { | ||
mappings.put(requestMatcher, attributes); | ||
return this; | ||
} | ||
|
||
public String getAttribute(HttpServletRequest request) { | ||
for (Map.Entry<RequestMatcher, String> entry : mappings.entrySet()) { | ||
if (entry.getKey().matches(request)) { | ||
return entry.getValue(); | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
public class AuthorizedUrl { | ||
public static final String PERMIT_ALL = "permitAll"; | ||
public static final String DENY_ALL = "denyAll"; | ||
public static final String AUTHENTICATED = "authenticated"; | ||
public static final String AUTHORITY = "hasAuthority"; | ||
|
||
private final RequestMatcher requestMatcher; | ||
|
||
public AuthorizedUrl(RequestMatcher requestMatcher) { | ||
this.requestMatcher = requestMatcher; | ||
} | ||
|
||
public AuthorizeRequestMatcherRegistry permitAll() { | ||
return access(PERMIT_ALL); | ||
} | ||
|
||
public AuthorizeRequestMatcherRegistry denyAll() { | ||
return access(DENY_ALL); | ||
} | ||
|
||
public AuthorizeRequestMatcherRegistry hasAuthority(String authority) { | ||
return access(AUTHORITY + "(" + authority + ")"); | ||
} | ||
|
||
public AuthorizeRequestMatcherRegistry authenticated() { | ||
return access(AUTHENTICATED); | ||
} | ||
|
||
private AuthorizeRequestMatcherRegistry access(String attribute) { | ||
return addMapping(requestMatcher, attribute); | ||
} | ||
} | ||
|
||
public Boolean isAuthorized(String attribute, Authentication authentication) { | ||
if (attribute.contains(AuthorizedUrl.AUTHORITY)) { | ||
Pattern pattern = Pattern.compile("\\((.*?)\\)"); | ||
Matcher matcher = pattern.matcher(attribute); | ||
if(matcher.find()) { | ||
String role = matcher.group(1).trim(); | ||
return authentication.getAuthorities().contains(role); | ||
} | ||
} else if (attribute.contains(AuthorizedUrl.AUTHENTICATED)) { | ||
return authentication.isAuthenticated(); | ||
} | ||
Comment on lines
+68
to
+77
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 - else if - else if ... 구문이 추가되겠네요. 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. 메소드 하나 내에서 난리친것을 인정합니다.. 반성합니다.. |
||
|
||
return false; | ||
} | ||
|
||
} |
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.
진님 말씀처럼 요구사항에 용어 정리가 잘 안되어 있었어요 🥲
첫번째 요구사항은 인증이 완료된 사용자의 정보를 응답하는게 맞습니다.
미션 문서를 만들 때는 인증을 위해 인증 정보를 전달하는 방식 중 하나가 로그인이라고 생각했었어요. (엄밀하게 말해서는 Form 기반 로그인이겠네요 ㅎㅎ)
인증 처리는 요청으로 들어온 인증 정보에 있는 값이 유효한지(요청에서 주장하는 사용자가 맞는지)를 확인하는 과정이고, 인증이 완료되었다는 것은 인증 정보로 들어온 principal, credential에 해당하는 사용자가 있다는 뜻이죠. 우리가 만드는 프레임워크에서는 인증이 완료 되었을 때 인증 객체(Authentication)에 isAuthenticated()를 호출하면 true 값이 반환 되구요.
어제 강의에서 다뤘던 것처럼 Form 기반 로그인은 인증 요청과 리소스 접근(인가) 요청이 분리되어 있기 때문에 인증 객체(를 담고있는 SecurityContext)의 영속화가 필요하구요. (이게 말씀하신 session에 setAttribute)
정리해보자면 아래와 같습니다.
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.
네네, 인증과 인가,,,, 과연 로그인은 그럼 어디에 속하는가가 조금 많이 헷갈렸는데요.
분명 인가에 대한 내용 같지만 과제 중간 제목에 로그인한 사용자에 대한 체크라는 부분이 있어서, session에 setAttribute한 부분까지 체크를 해야하는가? 하는 의문이 컸습니다. 그래서 실제로 MemberTest에보면 session set 까지 해주는 부분이 있고요^_ㅠ
말씀주신 내용 참고해서 다시 코드 개선해보겠습니다!
제가 요구사항을 완벽하게 숙지하지 못해서 더 헷갈린 것 같아요. 🙏