Smart-SSO单点登录(四):前后端分离

前言

Smart-SSO允许应用在使用前后端分离模式下完成接入。该模式无法借助 Cookie来传递调用凭证,也就是调用凭证不能通过后端直接写入Cookie来保持会话。我们支持了一种由前端主动来获取凭证,并缓存在客户端本地,每次HTTP请求通过在Header传递凭证参数方式来保持认证状态。同时,前后端一体模式下的accessToken失效自动发起refreshToken请求刷新凭证的能力,也需要由前端主动调起。

在开始前,你需要优先完成快速开始章节的学习,并保障smart-sso- server服务端已被正常的启动提供服务,相应的测试域名都已添加至Hosts。文章中使用Nginx来启动前端服务,也需提前准备,当然你也可以用其它的web服务器代替。

启动后端服务

找到smart-sso-demo-h5应用的启动类DemoH5Application.java,直接右击run启动,如下图所示可以看到应用被监听的8082端口。
在这里插入图片描述

启动前端服务

在smart-sso-demo-h5模块中找到/resources/static/index.html,用它替换掉nginx默认的index.html,并启动Nginx服务。
在这里插入图片描述
在这里插入图片描述

验证

浏览器访问http://localhost,会重定向到服务端登录页。输入正确的账号密码登录后,会回跳至smart-sso-demo-h5首页,如下图所示:
在这里插入图片描述
在这里插入图片描述

此时,浏览器访问http://server.smart-sso.com:8080。此时,它不再需要经过登录页,直接进入到了smart-sso-server首页,完成单点登录功能。当然也可以验证一下“单点退出”功能。
在这里插入图片描述

代码说明

后端-集成接口

前后端分离模式下,后端服务需要增加必要的集成接口

/**
 * 前后端分离模式下集成SSO所需的接口
 */
@RestController
@RequestMapping("/auth")
public class AuthController {

    /**
     * 返回SSO登录地址
     *
     * @param redirectUri 登录成功后的回跳地址
     * @return
     */
    @RequestMapping("/login_url")
    public Result<String> getLoginUrl(@RequestParam String redirectUri) {
        return Result.success(SSOUtils.buildLoginUrl(redirectUri));
    }

    /**
     * 返回SSO退出地址
     *
     * @param redirectUri 退出成功后的回跳地址
     * @return
     */
    @RequestMapping("/logout_url")
    public Result<String> getLogoutUrl(@RequestParam String redirectUri) {
        return Result.success(SSOUtils.buildLogoutUrl(redirectUri));
    }

    /**
     * 通过授权码获取SSO调用凭证
     *
     * @param code 授权码
     * @return
     */
    @RequestMapping(value = "/access-token", method = RequestMethod.GET)
    public Result<Token> getAccessToken(@RequestParam String code) {
        Result<Token> result = SSOUtils.getHttpAccessToken(code);
        if (!result.isSuccess()) {
            return result;
        }
        return Result.success(result.getData());
    }

    /**
     * 通过刷新凭证获取新的调用凭证
     *
     * @param refreshToken 刷新凭证
     * @return
     */
    @RequestMapping(value = "/refresh-token", method = RequestMethod.GET)
    public Result<Token> getRefreshToken(String refreshToken) {
        Result<Token> result = SSOUtils.getHttpRefreshToken(refreshToken);
        if (!result.isSuccess()) {
            return result;
        }
        return Result.success(result.getData());
    }
}

后端-跨域处理

前后端分离模式下,通常都是跨域请求,后端需要具备跨域处理能力。

/**
 * 跨域过滤器
 * 注:跨域过滤器@Order设置的数值,必须要优先级高于ClientContainer设置的Order,详见ClientProperties.order属性
 */
@Order(5)
@WebFilter
@Configuration
public class CorsFilter implements Filter {
    @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", "*");
        // 如果是预检请求,直接返回
        if ("OPTIONS".equals(request.getMethod())) {
            response.getWriter().print("");
            return;
        }
        chain.doFilter(req, res);
    }
}

后端-配置文件

注意:
1.smart.sso.exclude-urls在前后端分离模式下,全局过滤器需要忽略对集成所需接口的拦截;
2.smart.sso.h5-enabled必须是开启状态,默认为false;

