SpringSecurity实现动态菜单访问权限管理

目录

1.简单介绍权限数据库设置

  1.1重点讲解

2. 自定义UserDetails

 2.1.重点讲解

3. 自定义UserDetailsService

3.1 重点讲解

4.自定义 FilterInvocationSecurityMetadataSource

4.1重点讲解

5.自定义 AccessDecisionManager

5.1 重点讲解

6.配置WebSecurityConfigurerAdapter

6.1.重点讲解

7.效果展示


1.简单介绍权限数据库设置

权限数据库主要包含了五张表,分别是资源表、角色表、用户表、资源角色表、用户角色表,数据库关系模型如下:

SpringSecurity实现动态菜单访问权限管理_第1张图片

  1.1重点讲解

关于这个表,我说如下几点:
(1)user 表是用户表,存放了用户的基本信息。
(2)role 是角色表, name 字段表示角色的英文名称,按照 SpringSecurity 的规范,将以 ROLE_ 始, nameZh 字段表示角色的中文名称。
(3)menu 表是一个资源表,该表涉及到的字段有点多,由于我的前端采用了 Vue 来做,因此当用户 登录成功之后,系统将根据用户的角色动态加载需要的模块,所有模块的信息将保存在 menu 中, menu 表中的 path component iconCls keepAlive requireAuth 等字段都是 Vue
Router 中需要的字段,也就是说 menu 中的数据到时候会以 json 的形式返回给前端,再由 vue
动态更新 router menu 中还有一个字段 url ,表示一个 url pattern ,即路径匹配规则,假设有一
个路径匹配规则为 /admin/** , 那么当用户在客户端发起一个 /admin/user 的请求,将被
/admin/** 拦截到,系统再去查看这个规则对应的角色是哪些,然后再去查看该用户是否具备相
应的角色,进而判断该请求是否合法

2. 自定义UserDetails

package com.cr.mdsp.common.security.entity;

import com.bcy.ipg.user.model.Role;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;

@Data
public class SecurityUser implements UserDetails {

    private static final long serialVersionUID = 1L;

    private Integer id;

    /**
     * 账号
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 企业微信的成员UserId
     */
    private String qywxUserId;

    /**
     * 用户真实名称
     */
    private String realName;

    /**
     * 企业微信的成员别名
     */
    private String aliasName;

    /**
     * 头像
     */
    private String headImgUrl;

    /**
     * 手机号码
     */
    private String phone;

    /**
     * 邮箱
     */
    private String email;

    /**
     * 地址
     */
    private String address;

    /**
     * 启用/禁用成员。1表示启用成员,0表示禁用成员
     */
    private Integer enabled;

    /**
     * 用户所对应的角色
     */

    private List roles;

    /**
     *即直接从 roles 中获取当前用户所具有的角色,构造 SimpleGrantedAuthority 然后返回即可。
     */
    @Override
    @JsonInclude
    public Collection getAuthorities() {
        List authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled == 1 ? true : false;
    }
}

 2.1.重点讲解

UserDetails 接口默认有几个方法需要实现,这几个方法中,除了 isEnabled 返回了正常的 enabled外,其他的方法我都统一返回 true,因为我这里的业务逻辑并不涉及到账户的锁定、密码的过期等等, 只有账户是否被禁用,因此只处理了isEnabled 方法,这一块小伙伴可以根据自己的实际情况来调整。

    另外,UserDetails 中还有一个方法叫做 getAuthorities ,该方法用来获取当前用户所具有的角色,但 是小伙伴也看到了,我的 Hr 中有一个 roles 属性用来描述当前用户的角色,因此我的 getAuthorities 方法的实现如上

3. 自定义UserDetailsService

创建好 用户实体之后,接下来我们需要创建UserDetailsServiceImpl ,用来执行登录等操作,UserDetailsServiceImpl 需要实现 UserDetailsService 接口,如下:
package com.cr.dgp.web.config.security;

import com.bcy.ipg.user.model.User;
import com.bcy.ipg.user.service.RoleService;
import com.bcy.ipg.user.service.UserService;
import com.cr.mdsp.common.security.entity.SecurityUser;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        //根据用户名查询数据
        User user = userService.getUserByUsername(userName);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        SecurityUser securityUser = new SecurityUser();
        BeanUtils.copyProperties(user, securityUser);
        //在此处塞入该用户所对应的权限,可以在其他地方设入,其他方法自行查阅
        securityUser.setRoles(roleService.getRoleListByUserId(null, user.getId()));

        return securityUser;
    }

}

