OAuth2.0 SSO单点登录

一、什么是单点登录

单点登录,英文是 Single Sign On,缩写为 SSO。多个站点(192.168.1.20X)共用一台认证授权服务器(192.168.1.110,用户数据库和认证授权模块共用)。

image.png

OAuth 2.0 授权模式的选择image.png

基于授权码模式,实现 SSO 单点登录

image.png

1.初始化

包结构:

image.png

1.1 application.ymal内容

比较简单设置个端口号就行

server:
  port: 18080

1.2 spring security相关配置

1.2.1 新建ServletUtis:

主要作用的方法是 writeJSON ,该放在主要用handle下类中

image.png

1.2.2 新建AuthenticationEntryPointImpl类:

访问URL时未登录,返回错误提示:401 + 账号未登录

image.png

1.2.3 新建AccessDeniedHandlerImpl类:

已登录,但是没有权限,返回错误提示:403 + 没有该操作权限

image.png

1.2.4 新建统一响应类 CommonResult :

image.png

1.2.5 新建SecurityConfiguration类:

image.png

异常处理,分别是没有登录和没有权限,这里我们调用了自己的handler

image.png

2.跳转 SSO 登录

新建index.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>首页</title>
	<!-- jQuery:操作 dom、发起请求等 -->
	<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script>

	<script type="application/javascript">

    /**
     * 跳转单点登录
     */
    function ssoLogin() {
      const clientId = 'yudao-sso-demo-by-code'; // 可以改写成,你的 clientId
      const redirectUri = encodeURIComponent('http://127.0.0.1:18080/callback.html'); // 注意,需要使用 encodeURIComponent 编码地址
      const responseType = 'code'; // 1)授权码模式,对应 code;2)简化模式,对应 token
      window.location.href = 'http://127.0.0.1:1024/sso?client_id=' + clientId
        + '&redirect_uri=' + redirectUri
        + '&response_type=' + responseType;
    }

    /**
     * 修改昵称
     */
    function updateNickname() {
      const nickname = prompt("请输入新的昵称", "");
      if (!nickname) {
        return;
      }
      // 更新用户的昵称
      const accessToken = localStorage.getItem('ACCESS-TOKEN');
      $.ajax({
        url: "http://127.0.0.1:18080/user/update?nickname=" + nickname,
        method: 'PUT',
        headers: {
          'Authorization': 'Bearer ' + accessToken
        },
        success: function (result) {
          if (result.code !== 0) {
            alert('更新昵称失败,原因:' + result.msg)
            return;
          }
          alert('更新昵称成功!');
          $('#nicknameSpan').html(nickname);
        }
      });
    }

    /**
     * 刷新令牌
     */
    function refreshToken() {
      const refreshToken = localStorage.getItem('REFRESH-TOKEN');
      if (!refreshToken) {
        alert("获取不到刷新令牌");
        return;
      }
      $.ajax({
        url: "http://127.0.0.1:18080/auth/refresh-token?refreshToken=" + refreshToken,
        method: 'POST',
        success: function (result) {
          if (result.code !== 0) {
            alert('刷新访问令牌失败,原因:' + result.msg)
            return;
          }
          alert('更新访问令牌成功!');
          $('#accessTokenSpan').html(result.data.access_token);

          // 设置到 localStorage 中
          localStorage.setItem('ACCESS-TOKEN', result.data.access_token);
          localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token);
        }
      });
    }

    /**
     * 登出,删除访问令牌
     */
    function logout() {
      const accessToken = localStorage.getItem('ACCESS-TOKEN');
      if (!accessToken) {
        location.reload();
        return;
      }
      $.ajax({
        url: "http://127.0.0.1:18080/auth/logout",
        method: 'POST',
        headers: {
          'Authorization': 'Bearer ' + accessToken
        },
        success: function (result) {
          if (result.code !== 0) {
            alert('退出登录失败,原因:' + result.msg)
            return;
          }
          alert('退出登录成功!');
          // 删除 localStorage 中
          localStorage.removeItem('ACCESS-TOKEN');
          localStorage.removeItem('REFRESH-TOKEN');

          location.reload();
        }
      });
    }

    $(function () {
      const accessToken = localStorage.getItem('ACCESS-TOKEN');
      // 情况一:未登录
      if (!accessToken) {
        $('#noLoginDiv').css("display", "block");
        return;
      }

      // 情况二:已登录
      $('#yesLoginDiv').css("display", "block");
      $('#accessTokenSpan').html(accessToken);
      // 获得登录用户的信息
      $.ajax({
        url: "http://127.0.0.1:18080/user/get",
        method: 'GET',
        headers: {
          'Authorization': 'Bearer ' + accessToken
        },
        success: function (result) {
          if (result.code !== 0) {
            alert('获得个人信息失败,原因:' + result.msg)
            return;
          }
          $('#nicknameSpan').html(result.data.nickname);
        }
      });
    })
	</script>
</head>
<body>
<!-- 情况一:未登录:1)跳转 ruoyi-vue-pro 的 SSO 登录页 -->
<div id="noLoginDiv" style="display: none">
	您未登录,点击 <a href="#" onclick="ssoLogin()">跳转 </a> SSO 单点登录
</div>

<!-- 情况二:已登录:1)展示用户信息;2)刷新访问令牌;3)退出登录 -->
<div id="yesLoginDiv" style="display: none">
	您已登录!<button onclick="logout()">退出登录</button> <br />
	昵称:<span id="nicknameSpan"> 加载中... </span> <button onclick="updateNickname()">修改昵称</button> <br />
	访问令牌:<span id="accessTokenSpan"> 加载中... </span> <button onclick="refreshToken()">刷新令牌</button> <br />
</div>
</body>
<style>
    body { /** 页面居中 */
        border-radius: 20px;
        height: 350px;
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%,-50%);
    }
