refactor: remove authorization filter and replace it with the RequestInfoAuthorizationManager (#2081)

pull/2082/head
guqing 2022-05-11 10:26:11 +08:00 committed by GitHub
parent 6db4f1105a
commit b4e9b54ffe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 156 additions and 112 deletions

View File

@ -44,8 +44,8 @@ import run.halo.app.identity.authentication.ProviderContextFilter;
import run.halo.app.identity.authentication.ProviderSettings; import run.halo.app.identity.authentication.ProviderSettings;
import run.halo.app.identity.authentication.verifier.BearerTokenAuthenticationFilter; import run.halo.app.identity.authentication.verifier.BearerTokenAuthenticationFilter;
import run.halo.app.identity.authentication.verifier.JwtProvidedDecoderAuthenticationManagerResolver; import run.halo.app.identity.authentication.verifier.JwtProvidedDecoderAuthenticationManagerResolver;
import run.halo.app.identity.authorization.AuthorizationFilter;
import run.halo.app.identity.authorization.PolicyRule; import run.halo.app.identity.authorization.PolicyRule;
import run.halo.app.identity.authorization.RequestInfoAuthorizationManager;
import run.halo.app.identity.authorization.Role; import run.halo.app.identity.authorization.Role;
import run.halo.app.identity.authorization.RoleBinding; import run.halo.app.identity.authorization.RoleBinding;
import run.halo.app.identity.authorization.RoleRef; import run.halo.app.identity.authorization.RoleRef;
@ -83,7 +83,9 @@ public class WebSecurityConfig {
http http
.authorizeHttpRequests((authorize) -> authorize .authorizeHttpRequests((authorize) -> authorize
.antMatchers(providerSettings.getTokenEndpoint()).permitAll() .antMatchers(providerSettings.getTokenEndpoint()).permitAll()
.antMatchers("/api/**", "/apis/**").authenticated() .antMatchers("/static/js/**").permitAll()
.antMatchers("/api/**", "/apis/**").access(requestInfoAuthorizationManager())
.anyRequest().access(requestInfoAuthorizationManager())
) )
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.httpBasic(Customizer.withDefaults()) .httpBasic(Customizer.withDefaults())
@ -93,7 +95,6 @@ public class WebSecurityConfig {
.addFilterBefore(new BearerTokenAuthenticationFilter(authenticationManagerResolver()), .addFilterBefore(new BearerTokenAuthenticationFilter(authenticationManagerResolver()),
BasicAuthenticationFilter.class) BasicAuthenticationFilter.class)
.addFilterAfter(providerContextFilter, SecurityContextPersistenceFilter.class) .addFilterAfter(providerContextFilter, SecurityContextPersistenceFilter.class)
.addFilterBefore(authorizationFilter(), FilterSecurityInterceptor.class)
.sessionManagement( .sessionManagement(
(session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) (session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling((exceptions) -> exceptions .exceptionHandling((exceptions) -> exceptions
@ -103,10 +104,10 @@ public class WebSecurityConfig {
return http.build(); return http.build();
} }
public AuthorizationFilter authorizationFilter() { public RequestInfoAuthorizationManager requestInfoAuthorizationManager() {
// TODO fake role and role bindings, only used for testing/development // TODO fake role and role bindings, only used for testing/development
// It'll be deleted next time // It'll be deleted next time
return new AuthorizationFilter(name -> { return new RequestInfoAuthorizationManager(name -> {
// role getter // role getter
Role role = new Role(); Role role = new Role();
List<PolicyRule> rules = List.of( List<PolicyRule> rules = List.of(

View File

@ -0,0 +1,29 @@
package run.halo.app.identity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* A controller should ONLY be used during testing for this PR.
* TODO It'll be deleted next time
*
* @author guqing
* @since 2.0.0
*/
@Controller
@ResponseBody
@RequestMapping
public class HealthyController {
@RequestMapping("/healthy")
public String hello() {
return "I am very healthy.";
}
@GetMapping("/static/js/test.js")
public String fakeJs() {
return "console.log('hello world!')";
}
}

View File

@ -1,28 +0,0 @@
package run.halo.app.identity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* A controller should ONLY be used during testing for this PR.
* TODO It'll be deleted next time
*
* @author guqing
* @since 2.0.0
*/
@RestController
@RequestMapping("/posts")
public class HelloController {
@GetMapping
public String hello() {
return "Now you see me.";
}
@GetMapping("/{name}")
public String getByName(@PathVariable String name) {
return "Name:" + name + "-->Now you see me.";
}
}

View File

@ -1,28 +0,0 @@
package run.halo.app.identity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* A controller should ONLY be used during testing for this PR.
* TODO It'll be deleted next time
*
* @author guqing
* @since 2.0.0
*/
@RestController
@RequestMapping("/tags")
public class TagController {
@GetMapping
public String hello() {
return "Tag you see me.";
}
@GetMapping("/{name}")
public String getByName(@PathVariable String name) {
return "Tag name:" + name + "-->Now you see me.";
}
}

View File

@ -0,0 +1,53 @@
package run.halo.app.identity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* A controller should ONLY be used during testing for this PR.
* TODO It'll be deleted next time
*
* @author guqing
* @since 2.0.0
*/
@RestController
@RequestMapping
public class TestController {
@GetMapping("/posts")
public String hello() {
return "list posts.";
}
@GetMapping("/posts/{name}")
public String getPostByName(@PathVariable String name) {
return "Gets a post with the name: " + name;
}
@GetMapping("/categories/{name}")
public String getCategoryByName(@PathVariable String name) {
return "Gets a category with the name: " + name;
}
@GetMapping("/categories")
public String listCategories() {
return "list categories.";
}
@GetMapping("/tags")
public String tags() {
return "list tags";
}
@GetMapping("/{name}")
public String getByName(@PathVariable String name) {
return "Gets a tag with the name: " + name;
}
@RequestMapping("/healthy")
public String healthy() {
return "That's very healthy";
}
}

View File

@ -1,46 +1,43 @@
package run.halo.app.identity.authorization; package run.halo.app.identity.authorization;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull; import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.web.filter.OncePerRequestFilter;
/** /**
* An authorization filter that restricts access to the URL.
*
* @author guqing * @author guqing
* @since 2.0.0 * @since 2.0.0
*/ */
@Slf4j @Slf4j
public class AuthorizationFilter extends OncePerRequestFilter { public class RequestInfoAuthorizationManager
private AuthorizationRuleResolver ruleResolver; implements AuthorizationManager<RequestAuthorizationContext> {
private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl(); private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
public AuthorizationFilter(RoleGetter roleGetter, RoleBindingLister roleBindingLister) { private AuthorizationRuleResolver ruleResolver;
public RequestInfoAuthorizationManager(RoleGetter roleGetter,
RoleBindingLister roleBindingLister) {
this.ruleResolver = new DefaultRuleResolver(roleGetter, roleBindingLister); this.ruleResolver = new DefaultRuleResolver(roleGetter, roleBindingLister);
} }
@Override @Override
protected void doFilterInternal(@NonNull HttpServletRequest request, public AuthorizationDecision check(Supplier<Authentication> authenticationSupplier,
@NonNull HttpServletResponse response, @NonNull FilterChain filterChain) RequestAuthorizationContext requestContext) {
throws ServletException, IOException { HttpServletRequest request = requestContext.getRequest();
RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request); RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);
Authentication authentication = getAuthentication();
Authentication authentication = authenticationSupplier.get();
UserDetails userDetails = createUserDetails(authentication); UserDetails userDetails = createUserDetails(authentication);
AttributesRecord attributes = new AttributesRecord(userDetails, requestInfo); AttributesRecord attributes = new AttributesRecord(userDetails, requestInfo);
@ -52,13 +49,10 @@ public class AuthorizationFilter extends OncePerRequestFilter {
if (!authorizingVisitor.isAllowed()) { if (!authorizingVisitor.isAllowed()) {
// print errors // print errors
showErrorMessage(authorizingVisitor.getErrors()); showErrorMessage(authorizingVisitor.getErrors());
// handle it return new AuthorizationDecision(false);
accessDeniedHandler.handle(request, response,
new AccessDeniedException("Access is denied"));
return;
} }
log.debug(authorizingVisitor.getReason());
filterChain.doFilter(request, response); return new AuthorizationDecision(isGranted(authentication));
} }
private void showErrorMessage(List<Throwable> errors) { private void showErrorMessage(List<Throwable> errors) {
@ -70,15 +64,6 @@ public class AuthorizationFilter extends OncePerRequestFilter {
} }
} }
private Authentication getAuthentication() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new AuthenticationCredentialsNotFoundException(
"An Authentication object was not found in the SecurityContext");
}
return authentication;
}
private UserDetails createUserDetails(Authentication authentication) { private UserDetails createUserDetails(Authentication authentication) {
Assert.notNull(authentication, "The authentication must not be null."); Assert.notNull(authentication, "The authentication must not be null.");
return User.withUsername(authentication.getName()) return User.withUsername(authentication.getName())
@ -92,7 +77,13 @@ public class AuthorizationFilter extends OncePerRequestFilter {
this.ruleResolver = ruleResolver; this.ruleResolver = ruleResolver;
} }
public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) { private boolean isGranted(Authentication authentication) {
this.accessDeniedHandler = accessDeniedHandler; return authentication != null && isNotAnonymous(authentication)
&& authentication.isAuthenticated();
} }
private boolean isNotAnonymous(Authentication authentication) {
return !this.trustResolver.isAnonymous(authentication);
}
} }

View File

@ -17,15 +17,15 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import run.halo.app.identity.authorization.AuthorizationFilter; import run.halo.app.identity.authorization.RequestInfoAuthorizationManager;
/** /**
* Tests for {@link AuthorizationFilter}. * Tests for {@link RequestInfoAuthorizationManager}.
* *
* @author guqing * @author guqing
* @since 2.0.0 * @since 2.0.0
*/ */
public class AuthorizationFilterTest extends AuthorizationTestSuit { public class RequestInfoAuthorizationManagerTest extends AuthorizationTestSuit {
private MockMvc mockMvc; private MockMvc mockMvc;
@ -94,6 +94,22 @@ public class AuthorizationFilterTest extends AuthorizationTestSuit {
.andExpect(content().string("ok.")); .andExpect(content().string("ok."));
} }
@Test
public void cssResourceCanAlwaysAccess() throws Exception {
mockMvc.perform(get("/static/test.css"))
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andExpect(content().string("body { text-align: center; }"));
}
@Test
public void jsResourceCanAlwaysAccess() throws Exception {
mockMvc.perform(get("/static/test.js"))
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andExpect(content().string("console.log('hello world!')"));
}
@Test @Test
public void nonResourceRequestWhenHaveNoRightShould403() throws Exception { public void nonResourceRequestWhenHaveNoRightShould403() throws Exception {
mockMvc.perform(get("/healthy/halo") mockMvc.perform(get("/healthy/halo")
@ -133,20 +149,29 @@ public class AuthorizationFilterTest extends AuthorizationTestSuit {
} }
@Controller @Controller
@RequestMapping("/healthy") @ResponseBody
@RequestMapping
public static class HealthyController { public static class HealthyController {
@GetMapping @GetMapping("/healthy")
@ResponseBody
public String check() { public String check() {
return "ok."; return "ok.";
} }
@ResponseBody @GetMapping("/healthy/{name}")
@GetMapping("/{name}")
public String check(@PathVariable String name) { public String check(@PathVariable String name) {
return name + ": should not be seen."; return name + ": should not be seen.";
} }
@GetMapping("/static/test.js")
public String fakeJs() {
return "console.log('hello world!')";
}
@GetMapping("/static/test.css")
public String fakeCss() {
return "body { text-align: center; }";
}
} }
} }

View File

@ -46,8 +46,8 @@ import run.halo.app.identity.authentication.ProviderContextFilter;
import run.halo.app.identity.authentication.ProviderSettings; import run.halo.app.identity.authentication.ProviderSettings;
import run.halo.app.identity.authentication.verifier.BearerTokenAuthenticationFilter; import run.halo.app.identity.authentication.verifier.BearerTokenAuthenticationFilter;
import run.halo.app.identity.authentication.verifier.JwtProvidedDecoderAuthenticationManagerResolver; import run.halo.app.identity.authentication.verifier.JwtProvidedDecoderAuthenticationManagerResolver;
import run.halo.app.identity.authorization.AuthorizationFilter;
import run.halo.app.identity.authorization.PolicyRule; import run.halo.app.identity.authorization.PolicyRule;
import run.halo.app.identity.authorization.RequestInfoAuthorizationManager;
import run.halo.app.identity.authorization.Role; import run.halo.app.identity.authorization.Role;
import run.halo.app.identity.authorization.RoleBinding; import run.halo.app.identity.authorization.RoleBinding;
import run.halo.app.identity.authorization.RoleRef; import run.halo.app.identity.authorization.RoleRef;
@ -85,7 +85,9 @@ public class TestWebSecurityConfig {
http http
.authorizeHttpRequests((authorize) -> authorize .authorizeHttpRequests((authorize) -> authorize
.antMatchers(providerSettings.getTokenEndpoint()).permitAll() .antMatchers(providerSettings.getTokenEndpoint()).permitAll()
.antMatchers("/api/**", "/apis/**").authenticated() .antMatchers("/static/**").permitAll()
.antMatchers("/api/**", "/apis/**").access(requestInfoAuthorizationManager())
.anyRequest().access(requestInfoAuthorizationManager())
) )
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.httpBasic(Customizer.withDefaults()) .httpBasic(Customizer.withDefaults())
@ -95,14 +97,13 @@ public class TestWebSecurityConfig {
.addFilterBefore(new BearerTokenAuthenticationFilter(authenticationManagerResolver()), .addFilterBefore(new BearerTokenAuthenticationFilter(authenticationManagerResolver()),
BasicAuthenticationFilter.class) BasicAuthenticationFilter.class)
.addFilterAfter(providerContextFilter, SecurityContextPersistenceFilter.class) .addFilterAfter(providerContextFilter, SecurityContextPersistenceFilter.class)
.addFilterBefore(authorizationFilter(), FilterSecurityInterceptor.class)
.sessionManagement( .sessionManagement(
(session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); (session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build(); return http.build();
} }
public AuthorizationFilter authorizationFilter() { public RequestInfoAuthorizationManager requestInfoAuthorizationManager() {
return new AuthorizationFilter(name -> { return new RequestInfoAuthorizationManager(name -> {
// role getter // role getter
Role role = new Role(); Role role = new Role();
List<PolicyRule> rules = List.of( List<PolicyRule> rules = List.of(