首先贴出国内大神的开源SSO框架 KISSO

http://git.oschina.net/baomidou/kisso

废话不多说,开始~

这里写图片描述

贴一张KISSO文档上的原理图

其实就是用户在登录业务系统的时候。看一下本地是否有Cookie

如果没有Cookie。访问SSO项目。SSO也没有Cookie的话。

进行登录。登录成功将加密的Token写入Cookie

回传给业务系统。通过几次的加密。认证,双方认证OK后

在业务系统域名下写入Cookie。完成登录


一、KISSO服务器端集成(springMVC+redis)

1、maven依赖增加

<!-- kisso begin -->
       <dependency>
           <groupId>com.baomidou</groupId>
           <artifactId>kisso</artifactId>
           <version>3.6.10</version>
       </dependency>
       <dependency>
           <groupId>org.bouncycastle</groupId>
           <artifactId>bcprov-jdk14</artifactId>
           <version>1.50</version>
       </dependency>


       <dependency>
           <groupId>com.alibaba</groupId>
           <artifactId>fastjson</artifactId>
           <version>1.1.46</version>
       </dependency>


       <dependency>
           <groupId>com.google.code.gson</groupId>
           <artifactId>gson</artifactId>
           <version>2.8.0</version>
       </dependency>
       <!-- kisso end -->

2、web.xml(这里使用过滤器方式,不使用spring-mvc拦截器方式)

over.url:这里参数为不需要过滤器过滤的方法。

<!-- kisso -->
    <context-param>
        <param-name>kissoConfigLocation</param-name>
        <param-value>classpath:properties/sso.properties</param-value>
    </context-param>
    <listener>
        <listener-class>com.baomidou.kisso.web.KissoConfigListener</listener-class>
    </listener>

    <!-- SSOFilter -->
    <filter>
        <filter-name>SSOFilter</filter-name>
        <filter-class>com.baomidou.kisso.web.filter.SSOFilter</filter-class>
        <init-param>
            <param-name>over.url</param-name>
            <param-value>/phoneSms;/PhoneSmsCode;</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>SSOFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

其实就是看本地有没有Cookie 。没有就跳转到SSO项目

public class SSOClientFilter implements Filter {
    private static final Logger logger = Logger.getLogger("SSOFilter");
    private static String OVERURL = null;

    public SSOClientFilter() {
    }

    public void init(FilterConfig config) throws ServletException {
        OVERURL = config.getInitParameter("over.url");
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest)request;
        HttpServletResponse res = (HttpServletResponse)response;
        boolean isOver = HttpUtil.inContainURL(req, OVERURL);
        if(!isOver) {
            Token token = SSOHelper.getToken(req);
            if(token == null) {
                logger.fine("logout. request url:" + req.getRequestURL());
                SSOProperties prop = SSOConfig.getSSOProperties();
                String retStr = prop.get("sso.defined.proxyloginurl");
//                String retUrl = HttpUtil.getQueryString(req, "UTF-8");
//                this.logger.fine("loginAgain redirect pageUrl.." + retUrl);
                res.sendRedirect(HttpUtil.encodeRetURL(prop.get("sso.login.url"), "ReturnURL", retStr));
                return;
            }

            SsoUser user = (SsoUser) req.getSession().getAttribute(UserConstants.LOGIN_USER);
            if(user == null){
                SSOHelper.logout(req,res);
                return;
            }
            req.setAttribute("SSOTokenAttr", token);
        }

        chain.doFilter(request, response);
    }

    public void destroy() {
        OVERURL = null;
    }
}

3、KISSO的配置文件 sso.properties

这里具体就不贴出来了。 查阅一下文档写的非常详细

################ SSOConfig file #################
sso.role=应用名
sso.secretkey=秘钥
sso.cookie.domain=cookie存储的位置
sso.login.url=登录的url

#cookie setting

# crossdomain secretkey
sso.authcookie.secretkey=跨域对称加密的秘钥

#cache
sso.cache.class=缓存实现类。实现SSOCache接口 
sso.cache.expires=有效时间

