标签:步骤 detail efi load indexof collect 启动服务 keyword 2.3
最近阴差阳错的搞上了SpringSecurity3,因为是自己做的小系统,中间遇到了很多坑,基本每个坑都踩过了,网上也查了不少资料,发现有不少错误的,更是让我绕了一圈又一圈,现在把一些基本的东西总结一下。
先从整体上总结一下为什么使用SS,一般的,在不使用ss的情况下,我们基本会在每个业务方法执行前,插入一段用于验证权限的代码,从而判断当前用户是否有相应权限进行操作,这样做就会让业务方法和验证权限有了一个紧密的耦合;如果使用ss,我们就可以通过注解或者XML配置方式代替权限验证,使得业务和权限代码彻底分离,通过下图可以更形象的理解:
目前,权限管理采用最多的技术都是基于角色访问控制技术RBAC(Role Based Access Control)。一般来说,提供如下功能:1,角色管理界面,由用户定义角色,给角色赋权限;2,用户角色管理界面,由用户给系统用户赋予角色。什么是RBAC,说到底其实就是五张表,权限表-权限角色对应表-角色表-角色用户对应表-用户表,比较常见。但是ss3默认支持的并不是这种模式,而是通过XML配置角色及用户的方式实现的权限验证等操作,所以需要我们去实现SS中一些接口,让其支持RBAC,下面开始搭建一套支持RBAC技术的SS框架:
(1)数据库相关表格:
1.用户表Users
CREATE TABLE `users` (
`password` varchar(255) default NULL,
`username` varchar(255) default NULL,
`uid` int(11) NOT NULL auto_increment,
PRIMARY KEY (`uid`)
)
2.角色表Roles
CREATE TABLE `roles` (
`rolename` varchar(255) default NULL,
`rid` int(11) NOT NULL auto_increment,
PRIMARY KEY (`rid`)
)
3 用户_角色表users_roles
CREATE TABLE `users_roles` (
--用户表的外键
`uid` int(11) default NULL,
--角色表的外键
`rid` int(11) default NULL,
`urid` int(11) ,
PRIMARY KEY (`urid`),
)
4.资源表resources
CREATE TABLE `resources` (
-- 权限所对应的url地址
`url` varchar(255) default NULL,
--权限所对应的编码,例201代表发表文章
`resourcename` varchar(255) default NULL,
`rsid` int(11) ,
PRIMARY KEY (`rsid`)
)
5.角色_资源表roles_resources
CREATE TABLE `roles_resources` (
`rsid` int(11) default NULL,
`rid` int(11) default NULL,
`rrid` int(11) NOT NULL ,
PRIMARY KEY (`rrid`),
)
(2)在继续配置前,需要知道ss是如何通过权限验证的,实际上ss通过拦截器,拦截发来的请求,对其进行验证的。而具体验证的方式则是通过我们实现相关接口的方法来进行的。既然是拦截器,web.xml势必是优先配置的。
-
<!DOCTYPE web-app PUBLIC
-
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
-
"http://java.sun.com/dtd/web-app_2_3.dtd" >
-
-
-
<web-app>
-
<display-name>Archetype Created Web Application</display-name>
-
-
<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>
-
-
<servlet>
-
<servlet-name>spring</servlet-name>
-
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
-
<init-param>
-
<param-name>contextConfigLocation</param-name>
-
<param-value>classpath:spring-mvc.xml</param-value>
-
</init-param>
-
<load-on-startup>1</load-on-startup>
-
</servlet>
-
-
<servlet-mapping>
-
<servlet-name>spring</servlet-name>
-
<url-pattern>*.do</url-pattern>
-
</servlet-mapping>
-
-
-
-
<listener>
-
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
-
</listener>
-
<listener>
-
<listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
-
</listener>
-
-
-
<context-param>
-
<param-name>contextConfigLocation</param-name>
-
<param-value>classpath:applicationContext*.xml,classpath:spring-mybatis.xml</param-value>
-
</context-param>
-
<filter>
-
<filter-name>encodingFilter</filter-name>
-
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
-
<init-param>
-
<param-name>encoding</param-name>
-
<param-value>UTF-8</param-value>
-
</init-param>
-
</filter>
-
-
-
<listener>
-
<listener-class>
-
org.springframework.web.util.IntrospectorCleanupListener
-
</listener-class>
-
</listener>
-
-
-
<listener>
-
<listener-class>
-
org.springframework.security.web.session.HttpSessionEventPublisher
-
</listener-class>
-
</listener>
-
-
-
<session-config>
-
<session-timeout>20</session-timeout>
-
</session-config>
-
</web-app>
接下来是spring security3的一些配置,具体的每一个是什么意思,网上很多资料,这里不赘述了。总之,需要根据自己的需求,进行相应的修改。
-
<?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"
-
xmlns:aop="http://www.springframework.org/schema/aop"
-
xmlns:tx="http://www.springframework.org/schema/tx"
-
xmlns:context="http://www.springframework.org/schema/context"
-
xmlns:mvc="http://www.springframework.org/schema/mvc"
-
xsi:schemaLocation="
-
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
-
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
-
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
-
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
-
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
-
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd">
-
<http pattern="/css/**" security="none"></http>
-
<http pattern="/images/**" security="none"></http>
-
<http pattern="/img/**" security="none"></http>
-
<http pattern="/scripts/**" security="none"></http>
-
<http pattern="/font-awesome/**" security="none"></http>
-
<http pattern="/system/resources/**" security="none"></http>
-
<http pattern="/system/login.do" security="none"/>
-
<http auto-config="true" use-expressions="true">
-
<form-login login-page="/system/login.do" default-target-url="/system/sysManage.do"/>
-
<!--
-
error-if-maximum-exceeded 后登陆的账号会挤掉第一次登陆的账号
-
session-fixation-protection 防止伪造sessionid攻击,用户登录成功后会销毁用户当前的session。
-
-->
-
<!-- <session-management invalid-session-url="/user/timedout" session-fixation-protection="none">
-
<concurrency-control max-sessions="1" error-if-maximum-exceeded="true"/>
-
</session-management> -->
-
<custom-filter ref="myFilterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/>
-
</http>
-
-
<authentication-manager alias="authenticationManager">
-
<authentication-provider
-
user-service-ref="myUserDetailsServiceImpl">
-
-
</authentication-provider>
-
</authentication-manager>
-
-
<beans:bean id="myFilterSecurityInterceptor" class="com.product.sys.security.MyFilterSecurityInterceptor">
-
-
<beans:property name="accessDecisionManager" ref="myAccessDescisionManager" />
-
-
<beans:property name="fisMetadataSource" ref="mySecurityMetadataSource" />
-
-
<beans:property name="authenticationManager" ref="authenticationManager" />
-
</beans:bean>
-
<beans:bean id="mySecurityMetadataSource" class="com.product.sys.security.MySecurityMetadataSource"><beans:constructor-arg name="userMapper" ref="userMapper"></beans:constructor-arg></beans:bean>
-
<beans:bean id="myAccessDescisionManager" class="com.product.sys.security.MyAccessDescisionManager"></beans:bean>
-
-
</beans:beans>
到上面的这个配置文件,则是重中之重了,和ss3打交道,主要都是这个文件。简单说一下,我们需要实现一个自己的filter,在配置中就是myFilterSecurityInterceptor,而这个filter中,还需要我们额外注入三个bean,分别是accessDecisionManager、fisMetadataSource以及authenticationManager,这三个属性中除了fisMetadataSource可以自定义名称外,其他两个都在ss3的父类中定义好了,所以此处需要特别注意,在这里掉过坑了。另外这里说一下这三个分别的作用,accessDecisionManager中有decide(Authentication
authentication, Object object,Collection<ConfigAttribute> configAttributes)方法,该方法用于判断当前用户是否有权限进行操作,参数中authentication包含了当前用户所拥有的权限,configAttributes中包含了进行该步骤需要的权限,对其进行对比就可以判断该用户是否有权限进行操作。
-
/**
-
* @description 访问决策器,决定某个用户具有的角色,是否有足够的权限去访问某个资源 ;做最终的访问控制决定
-
-
*/
-
public class MyAccessDescisionManager implements AccessDecisionManager{
-
-
@Override
-
public void decide(Authentication authentication, Object object,
-
Collection<ConfigAttribute> configAttributes)
-
throws AccessDeniedException, InsufficientAuthenticationException {
-
// TODO Auto-generated method stub
-
System.out.println("MyAccessDescisionManager.decide()------------------验证用户是否具有一定的权限--------");
-
if(configAttributes==null) return;
-
Iterator<ConfigAttribute> it = configAttributes.iterator();
-
while(it.hasNext()){
-
String needResource = it.next().getAttribute();
-
//authentication.getAuthorities() 用户所有的权限
-
for(GrantedAuthority ga:authentication.getAuthorities()){
-
if(needResource.equals(ga.getAuthority())){
-
return;
-
}
-
}
-
}
-
throw new AccessDeniedException("--------MyAccessDescisionManager:decide-------权限认证失败!");
-
-
}
-
-
@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;
-
}
-
-
}
到这里,可以很自然的想到是权限和用户数据从哪里得到的,filterInvocationSecurityMetadataSource在被加载时候,会首先将权限的信息建立起来,这里我用一个map,key为url,value为该权限的名称,这一步是在构造方法中进行的,也就是服务器启动时候完成的。而当用户访问某一个地址时,ss会到该类中调用getAttributes(Object obj)方法,obj中包含了访问的url地址,我们需要做的就是将该url对应的权限名称返回给ss,而ss会将返回的这个对象,其实就是accessDecisionManager的decide方法中的configAttributes对象。
-
-
-
-
-
-
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
-
-
private UserMapper userMapper;
-
public UserMapper getUserMapper() {
-
return userMapper;
-
}
-
-
public void setUserMapper(UserMapper userMapper) {
-
this.userMapper = userMapper;
-
}
-
-
-
private static Map<String,Collection<ConfigAttribute>> resourceMap = null;
-
private AntPathMatcher urlMatcher = new AntPathMatcher();
-
-
public MySecurityMetadataSource(UserMapper userMapper) {
-
this.userMapper = userMapper;
-
loadResourcesDefine();
-
}
-
-
@Override
-
public Collection<ConfigAttribute> getAllConfigAttributes() {
-
return null;
-
}
-
-
private void loadResourcesDefine(){
-
resourceMap = new HashMap<String,Collection<ConfigAttribute>>();
-
-
System.out.println("MySecurityMetadataSource.loadResourcesDefine()--------------开始加载资源列表数据--------");
-
List<RolePO> roles = userMapper.findAllRoles();
-
for(RolePO role : roles){
-
List<ResourcePO> resources = role.getResources();
-
for(ResourcePO resource : resources){
-
Collection<ConfigAttribute> configAttributes = null;
-
ConfigAttribute configAttribute = new SecurityConfig(resource.getResourceName());
-
if(resourceMap.containsKey(resource.getUrl())){
-
configAttributes = resourceMap.get(resource.getUrl());
-
configAttributes.add(configAttribute);
-
}else{
-
configAttributes = new ArrayList<ConfigAttribute>() ;
-
configAttributes.add(configAttribute);
-
resourceMap.put(resource.getUrl(), configAttributes);
-
}
-
}
-
}
-
System.out.println("11");
-
Set<String> set = resourceMap.keySet();
-
Iterator<String> it = set.iterator();
-
while(it.hasNext()){
-
String s = it.next();
-
System.out.println("key:"+s+"|value:"+resourceMap.get(s));
-
}
-
}
-
-
-
-
@Override
-
public Collection<ConfigAttribute> getAttributes(Object obj)
-
throws IllegalArgumentException {
-
-
String url = ((FilterInvocation)obj).getRequestUrl();
-
System.out.println("MySecurityMetadataSource:getAttributes()---------------请求地址为:"+url);
-
Iterator<String> it = resourceMap.keySet().iterator();
-
while(it.hasNext()){
-
String _url = it.next();
-
if(_url.indexOf("?")!=-1){
-
_url = _url.substring(0, _url.indexOf("?"));
-
}
-
if(urlMatcher.match(_url,url)){
-
System.out.println("MySecurityMetadataSource:getAttributes()---------------需要的权限是:"+resourceMap.get(_url));
-
return resourceMap.get(_url);
-
}
-
-
}
-
Collection<ConfigAttribute> nouse = new ArrayList<ConfigAttribute>();
-
nouse.add(new SecurityConfig("无相应权限"));
-
return nouse;
-
}
-
-
@Override
-
public boolean supports(Class<?> arg0) {
-
System.out.println("MySecurityMetadataSource.supports()---------------------");
-
return true;
-
}
-
-
}
到这里,我们还有一个疑问,就是decide方法中的authentication对象(authentication.getAuthorities()包含当前用户拥有的权限),用户的对应角色和权限信息是从哪里获得的?其实这里是通过调用MyUserDetailsServiceImpl来获取的,该类需要实现UserDetailService接口,更具体一些实际上是通过loadUserByUsername进行获取用户权限信息的,这里注意返回的User不是我们自己定义的PO,而是ss3框架中的User。(这里说下为什么我自己的UserPO没有继承ss的User,就是因为User没有默认无参构造方法,导致mybatis无法创建对象,具体可能还是有办法的,比如重写mybatis的相关接口,比较麻烦,所以这里是先通过返回我们自己的UserPO后,再组装成ss需要的User对象进行的)这里在回到刚才AccessDescisionManager中的decide方法想一下,authentication.getAuthorities()其实获得的就是下面的Collection<GrantedAuthority>类型的对象。
最后下面的这段代码,我没有直接从username中直接获得resource,而是通过先获得role,再通过role获取resource,我感觉这样方便一些,sql也简单,当然有更好的可以替换掉。
-
@Component("myUserDetailsServiceImpl")
-
public class MyUserDetailsServiceImpl implements UserDetailsService{
-
-
@Resource
-
private UserMapper userMapper;
-
-
@Override
-
public UserDetails loadUserByUsername(String username)
-
throws UsernameNotFoundException {
-
System.out.println("username is " + username);
-
UserPO user = userMapper.getUserByUserName(username);
-
if(user == null) {
-
throw new UsernameNotFoundException(username);
-
}
-
Collection<GrantedAuthority> grantedAuths = obtionGrantedAuthorities(user);
-
System.out.println(user.getUsername());
-
return new User(
-
user.getUsername(),
-
user.getPassword(),
-
true,
-
true,
-
true,
-
true,
-
grantedAuths);
-
}
-
-
-
private Set<GrantedAuthority> obtionGrantedAuthorities(UserPO user) {
-
Set<GrantedAuthority> authSet = new HashSet<GrantedAuthority>();
-
List<RolePO> roles = user.getRoles();
-
-
for(RolePO role : roles) {
-
RolePO innerRole = userMapper.getRoleByRoleName(role.getRoleName());
-
List<ResourcePO> tempRes = innerRole.getResources();
-
for(ResourcePO res : tempRes) {
-
authSet.add(new GrantedAuthorityImpl(res.getResourceName()));
-
}
-
}
-
return authSet;
-
}
-
-
-
}
到这里,所有的权限-角色-用户信息已经可以串起来了。再来梳理一下流程,启动服务器时,通过FilterInvocationSecurityMetadataSource获得用户的所有角色及权限信息,当用户登陆时,通过MyUserDetailsServiceImpl中的loadUserByUsername获得该登陆用户所有的权限,发出请求时,通过FilterInvocationSecurityMetadataSource的getAttributes(Object
url)获得需要的权限名,最后在AccessDecisionManager中decide方法进行对比,如果用户拥有的权限名称和该url需要的权限名相同,那么放行,否则认证失败!清楚这些后,我们还需要一个filter,把上述流程串起来,就像提葡萄一样~
-
-
-
-
-
-
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter{
-
-
private FilterInvocationSecurityMetadataSource fisMetadataSource;
-
-
-
-
-
-
-
@Override
-
public Class<?> getSecureObjectClass() {
-
return FilterInvocation.class;
-
}
-
-
@Override
-
public SecurityMetadataSource obtainSecurityMetadataSource() {
-
return fisMetadataSource;
-
}
-
-
@Override
-
public void destroy() {}
-
-
@Override
-
public void doFilter(ServletRequest request, ServletResponse response,
-
FilterChain chain) throws IOException, ServletException {
-
System.out.println("------------MyFilterSecurityInterceptor.doFilter()-----------开始拦截器了....");
-
FilterInvocation fi = new FilterInvocation(request, response, chain);
-
InterceptorStatusToken token = super.beforeInvocation(fi);
-
-
try {
-
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
-
} catch (Exception e) {
-
e.printStackTrace();
-
}finally{
-
super.afterInvocation(token,null);
-
}
-
-
System.out.println("------------MyFilterSecurityInterceptor.doFilter()-----------拦截器该方法结束了....");
-
}
-
-
@Override
-
public void init(FilterConfig config) throws ServletException {
-
-
}
-
-
-
public void setFisMetadataSource(
-
FilterInvocationSecurityMetadataSource fisMetadataSource) {
-
this.fisMetadataSource = fisMetadataSource;
-
}
-
public FilterInvocationSecurityMetadataSource getFisMetadataSource() {
-
return fisMetadataSource;
-
}
-
-
}
如果全部照搬上边的代码,到这里就已经结束了。
但是昨天晚上遇到一个大坑,就是发现如果我在数据库中配置了该用户的相关权限url后,用户可以访问,如果用户没有该url的权限,该用户依然可以访问url,这是让我无比吃惊,因为大部分都是参考网络的资料写的,后来看了一下ss的源码,才发现可能是其他人写错了。这里简单说一下,因为单位电脑没有ss的源码,主要问题出在MyFilterSecurityInterceptor中的doFilter方法:InterceptorStatusToken token = super.beforeInvocation(fi); 当ss在未匹配到url的权限时,即MySecurityMetadataSource中的getAttributes返回的对象为空时,该方法beforeInvocation直接return
null,而实际decide方法在下方并未运行。
-
protected InterceptorStatusToken beforeInvocation(Object object) {
-
-
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
-
.....
-
}
-
-
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
-
-
if (attributes == null || attributes.isEmpty()) {
-
if (rejectPublicInvocations) {
-
throw new IllegalArgumentException("Secure object invocation " + object +
-
" was denied as public invocations are not allowed via this interceptor. "
-
+ "This indicates a configuration error because the "
-
+ "rejectPublicInvocations property is set to ‘true‘");
-
}
-
-
if (debug) {
-
logger.debug("Public object - authentication not attempted");
-
}
-
-
publishEvent(new PublicInvocationEvent(object));
-
-
return null;
-
}
-
-
if (debug) {
-
logger.debug("Secure object: " + object + "; Attributes: " + attributes);
-
}
-
-
if (SecurityContextHolder.getContext().getAuthentication() == null) {
-
credentialsNotFound(messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",
-
"An Authentication object was not found in the SecurityContext"), object, attributes);
-
}
-
-
Authentication authenticated = authenticateIfRequired();
-
-
-
try {
-
this.accessDecisionManager.decide(authenticated, object, attributes);
-
}
-
catch (AccessDeniedException accessDeniedException) {
-
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException));
-
-
throw accessDeniedException;
-
}
-
-
if (debug) {
-
logger.debug("Authorization successful");
-
}
-
-
if (publishAuthorizationSuccess) {
-
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
-
}
-
-
-
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
-
-
if (runAs == null) {
-
if (debug) {
-
logger.debug("RunAsManager did not change Authentication object");
-
}
-
-
-
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
-
} else {
-
if (debug) {
-
logger.debug("Switching to RunAs Authentication: " + runAs);
-
}
-
-
SecurityContext origCtx = SecurityContextHolder.getContext();
-
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
-
SecurityContextHolder.getContext().setAuthentication(runAs);
-
-
-
return new InterceptorStatusToken(origCtx, true, attributes, object);
-
}
-
}
在我看的所有BLOG中,当匹配不到时,全部返回了Null,而当我追到 super.beforeInvocation(fi)源码中时,发现当getAttributes返回null后,ss就会跳过AccessDecisionManager的decide方法,导致未进行判断!从而ss会让用户请求顺利的通过。之后,查了一下ss官方英文文档,如下描述:
Collection<ConfigAttribute> getAttributes(Object object)
throws IllegalArgumentException
Accesses the ConfigAttribute
s that apply to a given secure object.
-
Parameters:
object
- the object being secured-
Returns:
- the attributes that apply to the passed in secured object. Should return an empty collection if there are no applicable attributes.
-
Throws:
IllegalArgumentException
-
if the passed object is not of a type supported by the SecurityMetadataSource
implementation
红色标出了,应当返回一个空的对象集合如果没有相应权限的时候。而其他blog文返回的是null,导致后续跳过了decide方法!所以我在MySecurityMetadataSource中的getAttributes中写的是:
-
Collection<ConfigAttribute> nouse = new ArrayList<ConfigAttribute>();
-
nouse.add(new SecurityConfig("无相应权限"));
-
return nouse;
这样当没有权限时,才可以正常拦截!现在博文抄来抄去,正确的还好,但凡有错误。。真是坑死人。SpringSecurity3.2.5自定义角色及权限的教程
标签:步骤 detail efi load indexof collect 启动服务 keyword 2.3
原文地址:http://blog.csdn.net/swebin/article/details/70273739