1、话不多说,先说使用步骤然后分析源码:
首先使用 @EnableMethodSecurity 注解开启方法级别的权限认证
### 使用该注解开启方法级权限鉴定
@EnableMethodSecurity
使用了方法权限注解开启了方法级的权限鉴定之后,就可以使用如下注解直接在控制器上使用方法级的权限鉴定了。
### 使用方法级的权限鉴定
@PreAuthorize("hasAuthority('sys:user:update')")
具体使用如下:
@PutMapping("/updateUser")
@Operation(description = "更新用户信息")
@PreAuthorize("hasAuthority('sys:user:update')")
public R<Object> update(@RequestBody User user) {
// 如果密码不为空,则进行加密处理
if(StrUtil.isNotBlank(user.getPassword())){
user.setPassword(passwordEncoder.encode(user.getPassword()));
}
return R.ok(userService.updateById(user));
}
@PreAuthorize("hasAuthority('sys:user:update')") 实现逻辑是aop拦截,会拦截每次请求,判断当前访问的用户权限情况,源码如下:
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuthorize {
/**
* @return 在调用受保护的方法之前要计算的Spring EL表达式
*/
String value();
}
至于 @PreAuthorize 注解的拦截器是如何生效的那就要靠 @EnableMethodSecurity 注解,因为该注解中导入了 @Import({MethodSecuritySelector.class}) 并接入到了IOC容器,因此只需要看 MethodSecuritySelector 类具体是怎么处理方法级的认证的即可。部分关键源码如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({MethodSecuritySelector.class}) // 注解的作用就是向IOC容器注入该选择器类
public @interface EnableMethodSecurity {
boolean prePostEnabled() default true; // 注意此属性默认为true,后面的源码会用到
boolean securedEnabled() default false;
boolean jsr250Enabled() default false;
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
}
MethodSecuritySelector 的部分关键源码如下:
final class MethodSecuritySelector implements ImportSelector {
private final ImportSelector autoProxy = new AutoProxyRegistrarSelector();
MethodSecuritySelector() {
}
public String[] selectImports(@NonNull AnnotationMetadata importMetadata) {
if (!importMetadata.hasAnnotation(EnableMethodSecurity.class.getName())) {
return new String[0];
} else {
EnableMethodSecurity annotation = (EnableMethodSecurity)importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
List<String> imports = new ArrayList(Arrays.asList(this.autoProxy.selectImports(importMetadata)));
// 从上面可知 prePostEnabled 默认为true,其他都为false,
// 因此PrePostMethodSecurityConfiguration会被注册,其他需要待注解中手动属性设置
if (annotation.prePostEnabled()) {
imports.add(PrePostMethodSecurityConfiguration.class.getName());
}
if (annotation.securedEnabled()) {
imports.add(SecuredMethodSecurityConfiguration.class.getName());
}
if (annotation.jsr250Enabled()) {
imports.add(Jsr250MethodSecurityConfiguration.class.getName());
}
return (String[])imports.toArray(new String[0]);
}
}
}
PrePostMethodSecurityConfiguration 部分核心源码如下:
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
final class PrePostMethodSecurityConfiguration {
// 注册了一个切面
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor preAuthorizeAuthorizationMethodInterceptor(ObjectProvider<GrantedAuthorityDefaults> defaultsProvider,
ObjectProvider<MethodSecurityExpressionHandler> expressionHandlerProvider,
ObjectProvider<SecurityContextHolderStrategy> strategyProvider,
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
// 创建了一个前置鉴权管理器,前置意为请求被controller处理前
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
// 前置鉴权管理器设置了一个EL表达式处理器
manager.setExpressionHandler(
expressionHandlerProvider.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, context)));
// 该拦截器实际就是一个定制专门用于 preAuthorize 注解的切面
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize(manager(manager, registryProvider));
// 给拦截器设置上了当前登录用户的 SecurityContextHolder,方便获取认证对象
strategyProvider.ifAvailable(preAuthorize::setSecurityContextHolderStrategy);
// 给拦截器设置上了授权事件发布器
eventPublisherProvider.ifAvailable(preAuthorize::setAuthorizationEventPublisher);
return preAuthorize;
}
// 定义了一个EL表达式处理器
private static MethodSecurityExpressionHandler defaultExpressionHandler(
ObjectProvider<GrantedAuthorityDefaults> defaultsProvider, ApplicationContext context) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
defaultsProvider.ifAvailable((d) -> handler.setDefaultRolePrefix(d.getRolePrefix()));
handler.setApplicationContext(context);
return handler;
}
// 定义了一个授权管理器
static <T> AuthorizationManager<T> manager(AuthorizationManager<T> delegate,
ObjectProvider<ObservationRegistry> registryProvider) {
return new DeferringObservationAuthorizationManager<>(registryProvider, delegate);
}
}
下面重点看上面调用的拦截器切面中的 preAuthorize 方法来构造拦截器切面的具体内容吧!
从上面可以看到这个拦截器切面是一个针对特定 PreAuthorize 注解才生效的拦截器,而这个拦截器本身实现了PointcutAdvisor 接口, 而PointcutAdvisor中包含了一个Pointcut成员变量,也就是切点,在加上继承的Advisor接口中的Advice,就形成了一个完整的切面。
因此也就可以说 AuthorizationManagerBeforeMethodInterceptor 拦截器是一个专门用于拦截标有 PreAuthorize 注解的切面。
处理 PreAuthorize 切点中表达式分析过程如下:
上面 PrePostMethodSecurityConfiguration 关键源码中,我们看到注入了一个 MethodSecurityExpressionHandler 表达式处理器,实际使用的是他的实现类DefaultMethodSecurityExpressionHandler 处理器,其中的这部分源码解释可以看出如何使用表达式的过程。
因此,过程就是,授权管理器AuthorizationManager设置了一个方法级前置授权管理器PreAuthorizeAuthorizationManager 和一个EL表达式处理器 MethodSecurityExpressionHandler
然后AuthorizationManager作为参数用于构造切面
因此该切面就拥有的一个可处理EL表达式的处理器DefaultMethodSecurityExpressionHandler
hasAuthority 方法的处理逻辑:
从上面的 hasAuthority 方法的处理过程可以看出,最终是从我们封装的认证对象中获取权限集合,然后从该权限集合中获取权限的字符串名称,将他们组装成一个Set<String>集合,最终的鉴权过程就演变成了遍历字符串集合,判断传入 @PreAuthorize("hasAuthority('sys:user:update')") 的中的字符串sys:user:update 是否被包含在 Set 字符串集合中。原来如此啊!!!是不是突然觉得有一种拨开云雾的感觉呢。
因此,我们要做的就是在登录成功时,将用户角色对应的所有权限(也可以说是菜单或按钮的操作权,例如:sys:user:insert / sys:user:update 等等)全部查出封装成对应的权限对象 GrantedAuthority ,角色和权限不可混为一谈,角色是一组权限的集合,给一个用户赋予了一个角色也就相当于给了他一组权限。
此外,基于权限点和基于角色的授权两者只在验证名称上稍有不同,基于角色的授权中EL表达式中的权限标识会被拼接上 ROLE_ 前缀,如下:
@PreAuthorize("hasRole('admin')") 如果我们这样使用,那么最后和从认证对象中权限名称对比的就是 ROLE_admin ,因此数据库设置角色权限时,名称角色名称需要加上该 ROLE_前缀,可以这样设计: ROLE_admin
我的 Spring Security 参考配置如下:
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final UserDetailService userDetailService;
private final SmsService tencentSmsService;
// 访问白名单,可以设计成读取数据库或缓存
String[] whitelist = {"/sms/**","/auth/**"};
@Bean
@SneakyThrows
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
// 全局请求策略配置
http
.csrf().disable() // 禁用csrf跨站请求伪造
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(whitelist).permitAll() // 白名单全部放行
.requestMatchers(HttpMethod.OPTIONS).permitAll() // options请求放行
.anyRequest().authenticated() // 其他请求需要认证
)
// 禁用session,改用JWT的方式
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 添加自定义认证过滤器,按照默认的security所有过滤器顺序
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// 设置自定义认证、鉴权异常处理
.exceptionHandling()
// 自定义认证异常处理
.authenticationEntryPoint(blogAuthenticationEntryPoint())
// 自定义鉴权异常处理
.accessDeniedHandler(blogAccessDeniedHandler());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() {
List<AuthenticationProvider> providers = new ArrayList<>(3);
// 可以设置多个认证处理器
AuthenticationProvider usernameAuthenticationProvider = new DaoAuthenticationProvider();
MobileAuthenticationProvider mobileAuthenticationProvider = new MobileAuthenticationProvider(userDetailService,tencentSmsService);
providers.add(usernameAuthenticationProvider);
providers.add(mobileAuthenticationProvider);
return new ProviderManager(providers);
}
private BlogAuthenticationEntryPoint blogAuthenticationEntryPoint() {
return new BlogAuthenticationEntryPoint();
}
private BlogAccessDeniedHandler blogAccessDeniedHandler() {
return new BlogAccessDeniedHandler();
}
// 笔记1: 凡是在springsecurity的认证和授权过程中抛出的异常,不论是我们认为抛出的异常还是走源码过程抛出的异常都会被springsecurity过滤器链中的ExceptionTranslationFilter异常处理的过滤器捕获
// 并调用相应的接口方法进行异常处理,所以会导致这些异常的响应结果和我们自定义的响应结果不一致的情况,为了保证给前端的响应结果的一致性,我们有必要对相应的接口
// 进行自定义的实现,以此来保证响应结果的统一性。同时在完成自定义处理之后,我们必须将实现注册成Bean,并将他们加入到springsecurity过滤环节的对应节点,
// 也就是在HttpSecurity对象的对应节点进行配置,以此确保自定义实现的组件能够生效。相应接口有:认证失败处理AuthenticationEntryPoint、鉴权失败处理AccessDeniedHandler
// 笔记2: 自从springsecurity-5.7.X版本开始,官方将废弃WebSecurityConfigurerAdapter这个springsecurity的适配器类,转而推荐使用向容器注入SecurityFilterChain
// 安全过滤器链的方法配置springsecurity
// 笔记3: spring官方推荐使用构造函数的方式完成依赖注入,相应的我们可以使用lombok的相应注解完成,在开发过程中应当减少使用像@Autowired、@Resource的注解
// 笔记4: 在我们对springsecurity进行配置时,如果我们不对HttpSecurity对象配置formLogin属性的话,则在过滤器链中将不会存在UsernamePasswordAuthenticationFilter过滤器
// 原因是在springsecurity的默认配置中为HttpSecurity对象配置了formLogin属性,在该属性中配置了一个默认的登陆页面,同时new了一个UsernamePasswordAuthenticationFilter放了容器
// 但如果我们自定义配置springsecurity时没有配置formLogin属性则不会走UsernamePasswordAuthenticationFilter这一套的过滤器逻辑。所以,对于认证和授权的实现方案,我们可以总结为两套,
// 一套是,走我们自定义的过滤器并没有配置formLogin属性的认证方案;另一套是,走springsecurity默认的通过走UsernamePasswordAuthenticationFilter的这一套方案,走这套方案需要我们
// 在对springsecurity进行自定配置是为HttpSecurity对象配置formLogin属性,此外,我们还可以对这套方案中的一些流程处理进行自定义实现,比如自定义实现AuthenticationSuccessHandler
// AuthenticationFailureHandler等一些关键性流程的自我定制化实现,以此来更好的完成我们的认证授权。此外、如果我们也想对登出操作进行自定义的话,我们同理可以对HttpSecurity对象配置logout属性
// 然后自定义实现LogoutSuccessHandler
// SecurityContextHolderFilter、AuthorizationFilter
// 如果我们不是用默认的formLogin的方式,则需要自定义认证过滤器,然后在其中使用 AuthenticationManager 来认证登录信息
}