BSTEK Development Framework2(BDF2) : 3.9.单点登录相关

      在BDF2-CORE当中,默认就提供了对CAS SSO支持,如果我们已经有了现成的,并且已在CAS客户端Server上配置好相关证书信息,那么就可以通过修改BDF2-CORE模块中的相关属性,快速将BDF2应用接入到当前的CAS Server当中,具体需要修改的属性如下:

属性名类型默认值描述示例
bdf2.casLoginUrlString/cas.login.d当采用CAS进行SSO登录时,设置CAS Server的登录页面的URL地址bdf2.casLoginUrl=https://www.bstek.com:8443/cas-server/login
bdf2.casServerUrlString/cas.server设置CAS Server的URL地址bdf2.casServerUrl=https://www.bstek.com:8443/cas-server
bdf2.casClientServerUrlStringhttp://localhost:8080/bdf2-test设置要采用CAS SSO认证的客户端应用的地址bdf2.casClientServerUrl=http://localhost:8080/bdf2-test
bdf2.logoutSuccessURLString/bdf2.core.view.response.LogoutSuccess.d主框架右上角退出系统快捷图标点击时,退出系统成功后跳转的地址,这里设置为CAS SSO的logout,表示在系统内部退出完成(销毁Session之类操作完成)之后,再跳转到CAS SSO的logout进行SSO的登出操作。bdf2.logoutSuccessURL=https://www.bstek.com:8443/cas-server/logout
bdf2.authenticationTypeStringform这个属性目标支持两个值,一个就是默认的form,表示采用BDF2系统提供的登录表单登录;另一个就是cas,表示采用CAS SSO登录。bdf2.authenticationType=cas

      这里需要强调的是BDF2中对于CAS SSO的支持,我们做了完善的功能测试,以保证其不会有问题,所以如果您要使用这一功能,请确保您的CAS Server配置正确,确保部署BDF2应用的客户端Server对于 CAS Server证书配置正确,这样才能保证能把BDF2中CAS SSO支持用起来。一旦出现问题,多数都是您的环境配置问题,与BDF2对CAS SSO支持无关,一句话,您一定要能熟练使用CAS Server来构建SSO环境,不能一知半解,边猜边做。

      如果您需要采用其它的登录方式,这种登录既非系统提供的表单登录,也非CAS的SSO(可能是其它类型的SSO),那么就需要通过下面的两种方式实现。

       第一种方法,就是从BDF2-CORE-1.0.1开始提供的通过实现IRetrivePreAuthenticatedUser方式实现,该接口的源码如下所示:

IRetrivePreAuthenticatedUser接口源码
package com.bstek.bdf2.core.security;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.bstek.bdf2.core.business.IUser;
/**
 * @author Jacky.gao
 * @since 2013年7月5日
 * 获取通过其它方式已经登录的用户信息,比如通过SSO等
 */
public interface IRetrivePreAuthenticatedUser {
	/**
	 * 根据给出的request与response对象,取出当前已通过其它途径预认证的IUser对象,如果返回null表示预认证未通过,系统将不会处理
	 * @param request
	 * @param response
	 * @return 返回已被预认证通过的IUser对象
	 * @throws ServletException
	 */
	IUser retrive(HttpServletRequest request,HttpServletResponse response) throws ServletException;
}

       可以看到,这个接口当中只有一个retrive方法,该方法的作用就是要接口实现类返回当前已预认证通过的IUser接口实现类对象。该接口编写完成后需要配置到Spring环境当中,作为一个标准的Spring Bean,系统会自动检测到该接口实现类,这样用户未登录的情况下访问某个需要登录才能访问的页面时会自动调用这个接口实现类,返回已认证用户信息,从而完成用户自动登录的动作。下面的是一个非常简单的IRetrivePreAuthenticatedUser接口实现类,其源码如下:

