zoukankan      html  css  js  c++  java
  • 基于Spring Security2与 Ext 的权限管理设计与兑现

    基于Spring Security2与 Ext 的权限管理设计与实现

    一、Spring Security介绍

    Spring Security的前身Acegi,其配置及使用相对来说复杂一些,因为要配置的东西比较多,Sprng Security简化了以前的配置。大家有兴趣可以多多了解以前的版本,因为很多细节在前面的版本可以看得比较清楚,后面的版本需要看源代码才知道其实现的原理了。

    基于角色的设计与实现是绝大部分系统中比较常见的权限管理方式,对权限进行分组进行管理有助于减少权限管理的复杂程度。

    Spring Security目前提供了三种权限的管理方式:

    一、    基于URL的拦截方式
    二、    基于方法的AOP拦截方式
    三、    基于数据的权限拦截方式

    第一种是常见的权限管理方式,第二种有时也可以通过第一种去进行实现,方法级的拦截容易实现,不过要以友好的方式显示权限不足设计及实现就有些啰嗦。基于数据级的安全拦截实现上得更麻烦,在此不作介绍,我们只是针对第一种方式作设计。Spring Security在URL上已经提供了比较好的管理,不过其是以类似以下这种方式进行配置的。

    <http auto-config='true'>
        <intercept-url pattern="/login.jsp*" filters="none"/> 
        <intercept-url pattern="/**" access="ROLE_USER" />
        <form-login login-page='/login.jsp'/>
    </http>

    这种要动态实现角色与权限的管理就显得有些不足了,因此需要进行扩展实现,我们可以把一些不需要进行安全拦截的url放在Spring的以上配置中,设置filters=”none”,如images,css,js等,提高访问的速度。


    在扩展Spring Security之前,我们需要了解一下Spring Security的相关术语。

    Authentication (认证)对象
       其实就是一个可以通过Spring Security的认证的身份证明。如实现该接口的类UsernamePasswordAuthenticationToken,表示可以通过username及password作为身份验证。

     Authentication对象包含了

    •  Principal  标识是哪一个对象,可以认为是用户
    •    Credentials 信任的对象,如密码。
    •  Authorities 权限的集合,在我们的系统中可以认为是角色的集合 (authorities要赋予给principal的)


     SecurityContextHolder
        是Spring Security的核心对象,是安全上下文的访问的入口。如取得当前的登录用户可以从该类中的相应的方法取得。该类中包含ThreadLocal私有属性用于存取SecurityContext,         SecurityContext包含Authentication私有属性。如实现弹出窗口登录功能的时候,输入的用户名及密码并没有最终经过SPRING SECURITY的filter,那么如何使用得当前用户可以成功登录呢,其就是利用到这一点,通过该类拿到SecurityContext,然后设置一个认证的对象给它,SPRING SECURITY在看到该认证对象的时候,就会成功经过了身份的认证了。

    其实也可以这样理解,为了处理Http请求间认证,Spring Security使用HttpSessionIntegrationFilter,HttpSessionIntegrationFilter用于在HttpSession存储Http请求间的SecurityContext。不过我们可以通过SecurityContextHolder去拿到这个SecurityContext
    AuthenticationManager
        通过Providers 验证 在当前 ContextHolder中的Authentication对象是否合法。
    AccessDecissionManager
        经过投票机制来审批是否批准操作
    Interceptors(拦截器)
        拦截器(如FilterSecurityInterceptor,JoinPoint,MethodSecurityInterceptor等)用于协调授权,认证等操作。

    Spring Security是Spring中一个强大的安全管理框架,不过目前在我们系统中使用的仅是其中一部分的功能,则权限过滤安全检查的功能。如果抛开这个框架,我们实现权限管理的时候,可能使用最多的方案还是使用Filter来进行过滤,在Filter里判断当前的用户是否为登录用户,若是登录用户,则看是否有权限访问当前的资源,若为未登录用户,则跳至登录页面。

    二、扩展Spring Security

    扩展Spring Security基于角色的管理策略,通过角色分配,保证系统的安全。其安全的手段包括以下:

    1.    登录时需要加上验证码
    2.    所有的数据展示及访问页需要登录后才能访问
    3.    用户的数据库密码存储时使用Sha-256的加密算法
    4.    登录后的所有系统的访问URL均需要授权
    5.    登录多少次失败后,可锁定IP,约20分钟后才能自动解锁。(尚未实现)

    权限设计目前是采用基于角色控制的方式,用户需要访问系统的资源,首先必须要授予一个角色,而该角色具有访问系统资源的权限的能力,也可以认为是权限的集合。因此,一个用户要访问系统的某个资源(如产品列表),则首先要授予一个能够访问产品列表资源的角色(如productAdmin)。只要任一个用户拥有了该角色,即可以访问该资源。
       
    系统的安全涉及到两个不同的概念,认证和授权。前者是关于确认用户是否确实是他们所宣称的身份。用户进入系统的时候,首先要进行第一个操作就是进行身份认证,即Authentication。在系统中一般表现为用户用账号跟密码登录。如果都正确了,则可以登录系统。在现实中你可以这样理解,员工在进入公司之前,需要进行身份的确认。身份确认通过后,则可以进入公司。进入公司后,并不代表可以随便进入公司的每个办公室。这时就需要每个看当前员工具有哪些角色,即授权。授权则是关于确认用户是否有允许执行一个特定的操作。如当前员工是总经理,则可以进入总经理办公室,并且可以进入普通员工的办公区域。是因为总经理已经授权可以出入这些地方。
    在本系统中,权限表现为功能菜单及系统访问的URL。
        如:
            添加用户,其访问的url为 /system/saveAppUser.do
            删除用户,其访问的url为/system/deleteAppUser.do
            查询用户,其访问的url为/system/listAppUser.do
    因而用户、角色、权限之间的关系可以用如下的图描述:

    Spring EXT 角色 权限

    表设计


    一个用户可以有多个角色,每个角色有多个功能菜单,每个功能菜单会对应多个系统访问的URL
    表设计如下所示:
    系统功能 权限设计

    表说明:
    1.    app_user系统用户表,放置系统的所有用户
    2.    user_role用户角色,放置系统的所有角色
    3.    app_role角色表,放置用户角色
    4.    role_fun角色对应的功能表,放置角色拥有的功能
    5.    app_function系统的功能表,放置系统参与授权的所有功能
    6.    fun_url系统的功能对应的权限URL表

    目前我们需要扩展Spring Security的以下两部分功能
    1. 身份认证
    2. 授权

    Spring Security是由一组的filter来进行统一的过滤,不同的filter进行相应的权限过滤功能。不过在Security跟spring集成的过程中,其是由一个代理的类进行这些filter的统一管理。可以在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>*.do</url-pattern>
       </filter-mapping>
       <filter-mapping>
    	  <filter-name>springSecurityFilterChain</filter-name>
    	  <url-pattern>/index.jsp</url-pattern>
       </filter-mapping>
       <filter-mapping>
    	  <filter-name>springSecurityFilterChain</filter-name>
    	  <url-pattern>/file-upload</url-pattern>
       </filter-mapping>

    所有经过springSecurityFilterChain的url,都会转到DelegatingFilterProxy类的bean去处理。而该Bean在Spring Security 2.0中,已经内置于安全管理的缺省的配置当中,我们只需要把app-security.xml加入我们系统管理中来即可。如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <b:beans xmlns="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xmlns:b="http://www.springframework.org/schema/beans"
                 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
                  http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd"> 
        <http auto-config="true" access-denied-page="/403.jsp" lowercase-comparisons="true" 
         	access-decision-manager-ref="accessDecisionManager">
            <intercept-url pattern="/images/**" filters="none"/>
            <intercept-url pattern="/css/**" filters="none"/>
            <intercept-url pattern="/js/**" filters="none"/>
            <intercept-url pattern="/403*" filters="none"/>
            <intercept-url pattern="/404*" filters="none"/>
            <intercept-url pattern="/500*" filters="none"/> 
            <intercept-url pattern="/ext3/**" filters="none"/>
            <intercept-url pattern="/fckeditor/**" filters="none"/>  
            <intercept-url pattern="/jsonStruts**" filters="none"/>
            <form-login default-target-url="/index.jsp" login-page="/login.jsp" authentication-failure-url="/login.jsp?error=true" login-processing-url="/j_security_check" />
            <logout logout-url="/j_logout.do" logout-success-url="/login.jsp"/> 
       		<remember-me key="RememberAppUser"/>
        </http>
    	
    	 <b:bean id="accessDecisionManager" class="org.springframework.security.vote.AffirmativeBased">
    	 	<b:property name="allowIfAllAbstainDecisions" value="false"/>
            <b:property name="decisionVoters">
                <b:list>
                    <b:bean class="org.springframework.security.vote.RoleVoter">
                        <b:property name="rolePrefix" value="" />
                    </b:bean>
                    <b:bean class="org.springframework.security.vote.AuthenticatedVoter" />
                </b:list>
            </b:property>
        </b:bean>
    
    	<authentication-manager alias="authenticationManager"/>
    		
        <authentication-provider user-service-ref="appUserDao">
        	<password-encoder hash="sha-256" base64="true"/>
        </authentication-provider>
    
        <b:bean id="securityInterceptorFilter" class="com.htsoft.core.web.filter.SecurityInterceptorFilter" >
    		<custom-filter after="FILTER_SECURITY_INTERCEPTOR" />
    		<b:property name="securityDataSource" ref="securityDataSource"/>
    	</b:bean>
    	
    	<b:bean id="securityDataSource" class="com.htsoft.core.security.SecurityDataSource">
    		<b:property name="appRoleService" ref="appRoleService"/>
    		<b:property name="anonymousUrls">
    			<b:set>
    				<b:value>/login.do</b:value>
    				<b:value>/check.do</b:value>
    			</b:set>
    		</b:property>
    		<b:property name="publicUrls">
    			<b:set>
    				<b:value>/modelsMenu.do</b:value>
    				<b:value>/itemsMenu.do</b:value>
    				<b:value>/file-upload</b:value>
    				<b:value>/index.jsp</b:value>
    				
    				<b:value>/communicate/listPhoneBook.do</b:value>
    				<b:value>/communicate/listPhoneGroup.do</b:value>
    				<b:value>/communicate/moveMail.do</b:value>
    				<b:value>/communicate/listMailFolder.do</b:value>
    				<b:value>/communicate/removeMailFolder.do</b:value>
    				<b:value>/communicate/searchMail.do</b:value>
    				
    				<b:value>/system/getAppUser.do</b:value>
    				<b:value>/system/checkDiary.do</b:value>
    				<b:value>/system/selectDepartment.do</b:value>
    				<b:value>/system/listAppRole.do</b:value>
    			</b:set>
    		</b:property>
    	</b:bean>
    </b:beans>
    

    身份认证

    说明:当用户登录时,会根据用户账号及密码进行验证,验证由authenticationManager来进,其会调用实现UserDetailsService接口实现类完成,在本系统,是由appUserDaoImpl类来实现。

    而我们的用户及角色实体要成为安全框架识别的安全实体,需要相应实现不同的接口,如下所示:

     

    访问授权

    授权的管理是通过Filter来进行的,用户访问URL时,均需要经过Spring Security的URL进行授权。在本系统中,这个功能是通过SecurityInterceptorFilter来进行。

    系统启动时,会把所有的权限以[角色—URL列表]的形式放置在一个全局的Map中,用户访问系统的url时,就会根据当前用户所拥有的角色是否包含此URL。这个全局的权限匹配源则由SecurityDataSource来提供。由于登录用户在进入系统后,都会具备一些常用的功能,所以每个用户均有一个PUBLIC_ROLE的角色,代表可以访问系统的公告资源。该角色对应的可访问的URL,则配置在SecurityDataSource Bean中的publicUrls属性中。

    SecurityInterceptorFilter代码如下所示:

    package com.htsoft.core.web.filter;
    /*
     *  广州宏天软件有限公司 OA办公管理系统   -- http://www.jee-soft.cn
     *  Copyright (C) 2008-2009 GuangZhou HongTian Software Company
    */
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Set;
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.security.AccessDeniedException;
    import org.springframework.security.Authentication;
    import org.springframework.security.GrantedAuthority;
    import org.springframework.security.context.SecurityContextHolder;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    import com.htsoft.core.security.SecurityDataSource;
    /**
     * 权限拦载器
     * @author csx
     */
    public class SecurityInterceptorFilter extends OncePerRequestFilter {
    	
    	/**
    	 * 角色权限映射列表源,用于权限的匹配
    	 */
    	private HashMap<String, Set<String>> roleUrlsMap=null;
    	
    	private SecurityDataSource securityDataSource;
    
    	public void setSecurityDataSource(SecurityDataSource securityDataSource) {
    		this.securityDataSource = securityDataSource;
    	}
    
    	@Override
    	protected void doFilterInternal(HttpServletRequest request,
    			HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
    		if(logger.isDebugEnabled()){
    			logger.debug("...enter the SecurityInterceptorFilter doFilterInternal here...");
    		}
    		String url=request.getRequestURI();
    		//若有contextPath,则切出来
    		if(org.springframework.util.StringUtils.hasLength(request.getContextPath())){
    			String contextPath=request.getContextPath();
    			int index=url.indexOf(contextPath);
    			if(index!=-1){
    				url=url.substring(index+contextPath.length());
    			}
    		}
    		
    		Authentication auth= SecurityContextHolder.getContext().getAuthentication();//取得认证器
    		
    		boolean isSuperUser=false;
    		for(int i=0;i<auth.getAuthorities().length;i++){
    			//logger.info("角色名称:"+auth.getAuthorities()[i].getAuthority());
    			if("超级管理员".equals(auth.getAuthorities()[i].getAuthority())){
    				isSuperUser=true;
    			}
    		}
    		if(!isSuperUser){//非超级管理员
    			if(!isUrlGrantedRight(url,auth)){//如果未授权
    				logger.info("ungranted url:" + url);
    				throw new AccessDeniedException("Access is denied! Url:" + url + " User:" + SecurityContextHolder.getContext().getAuthentication().getName());
    			}
    		}
    		if(logger.isInfoEnabled()){
    			logger.info("pass the url:" + url);
    		}
    		//进行下一个Filter
    		chain.doFilter(request, response);
    	}
    	
    	/**
    	 * 检查该URL是否授权访问
    	 * @param url
    	 * @return
    	 */
    	private boolean isUrlGrantedRight(String url,Authentication auth){
    		
    		//遍历该用户下所有角色对应的URL,看是否有匹配的
    		for(GrantedAuthority ga:auth.getAuthorities()){
    			Set<String> urlSet=roleUrlsMap.get(ga.getAuthority());
    			//TODO AntPathMatcher here
    			if(urlSet!=null && urlSet.contains(url)){
    				return true;
    			}
    		}
    		return false;
    	}
    	
    	public void loadDataSource(){
    		roleUrlsMap=securityDataSource.getDataSource();
    	}
    	
    	@Override
    	public void afterPropertiesSet() throws ServletException {
    		loadDataSource();
    		if(roleUrlsMap==null){
    			throw new RuntimeException("没有进行设置系统的权限匹配数据源");
    		}
    	}
    }
    

     三、EXT的扩展实现

    至此,我们完成了对Spring Security的权限扩展,但是EXT访问的时候,我们的应用程序都是在一个页面上进行,也就是我们之前说的One Application One Page,几乎所有的请求都是通过Ajax的请求来时行,页面没有刷新,当权限不足的时候,我们如何提示用户呢?另外我们的功能菜单又是如何来根据用户的角色来显示出来呢?在此,我们把需要把角色、功能、权限URL需要进行统一管理。我们从以下几个方面来进行扩展。


    当用户权限不足时,我们需要提示用户无限访问该URL,在app-sercurity.xml中,我们配置了以下:

    <http auto-config="true" access-denied-page="/403.jsp" lowercase-comparisons="true" 
         	access-decision-manager-ref="accessDecisionManager">
            <intercept-url pattern="/images/**" filters="none"/>
            <intercept-url pattern="/css/**" filters="none"/>
            <intercept-url pattern="/js/**" filters="none"/>
            <intercept-url pattern="/403*" filters="none"/>
            <intercept-url pattern="/404*" filters="none"/>
            <intercept-url pattern="/500*" filters="none"/> 
            <intercept-url pattern="/ext3/**" filters="none"/>
            <intercept-url pattern="/fckeditor/**" filters="none"/>  
            <intercept-url pattern="/jsonStruts**" filters="none"/>
            <form-login default-target-url="/index.jsp" login-page="/login.jsp" authentication-failure-url="/login.jsp?error=true" login-processing-url="/j_security_check" />
            <logout logout-url="/j_logout.do" logout-success-url="/login.jsp"/> 
       		<remember-me key="RememberAppUser"/>
        </http>

    即当用户权限不足时,会跳至403页,因此我们可以在此上作文章,当跳至403页时,我们往response的头写一个标识,在ext的connection中获取返回结果时,我们根据这个标识来给用户提示一个友好的信息,如:

    403.jsp的代码实现:

    <%@ page pageEncoding="UTF-8"%><% 
     response.addHeader("__forbidden","true");
     String basePath=request.getContextPath();
    %>
    <html>
     <head>
      <title>访问拒绝</title>
       <style type="text/css">
       <!--
       .STYLE10 {
        font-family: "黑体";
        font-size: 36px;
       }
       -->  
       </style>
     </head>
     <body>
      <table width="510" border="0" align="center" cellpadding="0" cellspacing="0">
       <tr>
         <td><img src="<%=basePath%>/images/error_top.jpg" width="510" height="80" /></td>
         </tr>
       <tr>
         <td height="200" align="center" valign="top" background="<%=basePath%>/images/error_bg.jpg">
          <table width="80%" border="0" cellspacing="0" cellpadding="0">
             <tr>
               <td width="34%" align="right"><img src="<%=basePath%>/images/error.gif" width="128" height="128"></td>
               <td width="66%" valign="bottom" align="center">
                <span class="STYLE10">访问被拒绝</span>
                <div style="text-align: left;line-height: 22px;">
                 <font size="2">对不起,您的当前角色没有查看此页面的权限。请联系您的系统管理员,以获得相应的权限。点击这里返回主页。如果需要技术支持,点击这里发送邮件。</font>
              </div>
              <a href="#" onclick="javascript:document.location.href='<%=basePath%>/j_logout.do';">重 新 登 录</a> 
              <a href="#" onclick="javascript:history.back(-1);">后 退</a>
            </td>
           </table>
           </td>
       </tr>      
       <tr>
         <td><img src="<%=basePath%>/images/error_bootom.jpg" width="510" height="32" /></td>
          </tr>
     </table>
     </body>
    </html>

     处理该标识:

    App.init = function() {
    	Ext.util.Observable.observeClass(Ext.data.Connection);
    	Ext.data.Connection.on('requestcomplete', function(conn, resp,options ){
    		if (resp && resp.getResponseHeader){
    		    if(resp.getResponseHeader('__timeout')) {
    		    	Ext.ux.Toast.msg('操作提示:','操作已经超时,请重新登录!');
    	        	window.location.href=__ctxPath+'/index.jsp?randId=' + parseInt(1000*Math.random());
    	    	}
    	    	if(resp.getResponseHeader('__forbidden')){
    	    		Ext.ux.Toast.msg('系统访问权限提示:','你目前没有权限访问:{0}',options.url);
    	    	}
    		}
    	});
    …
    }
    

    Connection的这个requestcomplete事件是所有的Ajax请求都必须触发的,所以把它作为总的入口。
    另外,用户登录后,其功能的菜单如何来配置呢,因此应用程序是通过一个全局的menu.xml文件来进行功能菜单的管理,同时也包括其功能与URL的配置。

    <?xml version="1.0" encoding="UTF-8"?>
    <Menus>
    	<Items id="SystemSetting" text="系统设置" iconCls="menu-system">
    		...
    		<Item id="AppRoleView" iconCls="menu-role" text="角色设置">
    			<Function id="_AppRoleList" text="查看角色" iconCls="menu-list">
    				<url>/system/listAppRole.do</url>
    			</Function>
    			<Function id="_AppRoleAdd" text="添加角色" iconCls="menu-add">
    				<url>/system/listAppRole.do</url>
    				<url>/system/saveAppRole.do</url>
    			</Function>
    			<Function id="_AppRoleEdit" text="编辑角色" iconCls="menu-add">
    				<url>/system/listAppRole.do</url>
    				<url>/system/saveAppRole.do</url>
    			</Function>
    			<Function id="_AppRoleDel" text="删除角色" iconCls="menu-del">
    				<url>/system/listAppRole.do</url>
    				<url>/system/mulDelAppRole.do</url>
    			</Function>
    			<Function id="_AppRoleGrant" text="授权角色">
    				<url>/system/listAppRole.do</url>
    				<url>/system/grantAppRole.do</url>
    			</Function>
    		</Item>
    		...
    		<Item id="ReportTemplateView" iconCls="menu-report" text="报表管理">
    			...
    		</Item>
    	</Items>
    </Menus>

    这个XML文件会在应用程序启动添加至系统的全局变量中,以“角色”对应“URL”的Map提供数据源来进行。

    那么角色对应的URL是如何来构造的,这个相对简单一些,以上的功能及菜单,其均存在一个Id,如角色设置(AppRoleView),添加角色“_AppRoleAdd”。
    每个角色就保存这些ID,所以加载这些ID,就有办法把其下的URL全部加载出来,从而形成角色与URL的映射关系。

    另外,我们还可以把用户所拥有的权限,通过该用户拥有哪些角色,每个角色包括哪些权限的ID,从而构造出该用户的权限集合。如下所示,当用户登录后,我们把所有的ID集中放在用户的rights字段中,这样就可以通过ID来决定用户是否有权限访问某个功能按钮,从而达到功能级别的控制,如:

    //加载权限
    	Ext.Ajax.request({
    			url:__ctxPath+'/system/getCurrentAppUser.do',
    			method:'Get',
    			success:function(response,options){
    				var object=Ext.util.JSON.decode(response.responseText);
    				//取得当前登录用户的相关信息,包括权限
    				curUserInfo=new UserInfo(object.user.userId,object.user.fullname,object.user.rights);
    			}
    	});
    
    
    

     以下为user.rights的构造,是在用户登录的时候进行配置实现,为AppUserDaoImpl.java的部分代码

    public UserDetails loadUserByUsername(final String username)
    			throws UsernameNotFoundException, DataAccessException {
    		return (UserDetails) getHibernateTemplate().execute(
    				new HibernateCallback() {
    					public Object doInHibernate(Session session)
    							throws HibernateException, SQLException {
    						String hql = "from AppUser ap where ap.username=? and ap.delFlag = ?";
    						Query query = session.createQuery(hql);
    						query.setString(0, username);
    						query.setShort(1, Constants.FLAG_UNDELETED);
    						AppUser user = null;
    						try {
    							user = (AppUser) query.uniqueResult();
    							if (user != null) {
    								Hibernate.initialize(user.getRoles());
    								Hibernate.initialize(user.getDepartment());
    								
    								//进行合并权限的处理
    								Set<AppRole> roleSet=user.getRoles();
    								Iterator<AppRole> it=roleSet.iterator();
    								
    								while(it.hasNext()){
    									AppRole role=it.next();
    									if(role.getRoleId().equals(AppRole.SUPER_ROLEID)){//具有超级权限
    										user.getRights().clear();
    										user.getRights().add(AppRole.SUPER_RIGHTS);
    										break;
    									}else{
    										if(StringUtils.isNotEmpty(role.getRights())){
    											String[]items=role.getRights().split("[,]");
    											for(int i=0;i<items.length;i++){
    												if(!user.getRights().contains(items[i])){
    													user.getRights().add(items[i]);
    												}
    											}
    										}
    									}
    								}
    								
    							}
    						} catch (Exception ex) {
    							logger.warn("user:" + username
    									+ " can't not loding rights:"
    									+ ex.getMessage());
    						}
    						return user;
    					}
    				});
    	}
    
    

    而其最终的实现效果可以参见我另一篇博客:

    http://man1900.iteye.com/blog/517248 

  • 相关阅读:
    分层图最短路(DP思想) BZOJ2662 [BeiJing wc2012]冻结
    动态规划 BZOJ1925 地精部落
    线性DP SPOJ Mobile Service
    线性DP codevs2185 最长公共上升子序列
    数位DP POJ3208 Apocalypse Someday
    线性DP POJ3666 Making the Grade
    杨氏矩阵 线性DP? POJ2279 Mr.Young's Picture Permutations
    tarjan强连通分量 洛谷P1262 间谍网络
    树链剖分 BZOJ3589 动态树
    二分图 BZOJ4554 [Tjoi2016&Heoi2016]游戏
  • 原文地址:https://www.cnblogs.com/mannixiang/p/6659939.html
Copyright © 2011-2022 走看看