Skip to content
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

[김병희] 선택 - RoleHierarchy 리뷰 요청 드립니다. #23

Open
wants to merge 7 commits into
base: beng9re
Choose a base branch
from
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,20 @@ RequestMatcherDelegatingAuthorizationManager객체의 mappings 정보는 Authori
- /members는 "ADMIN" 사용자만에게만 권한을 부여하기 위해 HasAuthorityAuthorizationManager로 처리


## 추가 RoleHierarchy

RoleHierarchy 리팩터링
RoleHierarchy는 기본적으로 NullRoleHierarchy가 설정된 다음 계층 구조의 권한 설정이 생길 경우 RoleHierarchyImpl가 동작하도록 되어있다. 실제 시큐리티의 구조를 참고해서 아래와 같이 설정하도록 수정한다.

```java
@Bean
public RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.with()
.role("ADMIN").implies("USER")
.build();
}
```




14 changes: 12 additions & 2 deletions src/main/java/nextstep/app/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,18 @@ public SecuredMethodInterceptor securedMethodInterceptor() {
return new SecuredMethodInterceptor();
}

// @Bean
// public SecuredAspect securedAspect() {
// return new SecuredAspect();
// }


@Bean
public SecuredAspect securedAspect() {
return new SecuredAspect();
public RoleHierarchy roleHierarchy() {
Copy link
Contributor

Choose a reason for hiding this comment

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

요구사항 설정 👍

return RoleHierarchyImpl.with()
.role("ADMIN")
.implies("USER","GUEST")
.build();
}

@Bean
Expand All @@ -62,6 +71,7 @@ public RequestMatcherDelegatingAuthorizationManager requestAuthorizationManager(
mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members"), new HasAuthorityAuthorizationManager("ADMIN")));
mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/search"), new PermitAllAuthorizationManager()));
mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.POST, "/private"), new DenyAllAuthorizationManager()));
mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/hierarchy"), new HasAuthorityAuthorizationManager("USER").withRoleHierarchy(roleHierarchy())));
Copy link
Contributor

Choose a reason for hiding this comment

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

권한 계층을 조회하도록 하셨군요!
설정이 잘 되었는지 확인하기 위해 좋은 시도입니다 :)

단, roleHierarchy()을 매번 설정할 때 마다 주입해주는 형태로 구현해주셨는데
한번만 설정하는 방법은 없을 지 고민해보셔도 좋을 것 같아요!

mappings.add(new RequestMatcherEntry<>(new AnyRequestMatcher(), new PermitAllAuthorizationManager()));
return new RequestMatcherDelegatingAuthorizationManager(mappings);
}
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/nextstep/app/ui/MemberController.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ public ResponseEntity<List<Member>> search() {
return ResponseEntity.ok(members);
}


@GetMapping("/hierarchy")
public ResponseEntity<Void> hierarchy() {
return ResponseEntity.ok().build();
}


