SSO单点登录—CAS统一身份认证

单点登录SSO(Single Sign ON) 

如:在学校登录了OA系统,再打开考试系统、教务系统,都会实现自动登录。

统一身份认证CAS(Central Authentication Service)

CAS 是由耶鲁大学发起的企业级开源项目,历经20多年的完善,具有较高的稳定性、安全性。国内多数高校的SSO都基于CAS。

@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;
            }
        });
    }
}
SSOController
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";
}
CasConfig
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());
        }
    }
}
CasUtil
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;
    }

}
CasVO
posted @ 2020-12-19 15:16  一柒微笑  阅读(5977)  评论(0编辑  收藏  举报