Spring-Security入门(配合数据库设置权限,验证码功能)

来源:互联网 发布:世界线收束 知乎 编辑:程序博客网 时间:2024/04/26 07:15

spring-security(基于spring-security3)

  • 由于这次的项目需要对权限进行限制,所以在网上对security进行了学习,刚开始看的时候也是头脑非常混乱的,不过终归还是要一边写一边学,这样才能更好的理解,光是看是看不会的,接下来进入正题。

  • 首先来看看security工作的流程图(取自网上的截图) 

  • 一开始看不懂没关系,等把整个代码写完,在各个地方打上断点再来对照这个图就能理清头绪

  • 这里我一共使用到了4张表(t_manager,t_role,t_power,t_role_power)

    -- ----------------------------  -- Table structure for t_manager  -- ----------------------------  DROP TABLE IF EXISTS `t_manager`;  CREATE TABLE `t_manager` (    `id` bigint(20) NOT NULL AUTO_INCREMENT,    `manager_name` varchar(20) DEFAULT NULL,    `account` varchar(20) DEFAULT NULL,    `password` varchar(32) DEFAULT NULL,    `tel` varchar(13) DEFAULT NULL,    `email` varchar(100) DEFAULT NULL,    `fk_role_id` bigint(20) DEFAULT NULL,    PRIMARY KEY (`id`)  ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;    -- ----------------------------  -- Table structure for t_role  -- ----------------------------  DROP TABLE IF EXISTS `t_role`;  CREATE TABLE `t_role` (    `id` bigint(20) NOT NULL AUTO_INCREMENT,    `role_name` varchar(20) DEFAULT NULL,    `roleType` int(11) DEFAULT NULL,    PRIMARY KEY (`id`)  ) ENGINE=InnoDB AUTO_INCREMENT=36 DEFAULT CHARSET=utf8;    -- ----------------------------  -- Table structure for t_power  -- ----------------------------  DROP TABLE IF EXISTS `t_power`;  CREATE TABLE `t_power` (    `id` bigint(20) NOT NULL AUTO_INCREMENT,    `power_name` varchar(50) DEFAULT NULL,    `url` varchar(50) DEFAULT NULL,    PRIMARY KEY (`id`)  ) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8;  -- ----------------------------  -- Table structure for t_role_power  -- ----------------------------  DROP TABLE IF EXISTS `t_role_power`;  CREATE TABLE `t_role_power` (    `id` bigint(20) NOT NULL AUTO_INCREMENT,    `fk_role_power` bigint(20) DEFAULT NULL,    `fk_power_role` bigint(20) DEFAULT NULL,    PRIMARY KEY (`id`)  ) ENGINE=InnoDB AUTO_INCREMENT=96 DEFAULT CHARSET=utf8;
  • 表建好之后,导入security需要的jar包,然后开始配置

web.xml中的配置(只记录了与security相关的部分)

<context-param><param-name>contextConfigLocation</param-name><param-value>/WEB-INF/classes/spring-security.xml</param-value></context-param>    <listener>      <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>    </listener>     <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>

接下来是security的配置文件

<?xml version="1.0" encoding="UTF-8"?>  <beans:beans xmlns="http://www.springframework.org/schema/security"    xmlns:beans="http://www.springframework.org/schema/beans"    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xsi:schemaLocation="http://www.springframework.org/schema/beans             http://www.springframework.org/schema/beans/spring-beans-3.0.xsd             http://www.springframework.org/schema/security             http://www.springframework.org/schema/security/spring-security-3.1.xsd">      <!-- security="none"表示自己写的登录页面不拦截,还有一些公共资源不拦截 -->      <http pattern="/jsp/power/userLogin.jsp" security="none" />   <http pattern="/jsp/power/managerLogin.jsp" security="none"/>   <http pattern="/login/imgCode" security="none"/>    <http pattern="/resource/**" security="none"/>   <http pattern="/jsp/power/error.jsp" security="none"/>  <http pattern="/jsp/common/*" security="none"/>     <!-- 配置拦截页面   access-denied-page表示权限不通过时的失败页面-->                                  <http auto-config='true' access-denied-page="/jsp/power/error.jsp" use-expressions="true">                 <!-- 设置用户默认登录页面 default-target-url是指从登陆页面进行登陆时,默认的目标页面。如果请求不是从默认登录页面发出的则如果登录成功就进入到请求页面-->          <form-login login-page="/jsp/power/userLogin.jsp" default-target-url="/login/controller" always-use-default-target="true"/>                  <!--定义2个自定义过滤器,第一个过滤器是我们主要要使用到的过滤器,由于我们是要从数据库中取得权限所以会使用到,如果是在配置文件中直接写死的就不需要使用,但是不便于超级管理员随时分配权限,修改权限等。后一个是我在加上验证码功能时使用的过滤器,before是指这个过滤器的顺序必须在security的某些过滤器之前-->        <custom-filter ref="securityFilter" before="FILTER_SECURITY_INTERCEPTOR"/>        <custom-filter ref="ImgCodeFilter" before="FORM_LOGIN_FILTER"/>    </http>      = <!-- 资源权限控制 -->      <beans:bean id="securityFilter" class="com.lovo.netctoss.filter.MySecurityFilter">          <!-- 用户拥有的所有权限 -->          <beans:property name="authenticationManager" ref="myAuthenticationManager" />          <!-- 判断用户是否拥有所请求资源的权限 -->          <beans:property name="accessDecisionManager" ref="myAccessDecisionManager" />          <!-- 数据库中所有资源与权限之间的对应关系 -->          <beans:property name="securityMetadataSource" ref="mySecurityMetadataSource" />      </beans:bean>      <authentication-manager alias="myAuthenticationManager">          <!-- 权限控制 引用 id是myUserDetailsService的server -->          <authentication-provider user-service-ref="myUserDetailsService"/>      </authentication-manager>  </beans:beans>        <!--这个过滤器是用来判断验证码的  -->   <beans:bean id="ImgCodeFilter" class="com.lovo.netctoss.filter.ImgCodeFilter"><beans:property name="authenticationManager" ref="myAuthenticationManager" /> <beans:property name="authenticationSuccessHandler"><beans:bean class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">          <beans:property name="defaultTargetUrl" value="/login/controller"></beans:property></beans:bean></beans:property> <beans:property name="authenticationFailureHandler"><beans:bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">          <beans:property name="defaultFailureUrl" value="/jsp/power/userLogin.jsp"></beans:property></beans:bean></beans:property>   </beans:bean>

myUserDetailsService(登录验证)

注:这个类用户登录使用的,并取得用户拥有的所有权限,通过用户名到数据库进行查询,如果没有查到这个用户,则登录失败留在原网页,如果查出这个用户,则把用户的账号密码和查出的用户拥有的所有权限,封装到security自己的User类中返回(得到的形式是这样的UserDetail: Username: wangnima; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities:ROLE_ADMIN),接下来security会把前台发过来的密码和查到的密码进行比较,看是否登录成功,失败停留在原网页,这一步框架自己解决,不需要我们来判断,只需要返回User即可。import java.util.HashSet;import java.util.Set;import javax.annotation.Resource;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.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.stereotype.Service;import com.lovo.netctoss.operatesys.managermag.beans.ManagerBean;import com.lovo.netctoss.operatesys.managermag.service.IManagerService;@Service("myUserDetailsService")public class MyUserDetailsService implements UserDetailsService{@Resourceprivate IManagerService managerServiceImpl;@Overridepublic UserDetails loadUserByUsername(String account)throws UsernameNotFoundException {// TODO Auto-generated method stubManagerBean manager = managerServiceImpl.selectManagerByAccount(account);Set<GrantedAuthority> grantedAuths=obtionGrantedAuthorities(manager);boolean enables = true;      boolean accountNonExpired = true;      boolean credentialsNonExpired = true;      boolean accountNonLocked = true;  //封装成spring security的user      User userdetail = new User(manager.getManagerAccount(), manager.getManagerPwd(),manager.getManagerName(), enables, accountNonExpired, credentialsNonExpired, accountNonLocked, grantedAuths);             return userdetail;}//查找用户权限  public Set<GrantedAuthority> obtionGrantedAuthorities(ManagerBean manager){      Set<GrantedAuthority> authSet=new HashSet<GrantedAuthority>();     String [] powerNames  = manager.getRole().getPowerLists().split(",");      for(String powerName:powerNames){   authSet.add(new SimpleGrantedAuthority(powerName));   }    return authSet;  }}

mySecurityMetadataSource(资源与权限的关系)

注:我们发送的请求会进入mySecurityMetadataSource这个类中的getAttributes()方法中,String requestUrl = ((FilterInvocation) object).getRequestUrl();可以得到我们刚才发送请求的路径,然后通过loadResourceDefine()方法到数据库中查询所有的权限与对应可访问资源的关系,因为项目管理的权限较少,所以这里采用的是权限和可访问资源之间11的关系,如果复杂的话可以采用1对多,取出存入到resourceMap集合中(得到的是这样的{/jsp/user/*=[ROLE_USER], /jsp/power/*=[ROLE_POWER],/jsp/expense/*=[ROLE_MONEY],/jsp/log/*=[ROLE_LOG],/jsp/manager/*=[ROLE_MANAGER],/jsp/query/*=[ROLE_CHECK],/jsp/**=[ROLE_ADMIN]}),接下来验证用户请求的资源是否需要某种权限,如果需要则返回相应的权限(也就是这一句return resourceMap.get(URLChangeTool.subString(requestUrl)))这个地方通过请求url作为键得到的值是资源对应的权限集合,如果不需要则返回null。import java.util.ArrayList;import java.util.Collection;import java.util.HashMap;import java.util.Iterator;import java.util.List;import java.util.Map;import java.util.Map.Entry;import java.util.Set;import javax.annotation.Resource;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.stereotype.Service;import com.lovo.netctoss.operatesys.powermag.dao.IRoleDao;import com.lovo.netctoss.operatesys.powermag.dao.impl.RoleDaoImpl;import com.lovo.netctoss.pojos.URLResource;import com.lovo.netctoss.util.URLChangeTool;@Service("mySecurityMetadataSource")public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource{@Resourceprivate IRoleDao roleDaoImpl;private static Map<String, Collection<ConfigAttribute>> resourceMap = null;private List<URLResource> findResources;//加载所有资源与权限的关系  private void loadResourceDefine() {if(resourceMap==null){resourceMap = new HashMap<String, Collection<ConfigAttribute>>();findResources = roleDaoImpl.findResource();for(URLResource url_resources:findResources){Collection<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();//configAttribute放入权限名称(ROLE_xxxx    这种形式的,security默认的形式如果想改可以百度)ConfigAttribute configAttribute = new SecurityConfig(url_resources.getRole_name());configAttributes.add(configAttribute);resourceMap.put(url_resources.getAccess_url(), configAttributes);}//以权限名封装为Spring的security ObjectSet<Entry<String, Collection<ConfigAttribute>>> resourceSet = resourceMap.entrySet();          Iterator<Entry<String, Collection<ConfigAttribute>>> iterator = resourceSet.iterator();}} //返回所请求资源所需要的权限@Overridepublic Collection<ConfigAttribute> getAttributes(Object object)throws IllegalArgumentException {// TODO Auto-generated method stubString requestUrl = ((FilterInvocation) object).getRequestUrl();if(resourceMap == null) {          loadResourceDefine();      }<!--当访问的是主页面时,即只要登录过后的所有用户均可以访问,可以直接返回数据库中所有的权限集合,只要用户有其中一个就可以访问-->if(requestUrl.equals("/jsp/main/main.jsp")){Collection<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();for(URLResource url_resources:this.findResources){ConfigAttribute configAttribute = new SecurityConfig(url_resources.getRole_name());configAttributes.add(configAttribute);}return configAttributes;}return resourceMap.get(URLChangeTool.subString(requestUrl));}@Overridepublic boolean supports(Class<?> arg0) {// TODO Auto-generated method stubreturn true ;}@Overridepublic Collection<ConfigAttribute> getAllConfigAttributes() {// TODO Auto-generated method stubreturn null;}}

MySecurityFilter

注:这个类在中间起到的作用是无论是从登录页面发送的请求,还是直接访问你想要的资源,都会被这个过滤器拦截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.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{private FilterInvocationSecurityMetadataSource securityMetadataSource;@Overridepublic void doFilter(ServletRequest req, ServletResponse res,FilterChain chain) throws IOException, ServletException {// TODO Auto-generated method stubFilterInvocation fi=new FilterInvocation(req,res,chain);      invok(fi);}private void invok(FilterInvocation fi) throws IOException, ServletException{InterceptorStatusToken token = null;token = super.beforeInvocation(fi);try {          fi.getChain().doFilter(fi.getRequest(), fi.getResponse());      } finally {          super.afterInvocation(token, null);      }}@Overridepublic Class<?> getSecureObjectClass() {// TODO Auto-generated method stubreturn FilterInvocation.class;}@Overridepublic SecurityMetadataSource obtainSecurityMetadataSource() {// TODO Auto-generated method stubreturn this.securityMetadataSource;}public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {return securityMetadataSource;}public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource securityMetadataSource) {this.securityMetadataSource = securityMetadataSource;}@Overridepublic void init(FilterConfig arg0) throws ServletException {// TODO Auto-generated method stub}@Overridepublic void destroy() {// TODO Auto-generated method stub}}

myAccessDecisionManager(验证用户是否有权限访问该资源)

注authentication是用户拥有的权限,就是我们刚才返回User里面的,configAttributes是请求资源所需要的权限集合,然后循环进行比较,我这里是只要有其中一个就可以通过,如果该用户没有这个权限则抛出AccessDeniedException异常,这时后台会报错,但是会跳转到我们配置的失败页面中,这样一个流程就完成了。    import java.util.Collection;    import java.util.Iterator;        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;    import org.springframework.stereotype.Service;@Service("myAccessDecisionManager")public class MyAccessDecisionManager implements AccessDecisionManager{@Overridepublic void decide(Authentication authentication, Object object,Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,InsufficientAuthenticationException {// TODO Auto-generated method stub//如果请求的资源没有找到权限则放行,表示该资源为公共资源,都可以访问      if(configAttributes == null){                    return ;      }        Iterator<ConfigAttribute> ite=configAttributes.iterator();      while(ite.hasNext()){          ConfigAttribute ca =ite.next();         String needRole = ca.getAttribute();          for(GrantedAuthority ga:authentication.getAuthorities()){                      if(ga.getAuthority().equals("ROLE_ADMIN")){        return;        }                if(needRole.equals(ga.getAuthority())){                    return;              }          }      }      throw new AccessDeniedException("没有权限!!!");        }@Overridepublic boolean supports(ConfigAttribute arg0) {// TODO Auto-generated method stubreturn true;}@Overridepublic boolean supports(Class<?> arg0) {// TODO Auto-generated method stubreturn true;}}
  • 这样也就完成了一个流程,接下来是后面加的验证码功能(自己写一个过滤器对验证码进验证)
import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;    import org.springframework.security.authentication.AuthenticationServiceException;    import org.springframework.security.core.Authentication;    import org.springframework.security.core.AuthenticationException;    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;        public class ImgCodeFilter extends UsernamePasswordAuthenticationFilter {        @Override      public Authentication attemptAuthentication(HttpServletRequest request,              HttpServletResponse response) throws AuthenticationException {        String inputCode = request.getParameter("yzCode");        HttpSession session = request.getSession();    String generateCode = (String) session.getAttribute("code");    if(!inputCode.equals(generateCode)){        throw  new AuthenticationServiceException("验证码错误!");        }                    return super.attemptAuthentication(request, response);    }    }
  • 页面代码
这里要提一句的就是提交的路径必须是j_spring_security_check,输入框name必须是j_username和j_password,这个是security规定好的    <!--管理员登录  -->    <form id="ff" action="/security/j_spring_security_check" onsubmit="return lgcheck()"  method="post" >        <div style="margin-bottom:30px;padding-left:100px;color: rgba(151, 156, 249, 0.98) ">            <label style="font-size: 20px;">欢迎登录!</label>            </div>    <div style="margin-bottom:25px">       <input id="j_username" class="easyui-textbox" name="j_username" style="width:100%"  data-options="validType:'length[5,20]',label:'用户名:',required:true">    </div>    <div style="margin-bottom:25px">        <input id="j_password"  class="easyui-textbox" type="password"  name="j_password" style="width:100%"  data-options="validType:'length[5,10]',label:'密码:',required:true">    </div>    <div style="margin-bottom:25px;position: relative; ">        <input id="yzCode1" class="easyui-textbox" name="yzCode" style="width:55%" data-options="label:'验证码:'">        <img id="img" src="login/imgCode" style="width:30%; height: 20px;position: absolute; margin-left: 10px">        <img id="img2" onclick="javascript:rf()" src="resource/imgs/reload.png" style="margin-left:100px;width:18px;">    </div><input type="submit" value="登录" class="easyui-linkbutton" style="width: 78px;height: 26px;  float:left;  margin-top: 5px;"></form>
  • URLResource类

    public class URLResource {private String role_name;//权限名private String access_url;//该权限可以访问的资源public String getRole_name() {return role_name;}public void setRole_name(String role_name) {this.role_name = role_name;}@Overridepublic String toString() {return "URLResource [role_name=" + role_name + ", access_url="+ access_url + "]";}public String getAccess_url() {return access_url;}public void setAccess_url(String access_url) {this.access_url = access_url;}public URLResource() {super();// TODO Auto-generated constructor stub}  }
  • dao实现类中

    public List<URLResource> findResource() {ArrayList<PowerBean> powers = roleServiceMapper.selectAllPower();ArrayList<URLResource> urlRes = new ArrayList<URLResource>();String urls = null;for(PowerBean power :powers){URLResource urlresources = new URLResource();urls = power.getUrl();urlresources.setRole_name(power.getPowerName());urlresources.setAccess_url(urls);urlRes.add(urlresources);}return urlRes;
  • 最后是我自己写的转换路径的工具类仅供参考

    public class URLChangeTool {public static String subString(String reqUrl){System.out.println(reqUrl);if(reqUrl.matches("/[0-9a-zA-Z]{0,}") || reqUrl.matches("/jsp/[0-9a-zA-Z]{0,}")){//任意返回一个需要权限的资源url,这样因为还未登录所以回到默认登录页面return "/jsp/**";}return reqUrl.substring(0, reqUrl.lastIndexOf("/"))+"/*";    }  }

总结

  • spring security是对url进行限制的(没有登录时,UserDetail默认为字符串"anonymousUser")
  1. 未登录时访问不需要权限的资源
  • request---->MySecurityMetadataSource时,调用getAttribute方法,验证request是否需要权限(这里假定是不需要权限的资源)对应数据库中就没有相应的 权限,此时将 return null相当于这个资源是公共资源,只要登录过就能访问,不会进入MyAccessDecisionManager这个类,但是因为还没有登录所以如果要直接返回默认登录页面这时可以任意返回一个需要权限的资源(URLChangeTool中的有写到),这样就可以回到登录页面。
  1. 未登录时访问需要权限的资源
  • 与上面类似,不过由于return 了某一个权限,所以进入MyAccessDecisionManager这个类进行判断是否能通过,但authentication.getAuthorities()这个方法因为没有登录,所以 得到null,然后抛出异常 throw new AccessDeniedException("没有权限!!!");由于未登录,所以返回登录页面
  1. 登录后访问不需要权限的资源
  • 此时因为getAttribute方法返回null,不进入MyAccessDecisionManager,但经过MyUserDetailsService类后,由于该用户登录过,所以就通过,相当于访问公共资源
  1. 登录后访问需要权限的资源
  • 此时getAttribute方法返回该请求需要的所有权限,进入MyAccessDecisionManager,然后在needRole.equals(authentication.getAuthority()) 如果需要的权限和用户拥有的权限相对应 则通过这个过滤器,如果用户没有相应权限则抛出异常,此时由于登录过所以进入配置文件中配置的access-denied-page没有权限页面
0 0