Skip to content

Commit afda744

Browse files
ParkSeryuboorownie
andauthored
2단계 - 인가(Authorization) 리뷰 요청 드립니다. (#15)
* practice - /members/me * practice - authorization filter * feat: AuthorizationManager, AuthorizationDecision 구현 * refactor: 기존의 AuthorizationFilter 인가 로직을 RequestAuthorizationManager로 리팩토링 * test: 테스트 코드 구조화 - testFixture 추가 - 기존 테스트 코드 구조 변경 - 사용하지 않는 테스트 클래스 제거 * refactor: 기존의 SecuredMethodInterceptor 인가 로직을 SecuredAuthorizationManager로 리팩토링 * feat: RequestMatcherRegistry와 RequestMatcher를 작성하고, RequestMatcher의 구현체를 작성 * feat: 각 요청별 인가 로직을 담당하는 AuthorizationManager들 작성 * remove: 피드백 반영 - app 패키지의 불필요한 코드 제거 * refactor: 피드백 반영 - match 메소드의 null-safe한 코드로 변경 * refactor: 피드백 반영 - 인스턴스화 -> 상수 사용으로 변경 * refactor: 피드백 반영 - 역할과 책임 분리 * refactor: 피드백 반영 check -> authorize로 변경 --------- Co-authored-by: boorownie <[email protected]>
1 parent 7120f74 commit afda744

25 files changed

+448
-91
lines changed

src/main/java/nextstep/app/SecurityConfig.java

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,39 @@
11
package nextstep.app;
22

3+
import jakarta.servlet.http.HttpServletRequest;
4+
import java.util.ArrayList;
35
import nextstep.app.domain.Member;
46
import nextstep.app.domain.MemberRepository;
57
import nextstep.security.authentication.AuthenticationException;
68
import nextstep.security.authentication.BasicAuthenticationFilter;
79
import nextstep.security.authentication.UsernamePasswordAuthenticationFilter;
8-
import nextstep.security.authorization.CheckAuthenticationFilter;
9-
import nextstep.security.authorization.SecuredAspect;
10+
import nextstep.security.authorization.AuthorizationFilter;
11+
import nextstep.security.authorization.AuthorizationManager;
1012
import nextstep.security.authorization.SecuredMethodInterceptor;
13+
import nextstep.security.authorization.method.SecuredAuthorizationManager;
14+
import nextstep.security.authorization.web.AuthenticatedAuthorizationManager;
15+
import nextstep.security.authorization.web.AuthorityAuthorizationManager;
16+
import nextstep.security.authorization.web.DenyAllAuthorizationManager;
17+
import nextstep.security.authorization.web.RequestMatcherDelegatingAuthorizationManager;
1118
import nextstep.security.config.DefaultSecurityFilterChain;
1219
import nextstep.security.config.DelegatingFilterProxy;
1320
import nextstep.security.config.FilterChainProxy;
1421
import nextstep.security.config.SecurityFilterChain;
1522
import nextstep.security.context.SecurityContextHolderFilter;
1623
import nextstep.security.userdetails.UserDetails;
1724
import nextstep.security.userdetails.UserDetailsService;
25+
import nextstep.security.util.AnyRequestMatcher;
26+
import nextstep.security.util.MvcRequestMatcher;
27+
import nextstep.security.util.RequestMatcherEntry;
28+
import nextstep.security.authorization.web.PermitAllAuthorizationManager;
29+
import org.aopalliance.intercept.MethodInvocation;
1830
import org.springframework.context.annotation.Bean;
1931
import org.springframework.context.annotation.Configuration;
2032
import org.springframework.context.annotation.EnableAspectJAutoProxy;
2133

2234
import java.util.List;
2335
import java.util.Set;
36+
import org.springframework.http.HttpMethod;
2437

2538
@EnableAspectJAutoProxy
2639
@Configuration
@@ -44,12 +57,26 @@ public FilterChainProxy filterChainProxy(List<SecurityFilterChain> securityFilte
4457

4558
@Bean
4659
public SecuredMethodInterceptor securedMethodInterceptor() {
47-
return new SecuredMethodInterceptor();
60+
return new SecuredMethodInterceptor(securedAuthorizationManager());
61+
}
62+
63+
@Bean
64+
public AuthorizationManager<MethodInvocation> securedAuthorizationManager() {
65+
return new SecuredAuthorizationManager();
66+
}
67+
68+
@Bean
69+
public AuthorizationManager<HttpServletRequest> requestAuthorizationManager() {
70+
List<RequestMatcherEntry<AuthorizationManager>> mappings = new ArrayList<>();
71+
mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members"),
72+
new AuthorityAuthorizationManager<HttpServletRequest>(Set.of("ADMIN"))));
73+
mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members/me"),
74+
new AuthenticatedAuthorizationManager()));
75+
mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/search"),
76+
new PermitAllAuthorizationManager()));
77+
mappings.add(new RequestMatcherEntry<>(AnyRequestMatcher.INSTANCE, new DenyAllAuthorizationManager()));
78+
return new RequestMatcherDelegatingAuthorizationManager(mappings);
4879
}
49-
// @Bean
50-
// public SecuredAspect securedAspect() {
51-
// return new SecuredAspect();
52-
// }
5380

