java configuration方式

  • 上面是以xml文件的方式配置,接下来介绍以自定义java类的方式来配置spring security
  • 可先在719行:自定义过滤器一节了解认证流程。

    1. 注册DelegatingFilterProxy

    package com.study.config;
    import org.springframework.security.web.context.*;
    
    public class SecurityWebApplicationInitializer
        extends AbstractSecurityWebApplicationInitializer {
    
        @Override
        protected Class<?>[] getRootConfigClasses() {
            return new Class[] { WebSecurityConfig.class };
        }
    
    }
    
  • 继承该类相当于在web.xml中:

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    
  • 重写getRootConfigClasses()相当于把下一步Spring Security的配置加载到程序中。

    2. Spring Security安全配置

    package com.study.config;
    
    import javax.annotation.Resource;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.encoding.Md5PasswordEncoder;
    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.userdetails.UserDetailsService;
    import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
    import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
    
    import com.study.security.MyFilterSecurityInterceptor;
    
    @Configuration
    @EnableWebSecurity //启用web安全功能
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    //  @Resource
    //  private DataSource dataSource;
    
        //配置自定义的用户服务(即选择用户信息的查询方式是从用户库或数据库,怎么查询)
        @Resource(name="myUserDetailService")
        private UserDetailsService myUserDetailService;
    
        //第3点:自定义用户验证
        @Resource
        private AuthenticationProvider myAuthenticationProvider;
    
        //自定义认证成功处理器
        @Resource
        private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
    
        //第4点:自定义拦截器(自定义权限验证,而不是拦截规则)
        @Resource
        private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
    
        /**
         * 密码加密
         */
        @Bean
        public BCryptPasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        /**
        * 自定义认证过程
        */
        @Override
        protected void configure(AuthenticationManagerBuilder auth)
                throws Exception {
    
            /* 不重写的话默认为
            auth
                .inMemoryAuthentication()
                    .withUser("user").password("password").roles("USER"); //默认准备了一个用户
            */
    
            //方式一:内存用户存储
            /*
            auth.inMemoryAuthentication()
            .withUser("user1").password("123456").roles("USER").and()
            .withUser("admin").password("admin").roles("USER","ADMIN");
            */
            //
            //方式二:数据库表认证
            /*
            auth.jdbcAuthentication().dataSource(dataSource)
            .usersByUsernameQuery("select username,password,enable from t_user where username=?")
            .authoritiesByUsernameQuery("select username,rolename from t_role where username=?");
            */
            //方式三:自定义数据库
            auth.userDetailsService(myUserDetailService)
                .passwordEncoder(new Md5PasswordEncoder()); 
                //加后MyAuthenticationProvider里比对密码时能把表单的明文和数据库的密文对应
    
            //自定义用户验证
            auth.authenticationProvider(myAuthenticationProvider);
        }
    
    //  暴露默认的authenticationManager
    //  @Override
    //    public AuthenticationManager authenticationManagerBean() throws Exception {  
    //      return super.authenticationManagerBean();  
    //    }  
    
        /**
        * 配置Spring Security的Filter链
        */
        @Override
        public void configure(WebSecurity web)throws Exception {
            // 设置不拦截规则
             web.ignoring().antMatchers("/css/**","/js/**","/img/**","/font-awesome/**");  
        }
    
        /**
        * 配置如何通过拦截器保护请求:配置了自定义的过滤器、自定义登录登出页面
        * 前端请求的时候先经过这里检测请求是否需要验证或权限,不需要的话直接把请求分发给controller去处理,需要就认证过了再分发
        */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
              http
                .addFilterBefore(myFilterSecurityInterceptor,    
                    FilterSecurityInterceptor.class)//在正确的位置添加我们自定义的过滤器  
                .authorizeRequests() //需要权限,权限符合与否由上面过滤器检查
                .antMatchers("/admin/**").hasRole("ADMIN") //以admin开头的url需要admin权限
                .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
                .anyRequest().authenticated() //不与以上匹配的所有请求只需要登录验证
    //         .and().formLogin().and() //and相当于节点结束
    //          .httpBasic();
            // 自定义登录页面:permitAll,允许all用户访问login页面
            //未登录前访问受保护资源被转回login
                .and()
                /*直接/jsp/login的话会分配给controller去处理,则需要写controller映射到登录页;请求login.jsp报404
                有后缀则直接请求资源,那么url=/login还是由controller处理;
                那么最好是加后缀,才不用再走controller*/
                .formLogin().loginPage("/jsp/login.jsp") 
                .failureUrl("/jsp/login.jsp?error=1")
                //表单提交地址,4.x默认为/login;无需认证,但是请求方式必须post
                .loginProcessingUrl("/spring_security_check") 
                .usernameParameter("username")
                .passwordParameter("password")
                .successHandler(myAuthenticationSuccessHandler) //自定义认证成功处理器
                //.failureHandler(myAuthenticationFailureHandler)
                .permitAll()
                //这里也是,要么配资源路径,要么配有controller处理的url
                .defaultSuccessUrl("/index.do")
                .and()
                //配置session管理
                .sessionManagement()
                /*指定了maximumSessions需配合web.xml中HttpSessionEventPublisher
                清理掉过期session*/
                .maximumSessions(1) 
                .expiredUrl("/login.jsp?expired=1");
    
              //如果(默认)开启了CSRF,退出则需要使用POST访问,可使用以下方式解决,但不推荐
              http.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))  
              //登陆成功后跳转的地址,以及删除的cookie名称  
              .logoutSuccessUrl("/jsp/login.jsp?error=logout")  
             .invalidateHttpSession(true);  
        }
    
    }
    
  • 接收到请求,先在spring security中是否有符合的(/login、/logout、/spring_security_check、/**/favicon.ico、…),出现Checking match of request : ‘/login’; against ‘/logout’或Request ‘GET /login’ doesn’t match ‘POST /spring_security_check都表示当前请求与已存在url不匹配,后者指明spring_security_check必须是post形式的,若不匹配,则检测用户是否已认证,未认证,后台报AccessDenyException,前端转回login页面,已认证,分发给controller处理。所以用户请求的资源是他不能访问的就会报AccessDenyException。
  • 未登录时用户为匿名用户
  • 登录后若未给用户设置权限,则用户为无权限
  • 所以给请求设置权限,如果没有指定权限,则至少不能是匿名用户
  • 大致相当于配置

    <!-- 获取访问url对应的所有权限 -->
    <beans:bean id="secureResourceFilterInvocationDefinitionSource" 
        class="com.ultrapower.me.util.security.interceptor.SecureResourceFilterInvocationDefinitionSource" />
    <!-- 校验用户的权限是否足够 -->
    <beans:bean id="mesecurityAccessDecisionManager" 
        class="com.ultrapower.me.util.security.interceptor.SecurityAccessDecisionManager" />
    
    <!-- 自定义权限认证 -->
    <beans:bean id="securityInterceptor" 
     class="com.ultrapower.me.util.security.interceptor.SecurityInterceptor">
        <beans:property name="authenticationManager" ref="authenticationManager"/>
        <beans:property name="accessDecisionManager" ref="mesecurityAccessDecisionManager"/>
        <beans:property name="securityMetadataSource" 
            ref="secureResourceFilterInvocationDefinitionSource" />
    </beans:bean>
    
    <!-- 用户服务 -->
    <beans:bean id="myUserDetailsService" 
        class="com.ultrapower.me.util.security.support.myUserDetailsService"></beans:bean>
    
    <!-- 自定义用户验证-->
    <beans:bean id="myAuthenticationProvider"
        class="com.security.MyAuthenticationProvider">
        <beans:property name="userDetailsService" ref="myUserDetailsService" />
    </beans:bean>
    
    <!-- 1、自定义权限认证:依赖上面的bean -->
    <http auto-config="true"  use-expressions="false">      
        <intercept-url pattern="/login.jsp*" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
        <intercept-url pattern="/**" access="ROLE_USER"/>
        <form-login login-page="/login.jsp" default-target-url="/jsp/index/main.jsp" 
            authentication-failure-url="/login.jsp?error=true"/>
        <custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="securityInterceptor"/>
    </http>
    
    <!-- 2、自定义用户验证:依赖上面的bean-->
    <authentication-manager alias="authenticationManager"> //起别名,方便被下面自定义拦截器依赖
        <!--自定义用户验证-->
        <authentication-provider ref="myAuthenticationProvider" />
        <!--只自定义用户服务而不需要自定义用户验证的话
        <authentication-provider user-service-ref="myUserDetailsService">-->
            <password-encoder ref="myPasswordEncoder"/>
        </authentication-provider>
    </authentication-manager>
    