</style>
</html>

3.跳转回来附带 code 码

新建 callback.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>SSO 授权后的回调页</title>
	<!-- jQuery:操作 dom、发起请求等 -->
	<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script>
	<!-- 工具类 -->
	<script type="application/javascript">
    (function ($) {
      /**
       * 获得 URL 的指定参数的值
       *
       * @param name 参数名
       * @returns 参数值
       */
      $.getUrlParam = function (name) {
        const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
        const r = window.location.search.substr(1).match(reg);
        if (r != null) return unescape(r[2]); return null;
      }
    })(jQuery);
	</script>

	<script type="application/javascript">
    $(function () {
      // 获得 code 授权码
      const code = $.getUrlParam('code');
      if (!code) {
        alert('获取不到 code 参数,请排查!')
        return;
      }

      // 提交
      const redirectUri = 'http://127.0.0.1:18080/callback.html'; // 需要修改成,你回调的地址,就是在 index.html 拼接的 redirectUri
      $.ajax({
        url:  "http://127.0.0.1:18080/auth/login-by-code?code=" + code
          + '&redirectUri=' + redirectUri,
        method: 'POST',
        success: function( result ) {
          if (result.code !== 0) {
            alert('获得访问令牌失败,原因:' + result.msg)
            return;
          }
          alert('获得访问令牌成功!点击确认,跳转回首页')

          // 设置到 localStorage 中
          localStorage.setItem('ACCESS-TOKEN', result.data.access_token);
          localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token);

          // 跳转回首页
          window.location.href = '/index.html';
        }
      })
    })
	</script>
</head>
<body>
正在使用 code 授权码,进行 accessToken 访问令牌的获取
</body>
</html>

4.获得访问令牌

4.1 新建AuthController的 loginByCode接口:

image.png

4.2 新建 OAuth2Client 的 postAccessToken 方法:

image.png

image.png

BASE_URL对于接口在 cn.iocoder.yudao.module.system.controller.admin.oauth2 : OAuth2OpenControler ,该接口参数比较乱,关注授权码模式的参数就行

/**
     * 对应 Spring Security OAuth 的 TokenEndpoint 类的 postAccessToken 方法
     *
     * 授权码 authorization_code 模式时:code + redirectUri + state 参数
     * 密码 password 模式时:username + password + scope 参数
     * 刷新 refresh_token 模式时:refreshToken 参数
     * 客户端 client_credentials 模式:scope 参数
     * 简化 implicit 模式时:不支持
     *
     * 注意,默认需要传递 client_id + client_secret 参数
     */
    @PostMapping("/token")
    @PermitAll
    @Operation(summary = "获得访问令牌", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用")
    @Parameters({
            @Parameter(name = "grant_type", required = true, description = "授权类型", example = "code"),
            @Parameter(name = "code", description = "授权范围", example = "userinfo.read"),
            @Parameter(name = "redirect_uri", description = "重定向 URI", example = "https://www.iocoder.cn"),
            @Parameter(name = "state", description = "状态", example = "1"),
            @Parameter(name = "username", example = "tudou"),
            @Parameter(name = "password", example = "cai"), // 多个使用空格分隔
            @Parameter(name = "scope", example = "user_info"),
            @Parameter(name = "refresh_token", example = "123424233"),
    })
    @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
    public CommonResult<OAuth2OpenAccessTokenRespVO> postAccessToken(HttpServletRequest request,
                                                                     @RequestParam("grant_type") String grantType,
                                                                     @RequestParam(value = "code", required = false) String code, // 授权码模式
                                                                     @RequestParam(value = "redirect_uri", required = false) String redirectUri, // 授权码模式
                                                                     @RequestParam(value = "state", required = false) String state, // 授权码模式
                                                                     @RequestParam(value = "username", required = false) String username, // 密码模式
                                                                     @RequestParam(value = "password", required = false) String password, // 密码模式
                                                                     @RequestParam(value = "scope", required = false) String scope, // 密码模式
                                                                     @RequestParam(value = "refresh_token", required = false) String refreshToken) { // 刷新模式
        List<String> scopes = OAuth2Utils.buildScopes(scope);
        // 1.1 校验授权类型
        OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType);
        if (grantTypeEnum == null) {
            throw exception0(BAD_REQUEST.getCode(), StrUtil.format("未知授权类型({})", grantType));
        }
        if (grantTypeEnum == OAuth2GrantTypeEnum.IMPLICIT) {
            throw exception0(BAD_REQUEST.getCode(), "Token 接口不支持 implicit 授权模式");
        }

        // 1.2 校验客户端
        String[] clientIdAndSecret = obtainBasicAuthorization(request);
        OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1],
                grantType, scopes, redirectUri);

        // 2. 根据授权模式,获取访问令牌
        OAuth2AccessTokenDO accessTokenDO;
        switch (grantTypeEnum) {
            case AUTHORIZATION_CODE:
                accessTokenDO = oauth2GrantService.grantAuthorizationCodeForAccessToken(client.getClientId(), code, redirectUri, state);
                break;
            case PASSWORD:
                accessTokenDO = oauth2GrantService.grantPassword(username, password, client.getClientId(), scopes);
                break;
            case CLIENT_CREDENTIALS:
                accessTokenDO = oauth2GrantService.grantClientCredentials(client.getClientId(), scopes);
                break;
            case REFRESH_TOKEN:
                accessTokenDO = oauth2GrantService.grantRefreshToken(refreshToken, client.getClientId());
                break;
            default:
                throw new IllegalArgumentException("未知授权类型:" + grantType);
        }
        Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查
        return success(OAuth2OpenConvert.INSTANCE.convert(accessTokenDO));
    }

基于密码模式实现SSO单点登录

image.png

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值