测试IRetrivePreAuthenticatedUser接口实现类
package test;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import com.bstek.bdf2.core.business.IUser;
import com.bstek.bdf2.core.model.DefaultUser;
import com.bstek.bdf2.core.security.IRetrivePreAuthenticatedUser;
import com.bstek.bdf2.core.service.IDeptService;
import com.bstek.bdf2.core.service.IGroupService;
import com.bstek.bdf2.core.service.IPositionService;
@Component
public class TestRetrivePreAuthenticatedUser implements IRetrivePreAuthenticatedUser {
	@Autowired
	@Qualifier(IDeptService.BEAN_ID)
	private IDeptService deptService;
	@Autowired
	@Qualifier(IPositionService.BEAN_ID)
	private IPositionService positionService;
	@Autowired
	@Qualifier(IGroupService.BEAN_ID)
	private IGroupService groupService;
	public IUser retrive(HttpServletRequest request,
			HttpServletResponse response) throws ServletException {
		 //从其它源读取登录信息,比如某些硬件卡中读取登录信息等
        DefaultUser user=new DefaultUser("admin");
        user.setCompanyId("bstek");
        //为登录成功的用户设置所在部门、岗位及群组信息
        user.setDepts(deptService.loadUserDepts(user.getUsername()));
        user.setPositions(positionService.loadUserPositions(user.getUsername()));
        user.setGroups(groupService.loadUserGroups(user.getUsername()));
        //为登录成功的用户设置所在部门、岗位及群组信息结束
		return user;
	}
}

       第二种方法就是实现ISecurityInterceptor接口。

       在BDF2当中,除了通过IRetrivePreAuthenticatedUser接口实现获取预认证的登录用户外,还可以通过实现名为ISecurityInterceptor接口,获取取预认证的登录用户,完成用户登录认证。该接口的实现类,同样也需要配置到Spring环境当中,该接口的源码如下:

ISecurityInterceptor接口源码
package com.bstek.bdf2.core.security;
import org.springframework.security.web.context.HttpRequestResponseHolder;
/**
 * 一个供开发人员使用的在登录、认证之前或之后或失败后需要进行业务处理的接口,<br>
 * 开发人员可以根据需要,有选择的覆盖该类中的某个方法,比如需要在用户登录前进行一些处理,那么就可覆盖其中的beforeLogin方法,<br>
 * 依次类推,使用时,将实现类配置到spring当中即可,系统运行时会自动扫描该抽象类实现的存在,如果有就会加载处理
 * @author jacky.gao
 * @since 2013-1-22
 */
public interface ISecurityInterceptor {
    /**
     * 用户登录系统之前进行的处理动作
     * @param holder 一个用于包装HttpRequest/HttpResponse的对象
     */
    void beforeLogin(HttpRequestResponseHolder holder);
    
    /**
     * 用户登录系统成功之后进行的处理动作
     * @param holder 一个用于包装HttpRequest/HttpResponse的对象
     */
    void loginSuccess(HttpRequestResponseHolder holder);
    
    /**
     * 用户登录系统认证失败时需要处理的动作
     * @param holder 一个用于包装HttpRequest/HttpResponse的对象
     */
    void loginFailure(HttpRequestResponseHolder holder);
    
    /**
     * 用户在访问系统资源时(比如访问某URL),系统安全模块对用户进行授权之前需要处理的动作
     * @param holder 一个用于包装HttpRequest/HttpResponse的对象
     */
    void beforeAuthorization(HttpRequestResponseHolder holder);
    /**
     * 用户在访问系统资源时(比如访问某URL),系统安全模块对用户进行授权成功之后需要处理的动作
     * @param holder 一个用于包装HttpRequest/HttpResponse的对象
     */
    void authorizationSuccess(HttpRequestResponseHolder holder);
    /**
     * 用户在访问系统资源时(比如访问某URL或某模块),系统安全模块对用户进行授权失败之后需要处理的动作
     * @param holder 一个用于包装HttpRequest/HttpResponse的对象
     */
    void authorizationFailure(HttpRequestResponseHolder holder);
}

      对于我们上述预认证需求,就可以通过编写这个接口的实现类来实现。因为这个接口当中包含的方法较多,一般情况下,我们只需要实现这个接口当中的一个方法即可,所以我们可以通过扩展系统当中提供的已实现了ISecurityInterceptor接口的SecurityInterceptorAdapter类来实现,这个类中提供了关于ISecurityInterceptor接口所有方法的空实现,所以我们在扩展这个类时只需要覆盖需要的方法即可。比如我们下面的实现类:

