Spring-Security教程五

9次阅读

共计 15371 个字符,预计需要花费 39 分钟才能阅读完成。

在之前的几篇 security 教程中,资源和所对应的权限都是在 xml 中进行配置的,也就在 http 标签中配置 intercept-url,试想要是配置的对象不多,那还好,但是平常实际开发中都往往是非常多的资源和权限对应,而且写在配置文件里面写改起来还得该源码配置文件,这显然是不好的。因此接下来,将用数据库管理资源和权限的对应关系。数据库还是接着之前的,用 mysql 数据库,因此也不用另外引入额外的 jar 包。

数据库表的设计

数据库要提供给 security 的数据无非就是,资源(说的通俗点就是范围资源地址)和对应的权限,这里就有两张表,但是因为他们俩是多对多的关系,因此还要设计一张让这两张表关联起来的表,除此之外,还有一张用户表,有因为用户和角色也是多对多的关系,还要额外加一张用户和角色关联的表。这样总共下来就是五张表。下面就是对应的模型图:

建表和添加数据的 sql 语句:

DROP TABLE IF EXISTS `resc`;
CREATE TABLE `resc` (`id` bigint(20) NOT NULL DEFAULT '0',
  `name` varchar(50) DEFAULT NULL,
  `res_type` varchar(50) DEFAULT NULL,
  `res_string` varchar(200) DEFAULT NULL,
  `descn` varchar(200) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-- ----------------------------
-- Records of resc
-- ----------------------------
INSERT INTO `resc` VALUES ('1', '','URL','/adminPage.jsp',' 管理员页面 ');
INSERT INTO `resc` VALUES ('2', '','URL','/index.jsp','');
INSERT INTO `resc` VALUES ('3', null, 'URL', '/test.jsp', '测试页面');
 
-- ----------------------------
-- Table structure for resc_role
-- ----------------------------
DROP TABLE IF EXISTS `resc_role`;
CREATE TABLE `resc_role` (`resc_id` bigint(20) NOT NULL DEFAULT '0',
  `role_id` bigint(20) NOT NULL DEFAULT '0',
  PRIMARY KEY (`resc_id`,`role_id`),
  KEY `fk_resc_role_role` (`role_id`),
  CONSTRAINT `fk_resc_role_role` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`),
  CONSTRAINT `fk_resc_role_resc` FOREIGN KEY (`resc_id`) REFERENCES `resc` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-- ----------------------------
-- Records of resc_role
-- ----------------------------
INSERT INTO `resc_role` VALUES ('1', '1');
INSERT INTO `resc_role` VALUES ('2', '1');
INSERT INTO `resc_role` VALUES ('2', '2');
INSERT INTO `resc_role` VALUES ('3', '3');
 
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (`id` bigint(20) NOT NULL DEFAULT '0',
  `name` varchar(50) DEFAULT NULL,
  `descn` varchar(200) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'ROLE_ADMIN', '管理员角色');
INSERT INTO `role` VALUES ('2', 'ROLE_USER', '用户角色');
INSERT INTO `role` VALUES ('3', 'ROLE_TEST', '测试角色');
 
-- ----------------------------
-- Table structure for t_c3p0
-- ----------------------------
DROP TABLE IF EXISTS `t_c3p0`;
CREATE TABLE `t_c3p0` (`a` char(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-- ----------------------------
-- Records of t_c3p0
-- ----------------------------
 
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (`id` bigint(20) NOT NULL DEFAULT '0',
  `username` varchar(50) DEFAULT NULL,
  `password` varchar(50) DEFAULT NULL,
  `status` int(11) DEFAULT NULL,
  `descn` varchar(200) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'admin', 'admin', '1', '管理员');
INSERT INTO `user` VALUES ('2', 'user', 'user', '1', '用户');
INSERT INTO `user` VALUES ('3', 'test', 'test', '1', '测试');
 
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (`user_id` bigint(20) NOT NULL DEFAULT '0',
  `role_id` bigint(20) NOT NULL DEFAULT '0',
  PRIMARY KEY (`user_id`,`role_id`),
  KEY `fk_user_role_role` (`role_id`),
  CONSTRAINT `fk_user_role_role` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`),
  CONSTRAINT `fk_user_role_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('1', '1');
INSERT INTO `user_role` VALUES ('1', '2');
INSERT INTO `user_role` VALUES ('2', '2');
INSERT INTO `user_role` VALUES ('3', '3');

user 表中包含用户登陆信息,role 角色表中包含授权信息,resc 资源表中包含需要保护的资源。

实现从数据库中读取资源信息

Spring Security 需要的数据无非就是 pattern 和 access 类似键值对的数据,就像配置文件中写的那样:

<intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" />1
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
<intercept-url pattern="/**" access="ROLE_USER" />

其实当项目启动时,Spring Security 所做的就是在系统初始化时,将以上 XML 中的信息转换为特定的数据格式,而框架中其他组件可以利用这些特定格式的数据,用于控制之后的验证操作。现在我们将这些信息存储在数据库中,因此就要想办法从数据库中查询这些数据,所以根据 security 数据的需要,只需要如下 sql 语句就可以:

select re.res_string,r.name from role r,resc re,resc_role rr where 
        r.id=rr.role_id and re.id=rr.resc_id

在数据中执行这条语句做测试,得到如下结果:

这样的格式正是 security 所需要的数据。

构建一个数据库的操作的类

虽然上述的数据符合 security 的需要,但是 security 将这种数据类型进行了封装,把它封装成 Map<RequestMatcher, Collection<ConfigAttribute>> 这样的类型,其中 RequestMatcher 接口就是我们数据库中的 res_string , 其实现类为 AntPathRequestMatcher,构建一个这样的对象只要在 new 的时候传入 res_string 就可以了,Collection<ConfigAttribute> 这里对象构建起来就也是类似的,构建一个 ConfigAttribute 对象只需要在其实现类 SecurityConfig 创建的时候传入角色的名字就可以。代码如下:

/**
 * @classname JdbcRequestMapBulider
 * @author ZMC
 * @time 2017-1-10
 * 查询资源和角色,并构建 RequestMap
 */
public class JdbcRequestMapBulider
    extends JdbcDaoSupport{
    // 查询资源和权限关系的 sql 语句
    private String resourceQuery = "";
    
    public String getResourceQuery() {return resourceQuery;}
 
    // 查询资源
    public List<Resource> findResources() {ResourceMapping resourceMapping = new ResourceMapping(getDataSource(),
                resourceQuery);
        return resourceMapping.execute();}
    
    // 拼接 RequestMap
    public LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> buildRequestMap() {LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<>();
        
        List<Resource> resourceList = this.findResources();
        for (Resource resource : resourceList) {RequestMatcher requestMatcher = this.getRequestMatcher(resource.getUrl());
            List<ConfigAttribute> list = new ArrayList<ConfigAttribute>();
            list.add(new SecurityConfig(resource.getRole()));
            requestMap.put(requestMatcher, list);
        }
        return requestMap;
    }
    // 通过一个字符串地址构建一个 AntPathRequestMatcher 对象
    protected RequestMatcher getRequestMatcher(String url) {return new AntPathRequestMatcher(url);
    }
 
    public void setResourceQuery(String resourceQuery) {this.resourceQuery = resourceQuery;}
    
    /**
     * @classname Resource
     * @author ZMC
     * @time 2017-1-10
     * 资源内部类
     */
    private class Resource {
        private String url;// 资源访问的地址
        private String role;// 所需要的权限
 
        public Resource(String url, String role) {
            this.url = url;
            this.role = role;
        }
 
        public String getUrl() {return url;}
 
        public String getRole() {return role;}
    }
    
    private class ResourceMapping extends MappingSqlQuery {
        protected ResourceMapping(DataSource dataSource,
            String resourceQuery) {super(dataSource, resourceQuery);
            compile();}
        // 对结果集进行封装处理
        protected Object mapRow(ResultSet rs, int rownum)
            throws SQLException {String url = rs.getString(1);
            String role = rs.getString(2);
            Resource resource = new Resource(url, role);
            return resource;
        }
    }
}

说明:
resourceQuery 是查询数据的 sql 语句,该属性在配置 bean 的时候传入即可。

内部创建了一个 resource 来封装数据。

getRequestMatcher 方法就是用来创建 RequestMatcher 对象的

buildRequestMap 方法用来最后拼接成 LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> 共 security 使用。

替换原有功能的切入点

在将这部之前,先得了解大概下 security 的运行过程,security 实现控制的功能其实就是通过一系列的拦截器来实现的,当用户登陆的时候,会被 AuthenticationProcessingFilter 拦截,调用 AuthenticationManager 的实现类,同时 AuthenticationManager 会调用 ProviderManager 来获取用户验证信息,其中不同的 Provider 调用的服务不同,因为这些信息可以是在数据库上,可以是在 LDAP 服务器上,可以是 xml 配置文件上等,这个例子中就是为数据库;如果验证通过后会将用户的权限信息放到 spring 的全局缓存 SecurityContextHolder 中,以备后面访问资源时使用。当访问资源,访问 url 时,会通过 AbstractSecurityInterceptor 拦截器拦截,其中会调用 FilterInvocationSecurityMetadataSource 的方法来获取被拦截 url 所需的全部权限,其中 FilterInvocationSecurityMetadataSource 的常用的实现类为 DefaultFilterInvocationSecurityMetadataSource,这个类中有个很关键的东西就是 requestMap,也就是我们上面所得到的数据,在调用授权管理器 AccessDecisionManager,这个授权管理器会通过 spring 的全局缓存 SecurityContextHolder 获取用户的权限信息,还会获取被拦截的 url 和被拦截 url 所需的全部权限,然后根据所配的策略,如果权限足够,则返回,权限不够则报错并调用权限不足页面。

根据源码 debug 跟踪得出,其实资源权限关系就放在 DefaultFilterInvocationSecurityMetadataSource 的 requestMap,中的,这个 requestMap 就是我们 JdbcRequestMapBulider.buildRequestMap() 方法所需要的数据类型,因此,顺气自然就想到了我们自定义一个类继承 FilterInvocationSecurityMetadataSource 接口,将数据查出的数据放到 requestMap 中去。制定类 MyFilterInvocationSecurityMetadataSource 继承 FilterInvocationSecurityMetadataSource 和 InitializingBean 接口。

/**
 * @classname MyFilterInvocationSecurityMetadataSource
 * @author ZMC
 * @time 2017-1-10
 */
public class MyFilterInvocationSecurityMetadataSource implements
        FilterInvocationSecurityMetadataSource, InitializingBean {
 
    private final static List<ConfigAttribute> NULL_CONFIG_ATTRIBUTE = null;
    // 资源权限集合
    private Map<RequestMatcher, Collection<ConfigAttribute>> requestMap;
    
    // 查找数据库权限和资源关系
    private JdbcRequestMapBulider builder;
    
    /*
     * (non-Javadoc)
     * @see
     * org.springframework.security.access.SecurityMetadataSource#getAttributes
     * (java.lang.Object)
     * 更具访问资源的地址查找所需要的权限
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object)
            throws IllegalArgumentException {final HttpServletRequest request = ((FilterInvocation) object)
                .getRequest();
 
        Collection<ConfigAttribute> attrs = NULL_CONFIG_ATTRIBUTE;
        for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
                .entrySet()) {if (entry.getKey().matches(request)) {attrs = entry.getValue();
                break;
            }
        }
        return attrs;
    }
 
    /*
     * (non-Javadoc)
     * 
     * @see org.springframework.security.access.SecurityMetadataSource#
     * getAllConfigAttributes()
     * 获取所有的权限
     */
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {Set<ConfigAttribute> allAttributes = new HashSet<ConfigAttribute>();
        for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
                .entrySet()) {allAttributes.addAll(entry.getValue());
        }
        System.out.println("总共有这些权限:"+allAttributes.toString());
        return allAttributes;
    }
    /*
     * (non-Javadoc)
     * 
     * @see
     * org.springframework.security.access.SecurityMetadataSource#supports(java
     * .lang.Class)
     */
    @Override
    public boolean supports(Class<?> clazz) {return FilterInvocation.class.isAssignableFrom(clazz);
    }
    // 绑定 requestMap
    protected Map<RequestMatcher, Collection<ConfigAttribute>> bindRequestMap() {return builder.buildRequestMap();
    }
 
    /*
     * (non-Javadoc)
     * 
     * @see
     * org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
     */
    @Override
    public void afterPropertiesSet() throws Exception {this.requestMap = this.bindRequestMap();
    }
 
    public void refreshResuorceMap() {this.requestMap = this.bindRequestMap();
    }
 
    //get 方法
    public JdbcRequestMapBulider getBuilder() {return builder;}
    
    //set 方法
    public void setBuilder(JdbcRequestMapBulider builder) {this.builder = builder;}
 
}

说明:
requestMap 这个属性就是用来存放资源权限的集合

builder 为 JdbcRequestMapBulider 类型,用来查找数据库权限和资源关系

其他的代码中都有详细的注释

配置

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                        http://www.springframework.org/schema/context
                        http://www.springframework.org/schema/context/spring-context-3.1.xsd
                        http://www.springframework.org/schema/tx
                        http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
                        http://www.springframework.org/schema/security
                        http://www.springframework.org/schema/security/spring-security.xsd">
    
    <http pattern="/login.jsp" security="none"></http>
    <http auto-config="false">
        <form-login login-page="/login.jsp" default-target-url="/index.jsp"
            authentication-failure-url="/login.jsp?error=true" />
        <logout invalidate-session="true" logout-success-url="/login.jsp"
            logout-url="/j_spring_security_logout" />
        <!-- 通过配置 custom-filter 来增加过滤器,before="FILTER_SECURITY_INTERCEPTOR" 表示在 SpringSecurity 默认的过滤器之前执行。-->
        <custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR" />
    </http>
    
    <!-- 数据源 -->
    <beans:bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
        destroy-method="close">
        <!-- 此为 c3p0 在 spring 中直接配置 datasource c3p0 是一个开源的 JDBC 连接池 -->
        <beans:property name="driverClass" value="com.mysql.jdbc.Driver" />
        <beans:property name="jdbcUrl"
            value="jdbc:mysql://localhost:3306/springsecuritydemo?useUnicode=true&characterEncoding=UTF-8" />
        <beans:property name="user" value="root" />
        <beans:property name="password" value="" />
        <beans:property name="maxPoolSize" value="50"></beans:property>
        <beans:property name="minPoolSize" value="10"></beans:property>
        <beans:property name="initialPoolSize" value="10"></beans:property>
        <beans:property name="maxIdleTime" value="25000"></beans:property>
        <beans:property name="acquireIncrement" value="1"></beans:property>
        <beans:property name="acquireRetryAttempts" value="30"></beans:property>
        <beans:property name="acquireRetryDelay" value="1000"></beans:property>
        <beans:property name="testConnectionOnCheckin" value="true"></beans:property>
        <beans:property name="idleConnectionTestPeriod" value="18000"></beans:property>
        <beans:property name="checkoutTimeout" value="5000"></beans:property>
        <beans:property name="automaticTestTable" value="t_c3p0"></beans:property>
    </beans:bean>
    
 
 
    <beans:bean id="builder" class="com.zmc.demo.JdbcRequestMapBulider"> 
        <beans:property name="dataSource" ref="dataSource" /> 
        <beans:property name="resourceQuery"
        value="select re.res_string,r.name from role r,resc re,resc_role rr where 
        r.id=rr.role_id and re.id=rr.resc_id" /> 
    </beans:bean>
 
    
    <!-- 认证过滤器 -->
    <beans:bean id="filterSecurityInterceptor"
        class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
        <!-- 用户拥有的权限 -->
        <beans:property name="accessDecisionManager" ref="accessDecisionManager" />
        <!-- 用户是否拥有所请求资源的权限 -->
        <beans:property name="authenticationManager" ref="authenticationManager" />
        <!-- 资源与权限对应关系 -->
        <beans:property name="securityMetadataSource" ref="securityMetadataSource" />
    </beans:bean>
    
    <!-- 授权管理器 -->
    <beans:bean class="com.zmc.demo.MyAccessDecisionManager" id="accessDecisionManager">
    </beans:bean>
    <!-- 认证管理 -->
    <authentication-manager alias="authenticationManager">
        <authentication-provider>
            <jdbc-user-service data-source-ref="dataSource" id="usersService"
                users-by-username-query="select username,password,status as enabled from user where username = ?"
                authorities-by-username-query="select user.username,role.name from user,role,user_role 
                                       where user.id=user_role.user_id and 
                                       user_role.role_id=role.id and user.username=?" />
        </authentication-provider>
    </authentication-manager>
    <!-- 自定义的切入点 -->
    <beans:bean id="securityMetadataSource"
        class="com.zmc.demo.MyFilterInvocationSecurityMetadataSource">
        <beans:property name="builder" ref="builder"></beans:property>
    </beans:bean>
</beans:beans>

说明

  • http 中的 custom-filter 是特别要注意的,就是通过这个标签来增加过滤器的,其中 before=”FILTER_SECURITY_INTERCEPTOR” 表示在 SpringSecurity 默认的过滤器之前执行。
  • 在配置 builder 时候,resourceQuery 就是要查询的 sql 语句,dataSource 为数据源。其他的如 authenticationManager 在之前的博客配置中就有详细讲解。
  • 在配置认证过滤器的时候,accessDecisionManager,authenticationManager,securityMetadataSource 这三个属性是必填项,若缺失会报错。其中 authenticationManager 就是 authentication-manager 标签,securityMetadataSource

是自定义的 MyFilterInvocationSecurityMetadataSource,authenticationManager 这里还没有定义,因此再创建一个类叫 MyAccessDecisionManager,代码如下:

/**
 * @classname MyAccessDecisionManager
 * @author ZMC
 * @time 2017-1-10
 * 
 */
public class MyAccessDecisionManager implements AccessDecisionManager  {/* (non-Javadoc)
     * @see org.springframework.security.access.AccessDecisionManager#decide(org.springframework.security.core.Authentication, java.lang.Object, java.util.Collection)
     * 该方法决定该权限是否有权限访问该资源,其实 object 就是一个资源的地址,authentication 是当前用户的
     * 对应权限,如果没登陆就为游客,登陆了就是该用户对应的权限
     */
    @Override
    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 needPermission = configAttribute.getAttribute();  
            System.out.println("访问"+object.toString()+"需要的权限是:" + needPermission);  
            // 用户所拥有的权限 authentication  
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for(GrantedAuthority ga : authorities) {if(needPermission.equals(ga.getAuthority())) {return;}  
            }
        }
        // 没有权限  
        throw new AccessDeniedException("没有权限访问!");  
        
    }
 
    @Override
    public boolean supports(ConfigAttribute attribute) {
        // TODO Auto-generated method stub
        return true;
    }
 
    @Override
    public boolean supports(Class<?> clazz) {
        // TODO Auto-generated method stub
        return true;
    }
}

结果

用户对应的角色,和角色能访问的资源

admin 能访问的页面有 adminPage.jsp、index.jsp;user 能访问的有 index.jsp;test 能访问的有 test.jsp。

先测试 admin 用户:

user 用户测试:

test 用户测试:


微信公众号关注:ByteZ,获取更多学习资料

正文完
 0