3.1 重点讲解

        这里最主要是实现了 UserDetailsService 接口中的 loadUserByUsername 方法,在执行登录的过程中,这个方法将根据用户名去查找用户,如果用户不存在,则抛UsernameNotFoundException 异常,否则直接将查到的用户返回。
UserService用来执行数据库的查询操作, 本文的重点不在数据库查询所以不做详解了,只简单描述方法的用处。

4.自定义 FilterInvocationSecurityMetadataSource

        FilterInvocationSecurityMetadataSource 有一个默认的实现类DefaultFilterInvocationSecurityMetadataSource,该类的主要功能就是通过当前的请求地址,获取该地址需要的用户角色,咱们有样学样,自己也定义一下FilterInvocationSecurityMetadataSource,如下:

package com.cr.dgp.web.filter.security;

import com.bcy.ipg.user.model.Menu;
import com.bcy.ipg.user.model.Role;
import com.bcy.ipg.user.model.RoleMenu;
import com.bcy.ipg.user.service.MenuService;
import com.bcy.ipg.user.service.RoleMenuService;
import com.bcy.ipg.user.service.RoleService;
import org.apache.metamodel.util.CollectionUtils;
import org.pentaho.di.core.util.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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;
import java.util.Set;
import java.util.stream.Collectors;


