SpringBoot & SpringSecurity 下 - 动态角色和权限验证

之前写的是 SpringSecurity 的标准用法,每个接口需要提前定义哪些角色可以访问,下面讲解如何动态增加角色以及动态配置链接可访问角色。

一、修改 WebSecurityConfig

package com.bjy.qa.util.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;

/**
 * SpringSecurity 配置
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别的权限认证,后续不需要,要删除
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; // JWT 过滤器

    @Resource
    AuthenticationEntryPointImpl autoetaticaticcAutryPointImpl; // 认证 时的异常(当用户请求一个受保护的资源,又没登录时触发)
    @Resource
    AccessDeniedPointImpl accessDeniedPointImpl; // 用户访问无权限资源 时的异常(用户登录后,请求一个受保护的资源,又没权限时触发)

    @Resource
    CustomFilterInvocationSecurityMetadataSource appFilterInvocationSecurityMetadataSource;
    @Resource
    CustomAccessDecisionManager customerAccessDecisionManger;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable() // 关闭跨站请求防护
                .cors().and() // 配置 CORS支持
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 不通过 session 获取 SecurityContext

        // 配置自定义的权限验证类
        http
                .authorizeRequests().anyRequest().authenticated().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setSecurityMetadataSource(appFilterInvocationSecurityMetadataSource); // 自定义请求 URL 与配置属性之间的映射类(根据请求的 URL,查找适用于该 URL 的权限配置属性,比如角色,权限等信息)
                        o.setAccessDecisionManager(customerAccessDecisionManger); // 自定义密名账户验证类,它负责根据 Authentication 对象、访问主体的权限信息以及资源的安全配置,决定主体是否有权限访问资源 
                        return o;
                    }
                });

        // 配置异常处理器
        http
                .exceptionHandling()
                .authenticationEntryPoint(autoetaticaticcAutryPointImpl) // 认证 时的异常(当用户请求一个受保护的资源,又没登录时触发)
                .accessDeniedHandler(accessDeniedPointImpl); // 用户访问无权限资源 时的异常(用户登录后,请求一个受保护的资源,又没权限时触发)

        // 添加 JWT 过滤器
        http
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

主要改动如下:

1、去掉 EnableWebSecurity,不去也没问题

2、替换自定义权限和认证

 二、CustomAccessDecisionManager

package com.bjy.qa.util.security;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;

/**
 * 自定义密名账户验证类,它负责根据 Authentication 对象、访问主体的权限信息以及资源的安全配置,决定主体是否有权限访问资源
 */
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
    /**
     * 判断当前登录的用户是否具备当前请求URL所需要的角色信息。如果不具备,就抛出 AccessDeniedException 异常,否则不做任何事即可
     * @param authentication 当前登录用户的信息
     * @param object FilterInvocation对象,可以获取当前请求对象
     * @param configAttributes FilterInvocationSecurityMetadataSource 中的 getAttributes() 方法的返回值,即当前请求URL所需的角色
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        Collection<? extends GrantedAuthority> auths = authentication.getAuthorities(); // 获取当前登录用户的角色信息(之前在 JwtAuthenticationTokenFilter 中存入的用户角色信息)

        // 遍历当前请求 URL 所需要的角色信息(这里角色以 |admin|user| 形式放回所以只判断一次)
        for (ConfigAttribute configAttribute : configAttributes) {
            if ("ROLE_ANONYMOUS".equals(configAttribute.getAttribute())) {
                return;
            }

            // 遍历当前登录用户的角色信息,判断是否具备当前请求 URL 所需要的角色信息
            for (GrantedAuthority authority : auths) {
                if (configAttribute.getAttribute().contains("|" + authority.getAuthority() + "|")) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("拒绝访问 - 未授权");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

三、CustomFilterInvocationSecurityMetadataSource

package com.bjy.qa.util.security;

import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * 自定义请求 URL 与配置属性之间的映射类(根据请求的 URL,查找适用于该 URL 的权限配置属性,比如角色,权限等信息)
 */
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    private static List<MenuRole> menuRoleList; // 所有权限

    private List<MenuRole> getRoleMenu() {
        if (menuRoleList != null) {
            return menuRoleList;
        }

        // 临时写死 url 对应权限,后续要从数据库中查询
        menuRoleList = new ArrayList<>();
        MenuRole menuRole = new MenuRole();
        menuRole.setUrl("/user/user/login123/*"); // url 链接
        menuRole.setRole("|ADMIN|user|"); // 角色信息,以 | 包裹,这个链接哪些角色可以访问都写到一起
        CustomFilterInvocationSecurityMetadataSource.menuRoleList.add(menuRole);

        MenuRole menuRole1 = new MenuRole();
        menuRole1.setUrl("/user/user/logout");
        menuRole1.setRole("|admin|");
        CustomFilterInvocationSecurityMetadataSource.menuRoleList.add(menuRole1);
        return menuRoleList;
    }

    /**
     * 判断当前请求的 URL 是否需要权限验证,如果是返回角色信息(目前是现实的黑名单,未匹配到都放行)
     * @param url
     * @return
     */
    // TODO: 2023/7/25 后续这里放到 starter-cache 中,不用每次都匹配
    private String matcher(String url) throws IllegalArgumentException {
        AntPathMatcher antPathMatcher = new AntPathMatcher(); // Spring URL 匹配器,支持 Ant 风格的路径匹配表达式(支持通配符 * 和 **。`*匹配单路径段,**`匹配多路径段。)
        for (MenuRole menu : getRoleMenu()) {
            if (antPathMatcher.match(menu.getUrl(), url)) {
                return menu.getRole(); // 匹配到了,需要验证权限
            }
        }
        return "ROLE_ANONYMOUS"; // 没有匹配到,不需要验证权限
    }

    /**
     * 返回当前请求 URL 所需要的角色信息
     * @param object
     * @return
     * @throws IllegalArgumentException
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        return SecurityConfig.createList(matcher(requestUrl));
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return false;
    }
}

四、MenuRole

package com.bjy.qa.util.security;

public class MenuRole {
    String url;
    String role;

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }
}

五、测试

1、login 登录正常

2、logout接口有权限可以访问

3、getinfo没在黑名单,可以访问

4、没有权限,拒绝访问

 

参考文档:

https://blog.csdn.net/weixin_43740982/article/details/120979904 

 

posted @ 2023-07-25 08:27  rslai  阅读(324)  评论(0编辑  收藏  举报