Skip to content

Commit 289d47a

Browse files
authored
[고정완] - 인가(Authorization) 리뷰 요청 드립니다. (#9)
* refactor: Security 에서의 공통 Exception 처리를 FilterChainProxy 에서 담당하도록 변경 * test: 자신의 정보를 조회하는데 인증이 필요한 부분에 대한 테스트 작성 * feat: CheckAuthenticationFilter 로직을 대체하는 RequestAuthorizationManager 구현 * feat: RequestMatcher 와 RequestMatcherEntry 구현 * feat: 4가지 AuthorizationManager 구현; PermitAll, DenyAll, Authenticated, HasAuthority * feat: AuthorizationManager 에서 RequestMatcherEntry 를 활용하도록 Config 에 Bean 을 등록함 * refactor: SecuredMethodInterceptor 의 인가 로직을 SecuredAuthorizationManager 으로 이동 * fix: Secured 어노테이션이 없으면, MethodInterceptor 에서 인증 및 인가 과정을 거치지 않도록 수정함 * refactor: Rename HasAuthorityAuthorizationManager to AuthorityAuthorizationManager * fix: security 패키지에서 stream 문을 사용하지 않도록 변경 * fix: 삼항연산자 대신 if 문으로 early return 을 해주도록 모두 변경 * fix: Secured 어노테이션이 없는 메서드일 경우 인가 값이 false 가 되도록 수정 * refactor: deprecated 된 AuthorizationManager.check 대신, AuthorizationResult 인터페이스를 반환하는 AuthorizationManager.authorize 로 대체 * test: Fixture 를 enum 으로 생성 * test: 어드민 유저와 일반 유저의 인가 테스트 케이스를 분리
1 parent 7120f74 commit 289d47a

34 files changed

+934
-220
lines changed

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
package nextstep.app;
22

3+
import jakarta.servlet.http.HttpServletRequest;
34
import nextstep.app.domain.Member;
45
import nextstep.app.domain.MemberRepository;
56
import nextstep.security.authentication.AuthenticationException;
67
import nextstep.security.authentication.BasicAuthenticationFilter;
78
import nextstep.security.authentication.UsernamePasswordAuthenticationFilter;
8-
import nextstep.security.authorization.CheckAuthenticationFilter;
9-
import nextstep.security.authorization.SecuredAspect;
9+
import nextstep.security.authorization.AuthorizationFilter;
1010
import nextstep.security.authorization.SecuredMethodInterceptor;
11+
import nextstep.security.authorization.manager.AuthenticatedAuthorizationManager;
12+
import nextstep.security.authorization.manager.AuthorityAuthorizationManager;
13+
import nextstep.security.authorization.manager.AuthorizationManager;
14+
import nextstep.security.authorization.manager.DenyAllAuthorizationManager;
15+
import nextstep.security.authorization.manager.PermitAllAuthorizationManager;
16+
import nextstep.security.authorization.manager.RequestAuthorizationManager;
1117
import nextstep.security.config.DefaultSecurityFilterChain;
1218
import nextstep.security.config.DelegatingFilterProxy;
1319
import nextstep.security.config.FilterChainProxy;
@@ -22,6 +28,10 @@
2228
import java.util.List;
2329
import java.util.Set;
2430

31+
import static nextstep.security.authorization.matcher.RequestMatcherEntry.createDefaultMatcher;
32+
import static nextstep.security.authorization.matcher.RequestMatcherEntry.createMvcMatcher;
33+
import static org.springframework.http.HttpMethod.GET;
34+
2535
@EnableAspectJAutoProxy
2636
@Configuration
2737
public class SecurityConfig {
@@ -46,19 +56,20 @@ public FilterChainProxy filterChainProxy(List<SecurityFilterChain> securityFilte
4656
public SecuredMethodInterceptor securedMethodInterceptor() {
4757
return new SecuredMethodInterceptor();
4858
}
49-
// @Bean
50-
// public SecuredAspect securedAspect() {
51-
// return new SecuredAspect();
52-
// }
5359

5460
@Bean
5561
public SecurityFilterChain securityFilterChain() {
62+
final AuthorizationManager<HttpServletRequest> authorizationManager = new RequestAuthorizationManager(List.of(
63+
createMvcMatcher(GET, "/members", new AuthorityAuthorizationManager<>("ADMIN")),
64+
createMvcMatcher(GET, "/members/me", new AuthenticatedAuthorizationManager<>()),
65+
createMvcMatcher(GET, "/search", new PermitAllAuthorizationManager<>())
66+
), createDefaultMatcher(new DenyAllAuthorizationManager<>()));
5667
return new DefaultSecurityFilterChain(
5768
List.of(
5869
new SecurityContextHolderFilter(),
5970
new UsernamePasswordAuthenticationFilter(userDetailsService()),
6071
new BasicAuthenticationFilter(userDetailsService()),
61-
new CheckAuthenticationFilter()
72+
new AuthorizationFilter(authorizationManager)
6273
)
6374
);
6475
}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
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;
912

1013
import java.util.List;
14+
import java.util.Optional;
1115

1216
@RestController
1317
public class MemberController {
@@ -30,4 +34,22 @@ public ResponseEntity<List<Member>> search() {
3034
List<Member> members = memberRepository.findAll();
3135
return ResponseEntity.ok(members);
3236
}
37+
38+
@Secured("USER")
39+
@GetMapping("/members/me")
40+
public ResponseEntity<Member> me() {
41+
final Authentication authentication = SecurityContextHolder
42+
.getContext().getAuthentication();
43+
return ResponseEntity.ok(
44+
Optional.ofNullable(authentication)
45+
.flatMap(this::findMe)
46+
.orElseThrow(AuthenticationException::new)
47+
);
48+
}
49+
50+
private Optional<Member> findMe(Authentication authentication) {
51+
return memberRepository.findByEmail(
52+
authentication.getPrincipal().toString()
53+
);
54+
}
3355
}

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

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package nextstep.security.authentication;
22

33
import jakarta.servlet.FilterChain;
4+
import jakarta.servlet.ServletException;
45
import jakarta.servlet.http.HttpServletRequest;
56
import jakarta.servlet.http.HttpServletResponse;
67
import nextstep.security.context.SecurityContext;
@@ -10,6 +11,7 @@
1011
import org.springframework.util.StringUtils;
1112
import org.springframework.web.filter.OncePerRequestFilter;
1213

14+
import java.io.IOException;
1315
import java.nio.charset.StandardCharsets;
1416
import java.util.Base64;
1517
import java.util.List;
@@ -26,23 +28,23 @@ public BasicAuthenticationFilter(UserDetailsService userDetailsService) {
2628
}
2729

2830
@Override
29-
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
30-
try {
31-
Authentication authentication = convert(request);
32-
if (authentication == null) {
33-
filterChain.doFilter(request, response);
34-
return;
35-
}
36-
37-
Authentication authResult = this.authenticationManager.authenticate(authentication);
38-
SecurityContext context = SecurityContextHolder.createEmptyContext();
39-
context.setAuthentication(authResult);
40-
SecurityContextHolder.setContext(context);
41-
31+
protected void doFilterInternal(
32+
HttpServletRequest request,
33+
HttpServletResponse response,
34+
FilterChain filterChain
35+
) throws ServletException, IOException {
36+
Authentication authentication = convert(request);
37+
if (authentication == null) {
4238
filterChain.doFilter(request, response);
43-
} catch (Exception e) {
44-
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
39+
return;
4540
}
41+
42+
Authentication authResult = this.authenticationManager.authenticate(authentication);
43+
SecurityContext context = SecurityContextHolder.createEmptyContext();
44+
context.setAuthentication(authResult);
45+
SecurityContextHolder.setContext(context);
46+
47+
filterChain.doFilter(request, response);
4648
}
4749

4850
private Authentication convert(HttpServletRequest request) {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package nextstep.security.authorization;
2+
3+
import jakarta.servlet.FilterChain;
4+
import jakarta.servlet.ServletException;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import nextstep.security.authentication.Authentication;
8+
import nextstep.security.authentication.AuthenticationException;
9+
import nextstep.security.authorization.manager.AuthorizationManager;
10+
import nextstep.security.context.SecurityContextHolder;
11+
import org.springframework.web.filter.OncePerRequestFilter;
12+
13+
import java.io.IOException;
14+
15+
public class AuthorizationFilter extends OncePerRequestFilter {
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(
24+
HttpServletRequest request,
25+
HttpServletResponse response,
26+
FilterChain filterChain
27+
) throws ServletException, IOException {
28+
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
29+
if (authentication == null || !authentication.isAuthenticated()) {
30+
throw new AuthenticationException();
31+
}
32+
if (!authorizationManager.authorize(authentication, request).isGranted()) {
33+
throw new ForbiddenException();
34+
}
35+
filterChain.doFilter(request, response);
36+
}
37+
}

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: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import nextstep.security.authentication.Authentication;
44
import nextstep.security.authentication.AuthenticationException;
5+
import nextstep.security.authorization.manager.SecuredAuthorizationManager;
56
import nextstep.security.context.SecurityContextHolder;
67
import org.aopalliance.aop.Advice;
78
import org.aopalliance.intercept.MethodInterceptor;
@@ -11,8 +12,6 @@
1112
import org.springframework.aop.framework.AopInfrastructureBean;
1213
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
1314

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

1817
private final Pointcut pointcut;
@@ -23,16 +22,17 @@ public SecuredMethodInterceptor() {
2322

2423
@Override
2524
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-
}
25+
if (!invocation.getMethod().isAnnotationPresent(Secured.class)) {
26+
return invocation.proceed();
27+
}
28+
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
29+
if (authentication == null || !authentication.isAuthenticated()) {
30+
throw new AuthenticationException();
31+
}
32+
if (!SecuredAuthorizationManager.getInstance().authorize(
33+
authentication, invocation
34+
).isGranted()) {
35+
throw new ForbiddenException();
3636
}
3737
return invocation.proceed();
3838
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package nextstep.security.authorization.manager;
2+
3+
import nextstep.security.authentication.Authentication;
4+
5+
public class AuthenticatedAuthorizationManager<T> implements AuthorizationManager<T> {
6+
@Override
7+
public AuthorizationResult authorize(Authentication authentication, T target) {
8+
return AuthorizationDecision.of(
9+
authentication != null && authentication.isAuthenticated()
10+
);
11+
}
12+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package nextstep.security.authorization.manager;
2+
3+
import nextstep.security.authentication.Authentication;
4+
5+
import java.util.Set;
6+
7+
public class AuthorityAuthorizationManager<T> implements AuthorizationManager<T> {
8+
private final Set<String> authorities;
9+
10+
public AuthorityAuthorizationManager(String... authorities) {
11+
this.authorities = Set.of(authorities);
12+
}
13+
14+
@Override
15+
public AuthorizationResult authorize(Authentication authentication, T target) {
16+
return AuthorizationDecision.of(isGranted(authentication));
17+
}
18+
19+
private boolean isGranted(Authentication authentication) {
20+
return authentication != null
21+
&& authentication.isAuthenticated()
22+
&& anyMatch(authentication);
23+
}
24+
25+
private boolean anyMatch(Authentication authentication) {
26+
for (var authority : authentication.getAuthorities()) {
27+
if (authorities.contains(authority)) {
28+
return true;
29+
}
30+
}
31+
return false;
32+
}
33+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package nextstep.security.authorization.manager;
2+
3+
public enum AuthorizationDecision implements AuthorizationResult {
4+
GRANTED(true), NOT_GRANTED(false);
5+
6+
private final boolean granted;
7+
8+
AuthorizationDecision(boolean granted) {
9+
this.granted = granted;
10+
}
11+
12+
public static AuthorizationDecision of(boolean granted) {
13+
if (granted) {
14+
return GRANTED;
15+
}
16+
return NOT_GRANTED;
17+
}
18+
19+
public boolean isGranted() {
20+
return this.granted;
21+
}
22+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package nextstep.security.authorization.manager;
2+
3+
import nextstep.security.authentication.Authentication;
4+
5+
@FunctionalInterface
6+
public interface AuthorizationManager<T> {
7+
AuthorizationResult authorize(Authentication authentication, T target);
8+
}

0 commit comments

Comments
 (0)