@Component
public class CustomFilterInvocationSecurityMetadataSource implements
        FilterInvocationSecurityMetadataSource {
    @Autowired
    MenuService menuService;
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Autowired
    private RoleMenuService roleMenuService;
    @Autowired
    private RoleService roleService;

    @Override
    public Collection getAttributes(Object object) throws IllegalArgumentException {
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        if (antPathMatcher.match("/login*", requestUrl)) {
            return null;
        }
        List menus = menuService.getAllMenuList();
        for (Menu menu : menus) {
            if (StringUtil.isEmpty(menu.getUrl())) {
                continue;
            }
            if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
                List roles = menu.getRoles();
                int size = roles.size();
                String[] str = new String[size];
                for (int i = 0; i < menu.size(); i++) {
                    str[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(str);
            }
        }
        //没有匹配上的资源,都是登录访问
        return SecurityConfig.createList("ROLE_LOGIN");
    }

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

    @Override
    public boolean supports(Class aClass) {
        return true;
    }
}

4.1重点讲解

(1) 一开始注入了 MenuService,MenuService 的作用是用来查询数据库中 url pattern 和 role 的对应关系,查询结果是一个 List 集合,集合中是 Menu 类,Menu 类有两个核心属性,一个是 urlpattern,即匹配规则(比如 /admin/** ),还有一个是 List,即这种规则的路径需要哪些角色才能
访问。
(2)我们可以从 getAttributes(Object o) 方法的参数 o 中提取出当前的请求 url,然后将这个请求 url和数据库中查询出来的所有 url pattern一一对照,看符合哪一个url pattern,然后就获取到该
url pattern 所对应的角色,当然这个角色可能有多个,所以遍历角色,最后利用SecurityConfig.createList 方法来创建一个角色集合。
(3)第二步的操作中,涉及到一个优先级问题,比如我的地址是 /employee/basic/hello ,这个地址
既能被 /employee/** 匹配,也能被 /employee/basic/** 匹配,这就要求我们从数据库查询
的时候对数据进行排序,将 /employee/basic/** 类型的 url pattern 放在集合的前面去比较。
(4) 如果 getAttributes(Object o) 方法返回 null 的话,意味着当前这个请求不需要任何角色就能访问,甚至不需要登录。但是在我的整个业务中,并不存在这样的请求,我这里的要求是,所有未匹配到的路径,都是认证(登录)后可访问,因此我在这里返回一个 ROLE_LOGIN 的角色,这种角色在我的角色数据库中并不存在,因此我将在下一步的角色比对过程中特殊处理这种角色。
(5) 如果地址是 /login,这个是登录页,不需要任何角色即可访问,直接返回 null。
(6) getAttributes(Object o) 方法返回的集合最终会来到 AccessDecisionManager 类中,接下来我们再来看 AccessDecisionManager 类。

5.自定义 AccessDecisionManager

定义CustomAccessDecisionManager 类实现 AccessDecisionManager 接口,如下:
package com.cr.dgp.web.filter.security;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
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;

@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute configAttribute : configAttributes) {
            //当前请求需要的权限
            String needRole = configAttribute.getAttribute();
            if ("ROLE_LOGIN".equals(needRole)) {
                if (authentication instanceof AnonymousAuthenticationToken) {
                    throw new AccessDeniedException("尚未登录,请登录!");
                } else {
                    return;
                }
            }
            //当前用户所具有的权限
            Collection authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足,请联系管理员!");
    }

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

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

5.1 重点讲解

(1) decide 方法接收三个参数,其中第一个参数中保存了当前登录用户的角色信息,第三个参数则是UrlFilterInvocationSecurityMetadataSource 中的 getAttributes 方法传来的,表示当前请求需要的角色(可能有多个)。
(2) 如果当前请求需要的权限为 ROLE_LOGIN 则表示登录即可访问,和角色没有关系,此时我需要判断 authentication 是不是 AnonymousAuthenticationToken 的一个实例,如果是,则表示当前用户没有登录,没有登录就抛一个 BadCredentialsException 异常,登录了就直接返回,则这个
请求将被成功执行。
(3)遍历 collection,同时查看当前用户的角色列表中是否具备需要的权限,如果具备就直接返回,否则就抛异常。
(4) 这里涉及到一个 all 和 any 的问题:假设当前用户具备角色 A、角色 B,当前请求需要角色 B、角色 C,那么是要当前用户要包含所有请求角色才算授权成功还是只要包含一个就算授权成功?我这里采用了第二种方案,即只要包含一个即可。小伙伴可根据自己的实际情况调整 decide 方法中的逻辑。

6.配置WebSecurityConfigurerAdapter

最后在 webSecurityConfig 中完成简单的配置即可,如下:

package com.cr.dgp.web.config.security;

import com.cr.dgp.web.filter.security.CustomAccessDecisionManager;
import com.cr.dgp.web.filter.security.CustomFilterInvocationSecurityMetadataSource;
import com.xxl.job.core.biz.AdminBiz;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.web.context.request.RequestContextListener;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private CustomFilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
    @Autowired
    private CustomAccessDecisionManager accessDecisionManager;

    private UserDetailsService userDetailsService;

    @Autowired
    public void setUserDetailsService(UserDetailsServiceImpl userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public RequestContextListener requestContextListener() {
        return new RequestContextListener();
    }

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/resources/**");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable()
                .exceptionHandling().authenticationEntryPoint(new LoginUrlEntryPoint("/login"))
                .and()
                .headers()
                .frameOptions()
                .sameOrigin()
                .and()
                .authorizeRequests().withObjectPostProcessor(new ObjectPostProcessor() {
            @Override
            public  O postProcess(O o) {
                o.setAccessDecisionManager(accessDecisionManager);
                o.setSecurityMetadataSource(filterInvocationSecurityMetadataSource);
                return o;
            }
        })
                .antMatchers("/", "/" + AdminBiz.MAPPING, "/prometheus").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginPage("/login")
                .loginProcessingUrl("/login")
                .successHandler(new LoginSuccessHandler())
                .failureUrl("/login?error")
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/login?logout")
                .and()
                .sessionManagement()
                .sessionFixation()
                .newSession()
                .maximumSessions(-1)
                .maxSessionsPreventsLogin(true)
                .expiredUrl("/timeout")
                .sessionRegistry(sessionRegistry());
    }


    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }
}

6.1.重点讲解

(1) confifigure(HttpSecurity http) 方法中,通过 withObjectPostProcessor 将刚刚创建的
UrlFilterInvocationSecurityMetadataSource UrlAccessDecisionManager 注入进来。到时
候,请求都会经过刚才的过滤器(除了 confifigure(WebSecurity web) 方法忽略的请求)。
(2)successHandler 中配置登录成功时返回的 JSON ,登录成功时返回当前用户的信息。
(3)failureHandler 表示登录失败,登录失败的原因可能有多种,我们根据不同的异常输出不同的错误 提示即可。

7.效果展示

SpringSecurity实现动态菜单访问权限管理_第2张图片

灵感来源:

特别鸣谢一位名为:“江南一点雨” 作者所编写的开源项目  微人事

你可能感兴趣的:(java,spring,boot)