单点登录SSO(Single Sign ON)
如:在学校登录了OA系统,再打开考试系统、教务系统,都会实现自动登录。
统一身份认证CAS(Central Authentication Service)
CAS 是由耶鲁大学发起的企业级开源项目,历经20多年的完善,具有较高的稳定性、安全性。国内多数高校的SSO都基于CAS。
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"; }
1 package com.zxz.sso.controller; 2
3 import org.springframework.stereotype.Controller; 4 import org.springframework.web.bind.annotation.RequestMapping; 5
6 import javax.servlet.http.HttpServletRequest; 7 import javax.servlet.http.HttpServletResponse; 8
9 @Controller 10 @RequestMapping("/sso") 11 public class SSOController { 12
13 @RequestMapping("/myLogin.do") 14 public void ssoLogin(HttpServletRequest request, HttpServletResponse response) throws Exception{ 15 CasUtil casUtil = new CasUtil(); 16 casUtil.login(request, response, new CasUtil.ClientSystem() { 17 @Override 18 public boolean doLogin(CasVO casVO) { 19 // 获取CAS服务账户信息
20 String account = casVO.getAccount(); 21 // TODO 根据CAS登录业务系统
22
23 return true; 24 } 25 }); 26 } 27 }
1 package com.zxz.sso.controller; 2
3 /**
4 * CAS配置类 5 * 业务单点登录接口url : http://192.168.1.76/sso/myLogin.do
6 * targetUrl(登录成功后进入系统的页面) : http://192.168.1.76/main.html
7 * base64编码targetUrl: aHR0cDovLzE5Mi4xNjguMS43Ni9tYWluLmh0bWw= 8 * service(CAS服务器登录需要的service参数) : http://192.168.1.76/sso/myLogin.do?targetUrl=base64aHR0cDovLzE5Mi4xNjguMS43Ni9tYWluLmh0bWw=
9 * loginUrl(拼接成的CAS服务登录url) : http://localhost:8080/cas/login?service=http%3A%2F%2F192.168.1.76%2Fsso%2FmyLogin.do%3FtargetUrl%3Dbase64aHR0cDovLzE5Mi4xNjguMS43Ni9tYWluLmh0bWw%3D 10 * 验证票据的地址(获取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 11 */
12 public class CasConfig { 13 // CAS根地址
14 public static String CAS_BASE_PATH = "http://localhost:8080/cas/"; 15 // 业务系统登录入口
16 public static String MY_LOGIN_URI = "sso/myLogin.do"; 17 // CAS票据验证地址
18 public static String CAS_VALIDATE_URL = CAS_BASE_PATH + "serviceValidate"; 19 // CAS登录地址
20 public static String CAS_LOGIN_URL = CAS_BASE_PATH + "login"; 21 //登录成功默认跳转地址
22 public static String DEF_TARGET_URI = "main.html"; 23 // 默认编码字符串格式
24 public static String UTF_8 = "UTF-8"; 25 // SESSION中判断是否登录的KEY
26 public static String LOGIN_KEY = "isCasLogin"; 27 // 业务系统认证集成失败提示页
28 public static String SSO_ERROR_URI = "error.html"; 29 }
1 package com.zxz.sso.controller; 2
3 import org.w3c.dom.Document; 4 import org.w3c.dom.Node; 5 import org.w3c.dom.NodeList; 6 import sun.misc.BASE64Decoder; 7 import sun.misc.BASE64Encoder; 8
9 import javax.net.ssl.*; 10 import javax.servlet.http.HttpServletRequest; 11 import javax.servlet.http.HttpServletResponse; 12 import javax.servlet.http.HttpSession; 13 import javax.xml.parsers.DocumentBuilder; 14 import javax.xml.parsers.DocumentBuilderFactory; 15 import java.io.*; 16 import java.lang.reflect.Field; 17 import java.net.HttpURLConnection; 18 import java.net.URL; 19 import java.net.URLConnection; 20 import java.net.URLEncoder; 21 import java.nio.charset.Charset; 22 import java.security.SecureRandom; 23 import java.security.cert.CertificateException; 24 import java.security.cert.X509Certificate; 25
26
27 /**
28 * CAS服务器认证工具类 29 * 单点登录只需要调用login(request, response, clientSystem); 30 */
31 public class CasUtil { 32 /**
33 * 单点登录接口调用方法 34 * @param request 35 * @param response 36 * @throws IOException 37 */
38 public void login(HttpServletRequest request, HttpServletResponse response, ClientSystem clientSystem) throws IOException { 39 // 1.首先验证客户是否登录CAS服务器
40 HttpSession session = request.getSession(); 41 boolean isLogin = checkLogin(session); 42 String targetUrl = getTargetUrl(request); 43 if (isLogin) { 44 // 2.如果已经登录了CAS服务器,则跳转到 targetUrl
45 response.sendRedirect(targetUrl); 46 } else { 47 // 3.如果没有登录CAS服务器,则去验证Ticket
48 boolean hasTicket = checkHasTicket(request); 49 if (hasTicket) { 50 // 3.1如果有票据,则进行验证
51 CasVO casVO = checkTicket(request); 52 if (casVO.isLogin() && clientSystem.doLogin(casVO)){ 53 // 3.2给session中写入登录标识(用于checkLogin)
54 session.setAttribute(CasConfig.LOGIN_KEY, true); 55 // 3.3登录成功跳转至业务系统url
56 response.sendRedirect(targetUrl); 57 } else { 58 // cas账户信息异常,跳转配置的错误页面
59 String errorUrl = getErrorUrl(request); 60 response.sendRedirect(errorUrl); 61 } 62 } else { 63 // 3.2如果ticket不存在
64 String loginUrl = getLoginUrl(request); 65 System.err.println("loginUrl:"+loginUrl); 66 response.sendRedirect(loginUrl); 67 } 68 } 69 } 70
71
72
73 private CasVO checkTicket(HttpServletRequest request) throws IOException { 74 // 1.获取票据验证的url
75 String serviceValidateUrl = getServiceValidateUrl(request); 76 System.out.println("验证票据的地址:" + serviceValidateUrl); 77 // 2.get请求获取CAS服务器的登录信息
78 String casUserInfoXml = doGet(serviceValidateUrl); 79 casUserInfoXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + casUserInfoXml; 80 // 3.解析返回的xml结果
81 System.out.println(casUserInfoXml); 82 CasVO casVO = resolveCasXml(casUserInfoXml); 83 return casVO; 84 } 85
86 /**
87 * 业务系统的登录接口 88 */
89 interface ClientSystem { 90 boolean doLogin(CasVO casVO); 91 } 92
93 /**
94 * 解析CAS账户信息 95 * @param casUserInfoXml 96 * @return
97 */
98 private CasVO resolveCasXml(String casUserInfoXml) { 99 final String CAS_PREFIX = "cas:"; 100 final String LOGIN_SUCCESS_KEY = CAS_PREFIX + "authenticationSuccess"; 101 final String ACCOUNT_KEY = CAS_PREFIX + "user"; 102 final String ATTRIBUTES_KEY = CAS_PREFIX + "attributes"; 103 CasVO casVO = new CasVO(); 104 if (casUserInfoXml == null || "".equals(casUserInfoXml)) { 105 return casVO; 106 } 107 DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); 108 InputStream in = null; 109 try { 110 DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); 111 in = IOUtils.toInputStream(casUserInfoXml, Charset.forName(CasConfig.UTF_8)); 112 Document rootDoc = docBuilder.parse(in); 113 NodeList successNodeList = rootDoc.getElementsByTagName(LOGIN_SUCCESS_KEY); 114 if (successNodeList.getLength() > 0) { 115 Node successNode = successNodeList.item(0); 116 Document successDocument = successNode.getOwnerDocument(); 117 NodeList accountNodeList = successDocument.getElementsByTagName(ACCOUNT_KEY); 118 if (accountNodeList != null
119 && accountNodeList.getLength() > 0) { 120 Node accountNode = accountNodeList.item(0); 121 Node accountText = accountNode.getFirstChild(); 122 String nodeValue = accountText.getNodeValue(); 123 casVO.setAccount(nodeValue); 124 } 125 NodeList attrsNodeList = successDocument.getElementsByTagName(ATTRIBUTES_KEY); 126 if (attrsNodeList.getLength() > 0) { 127 Node attrsNode = attrsNodeList.item(0); 128 if (attrsNode.hasChildNodes()) { 129 Document attrsDoc = attrsNode.getOwnerDocument(); 130 Field[] fields = casVO.getClass().getDeclaredFields(); 131 for (Field field : fields) { 132 String fieldName = field.getName(); 133 String attrTagName = CAS_PREFIX + fieldName; 134 NodeList attrNodeList = attrsDoc.getElementsByTagName(attrTagName); 135 if (attrNodeList.getLength() > 0) { 136 Node attrNode = attrNodeList.item(0); 137 Node attrText = attrNode.getFirstChild(); 138 if (attrText != null) { 139 field.set(casVO, attrText.getNodeValue().trim()); 140 } 141 } 142 } 143 } 144 } 145 } 146 } catch (Exception e) { 147 // 解析用户信息失败!
148 e.printStackTrace(); 149 } finally { 150 IOUtils.closeQuietly(in); 151 } 152 return casVO; 153 } 154
155 /**
156 * 获取验证票据的url 157 * @param request 158 * @return
159 */
160 private String getServiceValidateUrl(HttpServletRequest request) throws IOException { 161 // 1.获取targetUrl
162 String targetUrl = getTargetUrl(request); 163 System.err.println("targetUrl:"+targetUrl); 164 // 2.targetUrl进行base64编码
165 String base64TargetUrl = new BASE64Encoder().encode(targetUrl.getBytes()); 166 System.err.println("base64编码的targetUrl:"+base64TargetUrl); 167 // 3.获取业务service的根路径
168 String serviceUrlRoot = getBasePath(request) + CasConfig.MY_LOGIN_URI; 169 // 4.组装service参数
170 String serviceUrl = serviceUrlRoot + "?targetUrl=base64" + base64TargetUrl; 171 System.err.println("原service:"+serviceUrl); 172 String service = URLEncoder.encode(serviceUrl, CasConfig.UTF_8); 173 // 5.获取ticket
174 String ticket = request.getParameter("ticket"); 175 // 6.组装CAS服务器验证票据的地址
176 String ticketUrl = CasConfig.CAS_VALIDATE_URL + "?ticket=" + ticket + "&service="+service; 177 return ticketUrl; 178 } 179
180 /**
181 * 获取登录成功后跳转的url 182 * @param request 183 * @return http://192.168.1.76/main.html
184 * @throws IOException 185 */
186 private String getTargetUrl(HttpServletRequest request) throws IOException { 187 String basePath = getBasePath(request); 188 String targetUrl = request.getParameter("targetUrl"); 189 if (targetUrl == null || "".equals(targetUrl)) { 190 targetUrl = basePath + CasConfig.DEF_TARGET_URI; 191 } else { 192 if (targetUrl.startsWith("base64")){ 193 targetUrl = targetUrl.substring("base64".length()); 194 byte[] bytes = new BASE64Decoder().decodeBuffer(targetUrl); 195 targetUrl = new String(bytes, CasConfig.UTF_8); 196 System.err.println("解码之后的target:"+targetUrl); 197 } 198 } 199 return targetUrl; 200 } 201
202 /**
203 * 获取cas账户信息错误的异常界面 204 * @param request 205 * @return
206 */
207 private String getErrorUrl(HttpServletRequest request) { 208 String basePath = getBasePath(request); 209 String errorUrl = basePath + CasConfig.SSO_ERROR_URI; 210 return errorUrl; 211 } 212
213 /**
214 * 获取项目的根路径 215 * @param request 216 * @return
217 */
218 private String getBasePath(HttpServletRequest request) { 219 String scheme = request.getScheme(); // 协议
220 String serverName = request.getServerName(); // 域名或者ip
221 int serverPort = request.getServerPort(); // 端口
222 String contextPath = request.getContextPath(); 223 String url = ""; 224 if ((serverPort == 80) || (serverPort == 443)) { 225 url = scheme + "://" + serverName + contextPath + "/"; 226 } else { 227 url = scheme + "://" + serverName + ":" + serverPort + contextPath + "/"; 228 } 229 return url; 230 } 231
232 /**
233 * 获取CAS服务器登录地址 234 * @param request 235 * @return http://localhost:8080/cas/login?service=http%3A%2F%2F192.168.1.76%2FmyLogin.do%3FtargetUrl%3Dbase64aHR0cDovLzE5Mi4xNjguMS43Ni9tYWluLmh0bWw%3D 236 * @throws IOException 237 */
238 private String getLoginUrl(HttpServletRequest request) throws IOException { 239 // 1.获取targetUrl
240 String targetUrl = getTargetUrl(request); 241 System.err.println("targetUrl:"+targetUrl); 242 // 2.targetUrl进行base64编码
243 String base64TargetUrl = new BASE64Encoder().encode(targetUrl.getBytes()); 244 System.err.println("base64编码的targetUrl:"+base64TargetUrl); 245 // 3.获取业务service的根路径
246 String serviceUrlRoot = getBasePath(request) + CasConfig.MY_LOGIN_URI; 247 // 4.组装service参数
248 String serviceUrl = serviceUrlRoot + "?targetUrl=base64" + base64TargetUrl; 249 String service = URLEncoder.encode(serviceUrl, CasConfig.UTF_8); 250 // 5.组装CAS登录的url
251 String loginUrl = CasConfig.CAS_LOGIN_URL+"?service="+service; 252 return loginUrl; 253 } 254
255 /**
256 * 检查ticket 257 * @param request 258 * @return
259 */
260 private boolean checkHasTicket(HttpServletRequest request) { 261 Object ticket = request.getParameter("ticket"); 262 if (ticket == null) { 263 return false; 264 } else { 265 return !String.valueOf(ticket).isEmpty(); 266 } 267 } 268
269 /**
270 * 检查登录 271 * @param session 272 * @return
273 */
274 private boolean checkLogin(HttpSession session) { 275 Object isLogin = session.getAttribute(CasConfig.LOGIN_KEY); 276 Boolean login = Boolean.valueOf(String.valueOf(isLogin)); 277 return login; 278 } 279
280
281 /**
282 * get请求 283 * @param
284 * @return
285 */
286 private String doGet(String urlStr) throws IOException { 287 URL url = new URL(urlStr); 288 InputStream in = null; 289 HttpURLConnection conn = null; 290 try { 291 skipSSL(); 292 conn = (HttpURLConnection) url.openConnection(); 293 conn.setConnectTimeout(5000); 294 conn.connect(); 295 in = conn.getInputStream(); 296 return IOUtils.toString(in, Charset.forName(CasConfig.UTF_8)); 297 } finally { 298 IOUtils.close(conn); 299 IOUtils.closeQuietly(in); 300 } 301 } 302
303 /**
304 * 绕过SSL验证 305 */
306 private void skipSSL() { 307 try { 308 HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { 309 public boolean verify(String hostname, SSLSession session) { 310 return true; 311 } 312 }); 313 SSLContext context = SSLContext.getInstance("TLS"); 314 context.init(null, new X509TrustManager[]{new X509TrustManager() { 315 public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { 316 } 317 public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { 318 } 319 public X509Certificate[] getAcceptedIssuers() { 320 return new X509Certificate[0]; 321 } 322 }}, new SecureRandom()); 323 HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory()); 324 } catch (Exception e) { 325 e.printStackTrace(); 326 } 327 } 328
329 public static abstract class IOUtils { 330 private static final int EOF = -1; 331 private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; 332 public static void close(URLConnection conn) { 333 if (conn instanceof HttpURLConnection) { 334 ((HttpURLConnection) conn).disconnect(); 335 } 336 } 337 public static void closeQuietly(InputStream input) { 338 closeQuietly((Closeable) input); 339 } 340 public static void closeQuietly(Closeable closeable) { 341 try { 342 if (closeable != null) { 343 closeable.close(); 344 } 345 } catch (IOException ioe) { 346 // ignore
347 } 348 } 349 public static String toString(InputStream input, Charset encoding) throws IOException { 350 StringWriter sw = new StringWriter(); 351 copy(input, sw, encoding); 352 return sw.toString(); 353 } 354 public static void copy(InputStream input, Writer output, Charset encoding) throws IOException { 355 encoding = encoding == null ? Charset.defaultCharset() : encoding; 356 InputStreamReader in = new InputStreamReader(input, encoding); 357 copy(in, output); 358 } 359 public static int copy(Reader input, Writer output) throws IOException { 360 long count = copyLarge(input, output); 361 if (count > Integer.MAX_VALUE) { 362 return -1; 363 } 364 return (int) count; 365 } 366 public static long copyLarge(Reader input, Writer output) throws IOException { 367 return copyLarge(input, output, new char[DEFAULT_BUFFER_SIZE]); 368 } 369 public static long copyLarge(Reader input, Writer output, char[] buffer) throws IOException { 370 long count = 0; 371 int n; 372 while (EOF != (n = input.read(buffer))) { 373 output.write(buffer, 0, n); 374 count += n; 375 } 376 return count; 377 } 378 public static InputStream toInputStream(String input, Charset encoding) { 379 return new ByteArrayInputStream(input.getBytes()); 380 } 381 } 382 }
1 package com.zxz.sso.controller; 2
3 /**
4 * 解析的CAS账户信息 5 */
6 public class CasVO { 7 private String account; 8 private String userName; 9
10 public boolean isLogin() { 11 return account != null && !"".equals(account); 12 } 13
14 public String getAccount() { 15 return account; 16 } 17
18 public void setAccount(String account) { 19 this.account = account; 20 } 21
22 public String getUserName() { 23 return userName; 24 } 25
26 public void setUserName(String userName) { 27 this.userName = userName; 28 } 29
30 }