Sa-Token整合OAuth2实现单点登录

Oauth2实现单点登录


今天春天太苦了!!先后经历了失恋,然后住院,春招也就找了半个月,幸亏秋招的时候有公司捞我了,不然今年要失业了!!!
话不多说,步入正题,单点登录这个问题我很久之前有研究过,但是使用SpringSecurity写的,只感觉很离谱,要的配置实在是太多了,这个月公司提前配置,要整这个东西,我用SpringSecurity这个Oauth2实现单点登录又做了一次,但这次死活不行,前后弄了一个多星期,真离谱!!
后来发现了Sa-token的这款工具,发现挺好用的,查了查,好像说比Shiro和SpringSecurity还要好,于是使用这个将单点登录,Oauth2分开做了一下,最后还整合了一下(这两个整合有点鸡肋,其实没必要,但有些业务是这样需求的,就比如我公司配置就要求进行整合…)

很多前置知识不懂可以去看sa-token的官网

Sa-token下的单点登录

认证中心

  1. 开放认证接口
@RestController
public class SsoServerController {

    //处理所有的sso请求
    @RequestMapping("/sso/*")
    public Object ssoRequest() {
        return SaSsoProcessor.instance.serverDister();
    }

    @Autowired
    private void configSso(SaSsoConfig sso) {
        // 配置:未登录时返回的View
        sso.setNotLoginView(() -> {
            return new ModelAndView("login.html");
        });

        // 配置:登录处理函数
        sso.setDoLoginHandle((name, pwd) -> {
            // 此处仅做模拟登录,真实环境应该查询数据进行登录
            if("admin".equals(name) && "123456".equals(pwd)) {
                StpUtil.login(10001);
                return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
            }
            return SaResult.error("登录失败!");
        });

        // 配置 Http 请求处理器 (在模式三的单点注销功能下用到,如不需要可以注释掉)
        sso.setSendHttp(url -> {
            try {
                // 发起 http 请求
                System.out.println("------ 发起请求:" + url);
                return Forest.get(url).executeAsString();
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        });
    }
}

登录的login.html如下

DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Sa-OAuth2-认证中心-登录页title>
    <style type="text/css">
        body{background-color: #F5F5D5;}
        *{margin: 0px; padding: 0px;}
        .login-box{width: 400px; margin: 20vh auto;}
        .login-box input{line-height: 25px; margin-bottom: 10px;}
        .login-box button{padding: 5px 15px; cursor: pointer; }
    style>
head>
<body>
<div class="login-box">
    <h2>Sa-OAuth2-认证中心-登录页h2> <br>
    账号:<input name="name" /> <br>
    密码:<input name="pwd" type="password" /> <br>
    <button onclick="doLogin()">登录button>
    <span style="color: #666;">(测试账号: sa 123456)span>
div>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js">script>
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js">script>
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')script>
<script type="text/javascript">

    // 登录方法
    function doLogin() {
        console.log('-----------');
        $.ajax({
            url: '/sso/doLogin',
            data: {
                name: $('[name=name]').val(),
                pwd: $('[name=pwd]').val()
            },
            dataType: 'json',
            success: function(res) {
                if(res.code == 200) {
                    layer.msg('登录成功!');
                    setTimeout(function() {
                        // location.reload(true);
                        location.reload();
                    }, 800);
                } else {
                    layer.alert(res.msg);
                }
            },
            error: function(e) {
                console.log('error');
            }
        });
    }
script>
body>
html>

yml文件配置如下:

# ??
server:
  port: 9000

# Sa-Token ??
sa-token:


  sso:
    # Ticket??? (??: ?)??????
    ticket-timeout: 300
    # ???????????
    allow-url: "*"
    # ??????????
    is-slo: true

    # ------- SSO-??????? ???????SSO????? is-slo=true ????
    # ???????
    isHttp: true
    # ?????????SSO???????????
    secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
    # ---- ????????????? Sa-Token ??http??????????????


spring:
  # Redis?? ?SSO?????????Redis??????
  redis:
    # Redis?????????0?
    database: 1
    # Redis?????
    host: 182.92.5.218
    # Redis???????
    port: 6379
    # Redis?????????????
    password: 123456


forest:
  # ?? forest ??????
  log-enabled: false


认证中心已经配置完毕,现在到客户端中心

客户端

  1. 配置yml,redis的配置要使用同一个redis
# ??
server:
  port: 9002

# sa-token??
sa-token:


  # SSO-????
  sso:
    # SSO-Server? ??????
    auth-url: http://localhost:9000/sso/auth
    # ??????????
    is-slo: true
  # ??Sa-Token?????Redis?? ??????SSO-Server??????Redis?


spring:
  # Redis?? ?SSO?????????Redis??????
  redis:
    # Redis?????????0?
    database: 1
    # Redis?????
    host: 182.92.5.218
    # Redis???????
    port: 6379
    # Redis?????????????
    password: 123456


  1. 配置controller
@RestController
public class SsoClientController {

    // 首页
    @RequestMapping("/")
    public String index() {
        return StpUtil.isLogin()?"登录成功":"未登录";
    }
    @RequestMapping("/test")
    public String test() {
        return "test....";
    }

    /*
     * SSO-Client端:处理所有SSO相关请求
     *         http://{host}:{port}/sso/login          -- Client端登录地址,接受参数:back=登录后的跳转地址
     *         http://{host}:{port}/sso/logout         -- Client端单点注销地址(isSlo=true时打开),接受参数:back=注销后的跳转地址
     *         http://{host}:{port}/sso/logoutCall     -- Client端单点注销回调地址(isSlo=true时打开),此接口为框架回调,开发者无需关心
     */
    //这里是必须配置的
    @RequestMapping("/sso/*")
    public Object ssoRequest() {
        return SaSsoProcessor.instance.clientDister();
    }

}

我们测试的是/test路径,但是这里还需要一个拦截器:

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
    /** 注册 [Sa-Token全局过滤器] */
    @Bean
    public SaServletFilter getSaServletFilter() {
        return new SaServletFilter()
                .addInclude("/**")
                .addExclude("/sso/*","/oauth2/*" ,"/favicon.ico")
                .setAuth(obj -> {
                    //没登录的时候就去登录
                    if(!StpUtil.isLogin()) {
                        String back = SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString());
                        SaHolder.getResponse().redirect("/sso/login?back=" + SaFoxUtil.encodeUrl(back));
                        SaRouter.back();
                    }
                })
                ;
    }
}

