基于SpringSecurity实现权限管理

先想一下,不让你用框架,让你自己去实现权限管理功能,你的脑海里能蹦出几个关键词?

  • Filter
  • Interceptor

这两个概念应该是一瞬间想到的,它们都可以帮助我们去实现权限功能,一个是servlet的规范,一个是spring的概念。在系统执行顺序中,Filter先于Interceptor执行。至于更详细的对比,网上有很多资料。

SpringSecurity对于权限这方面的实现流程

先了解下它里面比较重要的几个点:

  • Filter
  • AbstractSecurityInterceptor (权限管理security真正的拦截器,并绑定了AccessDecisionManager(处理权限认证)和FilterInvocationSecurityMetadataSource(提供请求路径和权限名称元数据) )
  • FilterInvocationSecurityMetadataSource (主要实现加载缓存权限功能路径和名称,以及提供从请求路径查找权限名称,供后续决策管理器去判定使用。)
  • AccessDecisionManager(主要判定用户是否拥有权限内的决策方法,有权限放行,无权限拒绝访问。)
  • WebSecurityConfigurerAdapter (配置类)
  • AccessDeniedHandler

具体代码实现

因为我是从正向流程说的整个过程,所以同学如果你按照顺序复制代码,会出现某些需要注入的属性一直显示红色,提示找不到这个类,不要着急,是因为这些类在下面的代码里,建议先整体浏览文章一遍,看懂后直接把代码全部复制进去调试。

首先实现一个我们自己的WebSecurityConfigurerAdapter ,注意 auth.userDetailsService 和 http.addFilterBefore 这两个方法,它是把我们自己实现的当前用户对应权限,和请求路径对应的权限,塞入整体流程的两个重要点。

