SSO单点登录—CAS统一身份认证
单点登录SSO(Single Sign ON)
如:在学校登录了OA系统,再打开考试系统、教务系统,都会实现自动登录。
统一身份认证CAS(Central Authentication Service)
CAS 是由耶鲁大学发起的企业级开源项目,历经20多年的完善,具有较高的稳定性、安全性。国内多数高校的SSO都基于CAS。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
@RequestMapping("/sso") public class SSOController { @RequestMapping("/myLogin.do") public void ssoLogin(HttpServletRequest request, HttpServletResponse response) throws Exception{ CasUtil casUtil = new CasUtil(); casUtil.login(request, response, new CasUtil.ClientSystem() { @Override public boolean doLogin(CasVO casVO) { // 获取CAS服务账户信息 String account = casVO.getAccount(); // TODO 根据CAS登录业务系统 return true; } }); } }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.zxz.sso.controller; /** * CAS配置类 * 业务单点登录接口url : http://192.168.1.76/sso/myLogin.do * targetUrl(登录成功后进入系统的页面) : http://192.168.1.76/main.html * base64编码targetUrl: aHR0cDovLzE5Mi4xNjguMS43Ni9tYWluLmh0bWw= * service(CAS服务器登录需要的service参数) : http://192.168.1.76/sso/myLogin.do?targetUrl=base64aHR0cDovLzE5Mi4xNjguMS43Ni9tYWluLmh0bWw= * loginUrl(拼接成的CAS服务登录url) : http://localhost:8080/cas/login?service=http%3A%2F%2F192.168.1.76%2Fsso%2FmyLogin.do%3FtargetUrl%3Dbase64aHR0cDovLzE5Mi4xNjguMS43Ni9tYWluLmh0bWw%3D * 验证票据的地址(获取CAS账户信息的地址) : http://localhost:8080/cas/serviceValidate?ticket=ST-11-N1w7Z-WjrjQDRFl5Y120MmgBZa0DESKTOP-KMSEFVL&service=http%3A%2F%2F192.168.1.76%2Fsso%2FmyLogin.do%3FtargetUrl%3Dbase64aHR0cDovLzE5Mi4xNjguMS43Ni9tYWluLmh0bWw%3D */ public class CasConfig { // CAS根地址 public static String CAS_BASE_PATH = "http://localhost:8080/cas/"; // 业务系统登录入口 public static String MY_LOGIN_URI = "sso/myLogin.do"; // CAS票据验证地址 public static String CAS_VALIDATE_URL = CAS_BASE_PATH + "serviceValidate"; // CAS登录地址 public static String CAS_LOGIN_URL = CAS_BASE_PATH + "login"; //登录成功默认跳转地址 public static String DEF_TARGET_URI = "main.html"; // 默认编码字符串格式 public static String UTF_8 = "UTF-8"; // SESSION中判断是否登录的KEY public static String LOGIN_KEY = "isCasLogin"; // 业务系统认证集成失败提示页 public static String SSO_ERROR_URI = "error.html"; }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.zxz.sso.controller; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import sun.misc.BASE64Decoder; import sun.misc.BASE64Encoder; import javax.net.ssl.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.*; import java.lang.reflect.Field; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.nio.charset.Charset; import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; /** * CAS服务器认证工具类 * 单点登录只需要调用login(request, response, clientSystem); */ public class CasUtil { /** * 单点登录接口调用方法 * @param request * @param response * @throws IOException */ public void login(HttpServletRequest request, HttpServletResponse response, ClientSystem clientSystem) throws IOException { // 1.首先验证客户是否登录CAS服务器 HttpSession session = request.getSession(); boolean isLogin = checkLogin(session); String targetUrl = getTargetUrl(request); if (isLogin) { // 2.如果已经登录了CAS服务器,则跳转到 targetUrl response.sendRedirect(targetUrl); } else { // 3.如果没有登录CAS服务器,则去验证Ticket boolean hasTicket = checkHasTicket(request); if (hasTicket) { // 3.1如果有票据,则进行验证 CasVO casVO = checkTicket(request); if (casVO.isLogin() && clientSystem.doLogin(casVO)){ // 3.2给session中写入登录标识(用于checkLogin) session.setAttribute(CasConfig.LOGIN_KEY, true); // 3.3登录成功跳转至业务系统url response.sendRedirect(targetUrl); } else { // cas账户信息异常,跳转配置的错误页面 String errorUrl = getErrorUrl(request); response.sendRedirect(errorUrl); } } else { // 3.2如果ticket不存在 String loginUrl = getLoginUrl(request); System.err.println("loginUrl:"+loginUrl); response.sendRedirect(loginUrl); } } } private CasVO checkTicket(HttpServletRequest request) throws IOException { // 1.获取票据验证的url String serviceValidateUrl = getServiceValidateUrl(request); System.out.println("验证票据的地址:" + serviceValidateUrl); // 2.get请求获取CAS服务器的登录信息 String casUserInfoXml = doGet(serviceValidateUrl); casUserInfoXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + casUserInfoXml; // 3.解析返回的xml结果 System.out.println(casUserInfoXml); CasVO casVO = resolveCasXml(casUserInfoXml); return casVO; } /** * 业务系统的登录接口 */ interface ClientSystem { boolean doLogin(CasVO casVO); } /** * 解析CAS账户信息 * @param casUserInfoXml * @return */ private CasVO resolveCasXml(String casUserInfoXml) { final String CAS_PREFIX = "cas:"; final String LOGIN_SUCCESS_KEY = CAS_PREFIX + "authenticationSuccess"; final String ACCOUNT_KEY = CAS_PREFIX + "user"; final String ATTRIBUTES_KEY = CAS_PREFIX + "attributes"; CasVO casVO = new CasVO(); if (casUserInfoXml == null || "".equals(casUserInfoXml)) { return casVO; } DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); InputStream in = null; try { DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); in = IOUtils.toInputStream(casUserInfoXml, Charset.forName(CasConfig.UTF_8)); Document rootDoc = docBuilder.parse(in); NodeList successNodeList = rootDoc.getElementsByTagName(LOGIN_SUCCESS_KEY); if (successNodeList.getLength() > 0) { Node successNode = successNodeList.item(0); Document successDocument = successNode.getOwnerDocument(); NodeList accountNodeList = successDocument.getElementsByTagName(ACCOUNT_KEY); if (accountNodeList != null && accountNodeList.getLength() > 0) { Node accountNode = accountNodeList.item(0); Node accountText = accountNode.getFirstChild(); String nodeValue = accountText.getNodeValue(); casVO.setAccount(nodeValue); } NodeList attrsNodeList = successDocument.getElementsByTagName(ATTRIBUTES_KEY); if (attrsNodeList.getLength() > 0) { Node attrsNode = attrsNodeList.item(0); if (attrsNode.hasChildNodes()) { Document attrsDoc = attrsNode.getOwnerDocument(); Field[] fields = casVO.getClass().getDeclaredFields(); for (Field field : fields) { String fieldName = field.getName(); String attrTagName = CAS_PREFIX + fieldName; NodeList attrNodeList = attrsDoc.getElementsByTagName(attrTagName); if (attrNodeList.getLength() > 0) { Node attrNode = attrNodeList.item(0); Node attrText = attrNode.getFirstChild(); if (attrText != null) { field.set(casVO, attrText.getNodeValue().trim()); } } } } } } } catch (Exception e) { // 解析用户信息失败! e.printStackTrace(); } finally { IOUtils.closeQuietly(in); } return casVO; } /** * 获取验证票据的url * @param request * @return */ private String getServiceValidateUrl(HttpServletRequest request) throws IOException { // 1.获取targetUrl String targetUrl = getTargetUrl(request); System.err.println("targetUrl:"+targetUrl); // 2.targetUrl进行base64编码 String base64TargetUrl = new BASE64Encoder().encode(targetUrl.getBytes()); System.err.println("base64编码的targetUrl:"+base64TargetUrl); // 3.获取业务service的根路径 String serviceUrlRoot = getBasePath(request) + CasConfig.MY_LOGIN_URI; // 4.组装service参数 String serviceUrl = serviceUrlRoot + "?targetUrl=base64" + base64TargetUrl; System.err.println("原service:"+serviceUrl); String service = URLEncoder.encode(serviceUrl, CasConfig.UTF_8); // 5.获取ticket String ticket = request.getParameter("ticket"); // 6.组装CAS服务器验证票据的地址 String ticketUrl = CasConfig.CAS_VALIDATE_URL + "?ticket=" + ticket + "&service="+service; return ticketUrl; } /** * 获取登录成功后跳转的url * @param request * @return http://192.168.1.76/main.html * @throws IOException */ private String getTargetUrl(HttpServletRequest request) throws IOException { String basePath = getBasePath(request); String targetUrl = request.getParameter("targetUrl"); if (targetUrl == null || "".equals(targetUrl)) { targetUrl = basePath + CasConfig.DEF_TARGET_URI; } else { if (targetUrl.startsWith("base64")){ targetUrl = targetUrl.substring("base64".length()); byte[] bytes = new BASE64Decoder().decodeBuffer(targetUrl); targetUrl = new String(bytes, CasConfig.UTF_8); System.err.println("解码之后的target:"+targetUrl); } } return targetUrl; } /** * 获取cas账户信息错误的异常界面 * @param request * @return */ private String getErrorUrl(HttpServletRequest request) { String basePath = getBasePath(request); String errorUrl = basePath + CasConfig.SSO_ERROR_URI; return errorUrl; } /** * 获取项目的根路径 * @param request * @return */ private String getBasePath(HttpServletRequest request) { String scheme = request.getScheme(); // 协议 String serverName = request.getServerName(); // 域名或者ip int serverPort = request.getServerPort(); // 端口 String contextPath = request.getContextPath(); String url = ""; if ((serverPort == 80) || (serverPort == 443)) { url = scheme + "://" + serverName + contextPath + "/"; } else { url = scheme + "://" + serverName + ":" + serverPort + contextPath + "/"; } return url; } /** * 获取CAS服务器登录地址 * @param request * @return http://localhost:8080/cas/login?service=http%3A%2F%2F192.168.1.76%2FmyLogin.do%3FtargetUrl%3Dbase64aHR0cDovLzE5Mi4xNjguMS43Ni9tYWluLmh0bWw%3D * @throws IOException */ private String getLoginUrl(HttpServletRequest request) throws IOException { // 1.获取targetUrl String targetUrl = getTargetUrl(request); System.err.println("targetUrl:"+targetUrl); // 2.targetUrl进行base64编码 String base64TargetUrl = new BASE64Encoder().encode(targetUrl.getBytes()); System.err.println("base64编码的targetUrl:"+base64TargetUrl); // 3.获取业务service的根路径 String serviceUrlRoot = getBasePath(request) + CasConfig.MY_LOGIN_URI; // 4.组装service参数 String serviceUrl = serviceUrlRoot + "?targetUrl=base64" + base64TargetUrl; String service = URLEncoder.encode(serviceUrl, CasConfig.UTF_8); // 5.组装CAS登录的url String loginUrl = CasConfig.CAS_LOGIN_URL+"?service="+service; return loginUrl; } /** * 检查ticket * @param request * @return */ private boolean checkHasTicket(HttpServletRequest request) { Object ticket = request.getParameter("ticket"); if (ticket == null) { return false; } else { return !String.valueOf(ticket).isEmpty(); } } /** * 检查登录 * @param session * @return */ private boolean checkLogin(HttpSession session) { Object isLogin = session.getAttribute(CasConfig.LOGIN_KEY); Boolean login = Boolean.valueOf(String.valueOf(isLogin)); return login; } /** * get请求 * @param * @return */ private String doGet(String urlStr) throws IOException { URL url = new URL(urlStr); InputStream in = null; HttpURLConnection conn = null; try { skipSSL(); conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(5000); conn.connect(); in = conn.getInputStream(); return IOUtils.toString(in, Charset.forName(CasConfig.UTF_8)); } finally { IOUtils.close(conn); IOUtils.closeQuietly(in); } } /** * 绕过SSL验证 */ private void skipSSL() { try { HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { public boolean verify(String hostname, SSLSession session) { return true; } }); SSLContext context = SSLContext.getInstance("TLS"); context.init(null, new X509TrustManager[]{new X509TrustManager() { public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { } public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }}, new SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory()); } catch (Exception e) { e.printStackTrace(); } } public static abstract class IOUtils { private static final int EOF = -1; private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; public static void close(URLConnection conn) { if (conn instanceof HttpURLConnection) { ((HttpURLConnection) conn).disconnect(); } } public static void closeQuietly(InputStream input) { closeQuietly((Closeable) input); } public static void closeQuietly(Closeable closeable) { try { if (closeable != null) { closeable.close(); } } catch (IOException ioe) { // ignore } } public static String toString(InputStream input, Charset encoding) throws IOException { StringWriter sw = new StringWriter(); copy(input, sw, encoding); return sw.toString(); } public static void copy(InputStream input, Writer output, Charset encoding) throws IOException { encoding = encoding == null ? Charset.defaultCharset() : encoding; InputStreamReader in = new InputStreamReader(input, encoding); copy(in, output); } public static int copy(Reader input, Writer output) throws IOException { long count = copyLarge(input, output); if (count > Integer.MAX_VALUE) { return -1; } return (int) count; } public static long copyLarge(Reader input, Writer output) throws IOException { return copyLarge(input, output, new char[DEFAULT_BUFFER_SIZE]); } public static long copyLarge(Reader input, Writer output, char[] buffer) throws IOException { long count = 0; int n; while (EOF != (n = input.read(buffer))) { output.write(buffer, 0, n); count += n; } return count; } public static InputStream toInputStream(String input, Charset encoding) { return new ByteArrayInputStream(input.getBytes()); } } }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.zxz.sso.controller; /** * 解析的CAS账户信息 */ public class CasVO { private String account; private String userName; public boolean isLogin() { return account != null && !"".equals(account); } public String getAccount() { return account; } public void setAccount(String account) { this.account = account; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } }
作者:[一柒微笑]