告别shiro-cas单点登录集成库,这款简单且强壮的Java Web安全引擎pac4j你值得拥有

pac4j官网地址传送门。

pac4j引擎全称为powerful authentication client for java,这是笔者根据其官网介绍推测的全称,不一定正确,姑且这样叫着。

一、缘何遇到该引擎

笔者在集成CAS单点登录服务时使用springboot+shiro搭配shiro-cas库,但是总是遇到非法令牌的问题即invalid_ticket,找了各种解释。

  1. CAS服务器令牌失效时间短的问题1
    笔者更改配置文件,尝试多次,貌似不起作用,官方说的这是默认且唯一的配置;
  2. 缺少其它依赖库2
    引入后也不得行;
  3. 客户端应用路径问题
    Url缺少“/”

总之成功的都是相似的,不成功的原因千奇百怪。

更要命的是说,该库在退出的时候也有bug,折腾了许久,未果。

就在GitHub上搜springboot shiro cas,就出来了使用pac4j引擎的项目,而且是一个很简单的纯测试项目。项目地址传送门。
在这里插入图片描述
下载跑起来测试一下,很舒服,直接成功。 😃
在这里插入图片描述

二、引擎能力

先来看一下这个安全引擎能够支持的框架,如下图,几乎包揽市面上的所有的框架,当然ShiroSpring Security也在其中。在对接的时候引擎抽象了共同点使得工作变得简单。
在这里插入图片描述
支持的认证协议有:

OAuth (Facebook, Twitter, Google…) - SAML - CAS - OpenID Connect - HTTP - Google App Engine
LDAP - SQL - JWT - MongoDB - CouchDB - IP address - Kerberos (SPNEGO) - REST API

支持的授权类型有:

Roles/permissions - Anonymous/remember-me/(fully) authenticated - CORS - CSRF - HTTP Security headers

三、引擎特征

  1. 简单
  2. 高效
  3. 强壮

四、十大核心组件

序号 组件英文名称 组件中文名称 功能描述
1 client 客户端 代表一个认证流程,执行登录逻辑并返回用户信息;UI认证的客户端称为间接客户端(indirect client),web服务认证的客户端称为直接客户端
2 authenticator 认证器 用于HTTP客户端认证身份, ProfileService的子组件,ProfileService不仅验证用户身份,还进行用户信息的创建、更新和删除
3 authorizer 授权器 基于网页上下文信息和用户信息进行权限验证
4 matcher 匹配器 定义安全性是否必须应用于安全过滤器
5 config 配置器 通过客户端、授权器和匹配器定义安全配置
6 user profile 用户身份 经过身份验证的用户的配置文件,具有标识符、属性、角色、权限、“记住我”性质和链接标识符
7 web context 用户身份 pac4j实现的 HTTP 请求和响应以及关联表示会话的实现SessionStore的抽象
8 security filter 安全过滤器 根据客户端和授权器的配置,通过检查用户是否经过身份验证以及授权是否有效来保护请求访问的 URL,如果用户未通过身份验证,则对直接客户端执行身份验证或为间接客户端启动登录过程
9 callback endpoint 回调点 表示间接客户端登录流程的结束
10 logout endpoint 登出点 处理应用或者身份服务器的登出

五、项目移植

笔者需要被集成的Web系统是基于Guns后台开发,版本是beetle版本,项目集成CAS基于spring-shiro-cas移植。

5.1 导包

1
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.0</version> <exclusions> <exclusion> <artifactId>slf4j-api</artifactId> <groupId>org.slf4j</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>io.buji</groupId> <artifactId>buji-pac4j</artifactId> <version>4.0.0</version> </dependency> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-cas</artifactId> <version>3.3.0</version> </dependency>

5.2 配置

1
cas: client-name: app server: url: http://127.0.0.1:8080/cas project: url: http://127.0.0.1:8082/iotProject

5.3 重写认证和授权函数