一些自定义方式
  • 简单的自定义登录页面对比

    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .and()
            .httpBasic();
    }
    
  • 相当于

    <http>
        <intercept-url pattern="/**" access="authenticated"/>
        <form-login />
        <http-basic />
    </http>
    
  • 自定义权限

    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests() //需要权限                         
                .antMatchers("/resources/**", "/signup", "/about").permitAll()                  
                .antMatchers("/admin/**").hasRole("ADMIN") 
                //使用hasRole()可以不写ROLE_前缀,但实际上我们指定的权限还是ROLE_ADMIN     
                .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
                /*按照顺序匹配,下面是对的*/
                //.antMatchers("/admin/**").hasRole("ADMIN")
                //.antMatchers("/**").hasRole("USER")
                /*下面是错的,因为满足第一个规则后将不会检查第二条,所以要从精确到广泛配置*/
                //.antMatchers("/**").hasRole("USER")
                //.antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated() //需要验证                                    
                .and()
            // ...
            .formLogin();
    }
    
  • 自定义登出页面

    protected void configure(HttpSecurity http) throws Exception {
        http
            .logout()
                .logoutUrl("/my/logout")
                .logoutSuccessUrl("/my/index") 
                .logoutSuccessHandler(logoutSuccessHandler) //自定义登出成功后的操作,上一行失效
                .invalidateHttpSession(true)
                //添加一个自定义登出处理器,SecurityContextLogoutHandler默认是最后一个处理器
                .addLogoutHandler(logoutHandler) 
                .deleteCookies(cookieNamesToClear)
                .and()
            ...
    }
    
  • 一般前面两步都是直接用配置文件实现,然后注入自定义UserDetailServiceImpl,也就这个要用java写而已

    自定义用户服务

  • 如果是要用数据库的话那肯定是要的,首先是用户实体类UserDetails必须改为符合数据库的字段,默认的也就用户名、密码加权限列表了,其次获取用户信息UserDetialsService的方式也要改成从数据库获取.
  • 首先是UserDetailsService,主要是根据用户名从数据库找出用户all信息,主要是用户名、密码和权限列表

    package com.study.security;
    
    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;
    
    import javax.annotation.Resource;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    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.Component;
    
    import com.study.model.Resources;
    import com.study.model.User;
    import com.study.service.ResourcesService;
    import com.study.service.UserService;
    
    @Component("myUserDetailService")
    public class MyUserDetailServiceImpl implements UserDetailsService{
    
        //这里可以来个日志
    
        @Resource
        private UserService userService;
    
        @Resource
        private ResourcesService resourcesService;
    
        @Override
        public UserDetails loadUserByUsername(String username)
                throws UsernameNotFoundException {
            //通过用户名查询用户信息
            User user = userService.findUserByName(username); 
            //系统实体类User implement UsersService,系统service类userService
            if(user ==null)
                throw new UsernameNotFoundException(username+" not exist!");  
            //存放角色名的集合
            Set<GrantedAuthority> authSet = new HashSet<GrantedAuthority>();
            //系统实体类Resources,即各个url
            Resources resources = new Resources();
            resources.setUsername(username);
            //找到特定用户名能访问的url集合
            List<Resources> list = resourcesService.loadMenu(resources);
            for (Resources r : list) {
                //根据资源找到角色名,添加到角色名集合
                authSet.add(new SimpleGrantedAuthority("ROLE_" +r.getResKey()));
                //resKey:资源名,拼成角色名
            }
            /*
            username():登录用户名
            password():从数据库查出来的密码
            enable: 用户状态,true为有效,false无效
            accoutNonExpired,credentialsNonExpired,accountNonLocked
            roles: Collections集合,当前登录用户的角色列表
            */
            return new org.springframework.security.core.userdetails.User(user.getUsername(), 
                    user.getPassword(), 
                    user.getEnable()==1?true:false, 
                    true, 
                    true,
                    true,
                    authSet); //返回的user不为空,则验证通过
        }
    
    }
    
  • 系统实体类User实现UserDetails,主要是查询数据库所有权限,返回权限列表

    public class User implements UserDetails {
    
        private static final long serialVersionUID = 8026813053768023527L;
    
        private String name;
        private String password;
        private Set<Role> roles; //可以不写
        //其他属性,随便,自定义
    
        /**
        * 返回系统所有角色
        */
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            //根据自定义逻辑来返回用户权限,如果用户权限返回空或者和拦截路径对应权限不同,验证不通过
            if(!roles.isEmpty()){
                List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();
                GrantedAuthority au = new SimpleGrantedAuthority(role.getrName());
                list.add(au);
                return list;
            }
            return null;
        }
    
        /**
        * 默认有一个方法返回当前用户的角色列表,后面介绍了它的重写;可选的
        */
    
        @Override
        public String getPassword() {
            return this.password;
        }
        @Override
        public String getUsername() {
            return this.username;
        }
    
        /* 
         *帐号是否不过期,false则验证不通过
         */
         @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        /* 
         * 帐号是否不锁定,false则验证不通过
         */
         @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        /* 
         * 凭证是否不过期,false则验证不通过
         */
         @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        /* 
         * 该帐号是否启用,false则验证不通过
         */
         @Override
        public boolean isEnabled() {
            return true;
        }
    }
    
  • User类代码实例:llcweb/domain/models/
  • 数据库表介绍:五张表————用户表、角色表、资源表、用户角色表、角色资源表。给用户分配角色,给角色分配资源(即权限)。一个用户可以对应多个角色,一个角色可以对应多个资源。
  • 其中资源表t_resources包含了后台系统的所有url
  • resKey即角色名

