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.verifier.BearerTokenAuthenticationFilter;
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.RequestInfoAuthorizationManager;
import run.halo.app.identity.authorization.Role;
import run.halo.app.identity.authorization.RoleBinding;
import run.halo.app.identity.authorization.RoleRef;
@ -83,7 +83,9 @@ public class WebSecurityConfig {
http
.authorizeHttpRequests((authorize) -> authorize
.antMatchers(providerSettings.getTokenEndpoint()).permitAll()
.antMatchers("/api/**", "/apis/**").authenticated()
.antMatchers("/static/js/**").permitAll()
.antMatchers("/api/**", "/apis/**").access(requestInfoAuthorizationManager())
.anyRequest().access(requestInfoAuthorizationManager())
)
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(Customizer.withDefaults())
@ -93,7 +95,6 @@ public class WebSecurityConfig {
.addFilterBefore(new BearerTokenAuthenticationFilter(authenticationManagerResolver()),
BasicAuthenticationFilter.class)
.addFilterAfter(providerContextFilter, SecurityContextPersistenceFilter.class)
.addFilterBefore(authorizationFilter(), FilterSecurityInterceptor.class)
.sessionManagement(
(session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling((exceptions) -> exceptions
@ -103,10 +104,10 @@ public class WebSecurityConfig {
return http.build();
}
public AuthorizationFilter authorizationFilter() {
public RequestInfoAuthorizationManager requestInfoAuthorizationManager() {
// TODO fake role and role bindings, only used for testing/development
// It'll be deleted next time
return new AuthorizationFilter(name -> {
return new RequestInfoAuthorizationManager(name -> {
// role getter
Role role = new Role();
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;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
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.UserDetails;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* An authorization filter that restricts access to the URL.
*
* @author guqing
* @since 2.0.0
*/
@Slf4j
public class AuthorizationFilter extends OncePerRequestFilter {
private AuthorizationRuleResolver ruleResolver;
private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
public class RequestInfoAuthorizationManager
implements AuthorizationManager<RequestAuthorizationContext> {
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);
}
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
throws ServletException, IOException {
public AuthorizationDecision check(Supplier<Authentication> authenticationSupplier,
RequestAuthorizationContext requestContext) {
HttpServletRequest request = requestContext.getRequest();
RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);
Authentication authentication = getAuthentication();
Authentication authentication = authenticationSupplier.get();
UserDetails userDetails = createUserDetails(authentication);
AttributesRecord attributes = new AttributesRecord(userDetails, requestInfo);
@ -52,13 +49,10 @@ public class AuthorizationFilter extends OncePerRequestFilter {
if (!authorizingVisitor.isAllowed()) {
// print errors
showErrorMessage(authorizingVisitor.getErrors());
// handle it
accessDeniedHandler.handle(request, response,
new AccessDeniedException("Access is denied"));
return;
return new AuthorizationDecision(false);
}
log.debug(authorizingVisitor.getReason());
filterChain.doFilter(request, response);
return new AuthorizationDecision(isGranted(authentication));
}
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) {
Assert.notNull(authentication, "The authentication must not be null.");
return User.withUsername(authentication.getName())
@ -92,7 +77,13 @@ public class AuthorizationFilter extends OncePerRequestFilter {
this.ruleResolver = ruleResolver;
}
public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
this.accessDeniedHandler = accessDeniedHandler;
private boolean isGranted(Authentication authentication) {
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.ResponseBody;
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
* @since 2.0.0
*/
public class AuthorizationFilterTest extends AuthorizationTestSuit {
public class RequestInfoAuthorizationManagerTest extends AuthorizationTestSuit {
private MockMvc mockMvc;
@ -94,6 +94,22 @@ public class AuthorizationFilterTest extends AuthorizationTestSuit {
.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
public void nonResourceRequestWhenHaveNoRightShould403() throws Exception {
mockMvc.perform(get("/healthy/halo")
@ -133,20 +149,29 @@ public class AuthorizationFilterTest extends AuthorizationTestSuit {
}
@Controller
@RequestMapping("/healthy")
@ResponseBody
@RequestMapping
public static class HealthyController {
@GetMapping
@ResponseBody
@GetMapping("/healthy")
public String check() {
return "ok.";
}
@ResponseBody
@GetMapping("/{name}")
@GetMapping("/healthy/{name}")
public String check(@PathVariable String name) {
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.verifier.BearerTokenAuthenticationFilter;
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.RequestInfoAuthorizationManager;
import run.halo.app.identity.authorization.Role;
import run.halo.app.identity.authorization.RoleBinding;
import run.halo.app.identity.authorization.RoleRef;
@ -85,7 +85,9 @@ public class TestWebSecurityConfig {
http
.authorizeHttpRequests((authorize) -> authorize
.antMatchers(providerSettings.getTokenEndpoint()).permitAll()
.antMatchers("/api/**", "/apis/**").authenticated()
.antMatchers("/static/**").permitAll()
.antMatchers("/api/**", "/apis/**").access(requestInfoAuthorizationManager())
.anyRequest().access(requestInfoAuthorizationManager())
)
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(Customizer.withDefaults())
@ -95,14 +97,13 @@ public class TestWebSecurityConfig {
.addFilterBefore(new BearerTokenAuthenticationFilter(authenticationManagerResolver()),
BasicAuthenticationFilter.class)
.addFilterAfter(providerContextFilter, SecurityContextPersistenceFilter.class)
.addFilterBefore(authorizationFilter(), FilterSecurityInterceptor.class)
.sessionManagement(
(session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
public AuthorizationFilter authorizationFilter() {
return new AuthorizationFilter(name -> {
public RequestInfoAuthorizationManager requestInfoAuthorizationManager() {
return new RequestInfoAuthorizationManager(name -> {
// role getter
Role role = new Role();
List<PolicyRule> rules = List.of(