CAS 认证之前后端分离

/ vuejava / 1 条评论 / 609浏览

项目介绍

公司目前要接入CAS服务,旧项目使用后端渲染、shiro管理权限,所以这次的任务就是完成旧项目认证还是前后分离的新项目.

对于使用后端渲染的旧项目使用的是pac4j代替shiro-cas完成认证.

前后端分离认证

注: 接口开发采用spring-boot

前后端分离有两种认证方式:

  1. 使用JWT验证, 纯前后分离(前端有自己的域名or服务) [推荐]
  2. 使用JWT + CAS验证, 半分离(开发属于两个项目, 上线之后可以把前端项目打包放入项目resource下面, 即合并为一个项目)

第二种方式最后是回到了后端渲染的方式, 接口与cas认证,然后签发Token给前端,页面跳转都可以由后端控制,但是失去了前后端分离的优点,不推荐.

我采用的是第一种方式

认证流程

  1. spring-boot 配置shiro, pac4j 及CAS域名等 这些网上都有教程.

  2. 配置CAS项目

在此需要注意, 在配置文件中需要加入

cas.httpWebRequest.header.xframe=false

(取消x-frame-options为deny限制,允许外部项目使用iframe嵌入cas-server登录页面) 其他配置可以参考网上教程, 附 [官网路径] 前端将会通过iframe引入登录页面完成交互

  1. 在spring-boot下创建三个接口
  @SneakyThrows
  @ResponseBody
  @GetMapping({ "v1/login-path" })
  @ApiOperation(value = "获取登录URL", notes = "获取登录URL")
  ResponseResult getLoginPath(HttpServletRequest request, Map<String, Object> result){
      // 登录路径
      result.put("loginPath", new StringBuffer(casLoginUrl).append("?service=").append(
              URLEncoder.encode(
                      new StringBuffer(callbackUrl).append("?").append("client_name=cas").toString(),
                      StandardCharsets.UTF_8.name()
              )).toString());
      return ResponseResult.builder().content(result).build();
  }
  @SneakyThrows
  @ResponseBody
  @GetMapping({ "v1/jwt" })
  @RequiresPermissions("lerp:navg:funs:all")
  @ApiOperation(value = "获取JWT", notes = "转换登录信息为JWT")
  void getJWT(HttpServletRequest request){
      String token = null;
      final PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
      if (principals != null) {
          final Pac4jPrincipal principal = principals.oneByType(Pac4jPrincipal.class);
          if (principal != null) {
              CommonProfile profile = principal.getProfile();
              token = jwtGenerator.generate(profile);
          }
      }
      // 通知登录成功
      SocketUtil.builder()
              .sourceId(UUID.randomUUID().toString())
              .targetId(SecurityUtils.getSubject().getSession().getId().toString())
              .content(token)
              .build()
              .emit("message");
  }
  @SneakyThrows
  @GetMapping({ "v1/creat-session" })
  @ApiOperation(value = "创建session", notes = "创建session")
  String getLoginPath(){
      // 如不存在session,则创建
      SecurityUtils.getSubject().getSession();
      return "creat-session";
  }
  // 如没有session会创建, 如存在则不会创建
  SecurityUtils.getSubject().getSession();
  <html>
  <script type="text/javascript" >
      <!-- 将sessionId发送给父级页面 -->
      window.parent.postMessage(JSON.stringify({ cookie: document.cookie }), '*');
  </script>
</html>

至此后端工作已经完成, 下面为前端需要的工作. 我前端项目采用的Vue

  1. 登录模块主要代码摘要
  <iframe id="loginFrame" class="lxb-login-frame-from" :src="iframePath"></iframe>
  <iframe :src="creatSessionPath" class="lxb-login-frame-creat-session"></iframe>

第一个iframe用来加载登录页面, 通过接口 v1/login-path 交互获取登录路径 第二个iframe用来创建session,拿到sessionId

                //接收子窗口消息
                let that = this;
                window.addEventListener('message', function (e) {
                    let data = e.data;
                    if (typeof(e.data) === 'string') {
                        data = JSON.parse(e.data);
                    }
                    // 子窗口回传sessionId
                    if (data.cookie) {
                        that.sessionId = data.cookie.split('=')[1];
                    }
                    // 登录组件加载完成
                    if (data.loaded) {
                        that.spinShow = false;
                    }
                    // 提交
                    if (data.submit) {
                        // that.spinShow = true;
                    }
                }, false);

通过 window.addEventListener 得到iframe子页面 window.parent.postMessage 传递的值 我在CAS登录页面中也使用了 window.parent.postMessage , 在页面加载完成和点击登录按钮分别 postMessage 不同的标识

  1. 最后一步获取登录成功后台生成的JWT
              this.socket = io(constant.loginSocketIo + '?clientId=' + this.sessionId);
              this.socket.on('connect', () => {
                  this.log('connected ');
              });
              this.socket.on('messageevent', (data) => {
                  // 登录成功
                  sessionStorage.setItem('token', data.msgContent);
                  this.$store.commit(LOGIN_STATE, true);
                  this.$router.push('/home');
              });

socket 我使用的是 socket.io 至此登录流程完成.

页面加载登录页面, 并且创建一个与接口交互的session, 用户登录成功之后我配置的登录成功将会进入 v1/jwt 生成 token, 并通过 socket 发送给前端, 前端收到token 之后, 将 token 存储在 sessionStorage 并 commit Vuex标识登录成功, 最后跳转页面到首页.

注: v1/creat-session 这个接口有两个功能

  1. 成 sessionId, 可以通过 sessionId 发送生成的 token
  2. 最重要的一点, 因为不止要完成前后端的认证, 还要兼容其他后端渲染系统, 但是后端渲染系统并不是通过 token 验证, 而是通过 session, 所以这儿创建 session 是与其他系统相通的关键. 前后端登录成功之后, 与接口交互通过 token, 如有需求跳转到其他系统, 则可以域名直接跳过去.

在完成前后端分离认证之前,我也在网上看了很多方式, 但是一直没有找到解决的办法, 故通过自己的想法完成登录, 如有更好的方法请分享一下.

最后还有一个问题就是怎么维护CAS的登录状态, 这个还没有找到解决的方法.

下篇写一下我的前端鉴权, 动态菜单, 以及动态路由.

  1. ~~~~~~~~

    回复