Skip to content

๐Ÿ™Œ 2๋‹จ๊ณ„ - ์ธ๊ฐ€(Authorization) ๋ฆฌ๋ทฐ ์š”์ฒญ ๋“œ๋ฆฝ๋‹ˆ๋‹ค. #10

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

Merged
merged 21 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d4add2f
feat: add /members/me API
parkjun5 Feb 9, 2025
bc949f3
ref: refactoring CheckAuthenticationFilter to AuthorizationFilter
parkjun5 Feb 9, 2025
6d5c693
feat: add AuthorizationManager for authorize
parkjun5 Feb 9, 2025
646c495
feat: add request matcher for find AuthorizationManager
parkjun5 Feb 9, 2025
432239c
feat: add AuthorizationManagers for delegate Authorities
parkjun5 Feb 9, 2025
e301ed4
ref: change test and Add SecureAnnotation to Get /members API
parkjun5 Feb 9, 2025
8b70fdd
feat: Filter And Interceptor delegate authorization control to Authorโ€ฆ
parkjun5 Feb 9, 2025
1f37f27
feat: add RequestMatcherDelegatingAuthorizationManager and config chaโ€ฆ
parkjun5 Feb 9, 2025
a0c8b51
docs: ์š”๊ตฌ ์‚ฌํ•ญ ์ •๋ฆฌ
parkjun5 Feb 9, 2025
b83d2a2
polishing
parkjun5 Feb 9, 2025
c1a2089
ref: Filter ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ธ๊ฐ€์™€ Interceptor ์—์„œ ํ™•์ธํ•˜๋Š” ์ธ๊ฐ€ ๋ถ„๋ฆฌ
parkjun5 Feb 9, 2025
0179bf5
polishing
parkjun5 Feb 9, 2025
fbbf311
ref:
parkjun5 Feb 9, 2025
bb7a79e
polishing
parkjun5 Feb 9, 2025
9bf5f29
polishing
parkjun5 Feb 9, 2025
8126c74
ref: Change AuthorityAuthorizationManager to Generic
Feb 12, 2025
7e23368
ref: use verify not check method
Feb 12, 2025
7a213b0
ref: AuthorizationDecision ์„ static ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝ
Feb 12, 2025
0e5d010
ref: mvc match logic not Stream change to ForEach
Feb 12, 2025
5c49d3e
ref: SecuredAuthorizationManager ์—์„œ AuthorityAuthorizationManager ์—๊ฒŒ โ€ฆ
Feb 12, 2025
fc947f5
test: ์ธ์ฆ ์‹คํŒจ์—์„œ๋Š” AccessDeniedException ์ธ๊ฐ€ ์‹คํŒจ์—๋Š” ForbiddenException ๋‚˜๋„๋ก ๋ณ€๊ฒฝ
Feb 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,39 @@
# spring-security-authorization

## ์‹ค์Šต

1. [x] GET /members/me ์—”๋“œํฌ์ธํŠธ ๊ตฌํ˜„ ๋ฐ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ
2. [x] ๊ถŒํ•œ ๊ฒ€์ฆ ๋กœ์ง์„ AuthorizationFilter๋กœ ๋ฆฌํŒฉํ„ฐ๋ง

## ๐Ÿš€ 1๋‹จ๊ณ„ - AuthorizationManager๋ฅผ ํ™œ์šฉ

์š”๊ตฌ์‚ฌํ•ญ

- [x] AuthorizationManager ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ธ๊ฐ€ ๊ณผ์ • ์ถ”์ƒํ™”
- [x] ์ธ๊ฐ€๋ฅผ ์ฒ˜๋ฆฌํ•ด์ค„ AuthorizationManager ์ƒ์„ฑ
- [x] RequestMatcherDelegatingAuthorizationManager ๋ฅผ ํ†ตํ•œ AuthorizationManager ํ•œ๋ฒˆ์— ๊ด€๋ฆฌ?
- [x] ์ธ๊ฐ€ ๊ณผ์ •์„ ์ถ”์ƒํ™”ํ•œ AuthorizationManager ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค. ์ด ๋•Œ ํ•„์š”ํ•œ AuthorizationDecision๋„ ํ•จ๊ป˜ ์ž‘์„ฑํ•œ๋‹ค. (์‹ค์ œ AuthorizationManager์—๋Š”
verify๋„ ์žˆ๋Š”๋ฐ ์ด ๋ถ€๋ถ„์— ๋Œ€ํ•œ ๊ตฌํ˜„์€ ์„ ํƒ)
- [x] SecuredMethodInterceptor์™€ Authorization Filter์—์„œ ์ž‘์„ฑ๋œ ์ธ๊ฐ€ ๋กœ์ง์„ AuthorizationManager๋กœ ๋ฆฌํŒฉํ„ฐ๋ง ํ•œ๋‹ค.

