Sa-Token 单点登录第五篇:前后端分离架构下的 SSO 对接步骤

大家好,由于单篇博客长度有限,我将针对 SSO 单点登录 开启一个系列文章,此为第五篇,想完整的了解 SSO 技术的同学欢迎点开我的账号动态参考本系列历史文章。

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
Gitee 开源地址:https://gitee.com/dromara/sa-token

凡是稍微上点规模的系统,统一认证中心都是绕不过去的槛。而单点登录 —— 便是我们搭建统一认证中心的关键。

前面几篇文章介绍了在前后端一体同域、跨域、同Redis、跨Redis 等架构下的单点登录,本篇将介绍:前后端分离架构下的 SSO 对接方案。

以之前的 SSO 模式二对接 demo 为例,将其改造为前后端分离模式,只需以下几步:

一、新建H5Controller开放接口

/**
 * 前后端分离架构下集成SSO所需的接口 
 */
@RestController
public class H5Controller {

    // 当前是否登录 
    @RequestMapping("/sso/isLogin")
    public Object isLogin() {
        return SaResult.data(StpUtil.isLogin());
    }

    // 返回SSO认证中心登录地址 
    @RequestMapping("/sso/getSsoAuthUrl")
    public SaResult getSsoAuthUrl(String clientLoginUrl) {
        String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(clientLoginUrl, "");
        return SaResult.data(serverAuthUrl);
    }

    // 根据ticket进行登录 
    @RequestMapping("/sso/doLoginByTicket")
    public SaResult doLoginByTicket(String ticket) {
        Object loginId = SaSsoProcessor.instance.checkTicket(ticket, "/sso/doLoginByTicket");
        if(loginId != null) {
            StpUtil.login(loginId);
            return SaResult.data(StpUtil.getTokenValue());
        }
        return SaResult.error("无效ticket:" + ticket); 
    }

