spring security总结
首先导入spring security所需要的jar包
spring-security-core-2.0.5.RELEASE.jar
spring-security-core-tiger-2.0.5.RELEASE.jar
一.配置过滤器
在web.xml中定义如下过滤器
二.在spring配置文件中添加security的命名空间
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配置文中中定义需要保护的资源
注意intercept-url的先后顺序,spring security使用第一个能匹配的intercept-url标签进行权限控制。
四.使用数据库获取用户权限
定义权限管理模块的表结构(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配置文件中定义
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=?"/>
users-by-username-query:根据用户名查找用户
authorities-by-username-query:根据用户名查找这个用户所有的角色名,将用户访问的URL地址和
查询结果与
匹配成功就允许访问,否则就返回到提示页面。
在
default-target-url="/index.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列必须是权限的名字,与
access匹配。
不能使用select *
完成的配置文件:
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">
default-target-url="/index.jsp" />
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=?"/>
登录页面
/* 用户角色中间表 */
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配置文件中原有的
过滤器代码:
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
ResourceMapping resourceMapping = new ResourceMapping(getDataSource(),
resourceQuery);
Map
for (Resource resource : (List
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
LinkedHashMap
requestMap = new LinkedHashMap
ConfigAttributeEditor editor = new ConfigAttributeEditor();
Map
for (Map.Entry
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)
将自定义过滤器放入过滤器链中
注意:FilterSecurityInterceptor过滤器会向request中写入一个标记,用于标记是否已经控制了当前请求,以避免对同一请求多次处理,导致第2个FilterSecurityInterceptor不会再次执行。
在
default-target-url="/index.jsp" />
自定义的过滤器就从配置文件中读取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);
自定义过滤器修改如下:
如果持久层使用的是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
ResourceMapping resourceMapping = new ResourceMapping();
Map
for (Resource resource : (List
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
LinkedHashMap
requestMap = new LinkedHashMap
ConfigAttributeEditor editor = new ConfigAttributeEditor();
Map
for (Map.Entry
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
List
List
for(int i=0;i
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写在配置文件中
问题:系统只会在初始化的时候从数据库中加载信息。无法识别数据库中信息的改变。
解决:每个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加密:
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=?"/>
盐值加密
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=?"/>
用户信息缓存,使用spring内置的ehCache实现
在src目录下新建ehcache.xml
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>
maxElementsInMemory="100"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="3600"
overflowToDisk="true"
/>
在程序中获取用户信息
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());
}
自定义访问拒绝页面
访问用户以带参形式访问
在资源URL地址后加*
如
insert into t_module values(1,'部门管理','/dept.jsp*');
insert into t_module values(2,'人员管理','/emp.jsp*');
自定义用户接口实现
由于将sql写在配置文件中只适用于小型系统,而且不灵活,在大型系统或实体关系复杂时需要自定义用户实现的接口。
自定义用户实现需要实现2个接口。
UserDetails:实体类需要实现的接口。
UserDetailsService:实体管理类需要实现的接口。
配置:
原有的通过固定sql语句的获取方式可以抛弃,改为使用user-service-ref属性注入的bean来实现对用户及用户所
拥有的资源的查找。
用户注销
注销功能由过滤器org.springframework.security.ui.logout.LogoutFilter负责完成。
注销
被注销的用户就是当前session中保存的用户,注销后页面自动重定向到default-target-url所指定的页面
spring security过滤器体系
spring security的一系列功能都是由一串过滤器来完成的。在spring配置文件中配置的
起到默认的过滤器进行声明和配置的作用。
管理会话
当项目中要求不能使用同一个账号同时登陆,按以下步骤实施
1.web.xml添加1个监听器
2.spring配置文件中的
并放在过滤器链的最前面。
默认情况下,使用同一账号后登陆的用户会踢出先登陆的用户。
如果想禁止第2个用户登录,则设置
对方法级的权限控制
1.添加依赖包 cglib-nodep-2.1_3.jar aspectjweaver.jar aspectjrt.jar
2.利用
利用注解同样可以实现方法级的保护
需要spring-security-core-tiger.jar包
启用注解保护
@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中要引用这个验证管理器,则给这个验证管理器取一个别名。
如果要采用其他的类来完成验证管理器的功能,可以使用以下标签
访问决策管理器
当使用命名空间配置时,默认的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对象
简单例子:
public class MyFilter extends HibernateDaoSupport implements FilterInvocationDefinitionSource,FactoryBean{
//保存权限信息(能够访问当前资源的角色名)
private List
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
if(list.size() <= 0){
return null;
}
String s = "";
Module module = (Module)list.get(0);
Set
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来实现的。
通知是1个实现了MethodSecurityInterceptor接口的类
methodSecurityInterceptor是spring中的一个类
accessDecisionManager属性注入访问决策管理器,authenticationManager属性注入验证管理器。
objectDefinitionSource属性注入1个实现了MethodDefinitionSource接口的类。
这个类的作用是根据用户调用的方法,找到相应的角色,
public class MethodFilter extends HibernateDaoSupport implements MethodDefinitionSource{
private List
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
if(list.size() == 0){
return null;
}
roleList.clear();
for(int i=0;i
Set
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;
}
}