DemoSecurityInterceptor类源码
package test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.stereotype.Component;
import com.bstek.bdf2.core.context.ContextHolder;
import com.bstek.bdf2.core.model.DefaultUser;
import com.bstek.bdf2.core.security.SecurityInterceptorAdapter;
import com.bstek.bdf2.core.service.IDeptService;
import com.bstek.bdf2.core.service.IGroupService;
import com.bstek.bdf2.core.service.IPositionService;
@Component
public class DemoSecurityInterceptor extends SecurityInterceptorAdapter {
	@Autowired
	@Qualifier(IDeptService.BEAN_ID)
	private IDeptService deptService;
	@Autowired
	@Qualifier(IPositionService.BEAN_ID)
	private IPositionService positionService;
	@Autowired
	@Qualifier(IGroupService.BEAN_ID)
	private IGroupService groupService;
	@Override
	public void beforeAuthorization(HttpRequestResponseHolder holder) {
		if(ContextHolder.getLoginUser()==null || ContextHolder.getLoginUserName()==null){
			//表示未登录
			//从其它源读取登录信息,比如某些硬件卡中读取登录信息等
			DefaultUser user=new DefaultUser("admin");
			user.setCompanyId("bstek");
			//为登录成功的用户设置所在部门、岗位及群组信息
			user.setDepts(deptService.loadUserDepts(user.getUsername()));
			user.setPositions(positionService.loadUserPositions(user.getUsername()));
			user.setGroups(groupService.loadUserGroups(user.getUsername()));
			//为登录成功的用户设置所在部门、岗位及群组信息结束
			
			//这里的IUser应该是从其它源里读取到的经过认证的合法的用户对象,再转换成IUser对象实例
			//接下来需要将这个user对象放置到session当中及Spring Security的环境当中,以告诉系统已成功登录
			this.registerLoginInfo(user, holder);
		}
	}
}

       上述代码当中比较关键的是最后一句,这个registerLoginInfo方法位于SecurityInterceptorAdapter类当中,它可以把认证的用户对象放到系统环境当中,用以标明用户已登录。

      

两种获取预认证用户的方法比较

 综合比较上述两种方法,我们推荐使用第一种,第一种方法就是为获取已认证的用户,实现自动登录而准备的,所以它看起来更加自然,与系统的结合性也更好。

       还有一种情况,那就是可能我们的系统当中存在多种登录方式,可能根据用户访问的URL后面的参数来决定跳转到哪个登录页面(我也不清楚什么时候会有这种变态需求),如果是这样,上面的代码就需要调整成下面的样子:

修改后的SecurityInterceptor实现类
package test;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.stereotype.Component;
import com.bstek.bdf2.core.context.ContextHolder;
import com.bstek.bdf2.core.security.SecurityInterceptorAdapter;
@Component
public class DemoSecurityInterceptor extends SecurityInterceptorAdapter {
	@Override
	public void beforeAuthorization(HttpRequestResponseHolder holder) {
		if(ContextHolder.getLoginUser()==null || ContextHolder.getLoginUserName()==null){
			//表示未登录
			String loginType=holder.getRequest().getParameter("loginType");
			if(loginType!=null && loginType.equals("abc")){
				throw new MyLoginException();
			}
			if(loginType!=null && loginType.equals("def")){
				throw new MyLogin1Exception();
			}
		}
	}
}

      这里的MyLoginException代码如下:

MyLoginException
package test;
public class MyLoginException extends RuntimeException {
}

      MyLogin1Exception代码与上述基本一样,这里不再罗列了。

      从上述的代码中可以看到,一旦发现要采用某种登录方式,我们就抛一个特定异常(比如MyLoginException等),接下来我们就需要来编写一个IExceptionHandler接口实现类,用于捕获我们之前抛出的异常,一旦捕获,我们可以进行页面跳转等相关我们需要的操作(比如跳转到我们需要的登录页面等),我们的IExceptionHandler接口实现类代码如下:

ExceptionHandler实现类
package test;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.stereotype.Component;
import com.bstek.bdf2.core.exception.IExceptionHandler;
@Component
public class DemoExceptionHandler implements IExceptionHandler {
	public void handle(HttpRequestResponseHolder holder,
			Throwable exception) {
		try{
			if(exception instanceof MyLoginException){
				holder.getResponse().sendRedirect("/login.jsp");
			}
			if(exception instanceof MyLogin1Exception){
				holder.getResponse().sendRedirect("/login.html");
			}
		}catch(Exception ex){
			throw new RuntimeException(ex);
		}
	}
	public boolean support(Throwable exception) {
		return ((exception instanceof MyLoginException) || (exception instanceof MyLogin1Exception));
	}
}

      上述代码比较简单,这里就不再解释了。同样这个实现编写完成之后需要配置到Spring环境当中。利用这么一种机制,大家可以发挥想象,类似的需求都可以通过这一功能机制实现,具体就不再啰嗦了。