    // 全局异常拦截 
    @ExceptionHandler
    public SaResult handlerException(Exception e) {
        e.printStackTrace(); 
        return SaResult.error(e.getMessage());
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

二、增加跨域过滤器CorsFilter.java

/**
 * 跨域过滤器
 */
@Component
@Order(-200)
public class CorsFilter implements Filter {

    static final String OPTIONS = "OPTIONS";

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // 允许指定域访问跨域资源
        response.setHeader("Access-Control-Allow-Origin", "*");
        // 允许所有请求方式
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        // 有效时间
        response.setHeader("Access-Control-Max-Age", "3600");
        // 允许的header参数
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with,satoken");

        // 如果是预检请求,直接返回
        if (OPTIONS.equals(request.getMethod())) {
            System.out.println("---------------------- 浏览器发来了 OPTIONS 预检请求 ----------------------");
            response.getWriter().print("");
            return;
        }

        chain.doFilter(req, res);
    }

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void destroy() {
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

三、新建前端项目

任意文件夹新建前端项目:sa-token-demo-sso-client-h5,在根目录添加测试文件:index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Sa-Token-SSO-Client端-测试页(前后端分离版)</title>
    </head>
    <body>
        <h2>Sa-Token SSO-Client 应用端(前后端分离版)</h2>
        <p>当前是否登录:<b class="is-login"></b></p>
        <p>
            <a href="javascript:location.href='sso-login.html?back=' + encodeURIComponent(location.href);">登录</a>
            <a href="javascript:location.href=baseUrl + '/sso/logout?satoken=' + localStorage.satoken + '&back=' + encodeURIComponent(location.href);">注销</a>
        </p>
        <script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
        <script type="text/javascript">

            // 后端接口地址 
            var baseUrl = "http://sa-sso-client1.com:9001";

            // 查询当前会话是否登录 
            $.ajax({
                url: baseUrl + '/sso/isLogin',
                type: "post", 
                dataType: 'json',
                headers: {
                    "X-Requested-With": "XMLHttpRequest",
                    "satoken": localStorage.getItem("satoken")
                },
                success: function(res){
                    $('.is-login').html(res.data + '');
                },
                error: function(xhr, type, errorThrown){
                    return alert("异常:" + JSON.stringify(xhr));
                }
            });

        </script>
    </body>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

四、添加登录处理文件sso-login.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Sa-Token-SSO-Client端-登录页</title>
        <style type="text/css">

        </style>
    </head>
    <body>
        <div class="login-box">

        </div>
        <script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
        <script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')</script>
        <script type="text/javascript">

            // 后端接口地址 
            var baseUrl = "http://sa-sso-client1.com:9001";

            var back = getParam('back', '/');
            var ticket = getParam('ticket');
            $(function() {
                if(ticket) {
                    doLoginByTicket(ticket);
                } else {
                    goSsoAuthUrl();
                }
            })

            // 重定向至认证中心 
            function goSsoAuthUrl() {
                sa.ajax('/sso/getSsoAuthUrl', {clientLoginUrl: location.href}, function(res) {
                    console.log(res);
                    location.href = res.data;
                })
            }

            // 根据ticket值登录 
            function doLoginByTicket(ticket) {
                sa.ajax('/sso/doLoginByTicket', {ticket: ticket}, function(res) {
                    console.log(res);
                    if(res.code == 200) {
                        localStorage.setItem('satoken', res.data);
                        location.href = decodeURIComponent(back); 
                    } else {
                        alert(res.msg);
                    }
                })
            }

            // 从url中查询到指定名称的参数值 
            function getParam(name, defaultValue){
                var query = window.location.search.substring(1);
                var vars = query.split("&");
                for (var i=0;i<vars.length;i++) {
                    var pair = vars[i].split("=");
                    if(pair[0] == name){return pair[1];}
                }
                return(defaultValue == undefined ? null : defaultValue);
            }

        </script>
        <script type="text/javascript">
            var sa = {};

            // 封装一下Ajax
            sa.ajax = function(url, data, successFn) {
                // sa.loading("正在努力加载...");
                $.ajax({
                    url: baseUrl + url,
                    type: "post", 
                    data: data,
                    dataType: 'json',
                    headers: {
                        satoken: localStorage.getItem('satoken')
                    },
                    success: function(res){
                        console.log('返回数据:', res);
                        successFn(res);
                    },
                    error: function(xhr, type, errorThrown){
                        if(xhr.status == 0){
                            return alert('无法连接到服务器,请检查网络');
                        }
                        return alert("异常:" + JSON.stringify(xhr));
                    }
                });
            }

        </script>
    </body>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93

五、测试运行

先启动Server服务端与Client服务端,再随便找个能预览html的工具打开前端项目(比如HBuilderX),
测试流程与之前的一体版一致,在此暂不赘述。

六、SSO-Server 端的前后端分离

疑问:上述代码都是针对 Client 端进行拆分,如果我想在 SSO-Server 端也进行前后端分离改造,应该怎么做?

答:解决思路都是大同小异的,与Client一样,我们需要把原本在 “后端处理的授权重定向逻辑” 拿到前端来实现。

新建 sa-token-demo-sso-server-h5 项目,在根目录下创建文件 sso-auth.html,复制以下代码:

<!DOCTYPE html>
<html lang="zh">
    <head>
        <title>Sa-SSO-Server 认证中心-登录</title>
        <meta charset="utf-8">
        <base th:href="@{/}" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
        <link rel="stylesheet" href="./login.css">
    </head>
    <body>
        <div class="view-box">
            <div class="bg-1"></div>
            <div class="content-box">
                <div class="login-box">
                    <div class="from-box">
                        <h2 class="from-title">Sa-SSO-Server 认证中心(前后端分离版)</h2>
                        <div class="from-item">
                            <input class="s-input" name="name" placeholder="请输入账号" />
                        </div>
                        <div class="from-item">
                            <input class="s-input" name="pwd" type="password" placeholder="请输入密码" />
                        </div>
                        <div class="from-item">
                            <button class="s-input s-btn login-btn">登录</button>
                        </div>
                        <div class="from-item reset-box">
                            <a href="javascript: location.reload();" >刷新</a>
                        </div>
                    </div>
                </div>
            </div>
            <!-- 底部 版权 -->
            <div style="position: absolute; bottom: 40px; width: 100%; text-align: center; color: #666;">
                This page is provided by Sa-Token-SSO 
            </div>
        </div>

        <!-- scripts -->
        <script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
        <script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
        <script src="./login.js"></script>

    </body>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

创建 login.css,复制以下代码:

*{margin: 0; padding: 0;}
body{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;}
::-webkit-input-placeholder{color: #ccc;}

/* 视图盒子 */
.view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;}
/* 背景 EAEFF3 */
.bg-1{height: 100%; background: #c0cCf4;}

/* 内容盒子 */
.content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;}

/* 登录盒子 */
/* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */
.login-box{width: 400px; margin: auto; max-width: 90%; height: 100%;}
.login-box{display: flex; align-items: center; text-align: center;}

/* 表单 */
.from-box{flex: 1; padding: 20px 50px; background-color: #FFF;}
.from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;}
.from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;}

/* 输入框 */
.from-item{border: 0px #000 solid; margin-bottom: 15px;}
.s-input{width: 100%; line-height: 32px; height: 32px; text-indent: 1em; outline: 0; border: 1px #ccc solid; border-radius: 3px; transition: all 0.2s;}
.s-input{font-size: 12px;}
.s-input:focus{border-color: #409eff}

/* 登录按钮 */
.s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;}
.s-btn:hover{background-color: #50aEFF;}

/* 重置按钮 */
.reset-box{text-align: left; font-size: 12px;}
.reset-box a{text-decoration: none;}
.reset-box a:hover{text-decoration: underline;}

/* loading框样式 */
.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}
.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}
.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

创建 login.js,复制以下代码:

// 服务端地址 
var baseUrl = "http://sa-sso-server.com:9000";

// sa 
var sa = {};

// 打开loading
sa.loading = function(msg) {
    layer.closeAll();   // 开始前先把所有弹窗关了
    return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load'});
};

// 隐藏loading
sa.hideLoading = function() {
    layer.closeAll();
};

// 封装一下Ajax
sa.ajax = function(url, data, successFn) {
    $.ajax({
        url: baseUrl + url,
        type: "post", 
        data: data,
        dataType: 'json',
        headers: {
            'X-Requested-With': 'XMLHttpRequest',
            'satoken': localStorage.getItem('satoken')
        },
        success: function(res){
            console.log('返回数据:', res);
            successFn(res);
        },
        error: function(xhr, type, errorThrown){
            if(xhr.status == 0){
                return alert('无法连接到服务器,请检查网络');
            }
            return alert("异常:" + JSON.stringify(xhr));
        }
    });
}

// ----------------------------------- 相关事件 -----------------------------------

// 检查当前是否已经登录,如果已登录则直接开始跳转,如果未登录则等待用户输入账号密码 
var pData = {
    client: getParam('client', ''), 
    redirect: getParam('redirect', ''), 
    mode: getParam('mode', '')
};
sa.ajax("/sso/getRedirectUrl", pData, function(res) {
    if(res.code == 200) {
        // 已登录,并且redirect地址有效,开始跳转  
        location.href = decodeURIComponent(res.data);
    } else if(res.code == 401) {
        console.log('未登录');
    } else {
        layer.alert(res.msg); 
    }
})

// 登录
$('.login-btn').click(function(){
    sa.loading("正在登录...");
    // 开始登录
    var data = {
        name: $('[name=name]').val(),
        pwd: $('[name=pwd]').val()
    };
    sa.ajax("/sso/doLogin", data, function(res) {
        sa.hideLoading();
        if(res.code == 200) {
            localStorage.setItem('satoken', res.data);
            layer.msg('登录成功', {anim: 0, icon: 6 }); 
            setTimeout(function() {
                location.reload();
            }, 800);
        } else {
            layer.msg(res.msg, {anim: 6, icon: 2 }); 
        }
    })
});


// 绑定回车事件
$('[name=name],[name=pwd]').bind('keypress', function(event){
    if(event.keyCode == "13") {
        $('.login-btn').click();
    }
});

// 输入框获取焦点
$("[name=name]").focus();

// 从url中查询到指定名称的参数值 
function getParam(name, defaultValue){
    var query = window.location.search.substring(1);
    var vars = query.split("&");
    for (var i=0;i<vars.length;i++) {
        var pair = vars[i].split("=");
        if(pair[0] == name){return pair[1] + (pair[2] ? '=' + pair[2] : '');}
    }
    return(defaultValue == undefined ? null : defaultValue);
}

// 打印信息 
var str = "This page is provided by Sa-Token, Please refer to: " + "https://sa-token.com/";
console.log(str);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107

七、启动测试

使用前端ide导入项目 sa-token-demo-sso-server-h5,浏览器访问 sso-auth.html 页面:

sso-type2-server-h5-auth.png

复制上述地址,将其配置到 Client 端的 yml 配置文件中,例如:

sa-token: 
    sso: 
        # SSO-Server端 统一认证地址 
        auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html
  • 1
  • 2
  • 3
  • 4

然后我们启动项目 sa-token-demo-sso-serversa-token-demo-sso2-client,按照之前的测试步骤访问:
http://sa-sso-client1.com:9001/,即可以前后端分离模式完成 SSO-Server 端的授权登录。


参考资料

← Sa-Token 单点登录第二篇:同域、同 Redis 下的 SSO 对接步骤 Sa-Token 单点登录第六篇:定制化登录页面 →

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | 鲁ICP备18046274号-5 | 鲁公网安备37011202002956号