## ๐Ÿš€ 2๋‹จ๊ณ„ - ์š”์ฒญ๋ณ„ ๊ถŒํ•œ ๊ฒ€์ฆ ์ •๋ณด ๋ถ„๋ฆฌ

์š”๊ตฌ์‚ฌํ•ญ

- [x] ์š”์ฒญ๋ณ„ ๊ถŒํ•œ ๊ฒ€์ฆ ์ •๋ณด๋ฅผ ๋ณ„๋„์˜ ๊ฐ์ฒด๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌ
- [x] RequestMatcherRegistry์™€ RequestMatcher๋ฅผ ์ž‘์„ฑํ•˜๊ณ , RequestMatcher์˜ ๊ตฌํ˜„์ฒด๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.
- [x] AnyRequestMatcher: ๋ชจ๋“  ๊ฒฝ์šฐ true๋ฅผ ๋ฆฌํ„ดํ•œ๋‹ค.
- [x] MvcRequestMatcher: method์™€ pattern(uri)๊ฐ€ ๊ฐ™์€์ง€ ๋น„๊ตํ•˜์—ฌ ๋ฆฌํ„ดํ•œ๋‹ค.
- [x] RequestMatcherEntry์˜ T entry๋Š” ์•„๋ž˜์— ํ•ด๋‹น๋˜๋Š” ๊ฐ ์š”์ฒญ๋ณ„ ์ธ๊ฐ€ ๋กœ์ง์„ ๋‹ด๋‹นํ•˜๋Š” AuthorizationManager๊ฐ€ ๋œ๋‹ค.
- [x] /login์€ ๋ชจ๋“  ์š”์ฒญ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก PermitAllAuthorizationManager๋กœ ์ฒ˜๋ฆฌ
- [x] /members/me๋Š” ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ์—๊ฒŒ๋งŒ ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•˜๊ธฐ ์œ„ํ•ด AuthenticatedAuthorizationManager๋กœ ์ฒ˜๋ฆฌ
- [x] /members๋Š” "ADMIN" ์‚ฌ์šฉ์ž๋งŒ์—๊ฒŒ๋งŒ ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•˜๊ธฐ ์œ„ํ•ด HasAuthorityAuthorizationManager๋กœ ์ฒ˜๋ฆฌ
- [x] ๊ทธ ์™ธ ๋ชจ๋“  ์š”์ฒญ์€ ๊ถŒํ•œ์„ ์ œํ•œํ•˜๊ธฐ ์œ„ํ•ด DenyAllAuthorizationManager๋กœ ์ฒ˜๋ฆฌ

์•„๋ž˜ ๊ฐ์ฒด์™€ ์‹œํ๋ฆฌํ‹ฐ ์ฝ”๋“œ ํ™•์ธ
// SpEL
// Role Authority
// Role Hierarchy
// AuthoritiesAuthorizationManager
// SecureMethodSecurityConfiguration
// SecuredAuthorizationManager
25 changes: 21 additions & 4 deletions src/main/java/nextstep/app/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
package nextstep.app;

import jakarta.servlet.http.HttpServletRequest;
import nextstep.app.domain.Member;
import nextstep.app.domain.MemberRepository;
import nextstep.security.authentication.AuthenticationException;
import nextstep.security.authentication.BasicAuthenticationFilter;
import nextstep.security.authentication.UsernamePasswordAuthenticationFilter;
import nextstep.security.authorization.CheckAuthenticationFilter;
import nextstep.security.authorization.SecuredAspect;
import nextstep.security.authorization.AuthorizationFilter;
import nextstep.security.authorization.SecuredMethodInterceptor;
import nextstep.security.authorization.manager.*;
import nextstep.security.config.DefaultSecurityFilterChain;
import nextstep.security.config.DelegatingFilterProxy;
import nextstep.security.config.FilterChainProxy;
import nextstep.security.config.SecurityFilterChain;
import nextstep.security.context.SecurityContextHolderFilter;
import nextstep.security.matcher.AnyRequestMatcher;
import nextstep.security.matcher.MvcRequestMatcher;
import nextstep.security.matcher.RequestMatcherEntry;
import nextstep.security.userdetails.UserDetails;
import nextstep.security.userdetails.UserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.http.HttpMethod;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