测试的时候,首先会跳转到本客户端下的login,然后重定向到认证中心的login,之后就重定向回来,再次进入该拦截器(这里是重点,会再次进入拦截器),然后判断已经登录了,所以跳过,进入/test路径成功,返回数据,整个单点登录就完成了,当你新建一个客户端,就无需再次登录了.

Oauth3.0

认证中心

sa-token下的Oauth2.0做起来也比较简单:

server:
  port: 8001

# sa-token??
sa-token:
  # token?? (????cookie??)
  token-name: satoken-server
  # OAuth2.0 ??
  oauth2:
    is-code: true
    is-implicit: true
    is-password: true
    is-client: true


spring:
  # redis??
  redis:
    # Redis?????????0?
    database: 1
    # Redis?????
    host: 182.92.5.218
    # Redis???????
    port: 6379
    # Redis?????????????
    password: 123456
    # ??????????
    timeout: 1000ms
    lettuce:
      pool:
        # ????????
        max-active: 200
        # ???????????????????????
        max-wait: -1ms
        # ???????????
        max-idle: 10
        # ???????????
        min-idle: 0




@RestController
public class SaOAuth2ServerController {

    // 处理所有OAuth相关请求
    @RequestMapping("/oauth2/*")
    public Object request() {
        System.out.println("------- 进入请求: " + SaHolder.getRequest().getUrl());
        return SaOAuth2Handle.serverRequest();
    }


    @Autowired
    public void setSaOAuth2Config(SaOAuth2Config config){
        config.
                // 未登录的视图
                setNotLoginView(()->{
                    return new ModelAndView("login.html");
                }).
                // 登录处理函数
                setDoLoginHandle((name, pwd) -> {
                    if("admin".equals(name) && "123456".equals(pwd)) {
                        StpUtil.login(1001);
                        return SaResult.ok();
                    }
                    return SaResult.error("账号名或密码错误");
                }).
                // 授权确认视图
                setConfirmView((clientId, scope)->{
                    Map<String, Object> map = new HashMap<>();
                    map.put("clientId", clientId);
                    map.put("scope", scope);
                    return new ModelAndView("confirm.html", map);
                })
        ;
    }
}

注意,这里的Oauth所有请求必须是/oauth2开头的,其他的不行,而sso单点登录的可以修改,只需要保持与客户端的一致就可以了,但这里不行

@Component
public class SaOAuth2TemplateImpl extends SaOAuth2Template {

    @Override
    public SaClientModel getClientModel(String clientId) {
        return new SaClientModel()
                .setClientId("1001")
                .setClientSecret("123456")
                .setAllowUrl("*")
                .setIsAutoMode(true).setContractScope("userinfo");

    }

