- 浏览: 76102 次
- 性别:
- 来自: 沈阳
文章分类
- 全部博客 (75)
- spring (5)
- hibernate (4)
- struts (1)
- ibatis (1)
- others (3)
- java (6)
- 整合 (5)
- linux (4)
- easyui (1)
- javascript (4)
- web (11)
- eclipse (0)
- freemarker (0)
- maven (0)
- mongodb (0)
- spring security (1)
- log4j (0)
- jbpm (1)
- jms (3)
- redis (1)
- mysql (3)
- solr (3)
- nginx (2)
- rabbitmq (6)
- druid (1)
- jenkins (2)
- vmware (1)
- memcached (2)
- idea (1)
- elasticsearch (2)
最新评论
首先导入spring security所需要的jar包
spring-security-core-2.0.5.RELEASE.jar
spring-security-core-tiger-2.0.5.RELEASE.jar
一.配置过滤器
在web.xml中定义如下过滤器
二.在spring配置文件中添加security的命名空间
三.在spring配置文中中定义需要保护的资源
注意intercept-url的先后顺序,spring security使用第一个能匹配的intercept-url标签进行权限控制。
四.使用数据库获取用户权限
<!-- 数据源 -->
<!-- 定义用户的权限根据注入的数据源获得 -->
定义权限管理模块的表结构(mysql数据库)
当用户登录时,spring security首先判断用户是否可以登录。用户登录后spring security获得该用户的
所有权限以判断用户是否可以访问资源。
spring配置文件中定义
<authentication-provider>
<jdbc-user-service data-source-ref="dataSource"
users-by-username-query="select username,password,enabled from t_account where username=?"
authorities-by-username-query="select r.descn from t_account_role ar join
t_account a on ar.a_id=a.id join t_role r on ar.r_id=r.id where a.username=?"/>
</authentication-provider>
users-by-username-query:根据用户名查找用户
authorities-by-username-query:根据用户名查找这个用户所有的角色名,将用户访问的URL地址和
查询结果与<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />标签进行匹配。
匹配成功就允许访问,否则就返回到提示页面。
在<http>标签中添加登录页面等信息
login-page:当用户登录时显示自定义登录页面
authentication-failure-url:登录失败时跳转到哪个页面
default-target-url:登录成功后跳转到哪个页面
注意:users-by-username-query指定的查询,必须至少按顺序返回3列,列名必须是username,password,enabled
authorities-by-username-query指定的查询,必须至少按顺序返回2列,第一列列名必须是username
第2列必须是权限的名字,与<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />中的
access匹配。
不能使用select *
完成的配置文件:
登录页面
页面中输入控件的name属性和form的action地址必须符合spring security的规定
登录失败页面
${SPRING_SECURITY_LAST_EXCEPTION.message} 获取spring生成的异常
将资源信息放入数据库中
表结构
1.在自定义的过滤器中获取资源的URL地址和角色名以取代spring配置文件中原有的<intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
过滤器代码:
将自定义的过滤器放入到原有的spring security过滤器链中(在spring配置文件中配置)
定义自定义过滤器(sql语句用于查询资源的URL地址 如:/index.jsp 和角色名 如ROLE_USER)
将自定义过滤器放入过滤器链中
注意:FilterSecurityInterceptor过滤器会向request中写入一个标记,用于标记是否已经控制了当前请求,以避免对同一请求多次处理,导致第2个FilterSecurityInterceptor不会再次执行。
在<http>中不需要再定义<intercept-url>,如下:
自定义的过滤器就从配置文件中读取sql,查询结果就是角色和资源,用户登录时就在session中保存了用户的角色。
注意:intercept-url的先后顺序,spring security使用第一个能匹配的intercept-url标签进行权限控制。
现在intercept-url来源于数据库,所以在sql查询时注意角色和资源的顺序。
建议在角色和资源的中间表中添加1个字段用于标识顺序,(按从严到宽的顺序)
表结构修改如下:
数据如下:
自定义过滤器修改如下:
如果持久层使用的是hibernate,那么只需要给自定义过滤器注入1个hibernateTemplate.
在自定义过滤器中就不需要再使用mapRow的方式。而是直接获取url和角色名即可。
例如:
而从灵活性的角度考虑,把hql写在配置文件中
问题:系统只会在初始化的时候从数据库中加载信息。无法识别数据库中信息的改变。
解决:每个jsp页面上重新内存
代码:每个jsp页面include如下代码:
控制用户信息
用户密码MD5加密:
盐值加密
用户信息缓存,使用spring内置的ehCache实现
在src目录下新建ehcache.xml
在程序中获取用户信息
自定义访问拒绝页面
访问用户以带参形式访问
在资源URL地址后加*
如
自定义用户接口实现
由于将sql写在配置文件中只适用于小型系统,而且不灵活,在大型系统或实体关系复杂时需要自定义用户实现的接口。
自定义用户实现需要实现2个接口。
UserDetails:实体类需要实现的接口。
UserDetailsService:实体管理类需要实现的接口。
配置:
原有的通过固定sql语句的获取方式可以抛弃,改为使用user-service-ref属性注入的bean来实现对用户及用户所
拥有的资源的查找。
用户注销
注销功能由过滤器org.springframework.security.ui.logout.LogoutFilter负责完成。
<a href="${pageContext.request.contextPath}/j_spring_security_logout">注销</a>
被注销的用户就是当前session中保存的用户,注销后页面自动重定向到default-target-url所指定的页面
spring security过滤器体系
spring security的一系列功能都是由一串过滤器来完成的。在spring配置文件中配置的<http>标签实际上就是
起到默认的过滤器进行声明和配置的作用。
管理会话
当项目中要求不能使用同一个账号同时登陆,按以下步骤实施
1.web.xml添加1个监听器
2.spring配置文件中的<http>标签添加子标签<concurrent-session-control/>
并放在过滤器链的最前面。
默认情况下,使用同一账号后登陆的用户会踢出先登陆的用户。
如果想禁止第2个用户登录,则设置
<concurrent-session-control exception-if-maximum-exceeded="true"/>
对方法级的权限控制
1.添加依赖包 cglib-nodep-2.1_3.jar aspectjweaver.jar aspectjrt.jar
2.利用<global-method-security>设置需要保护的方法及可以调用的权限
利用注解同样可以实现方法级的保护
需要spring-security-core-tiger.jar包
启用注解保护
拥护ROLE_ADMIN或ROLE_USER权限的用户可以调用该方法
SecurityContext安全上下文
SecurityContext securityContext = SecurityContextHolder.getContext();
SecurityContext中保存着实现了Authentication 接口的对象,如果用户尚未通过
认证,那么SecurityContext.getAuthenticaiton()方法就会返回null。
注意,如果使用了匿名用户,SecurityContext.getAuthenticaiton()返回的不是null.
只有在未启用过滤器链的情况下,SecurityContext.getAuthenticaiton()才返回空。
验证管理器
验证管理器用来识别用户的身份。使用命名空间会自动注册一个验证管理器的bean.
是org.springframework.security.providers.ProviderManager类的一个对象。
如果要在其他的bean中要引用这个验证管理器,则给这个验证管理器取一个别名。
如果要采用其他的类来完成验证管理器的功能,可以使用以下标签
访问决策管理器
当使用命名空间配置时,默认的AccessDecisionManager实例会自动注册。
默认的策略是使用一个AffirmativeBased作为AccessDecisionManager(访问决策管理器),投票者是RoleVoter和
AuthenticatedVote
利用访问决策管理器对方法进行保护
向过滤器注入访问决策管理器
不管是MethodSecurityInterceptor还是FilterSecurityInterceptor都使用 authenticationManager和accessDecisionManager属性用于验证用户,并且都是通过使用 objectDefinitionSource属性来定义受保护的资源。不同的是过滤器安全拦截器将URL资源与权限关联,而方法安全拦截器将业务方法与权限关联。
objectDefinitionSource属性需要注入的就是org.springframework.security.intercept.ObjectDefinitionSource这个接口的实现类。
该接口中有一个方法是:
ConfigAttributeDefinition getAttributes(Object object) throws IllegalArgumentException;
参数实际类型是org.springframework.security.intercept.web.FilterInvocation
这个方法用于获取保护资源对应的权限信息,返回一个ConfigAttributeDefinition对象。
ConfigAttributeDefinition对象内部维护1个列表,安全拦截器就通过调用getAttributes方法来获取ConfigAttributeDefinition对象,并将该对象和当前用户拥有的Authentication对象传递给accessDecisionManager(访问决策管理器)
访问决策管理器在将其传递给具体实现类维护的投票者,这些投票者从ConfigAttributeDefinition对象中获取这个存放了访问保护资源需要的权限信息的列表,然后遍历这个列表并与 Authentication对象中GrantedAuthority[]数据中的用户权限信息进行匹配,如果匹配成功,投票者就会投赞成票,否则就投反对票,最后访问决策管理器来统计这些投票决定用户是否能访问该资源。
FilterInvocationDefinitionSource接口和MethodDefinitionSource接口继承自ObjectDefinitionSource接口,并提供了2个默认实现类用以从配置文件读取权限信息。
是DefaultFilterInvocationDefinitionSource和DelegatingMethodDefinitionSource两个类,如果需要从其他数据来源读取则需要实现FilterInvocationDefinitionSource接口和MethodDefinitionSource接口。
自定义的过滤器必须注入objectDefinitionSource,accessDecisionManager,authenticationManager3个属性。
FilterInvocationDefinitionSource接口getAttributes实现思路
1.获取客户端访问的url地址。
2.将该url地址和数据库中存储的url地址匹配,找到所有有权访问该地址的权限(角色)名字。
3.把所有的权限名利用ConfigAttributeEditor类封装成ConfigAttributeDefinition对象
简单例子:
方法保护的原理是通过aop来实现的。
通知是1个实现了MethodSecurityInterceptor接口的类
methodSecurityInterceptor是spring中的一个类
accessDecisionManager属性注入访问决策管理器,authenticationManager属性注入验证管理器。
objectDefinitionSource属性注入1个实现了MethodDefinitionSource接口的类。
这个类的作用是根据用户调用的方法,找到相应的角色,
相应的方法名保存在数据库中。
spring-security-core-2.0.5.RELEASE.jar
spring-security-core-tiger-2.0.5.RELEASE.jar
一.配置过滤器
在web.xml中定义如下过滤器
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
二.在spring配置文件中添加security的命名空间
<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-2.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd">
三.在spring配置文中中定义需要保护的资源
<http auto-config='true'> <intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" /> <intercept-url pattern="/**" access="ROLE_USER" /> </http>
注意intercept-url的先后顺序,spring security使用第一个能匹配的intercept-url标签进行权限控制。
四.使用数据库获取用户权限
<!-- 数据源 -->
<beans:bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <beans:property name="driverClassName" value="com.mysql.jdbc.Driver"></beans:property> <beans:property name="url" value="jdbc:mysql://localhost:3306/sp"></beans:property> <beans:property name="username" value="root"></beans:property> <beans:property name="password" value="root"></beans:property> </beans:bean>
<!-- 定义用户的权限根据注入的数据源获得 -->
<authentication-provider> <jdbc-user-service data-source-ref="dataSource"/> </authentication-provider>
定义权限管理模块的表结构(mysql数据库)
alter table `t_account_role` drop foreign key `FK1C2BC93384B0A30E`; alter table `t_account_role` drop foreign key `FK1C2BC9332D31C656`; drop table if exists `t_account_role`; drop table if exists `t_account`; drop table if exists `t_role`; /* 用户表 */ CREATE TABLE `t_account` ( `id` int(11) NOT NULL, `username` varchar(255) default NULL, `password` varchar(255) default NULL, `enabled` int default NULL, /* 用户是否禁用 0:禁用 非0:可用*/ PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /* 角色表 */ CREATE TABLE `t_role` ( `id` int(11) NOT NULL, `name` varchar(255) default NULL, /* 角色名 */ `descn` varchar(255) default NULL, /* 角色在spring配置文件中的名字 如ROLE_ADMIN,ROLE_USER*/ PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /* 用户角色中间表 */ CREATE TABLE `t_account_role` ( `a_id` int(11) NOT NULL, `r_id` int(11) NOT NULL, PRIMARY KEY (`a_id`,`r_id`), KEY `FK1C2BC9332D31C656` (`r_id`), KEY `FK1C2BC93371CCC630` (`a_id`), CONSTRAINT `FK1C2BC93384B0A30E` FOREIGN KEY (`a_id`) REFERENCES `t_account` (`id`), CONSTRAINT `FK1C2BC9332D31C656` FOREIGN KEY (`r_id`) REFERENCES `t_role` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /* 初始化数据 */ insert into t_account values(1,'zhangsan','123',1); insert into t_account values(2,'lisi','321',1); insert into t_role values(1,'系统管理员','ROLE_ADMIN'); insert into t_role values(2,'普通用户','ROLE_USER'); insert into t_account_role values(1,2); insert into t_account_role values(2,1);
当用户登录时,spring security首先判断用户是否可以登录。用户登录后spring security获得该用户的
所有权限以判断用户是否可以访问资源。
spring配置文件中定义
<authentication-provider>
<jdbc-user-service data-source-ref="dataSource"
users-by-username-query="select username,password,enabled from t_account where username=?"
authorities-by-username-query="select r.descn from t_account_role ar join
t_account a on ar.a_id=a.id join t_role r on ar.r_id=r.id where a.username=?"/>
</authentication-provider>
users-by-username-query:根据用户名查找用户
authorities-by-username-query:根据用户名查找这个用户所有的角色名,将用户访问的URL地址和
查询结果与<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />标签进行匹配。
匹配成功就允许访问,否则就返回到提示页面。
在<http>标签中添加登录页面等信息
<http auto-config='true'> <intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/> <intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" /> <intercept-url pattern="/**" access="ROLE_USER" /> <form-login login-page="/login.jsp" authentication-failure-url="/error.jsp" default-target-url="/index.jsp" /> </http> <intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/>让没有登录的用户也可以访问登录页面
<form-login login-page="/login.jsp" authentication-failure-url="/error.jsp" default-target-url="/index.jsp" />
login-page:当用户登录时显示自定义登录页面
authentication-failure-url:登录失败时跳转到哪个页面
default-target-url:登录成功后跳转到哪个页面
注意:users-by-username-query指定的查询,必须至少按顺序返回3列,列名必须是username,password,enabled
authorities-by-username-query指定的查询,必须至少按顺序返回2列,第一列列名必须是username
第2列必须是权限的名字,与<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />中的
access匹配。
不能使用select *
完成的配置文件:
<?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-2.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd"> <!-- 数据源 --> <beans:bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <beans:property name="driverClassName" value="com.mysql.jdbc.Driver"></beans:property> <beans:property name="url" value="jdbc:mysql://localhost:3306/sp"></beans:property> <beans:property name="username" value="root"></beans:property> <beans:property name="password" value="root"></beans:property> </beans:bean> <http auto-config='true'> <intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/> <intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" /> <intercept-url pattern="/**" access="ROLE_USER,ROLE_ADMIN" /> <form-login login-page="/login.jsp" authentication-failure-url="/error.jsp" default-target-url="/index.jsp" /> </http> <authentication-provider> <jdbc-user-service data-source-ref="dataSource" users-by-username-query="select username,password,enabled from t_account where username=?" authorities-by-username-query="select a.username,r.descn from t_account_role ar join t_account a on ar.a_id=a.id join t_role r on ar.r_id=r.id where a.username=?"/> </authentication-provider> </beans:beans>
登录页面
<form action="${pageContext.request.contextPath}/j_spring_security_check" method="post"> 用户: <input type="text" name="j_username" value="${SPRING_SECURITY_LAST_USERNAME}"/><br /> 密码: <input type="password" name="j_password"/><br /> <input type="checkbox" name="_spring_security_remember_me" />两周之内不必登陆<br /> <input type="submit" value="登陆"/><input type="reset" value="重置"/> </form>
页面中输入控件的name属性和form的action地址必须符合spring security的规定
登录失败页面
${SPRING_SECURITY_LAST_EXCEPTION.message} 获取spring生成的异常
将资源信息放入数据库中
表结构
/* 用户表 */ CREATE TABLE `t_account` ( `id` int(11) NOT NULL, `username` varchar(255) default NULL, `password` varchar(255) default NULL, `enabled` int default NULL, /* 用户是否禁用 0:禁用 非0:可用*/ PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /* 角色表 */ CREATE TABLE `t_role` ( `id` int(11) NOT NULL, `name` varchar(255) default NULL, /* 角色名 */ `descn` varchar(255) default NULL, /* 角色在spring配置文件中的名字 如ROLE_ADMIN,ROLE_USER*/ PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /* 用户角色中间表 */ CREATE TABLE `t_account_role` ( `a_id` int(11) NOT NULL, `r_id` int(11) NOT NULL, PRIMARY KEY (`a_id`,`r_id`), KEY `FK1C2BC9332D31C656` (`r_id`), KEY `FK1C2BC93371CCC630` (`a_id`), CONSTRAINT `FK1C2BC93384B0A30E` FOREIGN KEY (`a_id`) REFERENCES `t_account` (`id`), CONSTRAINT `FK1C2BC9332D31C656` FOREIGN KEY (`r_id`) REFERENCES `t_role` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /* 资源表 */ CREATE TABLE `t_module` ( `id` int(11) NOT NULL, `name` varchar(255) default NULL, `address` varchar(255) default NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /* 资源角色的中间表 */ CREATE TABLE `t_module_role` ( `m_id` int(11) NOT NULL, `r_id` int(11) NOT NULL, PRIMARY KEY (`m_id`,`r_id`), KEY `FKA713071E2D31C656` (`r_id`), KEY `FKA713071ED78C9071` (`m_id`), CONSTRAINT `FKA713071ED78C9071` FOREIGN KEY (`m_id`) REFERENCES `t_module` (`id`), CONSTRAINT `FKA713071E2D31C656` FOREIGN KEY (`r_id`) REFERENCES `t_role` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /* 初始化数据 */ insert into t_account values(1,'zhangsan','123',1); insert into t_account values(2,'lisi','321',1); insert into t_role values(1,'系统管理员','ROLE_ADMIN'); insert into t_role values(2,'普通用户','ROLE_USER'); insert into t_account_role values(1,2); insert into t_account_role values(2,1); insert into t_module values(1,'部门管理','/dept.jsp'); insert into t_module values(2,'人员管理','/emp.jsp'); insert into `t_module_role` values(1,1); insert into `t_module_role` values(1,2); insert into `t_module_role` values(2,1);
1.在自定义的过滤器中获取资源的URL地址和角色名以取代spring配置文件中原有的<intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
过滤器代码:
import java.sql.ResultSet; import java.sql.SQLException; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.sql.DataSource; import org.springframework.beans.factory.FactoryBean; import org.springframework.jdbc.core.support.JdbcDaoSupport; import org.springframework.jdbc.object.MappingSqlQuery; import org.springframework.security.ConfigAttributeDefinition; import org.springframework.security.ConfigAttributeEditor; import org.springframework.security.intercept.web.DefaultFilterInvocationDefinitionSource; import org.springframework.security.intercept.web.FilterInvocationDefinitionSource; import org.springframework.security.intercept.web.RequestKey; import org.springframework.security.util.AntUrlPathMatcher; import org.springframework.security.util.UrlMatcher; public class JdbcFilterInvocationDefinitionSourceFactoryBean extends JdbcDaoSupport implements FactoryBean { private String resourceQuery; public boolean isSingleton() { return true; } public Class getObjectType() { return FilterInvocationDefinitionSource.class; } public Object getObject() { return new DefaultFilterInvocationDefinitionSource(this .getUrlMatcher(), this.buildRequestMap()); } protected Map<String, String> findResources() { ResourceMapping resourceMapping = new ResourceMapping(getDataSource(), resourceQuery); Map<String, String> resourceMap = new LinkedHashMap<String, String>(); for (Resource resource : (List<Resource>) resourceMapping.execute()) { String url = resource.getUrl(); String role = resource.getRole(); if (resourceMap.containsKey(url)) { String value = resourceMap.get(url); resourceMap.put(url, value + "," + role); } else { resourceMap.put(url, role); } } return resourceMap; } protected LinkedHashMap<RequestKey, ConfigAttributeDefinition> buildRequestMap() { LinkedHashMap<RequestKey, ConfigAttributeDefinition> requestMap = null; requestMap = new LinkedHashMap<RequestKey, ConfigAttributeDefinition>(); ConfigAttributeEditor editor = new ConfigAttributeEditor(); Map<String, String> resourceMap = this.findResources(); for (Map.Entry<String, String> entry : resourceMap.entrySet()) { RequestKey key = new RequestKey(entry.getKey(), null); editor.setAsText(entry.getValue()); requestMap.put(key, (ConfigAttributeDefinition) editor.getValue()); } return requestMap; } protected UrlMatcher getUrlMatcher() { return new AntUrlPathMatcher(); } public void setResourceQuery(String resourceQuery) { this.resourceQuery = resourceQuery; } 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; } } }
将自定义的过滤器放入到原有的spring security过滤器链中(在spring配置文件中配置)
定义自定义过滤器(sql语句用于查询资源的URL地址 如:/index.jsp 和角色名 如ROLE_USER)
<beans:bean id="filterInvocationDefinitionSource" class="com.lovo.JdbcFilterInvocationDefinitionSourceFactoryBean"> <beans:property name="dataSource" ref="dataSource"/> <beans:property name="resourceQuery" value=" select m.address,r.descn from t_module_role mr join t_module m on mr.m_id=m.id join t_role r on mr.r_id=r.id; "/> </beans:bean>
将自定义过滤器放入过滤器链中
<beans:bean id="filterSecurityInterceptor" class="org.springframework.security.intercept.web.FilterSecurityInterceptor" autowire="byType"> <custom-filter before="FILTER_SECURITY_INTERCEPTOR"/> <beans:property name="objectDefinitionSource" ref="filterInvocationDefinitionSource" /> </beans:bean>
注意:FilterSecurityInterceptor过滤器会向request中写入一个标记,用于标记是否已经控制了当前请求,以避免对同一请求多次处理,导致第2个FilterSecurityInterceptor不会再次执行。
在<http>中不需要再定义<intercept-url>,如下:
<http auto-config='true'> <form-login login-page="/login.jsp" authentication-failure-url="/error.jsp" default-target-url="/index.jsp" /> </http>
自定义的过滤器就从配置文件中读取sql,查询结果就是角色和资源,用户登录时就在session中保存了用户的角色。
注意:intercept-url的先后顺序,spring security使用第一个能匹配的intercept-url标签进行权限控制。
现在intercept-url来源于数据库,所以在sql查询时注意角色和资源的顺序。
建议在角色和资源的中间表中添加1个字段用于标识顺序,(按从严到宽的顺序)
表结构修改如下:
/* 资源角色的中间表 */ CREATE TABLE `t_module_role` ( `m_id` int(11) NOT NULL, `r_id` int(11) NOT NULL, `priority` int(11) default NULL, /* 用于标识角色和资源的匹配顺序 从严到宽 */ PRIMARY KEY (`m_id`,`r_id`), KEY `FKA713071E2D31C656` (`r_id`), KEY `FKA713071ED78C9071` (`m_id`), CONSTRAINT `FKA713071ED78C9071` FOREIGN KEY (`m_id`) REFERENCES `t_module` (`id`), CONSTRAINT `FKA713071E2D31C656` FOREIGN KEY (`r_id`) REFERENCES `t_role` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
数据如下:
insert into t_account values(1,'zhangsan','123',1); insert into t_account values(2,'lisi','321',1); insert into t_role values(1,'系统管理员','ROLE_ADMIN'); insert into t_role values(2,'普通用户','ROLE_USER'); insert into t_account_role values(1,2); insert into t_account_role values(2,1); insert into t_module values(1,'部门管理','/dept.jsp'); insert into t_module values(2,'人员管理','/emp.jsp'); insert into `t_module_role` values(1,1,3); insert into `t_module_role` values(1,2,2); insert into `t_module_role` values(2,1,1);
自定义过滤器修改如下:
<beans:bean id="filterInvocationDefinitionSource" class="com.lovo.JdbcFilterInvocationDefinitionSourceFactoryBean"> <beans:property name="dataSource" ref="dataSource"/> <beans:property name="resourceQuery" value=" select m.address,r.descn from t_module_role mr join t_module m on mr.m_id=m.id join t_role r on mr.r_id=r.id order by mr.priority "/> </beans:bean>
如果持久层使用的是hibernate,那么只需要给自定义过滤器注入1个hibernateTemplate.
在自定义过滤器中就不需要再使用mapRow的方式。而是直接获取url和角色名即可。
例如:
public class ModuleFilter implements FactoryBean { private String resourceQuery; public boolean isSingleton() { return true; } public Class getObjectType() { return FilterInvocationDefinitionSource.class; } public Object getObject() { return new DefaultFilterInvocationDefinitionSource(this .getUrlMatcher(), this.buildRequestMap()); } protected Map<String, String> findResources() { ResourceMapping resourceMapping = new ResourceMapping(); Map<String, String> resourceMap = new LinkedHashMap<String, String>(); for (Resource resource : (List<Resource>) resourceMapping.execute()) { String url = resource.getUrl(); String role = resource.getRole(); if (resourceMap.containsKey(url)) { String value = resourceMap.get(url); resourceMap.put(url, value + "," + role); } else { resourceMap.put(url, role); } } return resourceMap; } protected LinkedHashMap<RequestKey, ConfigAttributeDefinition> buildRequestMap() { LinkedHashMap<RequestKey, ConfigAttributeDefinition> requestMap = null; requestMap = new LinkedHashMap<RequestKey, ConfigAttributeDefinition>(); ConfigAttributeEditor editor = new ConfigAttributeEditor(); Map<String, String> resourceMap = this.findResources(); for (Map.Entry<String, String> entry : resourceMap.entrySet()) { RequestKey key = new RequestKey(entry.getKey(), null); editor.setAsText(entry.getValue()); requestMap.put(key, (ConfigAttributeDefinition) editor.getValue()); } return requestMap; } protected UrlMatcher getUrlMatcher() { return new AntUrlPathMatcher(); } public void setResourceQuery(String resourceQuery) { this.resourceQuery = resourceQuery; } 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{ public List<Resource> execute(){ List<Resource> rlist = new ArrayList<Resource>(); List<Role> list = hibernateTemplate.find(resourceQuery); for(int i=0;i<list.size();i++){ Role role = list.get(i); Set<Module> set = role.getModuleSet(); Iterator<Module> it = set.iterator(); while(it.hasNext()){ Module m = it.next(); Resource re = new Resource(m.getUrl(),role.getDescn()); rlist.add(re); } } return rlist; } } public void setHibernateTemplate(HibernateTemplate hibernateTemplate) { this.hibernateTemplate = hibernateTemplate; } private HibernateTemplate hibernateTemplate; }
而从灵活性的角度考虑,把hql写在配置文件中
<beans:bean id="moduleFilter" class="com.lovo.ModuleFilter"> <beans:property name="resourceQuery" value="from com.lovo.po.Role order by ind"> </beans:property> <beans:property name="hibernateTemplate" ref="hibernateTemplate"> </beans:property> </beans:bean>
问题:系统只会在初始化的时候从数据库中加载信息。无法识别数据库中信息的改变。
解决:每个jsp页面上重新内存
代码:每个jsp页面include如下代码:
<%@page import="org.springframework.context.ApplicationContext"%> <%@page import="org.springframework.web.context.support.WebApplicationContextUtils"%> <%@page import="org.springframework.beans.factory.FactoryBean"%> <%@page import="org.springframework.security.intercept.web.FilterSecurityInterceptor"%> <%@page import="org.springframework.security.intercept.web.FilterInvocationDefinitionSource"%> <% ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(application); FactoryBean factoryBean = (FactoryBean) ctx.getBean("&自定义过滤器的id"); FilterInvocationDefinitionSource fids = (FilterInvocationDefinitionSource) factoryBean.getObject(); FilterSecurityInterceptor filter = (FilterSecurityInterceptor) ctx.getBean("filterSecurityInterceptor"); filter.setObjectDefinitionSource(fids); %>
控制用户信息
用户密码MD5加密:
<authentication-provider> <password-encoder hash="md5"/> <jdbc-user-service data-source-ref="dataSource" users-by-username-query="select username,password,enabled from t_account where username=?" authorities-by-username-query="select a.username,r.descn from t_account_role ar join t_account a on ar.a_id=a.id join t_role r on ar.r_id=r.id where a.username=?"/> </authentication-provider>
盐值加密
<authentication-provider> <password-encoder hash="md5"> <salt-source user-property="username"/> </password-encoder> <jdbc-user-service data-source-ref="dataSource" users-by-username-query="select username,password,enabled from t_account where username=?" authorities-by-username-query="select a.username,r.descn from t_account_role ar join t_account a on ar.a_id=a.id join t_role r on ar.r_id=r.id where a.username=?"/> </authentication-provider>
用户信息缓存,使用spring内置的ehCache实现
<beans:bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"></beans:bean> <beans:bean id="userEhCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean"> <beans:property name="cacheManager" ref="cacheManager"></beans:property> <beans:property name="cacheName" value="userCache"></beans:property> </beans:bean> <beans:bean id="userCache" class="org.springframework.security.providers.dao.cache.EhCacheBasedUserCache"> <beans:property name="cache" ref="userEhCache"></beans:property> </beans:bean>
在src目录下新建ehcache.xml
<ehcache> <diskStore path="java.io.tmpdir"/> <defaultCache maxElementsInMemory="1000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" /> <cache name="userCache" maxElementsInMemory="100" eternal="false" timeToIdleSeconds="600" timeToLiveSeconds="3600" overflowToDisk="true" /> </ehcache>
在程序中获取用户信息
UserDetails ud = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); String name = ud.getUsername(); String pwd = ud.getPassword(); GrantedAuthority[] ga = ud.getAuthorities(); System.out.println(name + "," + pwd); for(GrantedAuthority g : ga){ System.out.println(g.getAuthority()); }
自定义访问拒绝页面
<http auto-config='true' access-denied-page="/error.jsp">
访问用户以带参形式访问
在资源URL地址后加*
如
insert into t_module values(1,'部门管理','/dept.jsp*'); insert into t_module values(2,'人员管理','/emp.jsp*');
自定义用户接口实现
由于将sql写在配置文件中只适用于小型系统,而且不灵活,在大型系统或实体关系复杂时需要自定义用户实现的接口。
自定义用户实现需要实现2个接口。
UserDetails:实体类需要实现的接口。
UserDetailsService:实体管理类需要实现的接口。
配置:
<!-- 实体管理类 --> <beans:bean id="userManager" class="com.lovo.UserManager"></beans:bean> <authentication-provider user-service-ref="userManager"> <!-- <jdbc-user-service data-source-ref="dataSource" users-by-username-query="select username,password,enabled from t_account where username=?" authorities-by-username-query="select a.username,r.descn from t_account_role ar join t_account a on ar.a_id=a.id join t_role r on ar.r_id=r.id where a.username=?"/> --> </authentication-provider>
原有的通过固定sql语句的获取方式可以抛弃,改为使用user-service-ref属性注入的bean来实现对用户及用户所
拥有的资源的查找。
用户注销
注销功能由过滤器org.springframework.security.ui.logout.LogoutFilter负责完成。
<a href="${pageContext.request.contextPath}/j_spring_security_logout">注销</a>
被注销的用户就是当前session中保存的用户,注销后页面自动重定向到default-target-url所指定的页面
spring security过滤器体系
spring security的一系列功能都是由一串过滤器来完成的。在spring配置文件中配置的<http>标签实际上就是
起到默认的过滤器进行声明和配置的作用。
管理会话
当项目中要求不能使用同一个账号同时登陆,按以下步骤实施
1.web.xml添加1个监听器
<listener> <listener-class>org.springframework.security.ui.session.HttpSessionEventPublisher</listener-class> </listener>
2.spring配置文件中的<http>标签添加子标签<concurrent-session-control/>
<http auto-config='true' access-denied-page="/noview.jsp"> <form-login login-page="/login.jsp" authentication-failure-url="/error.jsp" default-target-url="/index.jsp"/> <concurrent-session-control/> </http> <concurrent-session-control/>会产生1个org.springframework.security.concurrent.ConcurrentSessionFilter
并放在过滤器链的最前面。
默认情况下,使用同一账号后登陆的用户会踢出先登陆的用户。
如果想禁止第2个用户登录,则设置
<concurrent-session-control exception-if-maximum-exceeded="true"/>
对方法级的权限控制
1.添加依赖包 cglib-nodep-2.1_3.jar aspectjweaver.jar aspectjrt.jar
2.利用<global-method-security>设置需要保护的方法及可以调用的权限
<global-method-security> <protect-pointcut access="ROLE_ADMIN,ROLE_USER" expression="execution(* com.lovo.bo.AccountBo.get*(..))"/> <protect-pointcut access="ROLE_ADMIN" expression="execution(* com.lovo.bo.AccountBo.create*(..))"/> </global-method-security>
利用注解同样可以实现方法级的保护
需要spring-security-core-tiger.jar包
启用注解保护
<global-method-security secured-annotations="enabled"/>
@Secured({"ROLE_ADMIN","ROLE_USER"}) public void getOneAccount();
拥护ROLE_ADMIN或ROLE_USER权限的用户可以调用该方法
SecurityContext安全上下文
SecurityContext securityContext = SecurityContextHolder.getContext();
SecurityContext中保存着实现了Authentication 接口的对象,如果用户尚未通过
认证,那么SecurityContext.getAuthenticaiton()方法就会返回null。
注意,如果使用了匿名用户,SecurityContext.getAuthenticaiton()返回的不是null.
只有在未启用过滤器链的情况下,SecurityContext.getAuthenticaiton()才返回空。
验证管理器
验证管理器用来识别用户的身份。使用命名空间会自动注册一个验证管理器的bean.
是org.springframework.security.providers.ProviderManager类的一个对象。
如果要在其他的bean中要引用这个验证管理器,则给这个验证管理器取一个别名。
<authentication-manager alias="authenticationManager"/>
如果要采用其他的类来完成验证管理器的功能,可以使用以下标签
<beans:bean id="abc" class="org.springframework.security.providers.dao.DaoAuthenticationProvider"> <custom-authentication-provider/> </beans:bean>
访问决策管理器
当使用命名空间配置时,默认的AccessDecisionManager实例会自动注册。
默认的策略是使用一个AffirmativeBased作为AccessDecisionManager(访问决策管理器),投票者是RoleVoter和
AuthenticatedVote
<global-method-security secured-annotations="enabled" access-decision-manager-ref="accessDecisionManager"> </global-method-security>
利用访问决策管理器对方法进行保护
<beans:bean id="filterSecurityInterceptor" class="org.springframework.security.intercept.web.FilterSecurityInterceptor" autowire="byType"> <custom-filter before="FILTER_SECURITY_INTERCEPTOR"/> <beans:property name="objectDefinitionSource" ref="moduleFilter" /> <beans:property name="accessDecisionManager" ref="accessDecisionManager"></beans:property> </beans:bean>
向过滤器注入访问决策管理器
不管是MethodSecurityInterceptor还是FilterSecurityInterceptor都使用 authenticationManager和accessDecisionManager属性用于验证用户,并且都是通过使用 objectDefinitionSource属性来定义受保护的资源。不同的是过滤器安全拦截器将URL资源与权限关联,而方法安全拦截器将业务方法与权限关联。
objectDefinitionSource属性需要注入的就是org.springframework.security.intercept.ObjectDefinitionSource这个接口的实现类。
该接口中有一个方法是:
ConfigAttributeDefinition getAttributes(Object object) throws IllegalArgumentException;
参数实际类型是org.springframework.security.intercept.web.FilterInvocation
这个方法用于获取保护资源对应的权限信息,返回一个ConfigAttributeDefinition对象。
ConfigAttributeDefinition对象内部维护1个列表,安全拦截器就通过调用getAttributes方法来获取ConfigAttributeDefinition对象,并将该对象和当前用户拥有的Authentication对象传递给accessDecisionManager(访问决策管理器)
访问决策管理器在将其传递给具体实现类维护的投票者,这些投票者从ConfigAttributeDefinition对象中获取这个存放了访问保护资源需要的权限信息的列表,然后遍历这个列表并与 Authentication对象中GrantedAuthority[]数据中的用户权限信息进行匹配,如果匹配成功,投票者就会投赞成票,否则就投反对票,最后访问决策管理器来统计这些投票决定用户是否能访问该资源。
FilterInvocationDefinitionSource接口和MethodDefinitionSource接口继承自ObjectDefinitionSource接口,并提供了2个默认实现类用以从配置文件读取权限信息。
是DefaultFilterInvocationDefinitionSource和DelegatingMethodDefinitionSource两个类,如果需要从其他数据来源读取则需要实现FilterInvocationDefinitionSource接口和MethodDefinitionSource接口。
自定义的过滤器必须注入objectDefinitionSource,accessDecisionManager,authenticationManager3个属性。
FilterInvocationDefinitionSource接口getAttributes实现思路
1.获取客户端访问的url地址。
2.将该url地址和数据库中存储的url地址匹配,找到所有有权访问该地址的权限(角色)名字。
3.把所有的权限名利用ConfigAttributeEditor类封装成ConfigAttributeDefinition对象
简单例子:
public class MyFilter extends HibernateDaoSupport implements FilterInvocationDefinitionSource,FactoryBean{ //保存权限信息(能够访问当前资源的角色名) private List<String> roleNameList = new ArrayList<String>(); public ConfigAttributeDefinition getAttributes(Object object) throws IllegalArgumentException { FilterInvocation fi = (FilterInvocation)object; String requeatUrl = fi.getRequestUrl(); //获取客户端访问的url地址 requeatUrl = requeatUrl.toLowerCase(); //将url地址全部转换成小写 if(requeatUrl.indexOf("?") != -1){ //过滤请求参数 requeatUrl = requeatUrl.substring(0,requeatUrl.indexOf("?")); } //在数据库中查找能够访问此url地址的角色 List<Module> list = this.getHibernateTemplate().find("from com.lovo.po.Module where url like ?",requeatUrl+"%"); if(list.size() <= 0){ return null; } String s = ""; Module module = (Module)list.get(0); Set<Role> set = module.getRoleSet(); roleNameList.clear(); for(Role role : set){ roleNameList.add(role.getDescn()); s += role.getDescn() + ","; } s = s.substring(0,(s.length() - 1)); //将所有的角色名(ROLE_开头)拼接为一个字符串。 ConfigAttributeEditor editer = new ConfigAttributeEditor(); editer.setAsText(s); return (ConfigAttributeDefinition) editer.getValue(); //将包含所有角色名(ROLE_开头)的字符串转换为ConfigAttributeDefinition对象。 } public Collection getConfigAttributeDefinitions() { return Collections.unmodifiableCollection(this.roleNameList); } public boolean supports(Class clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } public Object getObject() throws Exception { return this; } public Class getObjectType() { return FilterInvocationDefinitionSource.class; } public boolean isSingleton() { // TODO Auto-generated method stub return true; } }
方法保护的原理是通过aop来实现的。
<aop:config> <aop:pointcut expression="execution(* com.lovo.bo.face.*.*(..))" id="me"/> <aop:advisor advice-ref="methodSecurityInterceptor" pointcut-ref="me"/> </aop:config>
通知是1个实现了MethodSecurityInterceptor接口的类
methodSecurityInterceptor是spring中的一个类
<beans:bean id="methodSecurityInterceptor" class="org.springframework.security.intercept.method.aopalliance.MethodSecurityInterceptor"> <beans:property name="objectDefinitionSource" ref="methodFilter" /> <beans:property name="accessDecisionManager" ref="accessDecisionManager"></beans:property> <beans:property name="authenticationManager" ref="authenticationManager"></beans:property> </beans:bean>
accessDecisionManager属性注入访问决策管理器,authenticationManager属性注入验证管理器。
objectDefinitionSource属性注入1个实现了MethodDefinitionSource接口的类。
<beans:bean id="methodFilter" class="com.lovo.method.MethodFilter"> <beans:property name="hibernateTemplate" ref="hibernateTemplate"></beans:property> </beans:bean>
这个类的作用是根据用户调用的方法,找到相应的角色,
public class MethodFilter extends HibernateDaoSupport implements MethodDefinitionSource{ private List<String> roleList = new ArrayList<String>(); public ConfigAttributeDefinition getAttributes(Method method, Class targetClass) { return null; } public ConfigAttributeDefinition getAttributes(Object object) throws IllegalArgumentException { ConfigAttributeEditor editor = new ConfigAttributeEditor(); String s = ""; ReflectiveMethodInvocation rmi = (ReflectiveMethodInvocation)object; String methodName = rmi.getThis().getClass().getName() + "." + rmi.getMethod().getName(); System.out.println(methodName); List<Module> list = this.getHibernateTemplate().find("from com.lovo.po.Module where url like ?",methodName); if(list.size() == 0){ return null; } roleList.clear(); for(int i=0;i<list.size();i++){ Module module = list.get(i); Set<Role> roleSet = module.getRoleSet(); Iterator<Role> it = roleSet.iterator(); while(it.hasNext()){ Role role = it.next(); s += role.getDescn() + ","; roleList.add(role.getDescn()); } } s = s.substring(0,(s.length() - 1)); editor.setAsText(s); editor.getValue(); return (ConfigAttributeDefinition) editor.getValue(); } public Collection getConfigAttributeDefinitions() { return roleList; } public boolean supports(Class clazz) { return true; } }
相应的方法名保存在数据库中。
相关推荐
### Spring Security核心概念与实践详解 #### Spring Security概述 在深入了解Spring Security之前,我们先回顾一下没有Spring Security的权限管理场景。在传统架构中,权限验证逻辑往往与业务逻辑紧密交织,导致...
总结,SpringSecurity是一个功能强大的安全框架,它为开发者提供了安全Web应用的全套解决方案。通过理解并熟练运用其核心概念和组件,我们可以构建出安全、健壮的应用程序。在学习过程中,分析提供的源代码和示例将...
Spring Security 是一个强大的和高度可定制的身份验证和访问控制框架,用于保护基于Java的应用程序。它为Web应用程序和企业级应用提供了全面的安全解决方案,包括用户认证、授权、CSRF(跨站请求伪造)防护以及会话...
"springsecurity-namespace"可能指的是Spring Security的XML命名空间配置。在Spring Security的早期版本中,使用XML配置是最常见的实践。例如,你可能会看到以下片段: ```xml **" access="hasRole('ROLE_ADMIN')...
Spring Security3 安全个人总结 Spring Security3 是一个基于 Java 的安全框架,提供了一个灵活的安全机制来保护基于 Spring 的应用程序。在这个总结中,我们将详细介绍 Spring Security3 的安全机制、原理和核心...
### Spring Security 官方文档中文版重要知识点解析 #### 一、Spring Security 概述 **1.1 Spring Security 是什么?** Spring Security 是一款基于 Spring 框架的安全插件,提供了完整的安全性解决方案,包括...
### Spring Security 概述与应用实践 #### 一、引言 在当今互联网时代,网络安全问题日益凸显,尤其是Web应用程序的安全性受到了前所未有的关注。为了应对这些挑战,Spring Security 应运而生,成为了一个非常重要...
总结来说,这个项目演示了如何在SpringBoot环境中,利用SpringSecurity保证安全性,结合WebSocket实现高效、实时的双向通信。这样的架构对于需要实时交互的Web应用,如在线聊天、实时股票交易、游戏等场景非常适用。...
【Spring Security 学习总结】 Spring Security 是一个强大的和高度可定制的身份验证和访问控制框架,用于保护基于 Java 的应用程序。本学习总结文档主要针对初学者,旨在剖析一个不安全的应用程序并阐述如何通过 ...
spring security 学习总结暑假的时候在学习了Spring安全并成功运用到了项目中。在实践中摸索出了一套结合JSON +智威汤逊(JSON网络令牌)+Spring引导+ Spring Security的技术的权限方案趁着国庆假期记录一下。 内容 ...
总结,Spring Security通过数据库存储授权信息,使得安全配置更加灵活,同时降低了维护成本。通过自定义`UserDetailsService`和数据库表结构,可以实现用户认证和授权的完全数据库化管理。这种模式适用于大型和复杂...
总结起来,"springSecurityTest.zip"文件提供了一个完整的环境,让你可以动手实践Spring Security的基础用法。通过学习其中的代码、笔记和文档,你将理解Spring Security如何与Spring框架协同工作,如何设置认证和...
总结,Spring Security 3.1作为一款强大的安全框架,虽然版本较旧,但仍值得开发者深入研究。通过学习和实践,我们可以掌握如何构建安全的Web应用,并理解其背后的原理,为今后的安全开发打下坚实基础。
**Spring Security 概述** Spring Security 是一个强大的且高度可定制的身份验证和授权框架,用于保护基于Java的应用程序。它提供了全面的安全解决方案,包括身份验证、授权、会话管理以及跨站请求伪造(CSRF)防护...
以上概览仅为Spring Security部分内容的总结,实际应用中需要根据具体需求选择合适的配置和技术栈。Spring Security提供了极其丰富的特性和灵活性,适用于从简单的Web应用到复杂的企业级系统的安全性需求。
SpringSecurity 是一个强大的安全框架,主要用于Java应用的安全管理,包括认证和授权。SpringSecurity 3.1 版本在此前的基础上进行了改进和调整,本文将深入探讨其关键知识点。 首先,SpringSecurity 的核心架构...