1
/** * 认证 * * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)throws AuthenticationException { final Pac4jToken pac4jToken = (Pac4jToken) authenticationToken; final List<CommonProfile> commonProfileList = pac4jToken.getProfiles(); final CommonProfile commonProfile = commonProfileList.get(0); logger.info("单点登录返回的信息" + commonProfile.toString()); // final Pac4jPrincipal principal = new Pac4jPrincipal(commonProfileList,getPrincipalNameAttribute()); UserAuthService shiroFactory = UserAuthServiceServiceImpl.me(); User user = shiroFactory.user(commonProfile.getId()); ShiroUser shiroUser = shiroFactory.shiroUser(user); final PrincipalCollection principalCollection = new SimplePrincipalCollection(shiroUser, getName()); return new SimpleAuthenticationInfo(principalCollection,commonProfileList.hashCode()); } /** * 授权/验权(todo 后续有权限在此增加) * * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { UserAuthService shiroFactory = UserAuthServiceServiceImpl.me(); ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal(); List<Long> roleList = shiroUser.getRoleList(); Set<String> permissionSet = new HashSet<>(); Set<String> roleNameSet = new HashSet<>(); for (Long roleId : roleList) { List<String> permissions = shiroFactory.findPermissionsByRoleId(roleId); if (permissions != null) { for (String permission : permissions) { if (ToolUtil.isNotEmpty(permission)) { permissionSet.add(permission); } } } String roleName = shiroFactory.findRoleNameByRoleId(roleId); roleNameSet.add(roleName); } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addStringPermissions(permissionSet); info.addRoles(roleNameSet); return info; }

5.4 调试

5.4.1 CAS验证原理

该图出自CAS官网,传送门
在这里插入图片描述

5.4.2 单点登录流程分析

  1. 在浏览器中输入项目地址,servlet开始处理HTTP请求

  2. 过滤器链中的过滤器执行动作逻辑,在ShiroFilterFactoryBean工厂类中定义一个包含4个过滤器的过滤器Map表,分别是3个自定义和一个默认的过滤器UserFilter

    filterChainDefinitionMap.put("/", "securityFilter");
    filterChainDefinitionMap.put("/callback", "callbackFilter");
    filterChainDefinitionMap.put("/logout", "logoutFilter");
    filterChainDefinitionMap.put("/**","user");
    
  3. securityFilter中,此时还没有任何用户的信息,仅仅是将访问的服务网站重定向到CAS服务器登陆地址,http://127.0.0.1:8080/cas/login?service=http%3A%2F%2F127.0.0.1%3A8082%2FiotProject%2Fcallback%3Fclient_name%3Dapp

  4. 在登陆网页上填写用户名和密码信息后,继续执行过滤器callbackFilter,该过滤器的功能是利用CasAuthenticator验证ticket获取到中央认证服务器上用户的身份信息,接着BaseClient创建用户信息UserProfile,并将用户信息保存到Session中完成信息的共享,在保存的函数中完成用户主体身份login的流程,完成后重定向到受保护的网站即我们的服务网站

  5. 请求再次进到过滤器链中,因为服务地址对应的后台访问接口为“/”,对应着主页,先来看一下该函数:

1
/** * 跳转到主页 */ @RequestMapping(value = "/", method = RequestMethod.GET) public String index(Model model, HttpServletRequest request, HttpServletResponse response) { //获取当前用户角色列表 ShiroUser user = ShiroKit.getUserNotNull(); List<Long> roleList = user.getRoleList(); if (roleList == null || roleList.size() == 0) { ShiroKit.getSubject().logout(); model.addAttribute("tips", "该用户没有角色,无法登陆"); return "/login.html"; } List<MenuNode> menus = userService.getUserMenuNodes(roleList); model.addAttribute("menus", menus); return "/index.html"; }

因此又会进入到securityFilter过滤器中,此时用户已经完成认证,认证成功后直接放行进到后台拦截器中即对应的接口函数中,后续需要用到权限的请求doGetAuthorizationInfo()函数即可,至此完成单点登录功能。

5.5 完成


  1. Cas校验INVALID_TICKET-not recognized ↩︎

  2. 单点登录出现“票根‘ST-xxxxxx-cas’不符合目标服务”的错误的解决办法 ↩︎