CAS 认证之前后端分离
in with 0 comment

CAS 认证之前后端分离

in with 0 comment

项目介绍

公司目前要接入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的登录状态, 这个还没有找到解决的方法.

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