    @Override
    public String getOpenid(String clientId, Object loginId) {
        return "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__";
    }
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Sa-OAuth2-认证中心-登录页</title>
    <style type="text/css">
        body{background-color: #F5F5D5;}
        *{margin: 0px; padding: 0px;}
        .login-box{width: 400px; margin: 20vh auto;}
        .login-box input{line-height: 25px; margin-bottom: 10px;}
        .login-box button{padding: 5px 15px; cursor: pointer; }
    </style>
</head>
<body>
<div class="login-box">
    <h2>Sa-OAuth2-认证中心-登录页</h2> <br>
    账号:<input name="name" /> <br>
    密码:<input name="pwd" type="password" /> <br>
    <button onclick="doLogin()">登录</button>
    <span style="color: #666;">(测试账号: sa 123456</span>
</div>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')</script>
<script type="text/javascript">

    // 登录方法
    function doLogin() {
        console.log('-----------');
        $.ajax({
            url: '/oauth2/doLogin',
            data: {
                name: $('[name=name]').val(),
                pwd: $('[name=pwd]').val()
            },
            dataType: 'json',
            success: function(res) {
                if(res.code == 200) {
                    layer.msg('登录成功!');
                    setTimeout(function() {
                        // location.reload(true);
                        location.reload();
                    }, 800);
                } else {
                    layer.alert(res.msg);
                }
            },
            error: function(e) {
                console.log('error');
            }
        });
    }
</script>
</body>
</html>

DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Sa-OAuth2-认证中心-确认授权页title>
    <style type="text/css">
        body{background-color: #F5F5D5;}
        *{margin: 0px; padding: 0px;}
        .login-box{width: 400px; margin: 20vh auto; padding: 70px; border: 1px #000 solid;}
        .login-box button{padding: 5px 15px; cursor: pointer; }
    style>
head>
<body>
<div class="login-box">
    <h2>Sa-OAuth2-认证中心-确认授权页h2> <br>
    <div>
        <div><b>应用ID:b><span th:utext="${clientId}">span>div>
        <div><b>请求授权:b><span th:utext="${scope}">span>div>
        <br><div>------------- 是否同意授权 -------------div><br>
        <div>
            <button onclick="yes()">同意button>
            <button onclick="no()">拒绝button>
        div>
    div>
div>
<script src="https://unpkg.zhimg.com/[email protected]/dist/jquery.min.js">script>
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js">script>
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')script>
<script type="text/javascript">

    // 同意授权
    function yes() {
        console.log('-----------');
        $.ajax({
            url: '/oauth2/doConfirm',
            data: {
                client_id: getParam('client_id'),
                scope: getParam('scope')
            },
            dataType: 'json',
            success: function(res) {
                if(res.code == 200) {
                    layer.msg('授权成功!');
                    setTimeout(function() {
                      //页面自动刷新,重定向回初始页面
                        location.reload(true);
                    }, 800);
                } else {
                    // 重定向至授权失败URL
                    layer.alert('授权失败!');
                }
            },
            error: function(e) {
                console.log('error');
            }
        });
    }

    // 拒绝授权
    function no() {
        var url = joinParam(getParam('redirect_uri'), "handle=refuse&msg=用户拒绝了授权");
        location.href = url;
    }

    // 从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);
    }

    // 在url上拼接上kv参数并返回
    function joinParam(url, parameStr) {
        if(parameStr == null || parameStr.length == 0) {
            return url;
        }
        var index = url.indexOf('?');
        // ? 不存在
        if(index == -1) {
            return url + '?' + parameStr;
        }
        // ? 是最后一位
        if(index == url.length - 1) {
            return url + parameStr;
        }
        // ? 是其中一位
        if(index > -1 && index < url.length - 1) {
            // 如果最后一位是 不是&, 且 parameStr 第一位不是 &, 就增送一个 &
            if(url.lastIndexOf('&') != url.length - 1 && parameStrindexOf('&') != 0) {
                return url + '&' + parameStr;
            } else {
                return url + parameStr;
            }
        }
    }


script>
body>
html>

客户端

我们先写yml配置:

server:
  port: 8002

# sa-token??
sa-token:
  # token?? (????cookie??)
  token-name: satoken-client

spring:
  # redis??
  redis:
    # Redis?????????0?
    database: 1
    # Redis?????
    host: 182.92.5.218
    # Redis???????
    port: 6379
    # Redis?????????????
    password: 123456
    # ??????????
    timeout: 1000ms
    lettuce:
      pool:
        # ????????
        max-active: 200
        # ???????????????????????
        max-wait: -1ms
        # ???????????
        max-idle: 10
        # ???????????
        min-idle: 0

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Sa-OAuth2-Client-测试页</title>
    <style type="text/css">
        body{background-color: #D0D9E0;}
        *{margin: 0px; padding: 0px;}
        .login-box{max-width: 1000px; margin: 30px auto; padding: 1em;}
        .info{line-height: 30px;}
        .btn-box{margin-top: 10px; margin-bottom: 15px;}
        .btn-box a{margin-right: 10px;}
        .btn-box a:hover{text-decoration:underline !important;}
        .login-box input{line-height: 25px; margin-bottom: 10px; padding-left: 5px;}
        .login-box button{padding: 5px 15px; margin-top: 20px; cursor: pointer; }
        .login-box a{text-decoration: none;}
        .pst{color: #666; margin-top: 15px;}
        .ps{color: #666; margin-left: 10px;}
        .login-box code{display: block; background-color: #F5F2F0; border: 1px #ccc solid; color: #600; padding: 15px; margin-top: 5px; border-radius: 2px; }
        .info b,.info span{color: green;}
    </style>
</head>
<body>
<div class="login-box">
    <h2>Sa-OAuth2-Client-测试页</h2> <br>
    <div class="info">
        <div>当前账号id:
            <b class="uid" th:utext="${uid}"></b>
        </div>
        <div>当前Openid<span class="openid"></span></div>
        <div>当前Access-Token<span class="access_token"></span></div>
        <div>当前Refresh-Token<span class="refresh_token"></span></div>
        <div>当前令牌包含Scope<span class="scope"></span></div>
        <div>当前Client-Token<span class="client_token"></span></div>
    </div>
    <div class="btn-box">
        <a href="javascript:logout();">注销</a>
        <a href="/">回到首页</a>
    </div>
    <hr><br>

    <h3>模式一:授权码(Authorization Code</h3>
    <p class="pst">授权码:OAuth2.0标准授权流程,先 (重定向) 获取Code授权码,再 (Rest API) 获取 Access-TokenOpenid </p>

    <a href="http://localhost:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://localhost:8002/">
        <button>点我开始授权登录(静默授权)</button>
    </a>
    <span class="ps">当请求链接不包含scope权限时,将无需用户手动确认,做到静默授权,当然此时我们也只能获取openid</span>
    <code>http://localhost:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://localhost:8002/</code>

    <a href="http://localhost:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo">
        <button>授权登录(显式授权)</button>
    </a>
    <span class="ps">当请求链接包含具体的scope权限时,将需要用户手动确认,此时我们除了openid以外还可以获取更多的资源</span>
    <code>http://localhost:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo</code>

    <button onclick="refreshToken()">刷新令牌</button>
    <span class="ps">我们可以拿着 Refresh-Token 去刷新我们的 Access-Token,每次刷新后旧Token将作废</span>
    <code>http://localhost:8001/oauth2/refresh?grant_type=refresh_token&client_id={value}&client_secret={value}&refresh_token={value}</code>

    <button onclick="getUserinfo()">获取账号信息</button>
    <span class="ps">使用 Access-Token 置换资源: 获取账号昵称、头像、性别等信息 (Access-Token具备userinfo权限时才可以获取成功) </span>
    <code>http://localhost:8001/oauth2/userinfo?access_token={value}</code>

    <br>
    <h3>模式二:隐藏式(Implicit</h3>
    <a href="http://localhost:8001/oauth2/authorize?response_type=token&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo">
        <button>隐藏式</button>
    </a>
    <span class="ps">越过授权码的步骤,直接返回token到前端页面( 格式:http//:domain.com#token=xxxx-xxxx )
    <code>http://localhost:8001/oauth2/authorize?response_type=token&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo</code>

    <br>
    <h3>模式三:密码式(Password</h3>
    <p class="pst">在下面输入Server端的用户名和密码,使用密码式进行 OAuth2 授权登录</p>
    账号:<input name="username">
    密码:<input name="password">
    <button onclick="passwordLogin()">登录</button>
    <code>http://localhost:8001/oauth2/token?grant_type=password&client_id={value}&client_secret={value}&username={value}&password={value}</code>

    <br>
    <h3>模式四:凭证式(Client Credentials</h3>
    <p class="pst">以上三种模式获取的都是用户的 Access-Token,代表用户对第三方应用的授权,在OAuth2.0中还有一种针对 Client级别的授权,
        即:Client-Token,代表应用自身的资源授权</p>
    <p class="pst">Client-Token具有延迟作废特性,即:在每次获取最新Client-Token的时候,旧Client-Token不会立即过期,而是作为Past-Token再次
        储存起来,资源请求方只要携带其中之一便可通过Token校验,这种特性保证了在大量并发请求时不会出现“新旧Token交替造成的授权失效”,
        保证了服务的高可用</p>

    <button onclick="getClientToken()">获取应用Client-Token</button>
    <code>http://localhost:8001/oauth2/client_token?grant_type=client_credentials&client_id={value}&client_secret={value}</code>

    <br><br>
    <span>更多资料请参考 Sa-Token 官方文档地址:</span>
    <a href="https://sa-token.cc/">https://sa-token.cc/</a>

    <div style="height: 200px;"></div>
</div>
<script src="https://unpkg.zhimg.com/[email protected]/dist/jquery.min.js"></script>
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')</script>
<script type="text/javascript">

    // 根据code授权码进行登录
    function doLogin(code) {
        $.ajax({
            url: '/codeLogin?code=' + code,
            dataType: 'json',
            success: function(res) {
                console.log('返回:', res);
                if(res.code == 200) {
                    setInfo(res.data);
                    layer.msg('登录成功!');
                } else {
                    layer.msg(res.msg);
                }
            },
            error: function(xhr, type, errorThrown){
                return layer.alert("异常:" + JSON.stringify(xhr));
            }
        });
    }
    var code = getParam('code');
    if(code) {
        alert("进入了...code")
        doLogin(code);
    }else{
        alert("2222222")
    }

    // 根据 Refresh-Token 去刷新 Access-Token
    function refreshToken() {
        var refreshToken = $('.refresh_token').text();
        if(refreshToken == '') {
            return layer.alert('您还没有获取 Refresh-Token ,请先授权登录');
        }
        $.ajax({
            url: '/refresh?refreshToken=' + refreshToken,
            dataType: 'json',
            success: function(res) {
                console.log('返回:', res);
                if(res.code == 200) {
                    setInfo(res.data);
                    layer.msg('登录成功!');
                } else {
                    layer.msg(res.msg);
                }
            },
            error: function(xhr, type, errorThrown){
                return layer.alert("异常:" + JSON.stringify(xhr));
            }
        });
    }

    // 模式三:密码式-授权登录
    function passwordLogin() {
        $.ajax({
            url: '/passwordLogin',
            data: {
                username: $('[name=username]').val(),
                password: $('[name=password]').val()
            },
            dataType: 'json',
            success: function(res) {
                console.log('返回:', res);
                if(res.code == 200) {
                    setInfo(res.data);
                    layer.msg('登录成功!');
                } else {
                    layer.msg(res.msg);
                }
            },
            error: function(xhr, type, errorThrown){
                return layer.alert("异常:" + JSON.stringify(xhr));
            }
        });
    }

    // 模式四:获取应用的 Client-Token
    function getClientToken () {
        $.ajax({
            url: '/clientToken',
            dataType: 'json',
            success: function(res) {
                console.log('返回:', res);
                if(res.code == 200) {
                    setInfo(res.data);
                    layer.msg('获取成功!');
                } else {
                    layer.msg(res.msg);
                }
            },
            error: function(xhr, type, errorThrown){
                return layer.alert("异常:" + JSON.stringify(xhr));
            }
        });
    }

    // 使用 Access-Token 置换资源: 获取账号昵称、头像、性别等信息
    function getUserinfo() {
        var accessToken = $('.access_token').text();
        if(accessToken == '') {
            return layer.alert('您还没有获取 Access-Token ,请先授权登录');
        }
        $.ajax({
            url: '/getUserinfo',
            data: {accessToken: accessToken},
            dataType: 'json',
            success: function(res) {
                if(res.code == 200) {
                    layer.alert(JSON.stringify(res.data));
                } else {
                    layer.alert(res.msg);
                }
            },
            error: function(xhr, type, errorThrown){
                return layer.alert("异常:" + JSON.stringify(xhr));
            }
        });
    }

    // 注销
    function logout() {
        $.ajax({
            url: '/logout',
            dataType: 'json',
            success: function(res) {
                location.href = '/';
            },
            error: function(xhr, type, errorThrown){
                return layer.alert("异常:" + JSON.stringify(xhr));
            }
        });
    }


    // 写入数据
    function setInfo(info) {
        console.log('info', info);
        for (var key in info) {
            $('.' + key).text(info[key]);
        }
        if($('.uid').text() == '') {
            $('.uid').html('<b style="color: #E00;">未登录</b>')
        }
    }
    setInfo({});

    // 从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>
</body>
</html>

我们以授权码模式来说,关键在于对http://localhost:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=…这个路径的请求,请求完这个路径后,会返回一个code码,这个code不是token,再接续写代码:

package com.hyb.client;

import javax.servlet.http.HttpServletRequest;

import cn.dev33.satoken.sso.SaSsoProcessor;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import com.ejlchina.okhttps.OkHttps;


import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;

/**
 * Sa-OAuth2 Client端 控制器
 * @author kong
 */
@RestController
public class SaOAuthClientController {

    // 相关参数配置
    private String clientId = "1001";								// 应用id
    private String clientSecret = "123456";		// 应用秘钥
    private String serverUrl = "http://localhost:8001";	// 服务端接口

    // 进入首页
    @RequestMapping("/")
    public Object index(HttpServletRequest request) {
        request.setAttribute("uid", StpUtil.getLoginIdDefaultNull());
        return new ModelAndView("index.html");
    }    // 进入首页





    // 根据Code码进行登录,获取 Access-Token 和 openid
    @RequestMapping("/codeLogin")
    public SaResult codeLogin(String code) {
        // 调用Server端接口,获取 Access-Token 以及其他信息
        String str = OkHttps.sync(serverUrl + "/oauth2/token")
                .addBodyPara("grant_type", "authorization_code")
                .addBodyPara("code", code)
                .addBodyPara("client_id", clientId)
                .addBodyPara("client_secret", clientSecret)
                .post()
                .getBody()
                .toString();
        SoMap so = SoMap.getSoMap().setJsonString(str);
        System.out.println("返回结果: " + so);

        // code不等于200  代表请求失败
        if(so.getInt("code") != 200) {
            return SaResult.error(so.getString("msg"));
        }

        // 根据openid获取其对应的userId
        SoMap data = so.getMap("data");
        long uid = getUserIdByOpenid(data.getString("openid"));
        data.set("uid", uid);

        // 返回相关参数
        StpUtil.login(uid);
        return SaResult.data(data);
    }

    // 根据 Refresh-Token 去刷新 Access-Token
    @RequestMapping("/refresh")
    public SaResult refresh(String refreshToken) {
        // 调用Server端接口,通过 Refresh-Token 刷新出一个新的 Access-Token
        String str = OkHttps.sync(serverUrl + "/oauth2/refresh")
                .addBodyPara("grant_type", "refresh_token")
                .addBodyPara("client_id", clientId)
                .addBodyPara("client_secret", clientSecret)
                .addBodyPara("refresh_token", refreshToken)
                .post()
                .getBody()
                .toString();
        SoMap so = SoMap.getSoMap().setJsonString(str);
        System.out.println("返回结果: " + so);

        // code不等于200  代表请求失败
        if(so.getInt("code") != 200) {
            return SaResult.error(so.getString("msg"));
        }

        // 返回相关参数 (data=新的Access-Token )
        SoMap data = so.getMap("data");
        return SaResult.data(data);
    }

    // 模式三:密码式-授权登录
    @RequestMapping("/passwordLogin")
    public SaResult passwordLogin(String username, String password) {
        // 模式三:密码式-授权登录
        String str = OkHttps.sync(serverUrl + "/oauth2/token")
                .addBodyPara("grant_type", "password")
                .addBodyPara("client_id", clientId)
                .addBodyPara("client_secret", clientSecret)
                .addBodyPara("username", username)
                .addBodyPara("password", password)
                .post()
                .getBody()
                .toString();
        SoMap so = SoMap.getSoMap().setJsonString(str);
        System.out.println("返回结果: " + so);

        // code不等于200  代表请求失败
        if(so.getInt("code") != 200) {
            return SaResult.error(so.getString("msg"));
        }

        // 根据openid获取其对应的userId
        SoMap data = so.getMap("data");
        long uid = getUserIdByOpenid(data.getString("openid"));
        data.set("uid", uid);

        // 返回相关参数
        StpUtil.login(uid);
        return SaResult.data(data);
    }

    // 模式四:获取应用的 Client-Token
    @RequestMapping("/clientToken")
    public SaResult clientToken() {
        // 调用Server端接口
        String str = OkHttps.sync(serverUrl + "/oauth2/client_token")
                .addBodyPara("grant_type", "client_credentials")
                .addBodyPara("client_id", clientId)
                .addBodyPara("client_secret", clientSecret)
                .post()
                .getBody()
                .toString();
        SoMap so = SoMap.getSoMap().setJsonString(str);
        System.out.println("返回结果: " + so);

        // code不等于200  代表请求失败
        if(so.getInt("code") != 200) {
            return SaResult.error(so.getString("msg"));
        }

        // 返回相关参数 (data=新的Client-Token )
        SoMap data = so.getMap("data");
        return SaResult.data(data);
    }

    // 注销登录
    @RequestMapping("/logout")
    public SaResult logout() {
        StpUtil.logout();
        return SaResult.ok();
    }

    // 根据 Access-Token 置换相关的资源: 获取账号昵称、头像、性别等信息
    @RequestMapping("/getUserinfo")
    public SaResult getUserinfo(String accessToken) {
        // 调用Server端接口,查询开放的资源
        String str = OkHttps.sync(serverUrl + "/oauth2/userinfo")
                .addBodyPara("access_token", accessToken)
                .post()
                .getBody()
                .toString();
        SoMap so = SoMap.getSoMap().setJsonString(str);
        System.out.println("返回结果: " + so);

        // code不等于200  代表请求失败
        if(so.getInt("code") != 200) {
            return SaResult.error(so.getString("msg"));
        }

        // 返回相关参数 (data=获取到的资源 )
        SoMap data = so.getMap("data");
        return SaResult.data(data);
    }

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


    // ------------ 模拟方法 ------------------
    // 模拟方法:根据openid获取userId
    private long getUserIdByOpenid(String openid) {
        // 此方法仅做模拟,实际开发要根据具体业务逻辑来获取userId
        return 10001;
    }

}

从上面可以看到,会看到一个/codeLogin请求,这个请求就是根据code这个码,去申请token,这个token可以解析出用户的账号和密码,到这里Oauth2的内容结束了.
总结: 先请求得到code,根据code得到信息,授权码模式的路径大概就是先/oauth2/authorize,然后到/codeLogin?code=…

Sa-token使用Oauth3实现单点登录

在sa-token里,将它们实现的sso和oauth做了一个比较:
https://sa-token.cc/doc.html#/fun/sso-vs-oauth2
并且对于Oauth做了一个解释:简单来讲,OAuth2.0的应用场景可以理解为单点登录的升级版,单点登录解决了多个系统间会话的共享,OAuth2.0在此基础上增加了应用之间的权限控制 (SO:有些系统采用OAuth2.0模式实现了单点登录,但这总给人一种“杀鸡焉用宰牛刀”的感觉)
所以说,在sa-token中,并没有使用Oauth2整合单点登录的例子,认为没有必要,而如果真的要使用,请看下面的步骤:

认证中心

  1. yml要将两种技术的配置结合在一起,不能写不一致的token名称:
# ??
server:
  port: 9000

# Sa-Token ??
sa-token:
  oauth2:
    is-code: true
    is-implicit: true
    is-password: true
    is-client: true

  sso:
    ticket-timeout: 300
    allow-url: "*"
    is-slo: true
    isHttp: true
    secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor


spring:
  # Redis?? ?SSO?????????Redis??????
  redis:
    # Redis?????????0?
    database: 1
    # Redis?????
    host: 182.92.5.218
    # Redis???????
    port: 6379
    # Redis?????????????
    password: 123456


forest:
  # ?? forest ??????
  log-enabled: false

  1. controller的配置也结合在一起:
@RestController
public class SsoServerController {

    //处理所有的sso请求,这里是处理单点登录的
    @RequestMapping("/sso/*")
    public Object ssoRequest() {
        return SaSsoProcessor.instance.serverDister();
    }

    //这里是处理授权的,授权的这个路径不能改变样子
    @RequestMapping("/oauth2/*")
    public Object request() {
        System.out.println("------- 进入请求: " + SaHolder.getRequest().getUrl());
        return SaOAuth2Handle.serverRequest();
    }

    @Autowired
    public void setSaOAuth2Config(SaOAuth2Config config){
        config.
                // 授权确认视图
                //
                    setConfirmView((clientId, scope)->{
                    Map<String, Object> map = new HashMap<>();
                    map.put("clientId", clientId);
                    map.put("scope", scope);
                    return new ModelAndView("confirm.html", map);
                })
        ;
    }

    @Autowired
    private void configSso(SaSsoConfig sso) {
        // 配置:未登录时返回的View
        sso.setNotLoginView(() -> {
            return new ModelAndView("login.html");
        });

        // 配置:登录处理函数
        sso.setDoLoginHandle((name, pwd) -> {
            // 此处仅做模拟登录,真实环境应该查询数据进行登录
            if("admin".equals(name) && "123456".equals(pwd)) {
                StpUtil.login(10001);
                return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
            }
            return SaResult.error("登录失败!");
        });

        // 配置 Http 请求处理器 (在模式三的单点注销功能下用到,如不需要可以注释掉)
        sso.setSendHttp(url -> {
            try {
                // 发起 http 请求
                System.out.println("------ 发起请求:" + url);
                return Forest.get(url).executeAsString();
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        });
    }
}
  1. 然后就是将上面Oauth流程中的confirm页面和sso流程中login页面粘贴到认证中心里
  2. 然后就是与SaOAuth2TemplateImpl一模一样的配置也写上

以上便是Oauth2实现单点登录的全部认证中心的配置,这里的大概逻辑就是,sso实现登录,oauth实现授权

客户端

客户端便有点讲究,我们必须要认清楚Oauth2.0实现单点登录的流程:
Sa-Token整合OAuth2实现单点登录_第1张图片
我们可以看到前面一部分是sso的内容,也就是如果访问应用路径不存在,就重定向到统一认证服务,这里直接使用和sso单点登录的拦截器就可以了(上面有配置),但上面有说明,认证成功重定向回来后,还会进入拦截器,所以,这个时候我们得在拦截器里做文章:

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
    /** 注册 [Sa-Token全局过滤器] */
    @Bean
    public SaServletFilter getSaServletFilter() {
        return new SaServletFilter()
                .addInclude("/**")
                .addExclude("/sso/*","/oauth2/*" ,"/favicon.ico")
                .setAuth(obj -> {
                    //没登录的时候就去登录
                    if(!StpUtil.isLogin()) {
                        String back = SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString());

                        SaHolder.getResponse().redirect("/sso/login?back=" + SaFoxUtil.encodeUrl(back));
                        SaRouter.back();
                    //登录后但还未授权就去授权
                    }else if (SaFoxUtil.isEmpty(SaHolder.getRequest().getParam("code"))){
                        String back = SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString());
                        SaHolder.getResponse().redirect("http://localhost:9000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=" + SaFoxUtil.encodeUrl(back)+"&scope=userinfo");
                        SaRouter.back();
                    }
                    //既登录又授权之后,就可以拿到code去请求token了,就是像上面说的请求codeLogin路径就可以了,可以不在这里写
                })
                ;
    }
}

流程如下: 第一次请求/test路径,没登录,去认证中心进行sso登录,登录后回调,回到拦截器,判断已经登录,来到第二个if判断,发现没授权,去请求授权,然后携带者code重定向回来,这个时候,已经登录,且code不为空,说明已经登录并授权,可以拿着这个code去申请token了,这个token里就包含着用户信息,这样就相当于登录且授权成功.
其他controller的配置如下:

@RestController
public class SsoClientController {

    // 首页
    @RequestMapping("/")
    public String index() {
        return StpUtil.isLogin()?"登录成功":"未登录";
    }
    @RequestMapping("/test")
    public String test() {
        return "test....";
    }

    /*
     * SSO-Client端:处理所有SSO相关请求
     *         http://{host}:{port}/sso/login          -- Client端登录地址,接受参数:back=登录后的跳转地址
     *         http://{host}:{port}/sso/logout         -- Client端单点注销地址(isSlo=true时打开),接受参数:back=注销后的跳转地址
     *         http://{host}:{port}/sso/logoutCall     -- Client端单点注销回调地址(isSlo=true时打开),此接口为框架回调,开发者无需关心
     */
    @RequestMapping("/sso/*")
    public Object ssoRequest() {
        return SaSsoProcessor.instance.clientDister();
    }

}
# ??
server:
  port: 9002

# sa-token??
sa-token:

  # SSO-????
  sso:
    # SSO-Server? ??????
    auth-url: http://localhost:9000/sso/auth
    # ??????????
    is-slo: true

spring:
  # Redis?? ?SSO?????????Redis??????
  redis:
    # Redis?????????0?
    database: 1
    # Redis?????
    host: 182.92.5.218
    # Redis???????
    port: 6379
    # Redis?????????????
    password: 123456


这个客户端的配置其实就是sso单点登录的配置是一样的,只不过加了点拦截器的配置而已

Jwt插件配置

这款插件可以使得token是jwt风格的

@Configuration
public class SaTokenConfigure {
    // Sa-Token 整合 jwt (Simple 简单模式)
    @Bean
    public StpLogic getStpLogicJwt() {
        return new StpLogicJwtForSimple();
    }
}

sa-token:
	  jwt-secret-key: ddhvsuidvsjkdgqwuidgwf

这个配置即可,还需要一个maven

        
            cn.dev33
            sa-token-jwt
            1.34.0
        

更多插件配置可以看官网

你可能感兴趣的:(Spring,java,开发语言)