# userConfig defined
sso.defined.my_public_key=业务系统的公钥(校验业务系统)
sso.defined.sso_private_key=SSO系统的私钥(加密使用)

4、服务端控制层

@Controller
public class SsoController {

    private static Logger log = Logger.getLogger(SsoController.class);
    @Autowired
    private SsoUserService ssoUserService;

    protected String redirectTo(String url) {
        StringBuffer rto = new StringBuffer("redirect:");
        rto.append(url);
        return rto.toString();
    }
    /**
     * 登录 (注解跳过权限验证)
     */
    @Login(action = Action.Skip)
    @RequestMapping("/login")
    public String login(RedirectAttributesModelMap modelMap, Model model, HttpServletRequest request, HttpServletResponse response) {
        log.info("登录 (注解跳过权限验证)");

        String returnUrl = request.getParameter(SSOConfig.getInstance().getParamReturl());
        Token token = SSOHelper.getToken(request);
        if (token == null) {
            /**
             * 正常登录 需要过滤sql及脚本注入
             */
            WafRequestWrapper wr = new WafRequestWrapper(request);
            String loginUser = wr.getParameter("loginUser");
            String loginPass = wr.getParameter("loginPass");

            //定义校验失败标识符
            boolean falg = false;
            //定义校验失败的字符串
            String falgStr = "";

            if (loginUser != null && !"".equals(loginUser)) {

                SsoUser user = ssoUserService.findByName(loginUser);
                if(user != null){
                    if(user.getPassword().equals(Md5Util.MD5Encode(loginPass))){

                        /*
                         * 设置登录 Cookie
                         * 最后一个参数 true 时添加 cookie 同时销毁当前 JSESSIONID 创建信任的 JSESSIONID
                         */
                        SSOToken st = new SSOToken(request, user.getId()+"");
//                        st.setData("jjc看源码哦");
                        //记住密码就设置
                        //SSOConfig.getInstance().setCookieMaxage(604800);
                        SSOHelper.setSSOCookie(request, response, st, true);

                    }else{//证明密码不匹配
                        falg = true;
                        falgStr = "密码不正确";
                    }

                }else{//证明没有用户名
                    falg = true;
                    falgStr = "用户名不正确";
                }

            } else {
                falg = true;
                falgStr = "";
            }

            if(falg){//校验失败。跳转回登录页
                model.addAttribute("msg", falgStr);
                if (StringUtils.isNotEmpty(returnUrl)) {
                    model.addAttribute("ReturnURL", returnUrl);
                }
                model.addAttribute("loginUser", loginUser);
                model.addAttribute("loginPass", loginPass);
                return "login";
            }else{//校验成功,重定向到业务系统
                // 重定向到指定地址 returnUrl
                if (StringUtils.isEmpty(returnUrl)) {
                    returnUrl = "/index.html";
                } else {
                    returnUrl = HttpUtil.decodeURL(returnUrl);
                }
                return redirectTo(returnUrl);
            }

        } else {
            if (StringUtils.isEmpty(returnUrl)) {
                returnUrl = "/index.html";
            }
            return redirectTo(returnUrl);
        }
    }

