客户端如何接收服务端认证信息
上一章节我们分析了CAS服务端是如何产生认证信息的,那么这一章节我们将解析客户端是如何接收服务端的认证信息。这里所说的客户端指的是接入CAS单点登录系统的应用,是一个独立的应用。客户端的源码分析都是基于CAS服务端提供的集成包cas-client-core。
1. 返回到CommonUtils.getResponseFromServer()
在我们分析客户端如何接收服务端认证信息时,可以回头去看看第八章节我们所分析到的最终其实是通过后端发起http请求获取到cas服务端的数据。那么我们回到这里继续分析客户端是如何解析服务端的认证信息。
*CommonUtils.java*
public final class CommonUtils {
public static String getResponseFromServer(final URL constructedUrl, final HttpURLConnectionFactory factory,
final String encoding) {
HttpURLConnection conn = null;
InputStreamReader in = null;
try {
//创建http请求链接
conn = factory.buildHttpURLConnection(constructedUrl.openConnection());
//获取响应流
if (CommonUtils.isEmpty(encoding)) {
in = new InputStreamReader(conn.getInputStream());
} else {
in = new InputStreamReader(conn.getInputStream(), encoding);
}
final StringBuilder builder = new StringBuilder(255);
int byteRead;
//从流中获取数据
while ((byteRead = in.read()) != -1) {
builder.append((char) byteRead);
}
//返回响应结果
return builder.toString();
} catch (final Exception e) {
LOGGER.error(e.getMessage(), e);
throw new RuntimeException(e);
} finally {
closeQuietly(in);
if (conn != null) {
conn.disconnect();
}
}
}
}
in为从CAS单点登录服务端返回的结果(见上一章节),返回xml用户认证信息。
2. 返回到retrieveResponseFromServer()
这里其实只是一个抽象类的调用,返回结果同上一小节xml用户认证信息是一样的。最后还是要看后续的结果是如何解析。
AbstractCasProtocolUrlBasedTicketValidator.java
public abstract class AbstractCasProtocolUrlBasedTicketValidator extends AbstractUrlBasedTicketValidator {
protected final String retrieveResponseFromServer(final URL validationUrl, final String ticket) {
return CommonUtils.getResponseFromServer(validationUrl, getURLConnectionFactory(), getEncoding());
}
}
3. 返回到ticketValidator.validate()
*AbstractUrlBasedTicketValidator.java*
public abstract class AbstractUrlBasedTicketValidator implements TicketValidator {
…………
public final Assertion validate(final String ticket, final String service) throws TicketValidationException {
//计算需要请求到CAS服务端的url地址(注意这里是普通登录模式,所以校验地址为://serviceValidate)
final String validationUrl = constructValidationUrl(ticket, service);
logger.debug("Constructing validation url: {}", validationUrl);
try {
logger.debug("Retrieving response from server.");
//到CAS单点登录服务进行校验,获取返回结果,也就是我们上文一直分析的内容
final String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);
if (serverResponse == null) {
throw new TicketValidationException("The CAS server returned no response.");
}
logger.debug("Server response: {}", serverResponse);
//解析从服务端获取到的信息
return parseResponseFromServer(serverResponse);
} catch (final MalformedURLException e) {
throw new TicketValidationException(e);
}
}
…………
}
parseResponseFromServer:解析返回值,也就是解析xml格式的响应信息。这是最重要的一点,从响应中解析出对应的数据信息,在上一章节中已经分析了服务端响应的
XML的格式,因此这里就会解析出服务端响应数据的值。
4. 返回到客户端第3个filter
*AbstractTicketValidationFilter.java*
public abstract class AbstractTicketValidationFilter extends AbstractCasFilter {
public final void doFilter(
......
try {
//通过ticket和service去校验ticket,并根据服务端响应结果构建assertion用户认证信息
final Assertion assertion = this.ticketValidator.validate(ticket,
constructServiceUrl(request, response));
logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName());
request.setAttribute(CONST_CAS_ASSERTION, assertion);
if (this.useSession) {
//如果需要使用session,则把获取到的assertion认证信息放入会话
request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);
}
onSuccessfulValidation(request, response, assertion);
if (this.redirectAfterValidation) {
//如果需要重定向,认证成功后重定向到客户端的service地址
logger.debug("Redirecting after successful ticket validation.");
response.sendRedirect(constructServiceUrl(request, response));
return;
}
} catch (final TicketValidationException e) {
logger.debug(e.getMessage(), e);
onFailedValidation(request, response);
if (this.exceptionOnValidationFailure) {
throw new ServletException(e);
}
//认证失败响应错误码403到前端
response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
return;
}
}
filterChain.doFilter(request, response);
}
…… ……
}
此抽象类的功能是将用户认证信息存入request的Attribute和request.getSession(),便于客户端应用获取。设置认证完成后的重定向地址response.sendRedirect(constructServiceUrl(request, response));重定向地址为:http://localhost:8088/cas-client/。
5. 重定向后再次调用第1个filter
重定向后,重新进入我们配置的filters。首先进入第1个filter,因为没有请求退出,没做什么就进入下一个filter。第2个filter,因session里assertion存在,走到下面代码后,进入第3个认证的filter中。
*AuthenticationFilter.java*
public class AuthenticationFilter extends AbstractCasFilter {
final HttpSession session = request.getSession(false);
final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;
//此时由于在第4节中已经设置了对应的值,因此assertion不为空,进入下一个过滤器
if (assertion != null) {
filterChain.doFilter(request, response);
return;
}
第3个filter,因为重定向后,此时ticket==null,省略了校验ticket的过程,相当于什么都没做,进入第4个filter。
6. 调用第4个filter
第四个过滤器在客户端的org.jasig.cas.client.util包中。
*HttpServletRequestWrapperFilter.java*
public final class HttpServletRequestWrapperFilter extends AbstractConfigurationFilter {
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
//从session中获取principal
final AttributePrincipal principal = retrievePrincipalFromSessionOrRequest(servletRequest);
//包装request请求
filterChain.doFilter(new CasHttpServletRequestWrapper((HttpServletRequest) servletRequest, principal),
servletResponse);
}
retrievePrincipalFromSessionOrRequest获取Principal,代码如下表:
protected AttributePrincipal retrievePrincipalFromSessionOrRequest(final ServletRequest servletRequest) {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpSession session = request.getSession(false);
//先判断session是否为空,不为空从session中获取assertion,否则从request中获取
final Assertion assertion = (Assertion) (session == null ? request
.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION) : session
.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION));
//如果assertion认证信息不为空就获取principal
return assertion == null ? null : assertion.getPrincipal();
}
简单总结就是从request或session里获取assertion,再从assertion里获取Principal。通过new CasHttpServletRequestWrapper((HttpServletRequest) servletRequest, principal)向request请求中添加principal属性,重新包装servletRequest,用于继续下一个filter。
*HttpServletRequestWrapperFilter.java的内部类*
final class CasHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final AttributePrincipal principal;
CasHttpServletRequestWrapper(final HttpServletRequest request, final AttributePrincipal principal) {
super(request);
this.principal = principal;
}
public Principal getUserPrincipal() {
return this.principal;
}
public String getRemoteUser() {
return principal != null ? this.principal.getName() : null;
}
…… ……
}
说白了这里的request请求的warp封装,就是方便客户端应用可以通过getRemoteUser()得到登录用户名。
7. 调用第5个filter
AssertionThreadLocalFilter.java
public final class AssertionThreadLocalFilter implements Filter {
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpSession session = request.getSession(false);
//获取assertion
final Assertion assertion = (Assertion) (session == null ? request
.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION) : session
.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION));
try {
//将assertion认证信息放入到线程中
AssertionHolder.setAssertion(assertion);
filterChain.doFilter(servletRequest, servletResponse);
} finally {
AssertionHolder.clear();
}
}
public void destroy() {
// nothing to do
}
}
这个过滤器通过代码分析可以看到AssertionHolder.setAssertion(assertion) 将Assertion绑定到本地线程ThreadLocal,那么后面的Fileter和Servlet就可以在本地线程中取得Assertion对象。
8. 获取用户认证信息并展示
最终当所有filter完成过滤后,通过我们自己的测试代码中spring找到“/”的映射。
@Controller
public class IndexController {
@RequestMapping("/")
@ResponseBody
public String index(HttpServletRequest request, HttpServletResponse response) {
String result = "execute test method</br>";
result +="request.sessionId: " + request.getSession().getId()+"</br>";
result +="request.getRemoteUser():" + request.getRemoteUser() +"</br>";
result +="request.getUserPrincipal():" + request.getUserPrincipal() +"</br>";
return result;
}
最终我们在自己的controller中可通过request.getRemoteUser()或者request.getUserPrincipal()获取当前登录认证后的用户信息。当然你要从会话中取对应的认证信息也是可行的。那么我们到此是真正的完成了所以的单独登录认证过程,重新回到了我们客户端应用系统中,那么剩下的就是客户端中的一系列的操作了。