背景
一个系统要做好权限控制是非常重要的一件事情,不仅要判断当前请求的用户是否已经登录,还要判断当前用户是否拥有访问某个接口的权限,这样才能防止纵向越权。最典型的场景,就是普通用户不能访问管理员的接口,首先大家能想到的一个解决方案,就是根据角色去判断,这确实是其中一个解决方案,并且Spring Security
也提供了@PreAuthorize
系列注解直接加到接口上,就可以根据角色进行控制。
使用注解也有一个缺点,那就是需要在N多接口上加注解,那有没有不使用注解的一种全局通用的解决方案呢?答案是肯定的。本文主要从Spring Security
提供的机制上,找出一个最佳实现,不仅可以根据角色判断,也能通过请求路径进行判断。因为角色的粒度还是比较粗,在权限体系里面,很多时候一条权限数据,就对应后端的一个接口,比如新增是一个权限,删除是一个权限。那么每个接口都有一个请求路径,我们可以根据这个请求路径进行更细粒度的权限控制。
本文开发环境介绍
开发依赖 | 版本 |
---|---|
Spring Boot | 3.0.6 |
pom.xml依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
主角AuthorizationManager登场
AccessDecisionManager
已经@Deprecated
,建议使用AuthorizationManager
取代。这是一个接口,主要实现check
方法AuthorizationDecision check(Supplier<Authentication> authentication, T object);
,如果有权限,则返回一个带true
的授权决策return new AuthorizationDecision(true);
;如果没有权限,则返回一个带false
的授权决策return new AuthorizationDecision(false);
动手实现一个DemoAuthorizationManager
源码如下
package com.wen3.oauth.ss.authclient.authorization;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.stereotype.Component;
import java.util.function.Supplier;
@RequiredArgsConstructor
@Component
@Slf4j
public class DemoAuthorizationManager<T> implements AuthorizationManager<T> {
private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private final HttpServletRequest httpServletRequest;
@Override
public AuthorizationDecision check(Supplier<Authentication> supplier, T object) {
Authentication authentication = supplier.get();
log.info("authentication: {}", authentication);
log.info("object: {}", object);
boolean isAnonymous = authentication != null && !this.trustResolver.isAnonymous(authentication)
&& authentication.isAuthenticated();
if(!isAnonymous) {
return new AuthorizationDecision(false);
}
String servletPath = httpServletRequest.getServletPath();
log.info("servletPath: {}", servletPath);
// TODO: 判断当前用户是否拥有访问servletPath的权限
boolean granted = true;
return new AuthorizationDecision(granted);
}
}
让DemoAuthorizationManager生效
package com.wen3.oauth.ss.authclient.autoconfigure;
import com.wen3.oauth.ss.authclient.handler.DemoBearerTokenAuthenticationEntryPoint;
import com.wen3.oauth.ss.authclient.processor.OAuth2AuthorizationRequestRedirectFilterPostProcessor;
import com.wen3.oauth.ss.authclient.processor.OAuth2LoginAuthenticationFilterPostProcessor;
import com.wen3.oauth.ss.authclient.processor.SpringOpaqueTokenIntrospectorPostProcessor;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
public class Oauth2ClientAutoConfiguration {
@Resource
private AuthorizationManager authorizationManager;
@Bean
public SecurityFilterChain authorizationClientSecurityFilterChain(HttpSecurity http) throws Exception {
AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry authorizationManagerRequestMatcherRegistry = http.authorizeHttpRequests();
authorizationManagerRequestMatcherRegistry.requestMatchers(
new AntPathRequestMatcher("/authorized"),
new AntPathRequestMatcher("/error"),
new AntPathRequestMatcher("/webjars/**"),
new AntPathRequestMatcher("/resources/**"),
new AntPathRequestMatcher("/index/**"),
new AntPathRequestMatcher("/**/*.css"),
new AntPathRequestMatcher("/**/*.ico")
).permitAll();
authorizationManagerRequestMatcherRegistry.anyRequest().access(authorizationManager);
http.formLogin();
return http.build();
}
}
- 关键生效的一行是`authorizationManagerRequestMatcherRegistry.anyRequest().access(authorizationManager);
- 这样就可以在DemoAuthorizationManager自定义我们的权限判断逻辑,角色也好,请求路径也好,或者其它特定需求都可以定制开发了
总结
Spring Security功能很强大,对安全认证这一块提供了非常丰富的支持,篇幅有限,只是演示了非常基础的功能。相信通过这篇文章的介绍,可以帮助大家解决一定的困惑。
本人对Spring Security的研究非常深入,几乎翻看了底层所有的源码,熟悉Spring Security运行的机制、原理,如果大家在使用Spring Security的过程中遇到什么难题,欢迎进一步沟通、交流。