SpringSecurity 3.2入门(9)自定义权限控制代码实现
1、 一个自定义的filter,必须包含authenticationManager,accessDecisionManager,securityMetadataSource三个属性,我们的所有控制将在这三个类中实现 。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package cn.jxufe.core.security; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.access.SecurityMetadataSource; import org.springframework.security.access.intercept.AbstractSecurityInterceptor; import org.springframework.security.access.intercept.InterceptorStatusToken; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; public class MySecurityFilter extends AbstractSecurityInterceptor implements Filter { protected Logger logger = LoggerFactory.getLogger(getClass()); // 与applicationContext-security.xml里的myFilter的属性securityMetadataSource对应, // 其他的两个组件,已经在AbstractSecurityInterceptor定义 private FilterInvocationSecurityMetadataSource securityMetadataSource; @Override public SecurityMetadataSource obtainSecurityMetadataSource() { return this.securityMetadataSource; } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { logger.debug("------------MyFilterSecurityInterceptor.doFilter()-----------开始拦截了...."); FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); } private void invoke(FilterInvocation fi) throws IOException, ServletException { // object为FilterInvocation对象 // 1.获取请求资源的权限 // 执行Collection<ConfigAttribute> attributes = // SecurityMetadataSource.getAttributes(object); logger.debug("--------------用户发送请求--------------"); InterceptorStatusToken token = null; token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); }catch (Exception e) { }finally { super.afterInvocation(token, null); } logger.debug("------------MyFilterSecurityInterceptor.doFilter()-----------拦截结束了...."); } public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() { return securityMetadataSource; } public void setSecurityMetadataSource( FilterInvocationSecurityMetadataSource securityMetadataSource) { this.securityMetadataSource = securityMetadataSource; } public void init(FilterConfig arg0) throws ServletException { // TODO Auto-generated method stub } public void destroy() { // TODO Auto-generated method stub } public Class<? extends Object> getSecureObjectClass() { // 下面的MyAccessDecisionManager的supports方面必须放回true,否则会提醒类型错误 return FilterInvocation.class; } }
2、用于启动时加载资源列表,还拥有判断是否拥有请求访问资源权限的方法。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package cn.jxufe.core.security; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Map.Entry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.util.AntPathMatcher; import cn.jxufe.core.dao.BaseDao; import cn.jxufe.core.entiry.Resource; import cn.jxufe.core.entiry.Role; public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { protected Logger logger = LoggerFactory.getLogger(getClass()); private AntPathMatcher urlMatcher = new AntPathMatcher(); private static Map<String, Collection<ConfigAttribute>> resourceMap = null; private BaseDao baseDao; public BaseDao getBaseDao() { return baseDao; } public void setBaseDao(BaseDao baseDao) { this.baseDao = baseDao; } //由spring调用 public MyFilterInvocationSecurityMetadataSource(BaseDao baseDao) { this.baseDao=baseDao; loadResourceDefine(); } //加载所有资源 private void loadResourceDefine() { logger.debug("容器启动(MySecurityMetadataSource:loadResourceDefine)"); logger.debug("--------------开始加载系统资源与权限列表数据--------------"); resourceMap = new HashMap<String,Collection<ConfigAttribute>>(); String sql="select * from t_system_resource_info"; List<Resource> resources = this.baseDao.findListBeanByArray(sql, Resource.class); for(Resource resource : resources){ Collection<ConfigAttribute> configAttributes = null; ConfigAttribute configAttribute = new SecurityConfig(resource.getName()); if(resourceMap.containsKey(resource.getPath())){ configAttributes = resourceMap.get(resource.getPath()); configAttributes.add(configAttribute); }else{ configAttributes = new ArrayList<ConfigAttribute>() ; configAttributes.add(configAttribute); resourceMap.put(resource.getPath(), configAttributes); } } logger.debug("--------------系统资源与权限列表数据加载结束--------------"); } /** * 根据请求的资源地址,获取它所拥有的权限 */ public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { //获取请求的url地址 String requestUrl = ((FilterInvocation) object).getRequestUrl(); logger.debug("--------------取得请求资源所需权限(MySecurityMetadataSource:getAttributes)--------------"); logger.debug("--------------请求地址为:"+requestUrl+"--------------"); 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,requestUrl)) return resourceMap.get(_url); } return null; //如果是想做到没配的资源默认可以访问的话,那么就返回空或者NULL /** * 使用下面的写法代替return null;没配的资源则不可以访问,建议开发的时候还是用上面的为好。 */ // Collection<ConfigAttribute> returnCollection = new ArrayList<ConfigAttribute>(); // returnCollection.add(new SecurityConfig("ROLE_NO_USER")); // return returnCollection; } public Collection<ConfigAttribute> getAllConfigAttributes() { Set<ConfigAttribute> allAttributes = new HashSet<ConfigAttribute>(); for (Entry<String, Collection<ConfigAttribute>> entry : resourceMap.entrySet()) { allAttributes.addAll(entry.getValue()); } logger.debug(allAttributes.toString()); return allAttributes; } public Collection<ConfigAttribute> getConfigAttributes(String...value) { return SecurityConfig.createList(value); } public boolean supports(Class<?> clazz) { return true; } }
3、访问决策器,决定某个用户具有的角色,是否有足够的权限去访问某个资源 ;做最终的访问控制决定。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package cn.jxufe.core.security; import java.util.Collection; import java.util.Iterator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; public class MyAccessDecisionManager implements AccessDecisionManager { protected Logger logger = LoggerFactory.getLogger(getClass()); /** * 认证用户是否具有权限访问该url地址 * */ public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { logger.debug("--------------匹配用户拥有权限和请求权限(MyAccessDecisionManager:decide)--------------"); logger.debug("--------------验证用户是否具有一定的权限--------------"); if(configAttributes == null) { return; } //所请求的资源拥有的权限(一个资源对多个权限) Iterator<ConfigAttribute> iterator = configAttributes.iterator(); while(iterator.hasNext()) { ConfigAttribute configAttribute = iterator.next(); //访问所请求资源所需要的权限 String needPermission = configAttribute.getAttribute(); logger.debug("--------------访问所请求资源所需要的权限为 " + needPermission+"--------------"); //用户所拥有的权限authentication for(GrantedAuthority ga : authentication.getAuthorities()) { if(needPermission.equals(ga.getAuthority())) { logger.debug("--------------权限验证通过--------------"); return; } } } //没有权限让我们去捕捉 logger.debug("--------------权限验证未通过--------------"); throw new AccessDeniedException(" 没有权限访问!"); } /** * 启动时候被AbstractSecurityInterceptor调用,决定AccessDecisionManager是否可以执行传递ConfigAttribute。 */ public boolean supports(ConfigAttribute attribute) { // System.out.println("MyAccessDescisionManager.supports()------------"+attribute.getAttribute()); return true; } /** * 被安全拦截器实现调用,包含安全拦截器将显示的AccessDecisionManager支持安全对象的类型 */ public boolean supports(Class<?> clazz) { logger.debug("MyAccessDescisionManager.supports()--------------------------------"); return true; } }
4、验证登录信息。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package cn.jxufe.core.security; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import cn.jxufe.core.dao.BaseDao; import cn.jxufe.core.entiry.User; public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private static final String USERNAME = "username"; private static final String PASSWORD = "password"; private static final String KAPTCHA = "kaptcha"; private BaseDao baseDao; public BaseDao getBaseDao() { return baseDao; } public void setBaseDao(BaseDao baseDao) { this.baseDao = baseDao; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (!request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); String password = obtainPassword(request); String kaptcha = obtainKaptcha(request); logger.debug("--------------用户:" + username +"正在登录---"+kaptcha+"-----------"); // 验证码校验 String kaptchaExpected = (String) request.getSession().getAttribute(com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY); if(!kaptcha.equals(kaptchaExpected)){ BadCredentialsException exception = new BadCredentialsException("验证码不匹配!"); throw exception; } // 账号与密码校验 username = username.trim(); String sql="select * from t_system_user_info where username=? and password=?"; Object[] args=new Object[]{ username, password }; User users = (User) this.baseDao.findUniqueBeanByArray(sql, User.class, args); if (users == null || !users.getPassword().equals(password)) { BadCredentialsException exception = new BadCredentialsException("用户名或密码不匹配!"); throw exception; } // 实现 Authentication UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); // 允许子类设置详细属性 setDetails(request, authRequest); // 运行UserDetailsService的loadUserByUsername 再次封装Authentication return this.getAuthenticationManager().authenticate(authRequest); } @Override protected String obtainUsername(HttpServletRequest request) { Object obj = request.getParameter(USERNAME); return null == obj ? "" : obj.toString(); } @Override protected String obtainPassword(HttpServletRequest request) { Object obj = request.getParameter(PASSWORD); return null == obj ? "" : obj.toString(); } protected String obtainKaptcha(HttpServletRequest request) { Object obj = request.getParameter(KAPTCHA); return null == obj ? "" : obj.toString(); } @Override protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { super.setDetails(request, authRequest); } }
5、登录成功后,根据用户名,返回一个Userdetail。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package cn.jxufe.core.security; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import cn.jxufe.core.dao.BaseDao; import cn.jxufe.core.entiry.Resource; @Component("userDetailsService") public class MyUserDetailsService implements UserDetailsService { protected Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private JdbcTemplate jdbcTemplate; @Autowired private BaseDao baseDao; //登录验证 @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { logger.debug("------------验证并授权(MyUserDetailsServiceImpl:loadUserByUsername)------------"); // 根据用户名获取帐户和权限信息 String sql = "SELECT username,password,enabled,name rname " + "FROM t_system_user_info u,t_system_role_info r,t_system_user_role ur " + "WHERE u.id=ur.user_id AND r.id=ur.role_id AND username= ?"; // 如果一个用户具有多个权限,连接查询会返回一个List List list =this.jdbcTemplate.queryForList(sql, new Object[] { username }); // 取出帐户和权限信息填充到User中返回 if (list == null || list.size() <= 0) // spring-security定义的异常 throw new UsernameNotFoundException("用户不存在!"); // 如果用户存在 Map<String, Object> map = (Map<String, Object>) list.get(0); // 密码 String password = (String) map.get("password"); // 帐户是否可用 boolean enabled = ((Integer) map.get("enabled") == 1); boolean accountNonExpired = true; boolean credentialsNonExpired = true; boolean accountNonLocked = true; // 帐户所具有的权限 Set<GrantedAuthority> authSet = new HashSet<GrantedAuthority>(); List<Resource> resources=this.getResourceByUsername(username); logger.debug("------------用户所拥有权限------------"); for (int i=0;i<resources.size();i++) { GrantedAuthority authority = new SimpleGrantedAuthority(resources.get(i).getName()); authSet.add(authority); } logger.debug("------------"+authSet.toString()+"------------"); // spring-security提供的类 User userdetail = new User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authSet); return userdetail; } private List<Resource> getResourceByUsername(String username){ String sql ="SELECT * FROM t_system_resource_info WHERE id IN(" + "SELECT DISTINCT resource_id FROM t_system_authority_resource WHERE authority_id IN(" + "SELECT authority_id FROM t_system_role_authority WHERE role_id IN(" + "SELECT role_id FROM t_system_user_role WHERE user_id =( " + "SELECT id FROM t_system_user_info WHERE username= ? ))))"; List<Resource> list =this.baseDao.findListBeanByArray(sql, Resource.class, new Object[] { username }); return list; } }
6、配置信息
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:security="http://www.springframework.org/schema/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-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd"> <!-- 配置不过滤的资源(静态资源及登录相关) --> <security:http pattern="/**/*.css" security="none" /> <security:http pattern="/**/*.js" security="none" /> <security:http pattern="/**/*.jpg" security="none" /> <security:http pattern="/**/*.jpeg" security="none" /> <security:http pattern="/**/*.gif" security="none" /> <security:http pattern="/**/*.png" security="none" /> <security:http pattern="/favicon.ico" security="none" /> <!-- 不过滤验证码 --> <security:http pattern="/captcha-image.htm" security="none" /> <!-- 不过滤登录页面 --> <security:http pattern="/login.htm" security="none" /> <security:http pattern="/login.jsp" security="none" /> <!-- 不过滤首页 --> <security:http pattern="/index.htm" security="none" /> <security:http pattern="/index.jsp" security="none" /> <!-- 配置SpringSecurity的http安全服务 --> <!-- 使用了 use-expressions="true 则 需使用hasRole('ROLE_USER')--> <!-- 配置了auto-config="true"loginFilter报错,如果你没有自定义的登录页面,它就会跳转到security默认的登录页面中。 --> <security:http access-denied-page="/accessDenied.jsp" entry-point-ref="authenticationProcessingFilterEntryPoint"> <security:session-management> <security:concurrency-control max-sessions="1" /> </security:session-management> <!-- 检测失效的sessionId,session超时时,定位到另外一个URL --> <security:session-management invalid-session-url="/sessionTimeOut.jsp" /> <!-- 配置登出信息,指定退出系统后,跳转页面 --> <security:logout logout-url="/logout" logout-success-url="/login.htm" invalidate-session="true" /> <!-- 认证和授权 --> <security:custom-filter ref="myLoginFilter" position="FORM_LOGIN_FILTER" /> <security:custom-filter ref="securityFilter" before="FILTER_SECURITY_INTERCEPTOR"/> </security:http> <!-- 认证管理器,配置SpringSecutiry的权限信息 --> <security:authentication-manager> <security:authentication-provider> <!-- 使用数据库中的用户名和密码 --> <security:jdbc-user-service data-source-ref="dataSource" /> </security:authentication-provider> </security:authentication-manager> <!-- 验证配置 , 认证管理器,实现用户认证的入口,主要实现UserDetailsService接口即可 --> <security:authentication-manager alias="myAuthenticationManager"> <!-- 使用自己数据库中的用户和角色表,获取用户拥有的权限 --> <security:authentication-provider user-service-ref="myUserDetailsServiceImpl" /> </security:authentication-manager> <!-- 登录验证器 --> <bean id="myLoginFilter" class="cn.jxufe.core.security.MyUsernamePasswordAuthenticationFilter"> <!-- 处理登录 --> <property name="filterProcessesUrl" value="/j_spring_security_check"></property> <property name="usernameParameter" value="username"></property> <property name="passwordParameter" value="password"></property> <property name="authenticationSuccessHandler" ref="loginLogAuthenticationSuccessHandler"></property> <property name="authenticationFailureHandler" ref="simpleUrlAuthenticationFailureHandler"></property> <property name="authenticationManager" ref="myAuthenticationManager"></property> <property name="baseDao" ref="baseDao"></property> </bean> <bean id="loginLogAuthenticationSuccessHandler" class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler"> <property name="defaultTargetUrl" value="/index.jsp"></property> </bean> <bean id="simpleUrlAuthenticationFailureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"> <property name="defaultFailureUrl" value="/login.jsp"></property> </bean> <!-- 认证过滤器 --> <bean id="securityFilter" class="cn.jxufe.core.security.MySecurityFilter"> <!-- 用户拥有的权限 --> <property name="authenticationManager" ref="myAuthenticationManager" /> <!-- 用户是否拥有所请求资源的权限 --> <property name="accessDecisionManager" ref="myAccessDecisionManager" /> <!-- 资源与权限对应关系 --> <property name="securityMetadataSource" ref="myFilterInvocationSecurityMetadataSource" /> </bean> <bean id="myUserDetailsServiceImpl" class="cn.jxufe.core.security.MyUserDetailsService" /> <bean id="myAccessDecisionManager" class="cn.jxufe.core.security.MyAccessDecisionManager"/> <bean id="myFilterInvocationSecurityMetadataSource" class="cn.jxufe.core.security.MyFilterInvocationSecurityMetadataSource"> <constructor-arg name="baseDao" ref="baseDao" /> </bean> <bean id="baseDao" class="cn.jxufe.core.dao.BaseDaoImpl" /> <!-- 定义上下文返回的消息的国际化 --> <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="basename" value="classpath:org/springframework/seurity/messages_zh_CN" /> </bean> <!-- 未登录的切入点 --> <bean id="authenticationProcessingFilterEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint"> <property name="loginFormUrl" value="/login.jsp"/> </bean> </beans>
(详细的拦截过程将会在下一章中学习)。
作者:何海洋
本博客内容主要以学习、研究和分享为主,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。