5481
@Bean
5582
public SecurityFilterChain securityFilterChain() {
@@ -58,7 +85,7 @@ public SecurityFilterChain securityFilterChain() {
5885
new SecurityContextHolderFilter(),
5986
new UsernamePasswordAuthenticationFilter(userDetailsService()),
6087
new BasicAuthenticationFilter(userDetailsService()),
61-
new CheckAuthenticationFilter()
88+
new AuthorizationFilter(requestAuthorizationManager())
6289
)
6390
);
6491
}

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

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

33
import nextstep.app.domain.Member;
44
import nextstep.app.domain.MemberRepository;
5+
import nextstep.security.authentication.Authentication;
6+
import nextstep.security.authentication.AuthenticationException;
57
import nextstep.security.authorization.Secured;
8+
import nextstep.security.context.SecurityContextHolder;
69
import org.springframework.http.ResponseEntity;
710
import org.springframework.web.bind.annotation.GetMapping;
811
import org.springframework.web.bind.annotation.RestController;
@@ -30,4 +33,15 @@ public ResponseEntity<List<Member>> search() {
3033
List<Member> members = memberRepository.findAll();
3134
return ResponseEntity.ok(members);
3235
}
36+
37+
@GetMapping("/members/me")
38+
public ResponseEntity<Member> me() {
39+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
40+
41+
String email = authentication.getPrincipal().toString();
42+
Member member = memberRepository.findByEmail(email)
43+
.orElseThrow(RuntimeException::new);
44+
45+
return ResponseEntity.ok(member);
46+
}
3347
}

src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import jakarta.servlet.FilterChain;
44
import jakarta.servlet.http.HttpServletRequest;
55
import jakarta.servlet.http.HttpServletResponse;
6+
import nextstep.security.authorization.ForbiddenException;
67
import nextstep.security.context.SecurityContext;
78
import nextstep.security.context.SecurityContextHolder;
89
import nextstep.security.userdetails.UserDetailsService;
@@ -40,8 +41,12 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
4041
SecurityContextHolder.setContext(context);
4142