@Configuration
@EnableWebSecurity
public class WebSecurityConfigur extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyAbstractSecurityInterceptor myAbstractSecurityInterceptor;
    @Autowired
    @Qualifier("myUserDetailsService")
    private UserDetailsService myUserDetailsService;
    @Autowired
    private CustomizeAuthenticationEntryPoint customizeAuthenticationEntryPoint;
    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
    @Autowired
    MyAccessDeniedHandler myAccessDeniedHandler;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new MyPasswordEncoder();
    }

    /**
     * 把我们自己实现的UserDetailsService的对象放入auth中
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
        ;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

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

        http.csrf().disable()//开启模拟请求
                .authorizeRequests()
                .and()
                .formLogin()
                .permitAll()//登录页面用户任意访问
                .successHandler(myAuthenticationSuccessHandler)//登录成功处理逻辑
                .failureHandler(myAuthenticationFailureHandler)//登录失败处理逻辑
                .and()
                .logout()//登出
                .permitAll()
                .deleteCookies("JSESSIONID")//登出之后删除cookie
                .and()
                .headers()
                .frameOptions().sameOrigin() //注销行为任意访问
                .and().exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler);
        //把我们自己定义的拦截器放入到HttpSecurity里
        http.addFilterBefore(myAbstractSecurityInterceptor, FilterSecurityInterceptor.class)
        ;
    }
}

再实现一个UserDetailsService的子类MyUserDetailsService,重写 loadUserByUsername() 方法,这个方法会在用户登录的时候调用, 在当前文章里全文搜索一下MyUserDetailsService 。我先走由于还没整合mybatis,所以先写死的数据,过后整合完毕,我会再修改这部分文章。

这里假设loadUserByUsername里查出来的权限就是“customer”和"all"。

@Service
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    @Qualifier("myPasswordEncoder")
    private PasswordEncoder passwordEncoder;
    @Autowired
    ControlAccount controlAccount;
    //账户未被锁定
    Boolean accountNonLocked = true;

    /**
     * @param username
     * @return
     * @throws UsernameNotFoundException
     * @description 此方法会在用户登录的时候执行
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //检查下当前账户是不是被锁定,三次就是锁定
        Integer num = controlAccount.getLockTable().get(username);
        if (null != num && 3 <= num.intValue()) {
            accountNonLocked = false;
        }
        //根据名称查出所拥有的角儿

        //根据角色查出对应的权限

        //把每一个权限构造如SimpleGrantedAuthority
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        if ("account".equals(username)) {
            GrantedAuthority grantedAuthority1 = new SimpleGrantedAuthority("customer");
            GrantedAuthority grantedAuthority2 = new SimpleGrantedAuthority("all");
            grantedAuthorities.add(grantedAuthority1);
            grantedAuthorities.add(grantedAuthority2);
        } else {
            throw new BadCredentialsException("用户不存在");
        }
        //返回security的user对象
        //this(username, password, true, true, true, true, authorities);
        User user = new User("account", passwordEncoder.encode("wushichao")
                , true, true, true, accountNonLocked, grantedAuthorities);

        return user;
    }

然后是重要的一个实现类:MyAbstractSecurityInterceptor,咱们要重写它的 doFilter和obtainSecurityMetadataSource方法,同时要把MyAccessDecisionManager塞入这个类里,忘记SecurityMetadataSource和AccessDecisionManager的同学,在这篇文章里全文搜索一下这俩关键词,重新回忆一下它们的功能。

@Service
public class MyAbstractSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
    @Autowired
    MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;

    @Autowired
    public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
        super.setAccessDecisionManager(myAccessDecisionManager);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        FilterInvocation filterInvocation = new FilterInvocation(servletRequest, servletResponse, filterChain);
        //beforeInvocation 里会调用FilterInvocationSecurityMetadataSource的getAttributes获取url对应的权限
        //和调用UserDetailsService的loadUserByUsername获取用户名对应的权限
        //然后调用MyAccessDecisionManager的decide去进行比对
        InterceptorStatusToken interceptorStatusToken = super.beforeInvocation(filterInvocation);
        try {
            filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
        } finally {
            super.afterInvocation(interceptorStatusToken, null);
        }

    }

    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.myFilterInvocationSecurityMetadataSource;
    }
}

**最后还剩两个实现类,其一是:MyFilterInvocationSecurityMetadataSource,重写里面的 getAttributes(XX) 方法,这个方法用来自己实现根据请求路径查出路径对应的权限,待会用来跟用户拥有的权限做对比。**别忘了supports方法要 return true.

@Service
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    Log log = LogFactory.getLog(MyFilterInvocationSecurityMetadataSource.class);

    @Autowired
    PermisionDao permisionDao;
    private HashMap<String, Collection<ConfigAttribute>> map = null;

    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        log.info("MyFilterInvocationSecurityMetadataSource:getAttributes come in");
        //1获取所有的路径对应的权限
        loadAllUrlResouceAuth();
        List<ConfigAttribute> list = new ArrayList<ConfigAttribute>();
        //2 从过滤器中取出请求request
        HttpServletRequest httpRequest = ((FilterInvocation) o).getHttpRequest();
        //3 匹配
        AntPathMatcher pathMatcher = new AntPathMatcher();
        Iterator<String> iterator = map.keySet().iterator();
        Collection<ConfigAttribute> collection = null;
        while (iterator.hasNext()) {
            String pattern = iterator.next();
            String path = httpRequest.getRequestURI().toString();
            if (pathMatcher.match(pattern, path)) {
                log.info("MyFilterInvocationSecurityMetadataSource:getAttributes 有匹配的路径和权限");
                collection = map.get(pattern);
            }
        }
        //collection为null的时候,不会进入Manager
        log.info("MyFilterInvocationSecurityMetadataSource:getAttributes  -collection:" + JSON.toJSONString(collection));
        return collection;
    }

    private void loadAllUrlResouceAuth() {
        map = new HashMap<>();
        //从数据库查出所有的权限数据集合
        List<Permision> permisionList = permisionDao.findAll();
        ConfigAttribute cfg;
        Collection<ConfigAttribute> array;
        //这里把url作为k,权限名作为v
        for (Permision permision : permisionList) {
            array = new ArrayList<>();
            cfg = new SecurityConfig(permision.getName());
            array.add(cfg);
            map.put(permision.getUrl(), array);
        }
    }

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

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

另一个是MyAccessDecisionManager,咱们要重写它的decide方法,另外两个supports方法,注意把它的return 改为true。在decide方法中,我们需要比对请求路径对应的权限和用户拥有的权限,如果有符合的就代表有权限,没有符合的就代表无权限。

@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
    Log log = LogFactory.getLog(MyAccessDecisionManager.class);
    @Autowired
    MyUserDetailsService myUserDetailsService;

    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        log.info("MyAccessDecisionManager:decide come in");
        //如果请求路径没有匹配到权限表的数据,则直接放行
        if (null == collection || 0 == collection.size()) {
            log.info("MyAccessDecisionManager:decide collection isnull");
            return;
        }
        ConfigAttribute configAttribute;

        //把请求路径匹配到的权限和用户匹配到的权限进行比对,有一种权限就可以放行
        for (Iterator<ConfigAttribute> iterator = collection.iterator(); iterator.hasNext(); ) {
            configAttribute = iterator.next();
            String attribute = configAttribute.getAttribute();
            for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                if (attribute.trim().equals(grantedAuthority.getAuthority())) {
                    log.info("MyAccessDecisionManager:decide 有匹配的权限");
                    return;
                }
            }
        }
        //如果前面都没有拦住,最后直接报错提示,无权限。这里抛异常后,后重新发起错误处理请求,打印下MyAbstractSecurityInterceptor里的日志,会发现进入两次
        //打印下httpRequest.getRequestURL()会发现,它的路径是/error
        log.info("MyAccessDecisionManager:decide no right");
        throw new AccessDeniedException("no right");
    }

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

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

请求报文

security自带的登录接口

请添加图片描述

http://localhost:8080/login?username=account&password=wushichao1

我自己写的controller的地址 /customer/login

请添加图片描述

http://localhost:8080/customer/login

结尾

好啦,以上就是实现权限的全部代码啦,由于里面设计到工具类、dao层查询的类、枚举类等,为了不影响大家的思路,这里没有贴出来,我把代码下载链接放到下面,需要的同学们可以自行去下载,里面的代码都是调试成功的。

下载链接:https://github.com/longqiyuye925/blogServer

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值