pac4j引擎全称为powerful authentication client for java,这是笔者根据其官网介绍推测的全称,不一定正确,姑且这样叫着。
笔者在集成CAS单点登录服务时使用springboot+shiro
搭配shiro-cas
库,但是总是遇到非法令牌的问题即invalid_ticket,找了各种解释。
默认且唯一
的配置;“/”
总之成功的都是相似的,不成功的原因千奇百怪。
更要命的是说,该库在退出的时候也有bug,折腾了许久,未果。
就在GitHub
上搜springboot shiro cas
,就出来了使用pac4j
引擎的项目,而且是一个很简单的纯测试项目。项目地址传送门。
下载跑起来测试一下,很舒服,直接成功。
先来看一下这个安全引擎能够支持的框架,如下图,几乎包揽市面上的所有的框架,当然Shiro
和Spring 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 | 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
移植。
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-coreartifactId>
<version>1.4.0version>
<exclusions>
<exclusion>
<artifactId>slf4j-apiartifactId>
<groupId>org.slf4jgroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.4.0version>
dependency>
<dependency>
<groupId>io.bujigroupId>
<artifactId>buji-pac4jartifactId>
<version>4.0.0version>
dependency>
<dependency>
<groupId>org.pac4jgroupId>
<artifactId>pac4j-casartifactId>
<version>3.3.0version>
dependency>
cas:
client-name: app
server:
url: http://127.0.0.1:8080/cas
project:
url: http://127.0.0.1:8082/iotProject
/**
* 认证
*
* @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;
}
在浏览器中输入项目地址,servlet
开始处理HTTP
请求
过滤器链中的过滤器执行动作逻辑,在ShiroFilterFactoryBean
工厂类中定义一个包含4个过滤器的过滤器Map
表,分别是3个自定义和一个默认的过滤器UserFilter
:
filterChainDefinitionMap.put("/", "securityFilter");
filterChainDefinitionMap.put("/callback", "callbackFilter");
filterChainDefinitionMap.put("/logout", "logoutFilter");
filterChainDefinitionMap.put("/**","user");
在securityFilter
中,此时还没有任何用户的信息,仅仅是将访问的服务网站重定向到CAS服务器登陆地址,http://127.0.0.1:8080/cas/login?service=http%3A%2F%2F127.0.0.1%3A8082%2FiotProject%2Fcallback%3Fclient_name%3Dapp
在登陆网页上填写用户名和密码信息后,继续执行过滤器callbackFilter
,该过滤器的功能是利用CasAuthenticator
验证ticket
获取到中央认证服务器上用户的身份信息,接着BaseClient
创建用户信息UserProfile
,并将用户信息保存到Session
中完成信息的共享,在保存的函数中完成用户主体身份login
的流程,完成后重定向到受保护的网站即我们的服务网站
请求再次进到过滤器链中,因为服务地址对应的后台访问接口为“/”
,对应着主页,先来看一下该函数:
/**
* 跳转到主页
*/
@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()
函数即可,至此完成单点登录功能。
Cas校验INVALID_TICKET-not recognized ↩︎
单点登录出现“票根‘ST-xxxxxx-cas’不符合目标服务”的错误的解决办法 ↩︎