@GetMapping("/members/me")
public Member findMe() {
final String email = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,45 @@

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import nextstep.security.authentication.Authentication;
import nextstep.security.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.filter.GenericFilterBean;

import java.io.IOException;

public class CheckAuthenticationFilter extends OncePerRequestFilter {
public class CheckAuthenticationFilter extends GenericFilterBean {

private final RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager;
private final AuthorizationManager<HttpServletRequest> authorizationManager;

public CheckAuthenticationFilter(RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager) {
this.requestMatcherDelegatingAuthorizationManager = requestMatcherDelegatingAuthorizationManager;
public CheckAuthenticationFilter(AuthorizationManager<HttpServletRequest> authorizationManager) {
this.authorizationManager = authorizationManager;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

AuthorizationDecision authorizationDecision = requestMatcherDelegatingAuthorizationManager.check(authentication, request);

if (authorizationDecision.isDeny()) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
try {
AuthorizationDecision authorizationDecision = authorizationManager.check(authentication, (HttpServletRequest) request);
authorizationCheck(authorizationDecision);
} catch (ForbiddenException e) {
((HttpServletResponse) response).setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}

filterChain.doFilter(request, response);
chain.doFilter(request, response);
}


private static void authorizationCheck(AuthorizationDecision authorizationDecision) {
if (authorizationDecision.isDeny()) {
throw new ForbiddenException();
Copy link
Contributor

Choose a reason for hiding this comment

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

만약 인증이 되지않은 경우 401응답이 필요할 수 있는데 현재는 모든 에로 상황에서 403이 응답이 되는 것 같네요!

}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import nextstep.security.authentication.Authentication;

import java.util.Collection;

public class HasAuthorityAuthorizationManager implements AuthorizationManager {
Copy link
Contributor

Choose a reason for hiding this comment

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

시큐리티 코드를 보면 AuthorityAuthorizationManager 와 AuthoritiesAuthorizationManager가 있는데요
이 구조를 분석해보시고 HasAuthorityAuthorizationManager 에 대입해보셔도 좋을 것 같아요.

private final String allowRole;
private RoleHierarchy roleHierarchy = new NullRoleHierarchy();

public HasAuthorityAuthorizationManager(String allowRole) {
this.allowRole = allowRole;
Expand All @@ -15,10 +18,21 @@ public AuthorizationDecision check(Authentication authentication, Object object)
return AuthorizationDecision.deny();
}

if (authentication.getAuthorities().contains(allowRole)) {
if (isHasAccess(authentication)) {
return AuthorizationDecision.granted();
}

return AuthorizationDecision.deny();
}

private boolean isHasAccess(Authentication authentication) {
Collection<String> reachableGrantedAuthorities = roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities());

return reachableGrantedAuthorities.contains(allowRole);
}

public HasAuthorityAuthorizationManager withRoleHierarchy(RoleHierarchy roleHierarchy) {
this.roleHierarchy = roleHierarchy;
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package nextstep.security.authorization;

import java.util.Collection;

public class NullRoleHierarchy implements RoleHierarchy {
Copy link
Contributor

Choose a reason for hiding this comment

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

널 객체 패턴 👍


@Override
public Collection<String> getReachableGrantedAuthorities(Collection<String> authorities) {
return authorities;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package nextstep.security.authorization;

import java.util.Collection;

public interface RoleHierarchy {
Collection<String> getReachableGrantedAuthorities(
Collection<String> authorities);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package nextstep.security.authorization;

import java.util.*;

public class RoleHierarchyImpl implements RoleHierarchy {
Copy link
Contributor

Choose a reason for hiding this comment

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

RoleHierarchy 구현 잘 해주셨습니다 👍

private final Map<String, Set<String>> hierarchyPath;

private RoleHierarchyImpl(Map<String, Set<String>> hierarchyPath) {
this.hierarchyPath = hierarchyPath;
}

@Override
public Collection<String> getReachableGrantedAuthorities(Collection<String> authorities) {
Set<String> grantedAuthorities = new HashSet<>();

for (String authority : authorities) {
if (hierarchyPath.get(authority) != null) {
grantedAuthorities.addAll(hierarchyPath.get(authority));
}
}

return grantedAuthorities;
}

public static RoleBuilder with() {
return new RoleBuilder();
}

public static class RoleBuilder {
private static final Map<String, Set<String>> hierarchy = new HashMap<>();
private String role;

public RoleBuilder role(String role) {
this.role = role;
hierarchy.put(role, new HashSet<>(Set.of(role)));
return this;
}

public ImpliesBuilder implies(String... implies) {
hierarchy.get(role).addAll(Set.of(implies));

for (int i = 0; i < implies.length; i++) {
Set<String> paths = hierarchy.getOrDefault(implies[i], new HashSet<>());
paths.addAll(List.of(implies).subList(i, implies.length));
hierarchy.put(implies[i], paths);
}

return new ImpliesBuilder();
}

public static class ImpliesBuilder {
public RoleHierarchyImpl build() {
return new RoleHierarchyImpl(hierarchy);
}
}
}
}
69 changes: 37 additions & 32 deletions src/test/java/nextstep/app/FormLoginTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,7 @@ void user_login_after_members() throws Exception {
@Test
@DisplayName("인증된 사용자는 자신의 정보를 조회할 수 있다.")
void request_success_members_me() throws Exception {
MockHttpSession session = new MockHttpSession();

mockMvc.perform(post("/login")
.param("username", TEST_USER_MEMBER.getEmail())
.param("password", TEST_USER_MEMBER.getPassword())
.session(session)
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
).andExpect(status().isOk());
MockHttpSession session = login(TEST_USER_MEMBER);

mockMvc.perform(get("/members/me")
.session(session)
Expand All @@ -157,14 +150,7 @@ void request_fail_members_me() throws Exception {
@Test
@DisplayName("ADMIN 권한을 가진 사용자가 요청할 경우 모든 회원 정보를 조회할 수 있다.")
void request_search_success_with_admin_user() throws Exception {
MockHttpSession session = new MockHttpSession();

mockMvc.perform(post("/login")
.param("username", TEST_ADMIN_MEMBER.getEmail())
.param("password", TEST_ADMIN_MEMBER.getPassword())
.session(session)
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
).andExpect(status().isOk());
MockHttpSession session = login(TEST_ADMIN_MEMBER);

mockMvc.perform(get("/members")
.session(session)
Expand All @@ -177,14 +163,7 @@ void request_search_success_with_admin_user() throws Exception {
@Test
@DisplayName("제한된 URI은 ADMIN 이여도 접근할수 없다")
void request_private_with_admin_user() throws Exception {
MockHttpSession session = new MockHttpSession();

mockMvc.perform(post("/login")
.param("username", TEST_ADMIN_MEMBER.getEmail())
.param("password", TEST_ADMIN_MEMBER.getPassword())
.session(session)
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
).andExpect(status().isOk());
MockHttpSession session = login(TEST_ADMIN_MEMBER);

mockMvc.perform(post("/private")
.session(session)
Expand All @@ -196,19 +175,45 @@ void request_private_with_admin_user() throws Exception {
@Test
@DisplayName("제한된 URI은 접근할수 없다")
void request_private_with_user_user() throws Exception {
MockHttpSession session = new MockHttpSession();

mockMvc.perform(post("/login")
.param("username", TEST_USER_MEMBER.getEmail())
.param("password", TEST_USER_MEMBER.getPassword())
.session(session)
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
).andExpect(status().isOk());
MockHttpSession session = login(TEST_USER_MEMBER);

mockMvc.perform(post("/private")
.session(session)
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
)
.andExpect(status().isForbidden());
}


@Test
@DisplayName("USER 권한 이상을 가진 사용자는 hierarchy url에 접근이 가능하다.")
void request_success_hierarchy() throws Exception {
MockHttpSession adminSession = login(TEST_ADMIN_MEMBER);
MockHttpSession userSession = login(TEST_USER_MEMBER);

mockMvc.perform(get("/hierarchy")
.session(adminSession)
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
)
.andExpect(status().isOk());


mockMvc.perform(get("/hierarchy")
.session(userSession)
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
)
.andExpect(status().isOk());
}

private MockHttpSession login(Member member) throws Exception {
MockHttpSession session = new MockHttpSession();

mockMvc.perform(post("/login")
.param("username", member.getEmail())
.param("password", member.getPassword())
.session(session)
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
).andExpect(status().isOk());
return session;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,50 @@ void admin_authenticate() {

assertThat(check.isDeny()).isFalse();
}

@DisplayName("roleHierarchy 를 설정하면 상위 권한에서 인가를 허용한다")
@Test
void roleHierarchy_authenticate() {
// given
final String allowedRole = "ADMIN";
RoleHierarchyImpl roleHierarchy = RoleHierarchyImpl.with()
.role("ADMIN")
.implies("USER")
.build();

HasAuthorityAuthorizationManager authenticatedAuthorizationManager = new HasAuthorityAuthorizationManager(allowedRole);
authenticatedAuthorizationManager.withRoleHierarchy(roleHierarchy);

Authentication authentication = TestAuthentication.admin();


// when
AuthorizationDecision check = authenticatedAuthorizationManager.check(authentication, new MockHttpServletRequest());

// then
assertThat(check.isDeny()).isFalse();
}

@DisplayName("roleHierarchy 를 설정하면 같은 권한에 인가를 허용한다")
@Test
void roleHierarchy_equal_authenticate() {
// given
final String allowedRole = "USER";
RoleHierarchyImpl roleHierarchy = RoleHierarchyImpl.with()
.role("ADMIN")
.implies("USER")
.build();

HasAuthorityAuthorizationManager authenticatedAuthorizationManager = new HasAuthorityAuthorizationManager(allowedRole);
authenticatedAuthorizationManager.withRoleHierarchy(roleHierarchy);

Authentication authentication = TestAuthentication.user();


// when
AuthorizationDecision check = authenticatedAuthorizationManager.check(authentication, new MockHttpServletRequest());

// then
assertThat(check.isDeny()).isFalse();
}
}
Loading