smart:
  sso:
    #服务端地址
    server-url: http://server.smart-sso.com:8080
    #客户端密钥信息
    client-id: 1001
    client-secret: kpA1y7k1uyrcoGhrKvA1Ag==
    #前后端分离必须忽略获取凭证相关的urls
    exclude-urls: /auth/*
    #前后端分离开关
    h5-enabled: true

前端-凭证校验

前端页必须具备凭证获取、校验及刷新的能力,可以参考index.html自行封装。

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Smart-SSO-Demo-H5</title>
</head>

<body>
    <h2>Smart-SSO-Demo-H5</h2>

    <br /><b>用户名</b><span id="_username"></span><br />

    <br /><b>用户已分配的菜单</b><span id="_userMenus"></span>

    <br /><b>用户已分配的权限</b><span id="_userPermissions"></span>

    <br /><a href="javascript:goSSOLogoutUrl()">单点退出</a>
</body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>window.jQuery || alert('jQuery.js未加载成功,请检查网络或更换CDN')</script>
<script type="text/javascript">
    // 后端接口地址
    var baseUrl = "http://demo.smart-sso.com:8082";
    $(function () {
        if (validateLogin()) {
            getUserinfo();
        }
    })

    // 校验是否需要跳转到SSO认证中心
    function validateLogin() {
        var accessToken = localStorage.getItem("accessToken");
        if (accessToken) {
            return true;
        }
        var code = getParam('code');
        if (code && getAccessToken(code)) {
            removeCodeParamAndRedirect();
            return true;
        }
        goSSOLoginUrl();
        return false;
    }

    // 获取用户信息
    function getUserinfo() {
        smart.ajax('GET', baseUrl + "/userinfo", {}, function (res) {
                var userinfo = res.data;

                // 用户名
                $("#_username").html(userinfo.username);

                // 用户已分配的菜单
                var userMenus = '';
                userinfo.userMenus.forEach(function (menu) {
                    userMenus += '<li>' + menu + '</li>';
                });
                $('#_userMenus').html(userMenus);

                // 用户已分配的权限
                var userPermissions = '';
                userinfo.userPermissions.forEach(function (permission) {
                    userPermissions += '<li>' + permission + '</li>';
                });
                $('#_userPermissions').html(userPermissions);
            });
    }

    // 重定向至认证中心
    function goSSOLoginUrl() {
        $.getJSON(baseUrl + '/auth/login_url?redirectUri=' + location.href, function (res) {
            window.location.href = res.data;
        })
    }

    // 重定向至服务端退出地址
    function goSSOLogoutUrl() {
        $.getJSON(baseUrl + '/auth/logout_url?redirectUri=' + location.href, function (res) {
            window.location.href = res.data;
        })
    }

    // 获取accessToken
    function getAccessToken(code) {
        var bool = false;
        $.ajax({
            url: baseUrl + "/auth/access-token?code=" + code,
            type: 'GET',
            async: false,
            dataType: 'json',
            success: function (res) {
                if (res.code == 1) {
                    localStorage.setItem("accessToken", res.data.accessToken);
                    localStorage.setItem("refreshToken", res.data.refreshToken);
                    bool = true;
                }
            }
        });
        return bool;
    }

    // 获取refreshToken
    function getRefreshToken() {
        var bool = false;
        $.ajax({
            url: baseUrl + "/auth/refresh-token?refreshToken=" + localStorage.getItem("refreshToken"),
            type: 'GET',
            async: false,
            dataType: 'json',
            success: function (res) {
                if (res.code == 1) {
                    localStorage.setItem("accessToken", res.data.accessToken);
                    localStorage.setItem("refreshToken", res.data.refreshToken);
                    bool = true;
                }
            }
        });
        return bool;
    }

    // 从url中获取指定名称的参数值
    function getParam(name) {
        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 null;
    }

    // 重定向方式去除地址栏中的code参数
    function removeCodeParamAndRedirect() {
        var url = new URL(window.location.href);
        url.searchParams.delete('code');
        window.location.href = url.toString();
    }
</script>
<script type="text/javascript">
    var smart = {};
    // 为业务请求封装Ajax方法,请求头自动附带token,请求返回处理token刷新和失效跳转等逻辑
    smart.ajax = function (method, url, data, successFn) {
        $.ajax({
            type: method,
            url: url,
            data: data,
            dataType: 'json',
            headers: {
                // 传递调用凭证,名称可通过后台配置项自定义,默认为smart-sso-token
                'smart-sso-token': localStorage.getItem("accessToken"),
                // 跨域请求需自行设置X-Requested-With参数,Ajax仅在非跨域请求默认携带
                'X-Requested-With': 'XMLHttpRequest'
            },
            success: function (res) {
                // 响应成功
                if (res.code == 1) {
                    successFn(res);
                }
                // 后端登录失效,需要清理前端缓存,并跳转至认证中心
                else if (res.code == 10) {
                    localStorage.clear();
                    goSSOLoginUrl();
                }
                /**
                 * accessToken已过期,refreshToken已过期,客户端需要调用refreshToken刷新accessToken
                 * 1、如果refreshToken成功,之前因为accessToken过期请求失败的接口再发起一遍
                 * 2、否则,跳转至认证中心
                 */
                else if (res.code == 15) {
                    if (getRefreshToken()) {
                        smart.ajax(method, url, data, successFn);
                    }
                    else {
                        goSSOLoginUrl();
                    }
                }
                // 其它异常,弹出提示
                else {
                    alert(res.message);
                }
            },
            error: function (xhr, type, errorThrown) {
                alert(JSON.stringify(xhr));
            }
        });
    }
</script>

</html>
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值