Expand All @@ -44,21 +50,32 @@ public FilterChainProxy filterChainProxy(List<SecurityFilterChain> securityFilte

@Bean
public SecuredMethodInterceptor securedMethodInterceptor() {
return new SecuredMethodInterceptor();
AuthorityAuthorizationManager<Set<String>> authorityAuthorizationManager = new AuthorityAuthorizationManager<>();
return new SecuredMethodInterceptor(new SecuredAuthorizationManager(authorityAuthorizationManager));
}
// @Bean
// public SecuredAspect securedAspect() {
// return new SecuredAspect();
// }

@Bean
public RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

์ ์ ˆํ•˜๊ฒŒ ์ž˜ ์ถ”๊ฐ€ํ•ด์ฃผ์…จ๋„ค์š” :)

List<RequestMatcherEntry<AuthorizationManager<HttpServletRequest>>> mappings = new ArrayList<>();
mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members/me"), new AuthenticatedAuthorizationManager()));
mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members"), new AuthorityAuthorizationManager<>()));
mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/search", "/login"), new PermitAllAuthorizationManager()));
mappings.add(new RequestMatcherEntry<>(new AnyRequestMatcher(), new DenyAllAuthorizationManager()));
return new RequestMatcherDelegatingAuthorizationManager(mappings);
}

@Bean
public SecurityFilterChain securityFilterChain() {
return new DefaultSecurityFilterChain(
List.of(
new SecurityContextHolderFilter(),
new UsernamePasswordAuthenticationFilter(userDetailsService()),
new BasicAuthenticationFilter(userDetailsService()),
new CheckAuthenticationFilter()
new AuthorizationFilter(requestMatcherDelegatingAuthorizationManager())
)
);
}
Expand Down
18 changes: 17 additions & 1 deletion src/main/java/nextstep/app/ui/MemberController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

import nextstep.app.domain.Member;
import nextstep.app.domain.MemberRepository;
import nextstep.security.authentication.AuthenticationException;
import nextstep.security.authentication.UsernamePasswordAuthenticationToken;
import nextstep.security.authorization.Secured;
import nextstep.security.context.SecurityContextHolder;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.NoSuchElementException;