    @ResponseBody
    @RequestMapping("/replylogin")
    public void replylogin(HttpServletRequest request, HttpServletResponse response) {
        log.info("SSO回复子系统");

        StringBuffer replyData = new StringBuffer();
        replyData.append(request.getParameter("callback")).append("({\"msg\":\"");
        Token token = SSOHelper.getToken(request);
        if (token != null) {
            String askData = request.getParameter("askData");
            if (askData != null && !"".equals(askData)) {
                /**
                 *
                 * 用户自定义配置获取
                 *
                 * <p>
                 * 由于不确定性,kisso 提倡,用户自己定义配置。
                 * </p>
                 *
                 */
                SSOProperties prop = SSOConfig.getSSOProperties();

                //下面开始验证票据,签名新的票据每一步都必须有。
                AuthToken at = SSOHelper.replyCiphertext(request, askData);
                if (at != null) {

                    //1、业务系统公钥验证签名合法性(此处要支持多个跨域端,取 authToken 的 app 名找到对应系统公钥验证签名)
                    at = at.verify(prop.get("sso.defined." + at.getApp() + "_public_key"));
                    if (at != null) {

                        //at.getUuid() 作为 key 设置 authToken 至分布式缓存中,然后 sso 系统二次验证
                        //at.setData(data); 设置自定义信息,当然你也可以直接 at.setData(token.jsonToken()); 把当前 SSOToken 传过去。

                        at.setUid(token.getUid());//设置绑定用户ID
                        at.setTime(token.getTime());//设置登录时间

                        //2、SSO 的私钥签名
                        at.sign(prop.get("sso.defined.sso_private_key"));

                        //3、生成回复密文票据
                        replyData.append(at.encryptAuthToken());
                    } else {
                        //非法签名, 可以重定向至无权限界面,自己处理
                        replyData.append("-2");
                    }
                } else {
                    //非法签名, 可以重定向至无权限界面,自己处理
                    replyData.append("-2");
                }
            }
        } else {
            // 未登录
            replyData.append("-1");
        }
        try {
            replyData.append("\"})");
            AjaxHelper.outPrint(response, replyData.toString(), "UTF-8");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 统一退出,调用对外提供退出的所有接口
     */
    @RequestMapping("/logout")
    public String logout(HttpServletRequest request,HttpServletResponse response) {
        SSOHelper.clearLogin(request, response);
        return "logout";
    }


    /**
     */
    @RequestMapping("/index")
    public String index(HttpServletRequest request,HttpServletResponse response) {
        return "index";
    }
}

5、redis集成(退出使用,具体请查阅官方文档)

public class SSORedisCaChe implements SSOCache {

StringRedisTemplate stringRedisTemplate = (StringRedisTemplate) WebApplicationContextHelper.getBean("stringRedisTemplate");

@Override
public Token get(String s, int i) {
    if(exists(s)){
        String result = null;
        ValueOperations operations = stringRedisTemplate.opsForValue();
        result = (String)operations.get(s);
        result = result.replaceAll("\u0000" , "");
        Token token = SSOConfig.getInstance().getParser().parseObject(result, Token.class);
        return  token;
    }
    return null;
}

@Override
public boolean set(String s, Token token, int i) {
    boolean result = false;
    try {
        delete(s);
        ValueOperations valueOperations = stringRedisTemplate.opsForValue();
        valueOperations.set(s,token.jsonToken(),i);
        result = true;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return result;

}

@Override
public boolean delete(String s) {
    boolean result = false;
    try{
        if (exists(s)) {
            stringRedisTemplate.delete(s);
            return  true;
        }
    }catch (Exception e){
        e.printStackTrace();
    }
    return result;
}

public boolean exists(final String key) {
    return stringRedisTemplate.hasKey(key);
}

}

二、客户端集成

1、maven一致
2、web.xml

SSOClientFilter 客户端自定义的过滤器。模仿kisso的过滤器

<!-- kisso -->
    <context-param>
        <param-name>kissoConfigLocation</param-name>
        <param-value>classpath:properties/sso.properties</param-value>
    </context-param>
    <listener>
        <listener-class>com.baomidou.kisso.web.KissoConfigListener</listener-class>
    </listener>

    <!-- SSOFilter -->
    <filter>
        <filter-name>SSOClientFilter</filter-name>
        <filter-class>com.hc360.yunxin.filter.SSOClientFilter</filter-class>
        <init-param>
            <param-name>over.url</param-name>
            <param-value>/login;/verify;/resources/;/code;/proxylogin;/oklogin;/accountsecurity/mailboxBound;/accountsecurity/mailboxBoundUpdate</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>SSOClientFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

3、sso.properties

################ SSOConfig file #################
sso.role=业务系统名字 , 拼接使用
sso.secretkey=加密秘钥
sso.cookie.domain=写入cookie地址
sso.login.url=登录url-》跳转SSO
sso.logout.url=登出url
# crossdomain secretkey
sso.authcookie.secretkey=

#cache
sso.cache.class=缓存
sso.cache.expires=失效时间 秒单位


# userConfig defined
sso.defined.proxyloginurl=sso回复客户端的地址
sso.defined.askurl=sso认证地址
sso.defined.oklogin=客户端写入cookie的action

sso.defined.clientIndex=客户端自己的主页
sso.defined.clientTimeout=自定义超时链接

sso.defined.my_private_key=业务系统的私钥
sso.defined.my_public_key=业务系统的公钥
sso.defined.sso_public_key=sso系统的公钥

4、客户端Controller

@Controller
public class SsoController {

    @Autowired
    private SsoUserService ssoUserService;

    protected String redirectTo(String url) {
        StringBuffer rto = new StringBuffer("redirect:");
        rto.append(url);
        return rto.toString();
    }

    @RequestMapping("/index")
    public String index(Model model, HttpServletRequest request,HttpServletResponse response) throws UnsupportedEncodingException {
        Token token = SSOHelper.getToken(request);
        response.setCharacterEncoding("UTF-8");
        request.setCharacterEncoding("UTF-8");
        if (token == null) {
            /**
             * 重定向至代理跨域地址页
             */
            return redirectTo("http://sso.test.com:8081/kisso_crossdomain_sso/login.html?ReturnURL=http%3A%2F%2Fmy.web.com%3A8082%2F/kisso_crossdomain_my/proxylogin.html");
        } else {
//            model.addAttribute("userId", token.getUid());
            SsoUser SessionssoUser = (SsoUser) request.getSession().getAttribute(UserConstants.LOGIN_USER);
            model.addAttribute("user",  ssoUserService.findByID(SessionssoUser.getId()));

        }
        return "index";
    }

    /**
     * 跨域登录
     */
    @RequestMapping("/proxylogin")
    public String proxylogin(Model model,HttpServletRequest request,HttpServletResponse response) {
        /**
         *
         * 用户自定义配置获取
         *
         * <p>
         * 由于不确定性,kisso 提倡,用户自己定义配置。
         * </p>
         *
         */
        SSOProperties prop = SSOConfig.getSSOProperties();

        //业务系统私钥签名 authToken 自动设置临时会话 cookie 授权后自动销毁
        AuthToken at = SSOHelper.askCiphertext(request, response, prop.get("sso.defined.my_private_key"));

        //at.getUuid() 作为 key 设置 authToken 至分布式缓存中,然后 sso 系统二次验证

        //askurl 询问 sso 是否登录地址
        model.addAttribute("askurl", prop.get("sso.defined.askurl"));

        //askTxt 询问 token 密文
        model.addAttribute("askData", at.encryptAuthToken());

        //my 确定是否登录地址
        model.addAttribute("okurl", prop.get("sso.defined.oklogin"));
        return "proxylogin";
    }

    /**
     * 跨域登录成功
     */
    @ResponseBody
    @RequestMapping("/oklogin")
    public void oklogin( HttpServletRequest request,HttpServletResponse response) {
        SSOProperties prop = SSOConfig.getSSOProperties();
        //String returl = prop.get("sso.defined.clientTimeout");
        String returl =prop.get("sso.logout.url");
        /*
         * <p>
         * 回复密文是否存在
         * </p>
         * <p>
         * SSO 公钥验证回复密文是否正确
         * </p>
         * <p>
         * 设置 业务系统自己的 Cookie
         * </p>
         */
        String replyTxt = request.getParameter("replyTxt");
        if (replyTxt != null && !"".equals(replyTxt)) {


            AuthToken at = SSOHelper.ok(request, response, replyTxt, prop.get("sso.defined.my_public_key"),
                    prop.get("sso.defined.sso_public_key"));

            if (at != null) {
                returl = prop.get("sso.defined.clientIndex");
                SSOToken st = new SSOToken();
                st.setUid(at.getUid());
                st.setTime(at.getTime());
//                st.setData(at.getData());
                /*
                 * 设置 true 时添加 cookie 同时销毁当前 JSESSIONID 创建信任的 JSESSIONID
                 */
                SSOHelper.setSSOCookie(request, response, st, true);
                //设置完Cookie  设置Session
                request.getSession().setAttribute(UserConstants.LOGIN_USER, ssoUserService.findByID(Integer.parseInt(at.getUid())));
            }
        }
        try {
            AjaxHelper.outPrint(response, "{\"returl\":\"" + returl + "\"}", "UTF-8");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 跨域登录超时
     */
    @RequestMapping("/timeout")
    public String timeout() {
        return "timeout";
    }


    /*
     *
     * 如果实现 SSOCache 缓存, kisso 自动缓存 token 退出只需要 SSOHelper.clearLogin(request, response);
     *
     * 自动清理 token 缓存信息, 同时各个系统都会自动退出。 建议这么!!退出更优雅。。。
     *
     * --------------- 悲剧的开启 ---------------
     *
     * 如果你不这么干那么您只能挨个不同域退出一遍,最终全站退出。
     *
     */
    @RequestMapping("/logout")
    public String logout( HttpServletRequest request,HttpServletResponse response) {
        /**
         * <p>
         * SSO 退出,清空退出状态即可
         * </p>
         *
         * <p>
         * 子系统退出 SSOHelper.logout(request, response); 注意 sso.properties 包含 退出到
         * SSO 的地址 , 属性 sso.logout.url 的配置
         * </p>
         */
        SSOHelper.clearLogin(request, response);
        return "redirect:/index";
    }

}

5、redis与服务一致

6、客户端发送跨域,请求SSO认证的静态页面

<body>
<script type="text/javascript">
    function proxyLogin(askurl, askData, okurl) {
        var killAjax = true;
//      setTimeout(function() {
//          checkajaxkill();
//      }, 30000);
        var ajaxCall = jQuery.getJSON(askurl + "?callback=?", {askData:askData}, function(d){
            killAjax = false;
            if(d.msg == "-1"){
                window.location.href = SSOLoginUrl;
            }else{
                jQuery.post(okurl, {replyTxt:d.msg} , function(e) {
                    window.location.href = e.returl;
                }, "json");
            }
        });
//      function checkajaxkill(){
//          if(killAjax){
//              ajaxCall.abort();
//              window.location.href = SSOTimeout;
//          }
//      }
    }
    proxyLogin("$!{askurl}", "$!{askData}", "$!{okurl}");
</script>
<div align="center" style="margin-top: 180px;">
    <img src="resources/img/loading.gif"> 页面正在加载中,请稍候……
</div>
</body>

登录总结流程:

这里写图片描述

1、用户访问业务系统,通过自定义拦截器跳转到SSO系统进行认证。

2、在SSO系统登陆成功后。进行浏览器重定向

3、重定向到业务系统的Controller

4、业务系统的Controller将自己的私钥加密临时会话Cookie信息,发送给SSO进行认证。(认证时需要使用JSONP方式跨域发送请求 KISSO框架处理方式)

5、SSO系统首先会看看自己本地是否有Cookie,如果有的话将加密信息进行解密,拿到信息中业务系统的名字,拼接配置文件的配置。拿到业务系统公钥进行校验。 成功后在使用SSO系统的私钥进行加密信息返回

6、JSONP消息拿到后,使用业务系统的公钥以及SSO系统的公钥校验回传的信息是否正确。无问题后。将本地设置好Cookie uid 并跳转业务系统

登出流程总结:

这里写图片描述

根据KISSO的接口自定义Cache通过集成Redis保存加密Token
通过以下方法进行登出
SSOHelarLogin(request, response);
原理:
将Redis中的数据清除后,其他系统在操作时通过KISSO的过滤器会通过自定义cache读取缓存信息,如数据清除。将清除其他系统本地的Cookie。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