4243
filterChain.doFilter(request, response);
43-
} catch (Exception e) {
44+
} catch (AuthenticationException e) {
4445
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
46+
} catch (ForbiddenException e) {
47+
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
48+
} catch (Exception e) {
49+
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
4550
}
4651
}
4752

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package nextstep.security.authorization;
2+
3+
import nextstep.security.authorization.web.AuthorizationResult;
4+
5+
public class AuthorizationDecision implements AuthorizationResult {
6+
public static final AuthorizationDecision ALLOW = new AuthorizationDecision(true);
7+
public static final AuthorizationDecision DENY = new AuthorizationDecision(false);
8+
9+
private final boolean granted;
10+
11+
public AuthorizationDecision(boolean granted) {
12+
this.granted = granted;
13+
}
14+
15+
public boolean isGranted() {
16+
return granted;
17+
}
18+
19+
public static AuthorizationDecision of(boolean granted) {
20+
return granted ? ALLOW : DENY;
21+
}
22+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package nextstep.security.authorization;
2+
3+
import nextstep.security.authentication.Authentication;
4+
import nextstep.security.authorization.web.AuthorizationResult;
5+
import nextstep.security.context.SecurityContextHolder;
6+
import org.springframework.web.filter.OncePerRequestFilter;
7+
8+
import jakarta.servlet.FilterChain;
9+
import jakarta.servlet.ServletException;
10+
import jakarta.servlet.http.HttpServletRequest;
11+
import jakarta.servlet.http.HttpServletResponse;
12+
import java.io.IOException;
13+
14+
public class AuthorizationFilter extends OncePerRequestFilter {
15+
16+
private final AuthorizationManager<HttpServletRequest> authorizationManager;
17+
18+
public AuthorizationFilter(AuthorizationManager<HttpServletRequest> authorizationManager) {
19+
this.authorizationManager = authorizationManager;
20+
}
21+
22+
@Override
23+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
24+
throws ServletException, IOException {
25+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
26+
AuthorizationResult authorizeResult = authorizationManager.authorize(authentication, request);
27+
28+
if (!authorizeResult.isGranted()) {
29+
throw new ForbiddenException();
30+
}
31+
32+
filterChain.doFilter(request, response);
33+
}
34+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package nextstep.security.authorization;
2+
3+
import nextstep.security.authentication.Authentication;
4+
import nextstep.security.authorization.web.AuthorizationResult;
5+
6+
@FunctionalInterface
7+
public interface AuthorizationManager<T> {
8+
@Deprecated
9+
AuthorizationDecision check(Authentication authentication, T object);
10+
11+
default AuthorizationResult authorize(Authentication authentication, T object) {
12+
return check(authentication, object);
13+
}
14+
}

src/main/java/nextstep/security/authorization/CheckAuthenticationFilter.java

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

src/main/java/nextstep/security/authorization/SecuredMethodInterceptor.java

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package nextstep.security.authorization;
22

33
import nextstep.security.authentication.Authentication;
4-
import nextstep.security.authentication.AuthenticationException;
4+
import nextstep.security.authorization.web.AuthorizationResult;
55
import nextstep.security.context.SecurityContextHolder;
66
import org.aopalliance.aop.Advice;
77
import org.aopalliance.intercept.MethodInterceptor;
@@ -11,29 +11,25 @@
1111
import org.springframework.aop.framework.AopInfrastructureBean;
1212
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
1313

14-
import java.lang.reflect.Method;
15-
1614
public class SecuredMethodInterceptor implements MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
1715

16+
private final AuthorizationManager<MethodInvocation> authorizationManager;
1817
private final Pointcut pointcut;
1918

20-
public SecuredMethodInterceptor() {
19+
public SecuredMethodInterceptor(AuthorizationManager<MethodInvocation> authorizationManager) {
20+
this.authorizationManager = authorizationManager;
2121
this.pointcut = new AnnotationMatchingPointcut(null, Secured.class);
2222
}
2323

2424
@Override
2525
public Object invoke(MethodInvocation invocation) throws Throwable {
26-
Method method = invocation.getMethod();
27-
if (method.isAnnotationPresent(Secured.class)) {
28-
Secured secured = method.getAnnotation(Secured.class);
29-
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
30-
if (authentication == null) {
31-
throw new AuthenticationException();
32-
}
33-
if (!authentication.getAuthorities().contains(secured.value())) {
34-
throw new ForbiddenException();
35-
}
26+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
27+
AuthorizationResult authorizationResult = authorizationManager.authorize(authentication, invocation);
28+
29+
if (!authorizationResult.isGranted()) {
30+
throw new ForbiddenException();
3631
}
32+
3733
return invocation.proceed();
3834
}
3935

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package nextstep.security.authorization.method;
2+
3+
import java.lang.reflect.Method;
4+
import java.util.Collection;
5+
import java.util.Collections;
6+
import java.util.Set;
7+
import nextstep.security.authentication.Authentication;
8+
import nextstep.security.authorization.AuthorizationDecision;
9+
import nextstep.security.authorization.AuthorizationManager;
10+
import nextstep.security.authorization.Secured;
11+
import nextstep.security.authorization.web.AuthorityAuthorizationManager;
12+
import org.aopalliance.intercept.MethodInvocation;
13+
14+
public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
15+
16+
private AuthorityAuthorizationManager<Collection<String>> authorityAuthorizationManager;
17+
18+
public void setAuthorityAuthorizationManager(Collection<String> authorities) {
19+
authorityAuthorizationManager = new AuthorityAuthorizationManager<>(authorities);
20+
}
21+
22+
@Override
23+
public AuthorizationDecision check(Authentication authentication, MethodInvocation invocation) {
24+
Collection<String> authorities = getAuthorities(invocation);
25+
26+
if (authorities.isEmpty()) {
27+
return null;
28+
}
29+
setAuthorityAuthorizationManager(authorities);
30+
return authorities.isEmpty() ? null : authorityAuthorizationManager.check(authentication, authorities);
31+
}
32+
33+
private Collection<String> getAuthorities(MethodInvocation invocation) {
34+
Method method = invocation.getMethod();
35+
36+
if (!method.isAnnotationPresent(Secured.class)) {
37+
return Collections.emptySet();
38+
}
39+
40+
Secured secured = method.getAnnotation(Secured.class);
41+
return Set.of(secured.value());
42+
}
43+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package nextstep.security.authorization.web;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import nextstep.security.authentication.Authentication;
5+
import nextstep.security.authorization.AuthorizationDecision;
6+
import nextstep.security.authorization.AuthorizationManager;
7+
8+
public class AuthenticatedAuthorizationManager implements AuthorizationManager<HttpServletRequest> {
9+
@Override
10+
public AuthorizationDecision check(Authentication authentication, HttpServletRequest object) {
11+
if (authentication != null && authentication.isAuthenticated()) {
12+
return AuthorizationDecision.ALLOW;
13+
}
14+
return AuthorizationDecision.DENY;
15+
}
16+
}

0 commit comments

Comments
 (0)