自定义过滤器

  • 首先整理spring security的步骤:
  1. 在spring security中每一个url都是一个资源,系统启动时,spring security第一个拦截器FilterSecurityInterceptor调用SecurityMetadataSource将所有url与其权限对应。
  2. 当用户发起请求时,spring security检查该请求是否需要特定权限(需不需要都在WebSecurityConfigurerAdapter中指定了),不需要的话直接访问,连是否登录都不验证。
  3. 需要权限,AbstractAuthenticationProcessingFilter先验证当前用户是否已登录,否则跳转登录页面。
  4. 已登录,FilterSecurityInterceptor判断该用户是否拥有当前请求所需的权限,有则允许访问,没有则给出提示信息。
  • 我们这一节说的过滤器特指这个FilterSecurityInterceptor。基于数据库的权限验证必须要自定义过滤器,因为它主要就是做权限验证的,包括资源与权限对应关系的查询、当前用户是否持有请求资源所需权限的判断。
  • 前面不是用java注册spring security的话就需要以下配置

    <!--还要在http中添加<custom-filter before="FILTER_SECURITY_INTERCEPTOR" 
    ref="securityInterceptor"/>-->
    
    <!--ProviderManager类的实例注入authenticationManager-->
    <authentication-manager alias="authenticationManager">
    <!-- 获取访问url对应的所有权限 -->
    <beans:bean id="secureResourceFilterInvocationDefinitionSource" 
        class="com.ultrapower.me.util.security.interceptor.SecureResourceFilterInvocationDefinitionSource" />
    <!-- 校验用户的权限是否足够 -->
    <beans:bean id="mesecurityAccessDecisionManager" 
        class="com.ultrapower.me.util.security.interceptor.SecurityAccessDecisionManager" />
    
    <!-- 自定义拦截器:依赖上面的bean -->
    <beans:bean id="securityInterceptor" 
     class="com.ultrapower.me.util.security.interceptor.SecurityInterceptor">
        <beans:property name="authenticationManager" ref="authenticationManager"/>
        <beans:property name="accessDecisionManager" ref="mesecurityAccessDecisionManager"/>
        <beans:property name="securityMetadataSource" 
            ref="secureResourceFilterInvocationDefinitionSource" />
    </beans:bean>
    
  • 开始自定义过滤器

    package com.study.security;
    
    import java.io.IOException;
    
    import javax.annotation.PostConstruct;
    import javax.annotation.Resource;
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.access.SecurityMetadataSource;
    import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
    import org.springframework.security.access.intercept.InterceptorStatusToken;
    import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
    import org.springframework.security.web.FilterInvocation;
    import org.springframework.stereotype.Component;
    
    @Component
    public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements 
        Filter {
        //所有资源与权限的关系(需要在外面自定义):获取请求资源的权限
        @Autowired
        private MySecurityMetadataSource securityMetadataSource;
    
        //用户是否拥有所请求资源的权限(需要在外面自定义):判断是否拥有请求资源所需的权限
        @Autowired
        private MyAccessDecisionManager accessDecisionManager;
    
        /* 默认装配够用了
        @Resource(name="myAuthenticationManager")
        private AuthenticationManager authenticationManager;  
        */
    
        @Resource
        private AuthenticationConfiguration authenticationConfiguration;
    
    //  @Autowired
    //  public void setAuthenticationConfiguration(AuthenticationConfiguration authenticationConfiguration) {
    //       this.authenticationConfiguration = authenticationConfiguration;
    //  }
    
        @PostConstruct
        public void init() throws Exception{
            super.setAccessDecisionManager(accessDecisionManager);
            super.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());   
        }
    
        @Override
        public SecurityMetadataSource obtainSecurityMetadataSource() {
            return this.securityMetadataSource;
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response,
                FilterChain chain) throws IOException, ServletException {
            FilterInvocation fi = new FilterInvocation(request, response, chain);
    
            /*beforeInvocation做的事*/
            //super.beforeInvocation(fi);源码
            //获取请求资源的权限
            //执行Collection<ConfigAttribute> attributes =
                SecurityMetadataSource.getAttributes(object); //object为FilterInvocation对象
            //是否拥有权限;authentication:封装了用户拥有的权限
            //this.accessDecisionManager.decide(authenticated, object, attributes);
            InterceptorStatusToken token = super.beforeInvocation(fi);
    
            try {
                //执行下一个拦截器
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            } finally {
                super.afterInvocation(token, null);
            }
        }
    
        @Override
        public void init(FilterConfig arg0) throws ServletException {
        }
    
        @Override
        public void destroy() {
    
        }
    
        @Override
        public Class<? extends Object> getSecureObjectClass() {
            //下面的MyAccessDecisionManager的supports方法必须返回true,否则会提醒类型错误
            return FilterInvocation.class;
        }
    }
    
  • 自定义过滤器需要用到的用户是否拥有所请求资源的权限:MyAccessDecisionManager。默认使用AbstractAccessDecisionManager,具体是继承了AbstractAccessDecisionManager的AffirmativeBased,还有其他其他的决策管理器,不过不知道怎么启用,只是投票方式不同而已,有的是只要有赞成票就通过权限验证,有的是只要有反对票就不通过。但是看不懂底层是根据什么投赞成票和反对票的,反正不通过就抛AccessDenyException,一层层向上抛出,由ExceptionTranslationFilter打印异常日志。

    package com.study.security;
    
    import java.util.Collection;
    import java.util.Iterator;
    
    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;
    
    @Component
    public class MyAccessDecisionManager implements AccessDecisionManager {
    
        /**
        * 决定authentication拥有的权限中有没有指定资源的权限
        * object:受保护对象,其可以是一个MethodInvocation、JoinPoint或FilterInvocation
        * authentication:封装了用户拥有的权限
        * configAttributes:受保护对象的相关配置属性,主要是访问资源需要的权限
        */
        public void decide(Authentication authentication, Object object,
            Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
    
            if(configAttributes == null) {
                return;
            }
    
            //所请求的资源需要的权限(一个资源对多个权限)
            Iterator<ConfigAttribute> iterator = configAttributes.iterator();
            while(iterator.hasNext()) {
                ConfigAttribute configAttribute = iterator.next();
                //访问所请求资源所需要的权限
                String Permission = configAttribute.getAttribute();
                System.out.println("needPermission is " + Permission);
                //用户所拥有的权限authentication
                for(GrantedAuthority ga : authentication.getAuthorities()) {
                    if(Permission.equals(ga.getAuthority())) {
                        return;
                    }
                }
            }
    
            //没有权限,后台抛异常而前端显示access-denied-page页面
            throw new AccessDeniedException(" 没有权限访问或未重新登录! ");
        }
    
         /**
         * 判断AccessDecisionManager是否能够处理对应的ConfigAttribute
         */
        public boolean supports(ConfigAttribute attribute) {
            // TODO Auto-generated method stub
            return true;
        }
    
        /**
         * 判断AccessDecisionManager是否支持对应的受保护对象类型
         */
        public boolean supports(Class<?> clazz) {
            // TODO Auto-generated method stub
            return true;
        }
    
    }
    
  • 自定义过滤器需要用到的获取资源权限(即查询t_resources表中所有resKey与resUrl的关系):MySecurityMetadataSource

    package com.study.security;
    
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import javax.annotation.PostConstruct;
    import javax.annotation.Resource;
    
    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 com.study.dao.ResourcesDao;
    import com.study.model.Resources;
    
    @Component
    public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    
        @Resource
        private ResourcesDao resourcesDao;
    
        private static Map<String, Collection<ConfigAttribute>> resourceMap = null;
    
        public Collection<ConfigAttribute> getAllConfigAttributes() {
            return null;
        }
    
        public boolean supports(Class<?> clazz) {
            return true;
        }
    
        /**
         * @PostConstruct是Java EE 5引入的注解,
         * Spring允许开发者在受管Bean中使用它。当DI容器实例化当前受管Bean时,
         * @PostConstruct注解的方法会被自动触发,从而完成一些初始化工作
         */
        @PostConstruct
        private void loadResourceDefine() { //加载所有资源与权限的关系
            if (resourceMap == null) {
                resourceMap = new HashMap<String, Collection<ConfigAttribute>>();
                //数据库查询所有资源
                List<Resources> list = resourcesDao.queryAll(new Resources());
                //Resources:本项目自定义的实体
                for (Resources resources : list) {
                    Collection<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();
                    // 通过资源名称来表示具体的权限 注意:必须"ROLE_"开头
                    /*SecurityConfig:ConfigAttribute实现类;
                    只有一个成员,是个常量,构造函数传入string赋予此常量,
                    configAttribute.getAttribute()返回此常量*/
                    ConfigAttribute configAttribute = new SecurityConfig("ROLE_" + resources.getResKey());
                    configAttributes.add(configAttribute);
                    resourceMap.put(resources.getResUrl(), configAttributes);
                }
            }
        }
    
        //返回所请求资源所需要的权限
        public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
            //从过滤链中获取当前请求
            String requestUrl = ((FilterInvocation) object).getRequestUrl();
            //如果资源角色库为空的话说明还未初始化
            if(resourceMap == null) {
                loadResourceDefine(); //强制初始化
            }
            //System.err.println("resourceMap.get(requestUrl); "+resourceMap.get(requestUrl));
            //如果是get请求的话去掉后面参数
            if(requestUrl.indexOf("?")>-1){
                requestUrl=requestUrl.substring(0,requestUrl.indexOf("?"));
            }
            //获取该请求对应权限,url作为key
            Collection<ConfigAttribute> configAttributes = resourceMap.get(requestUrl);
            return configAttributes;
        }
    }
    
  • ExceptionTranslationFilter处理AuthenticationException和AccessDeniedException两种异常。

    private void handleSpringSecurityException(HttpServletRequest request,     
        HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException {
    
        //如果异常是AuthenticationException(未登录即访问受保护资源)
        if (exception instanceof AuthenticationException) { 
            //打印异常
            this.logger.debug("Authentication exception occurred; 
                redirecting to authentication entry point", exception);
            /*传入此方法后主要是调用AuthenticationEntryPoint的默认实现类
            LoginUrlAuthenticationEntryPoint中的commence方法重定向到登录页*/
            this.sendStartAuthentication(request, response, chain, 
                (AuthenticationException)exception);
            /* sendStartAuthentication源码
            protected void sendStartAuthentication(HttpServletRequest request, 
                    HttpServletResponse response, FilterChain chain, 
                    AuthenticationException reason) throws ServletException, IOException {
                SecurityContextHolder.getContext().setAuthentication((Authentication)null);
                this.requestCache.saveRequest(request, response);
                this.logger.debug("Calling Authentication entry point.");
                this.authenticationEntryPoint.commence(request, response, reason);
            }
            */
        } 
    
        //如果是AccessDeniedException
        else if (exception instanceof AccessDeniedException) {
    
            //且是匿名用户(未登录即访问受保护资源;一般是该异常,少为AuthenticationException)
            if (this.authenticationTrustResolver.isAnonymous(SecurityContextHolder
                    .getContext().getAuthentication())) {
                this.logger.debug("Access is denied (user is anonymous); 
                    redirecting to authentication entry point", exception);
                //重定向到登录页,且把AccessDeniedException包装成AuthenticationException
                this.sendStartAuthentication(request, response, chain, 
                    new InsufficientAuthenticationException("Full authentication is required 
                        to access this resource"));
            } 
            //非匿名用户(已登录但权限不足)
            else {
                this.logger.debug("Access is denied (user is not anonymous); 
                    delegating to AccessDeniedHandler", exception);
                //重定向到403页面;AccessDeniedHandler默认实现类:AccessDeniedHandlerImpl
                this.accessDeniedHandler.handle(request, response
                    ,(AccessDeniedException)exception);
            }
        }
    
    }
    

    参考文章

  • SSM整合SpringSecurity实现权限管理实例 javaconfig配置方式:做到了完全不用web.xml等任何配置文件,连bean的装载都全靠java类和注解完成。
  • spring官方手册(从第6点看起)
  • 在springboot中整合spring security并自定义验证代码
  • SpringMVC + security模块 框架整合详解
  • SpringSecurity——基于Spring、SpringMVC和MyBatis自定义SpringSecurity权限认证规则
  • 自定义过滤器中有关AccessDecisionManager决策管理器的部分
  • 有关ExceptionTranslationFilter

    代码实例

  • thz-manager-web/config