@RestController
public class MemberController {
Expand All @@ -18,16 +22,28 @@ public MemberController(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}

@Secured("ADMIN")
@GetMapping("/members")
public ResponseEntity<List<Member>> list() {
List<Member> members = memberRepository.findAll();
return ResponseEntity.ok(members);
}

@Secured("ADMIN")
@Secured({"ADMIN", "MEMBER"})
@GetMapping("/search")
public ResponseEntity<List<Member>> search() {
List<Member> members = memberRepository.findAll();
return ResponseEntity.ok(members);
}

@GetMapping("/members/me")
public ResponseEntity<Member> getMyInfo() {
if (SecurityContextHolder.getContext().getAuthentication() instanceof UsernamePasswordAuthenticationToken token
&& token.getPrincipal() instanceof String email) {
Member member = memberRepository.findByEmail(email).orElseThrow(NoSuchElementException::new);
return ResponseEntity.ok(member);
}

throw new AuthenticationException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import nextstep.security.authorization.ForbiddenException;
import nextstep.security.context.SecurityContext;
import nextstep.security.context.SecurityContextHolder;
import nextstep.security.userdetails.UserDetailsService;
Expand Down Expand Up @@ -40,6 +41,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
SecurityContextHolder.setContext(context);

filterChain.doFilter(request, response);
} catch (ForbiddenException e) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package nextstep.security.authorization;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.FORBIDDEN)
public class AccessDeniedException extends RuntimeException {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package nextstep.security.authorization;

public class AuthorizationDecision {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๋ฏธ์…˜์—์„œ๋Š” spring security์˜ ๊ธฐ๋Šฅ์ด ์ถ•์†Œ๋˜์–ด true, false๊ฐ€ ์ž˜ ์•ˆ์™€๋‹ฟ์œผ์‹ค ์ˆ˜ ์žˆ์œผ๋‚˜ ์‹ค์ œ๋กœ๋Š” ๊ทธ๋ณด๋‹ค ๋” ๋‹ค์–‘ํ•œ ๊ตฌํ˜„์ฒด๋“ค์ด ์ƒ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

https://github.com/spring-projects/spring-security/blob/main/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationDecision.java


private static final AuthorizationDecision GRANTED = new AuthorizationDecision(true);
private static final AuthorizationDecision DENIED = new AuthorizationDecision(false);

private final boolean isGranted;

public static AuthorizationDecision granted() {
return GRANTED;
}

public static AuthorizationDecision denied() {
return DENIED;
}

protected AuthorizationDecision(final boolean isGranted) {
this.isGranted = isGranted;
}

public boolean isDenied() {
return !isGranted;
}
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,28 @@
package nextstep.security.authorization;

import nextstep.security.authentication.Authentication;
import nextstep.security.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import nextstep.security.authentication.Authentication;
import nextstep.security.authorization.manager.RequestMatcherDelegatingAuthorizationManager;
import nextstep.security.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Set;

public class CheckAuthenticationFilter extends OncePerRequestFilter {
private static final String DEFAULT_REQUEST_URI = "/members";
public class AuthorizationFilter extends OncePerRequestFilter {

private final RequestMatcherDelegatingAuthorizationManager authorizationManager;

public AuthorizationFilter(RequestMatcherDelegatingAuthorizationManager authorizationManager) {
this.authorizationManager = authorizationManager;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (!DEFAULT_REQUEST_URI.equals(request.getRequestURI())) {
filterChain.doFilter(request, response);
return;
}

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}

Set<String> authorities = authentication.getAuthorities();
if (!authorities.contains("ADMIN")) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}

authorizationManager.verifyInFilter(request, authentication);
filterChain.doFilter(request, response);
}
}
2 changes: 1 addition & 1 deletion src/main/java/nextstep/security/authorization/Secured.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Secured {
String value();
String[] value() default {"ADMIN"};
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,24 @@
import org.aspectj.lang.reflect.MethodSignature;

import java.lang.reflect.Method;
import java.util.Arrays;

@Aspect
public class SecuredAspect {

@Before("@annotation(nextstep.security.authorization.Secured)")
public void checkSecured(JoinPoint joinPoint) throws NoSuchMethodException {
Method method = getMethodFromJoinPoint(joinPoint);
String secured = method.getAnnotation(Secured.class).value();
String[] secured = method.getAnnotation(Secured.class).value();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new AuthenticationException();
}
if (!authentication.getAuthorities().contains(secured)) {

boolean hasNoRole = authentication.getAuthorities()
.stream()
.noneMatch(auth -> Arrays.asList(secured).contains(auth));
if (hasNoRole) {
throw new ForbiddenException();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package nextstep.security.authorization;

import nextstep.security.authentication.Authentication;
import nextstep.security.authentication.AuthenticationException;
import nextstep.security.authorization.manager.AuthorizationManager;
import nextstep.security.context.SecurityContextHolder;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
Expand All @@ -11,29 +11,21 @@
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;

import java.lang.reflect.Method;

public class SecuredMethodInterceptor implements MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {

private final Pointcut pointcut;
private final AuthorizationManager<MethodInvocation> authorizationManager;

public SecuredMethodInterceptor() {
public SecuredMethodInterceptor(AuthorizationManager<MethodInvocation> authorizationManager) {
this.authorizationManager = authorizationManager;
this.pointcut = new AnnotationMatchingPointcut(null, Secured.class);
}

@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
if (method.isAnnotationPresent(Secured.class)) {
Secured secured = method.getAnnotation(Secured.class);
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new AuthenticationException();
}
if (!authentication.getAuthorities().contains(secured.value())) {
throw new ForbiddenException();
}
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
authorizationManager.verify(authentication, invocation);
return invocation.proceed();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package nextstep.security.authorization.manager;

import jakarta.servlet.http.HttpServletRequest;
import nextstep.security.authentication.Authentication;
import nextstep.security.authorization.AuthorizationDecision;

public class AuthenticatedAuthorizationManager implements AuthorizationManager<HttpServletRequest> {

@Override
public AuthorizationDecision check(Authentication authentication, HttpServletRequest object) {
if (authentication == null || !authentication.isAuthenticated()) {
return AuthorizationDecision.denied();
}

return AuthorizationDecision.granted();
}
}
Loading