切换原因

shiro-1.3.x以后的版本中,shiro-cas包里面的所有类都被标识为deprecated,详细:https://github.com/apache/shiro/pull/33。
个人认为不继续维护shiro-cas而切换到pac4j主要有以下几点原因:

  1. shiro-cas非常的不灵活,比如在CasFilteronLoginSuccess(..)登录成功事件,就只考虑到了通过cas跳转过来的情况,直接就执行了issueSuccessRedirect(..)跳转到保存的请求地址,并没有考虑到如果是ajax请求该如何处理。
  2. 认证协议太多,如果需要扩展一个微博登录(oauth2)那么还得加一个shiro-oauth?pac4j原生就支持很多的认证协议 OAuth (Facebook, Twitter, Google…) - SAML - CAS - OpenID Connect - HTTP - OpenID - Google App Engine LDAP - SQL - JWT - MongoDB - Stormpath - IP address

pac4j介绍

pac4j定位是一个java安全引擎(Java security engine),所以不止包含支持各种认证协议进行登录,同时也支持 角色权限的管理、记住登录、CORS、CSRF、输出Http安全头 等功能,并且可以和很多著名的框架结合,比如官方列举的 J2E、Spring Web MVC (Spring Boot)、Spring Security (Spring Boot)、Shiro、Play 2.x、Vertx、Spark Java、Ratpack、Undertow、CAS server、Dropwizard、Knox、Jooby 。

由于我们的项目使用的Apache shiro,并且也经过了很长时间的开发和对shiro的扩展,所以如果没有特别大的需求,不会去替换掉shiro框架,所以采用了buji-cas4j来提供shiro和pac4j的结合。

pac4j大量用到了java8的特性编写,所以最低配置要求java8以及shiro 1.3.x+。

替换shiro-cas

引入依赖

  1. 移除shiro-cas的依赖
  2. 加入pac4j-cas依赖(目前我们使用的1.9.2),自身已依赖的pac4j-core无需再自己添加
  3. 加入buji-pac4j依赖(目前我们使用的2.0.2)

去掉shiro-cas相关bean

各自对shiro-cas扩展程度不同,基本上去掉依赖以后所有ide报错的地方都需要改,这里就记录一下我们项目中修改的东西:

  1. 自定义的Realm不再继承自CasRealm,修改为Pac4jRealm
  2. 去掉CasFilter过滤器,改用buji-pac4j提供的CallbackFilter(下面会介绍如果配置这个bean)
  3. 将CasSubjectFactory修改为Pac4jsubjectFactory

配置pac4j-cas

  1. 首先定义CasConfiguration(loginUrl,prefixUrl),loginUrl为完整的cas登录地址,比如client项目的https://passport.sqzryang.com/login?service=https://client.sqzryang.com,prefixUrl则为cas路径前缀,根据cas的版本号拼接请求地址,用于验证sts是否正确并且返回登录成功后的信息。
  2. 定义CasClient,property configuration(CasConfiguration) and callbackUrl(String),在pac4j中,每一个client相当于一种认证协议,比如我们需要weibo登录则应该配置一个WeiboClient,具体回掉的时候应该采用哪个client进行验证授权则需要下面配置的Clients
  3. 定义Clients,在这里面可以定义你所有的Client以及默认的client还有关于如何区分回掉的哪个client应该取某个参数的配置,具体详细看源码
  4. 定义Config,在config里面还有关于权限方面的配置以及session存储的一些配置,由于这部分我交给shiro去管理,所以只传入了clients即可
  5. 以上四个都是pac4j的配置,接下来配置一个由buji-pac4j提供用于和shiro结合的filter:CallbackFilter,直接传入config即可
  6. 定义好CallbackFilter以后,在ShiroFilterFactoryBean中注册好filters,并且配置好filterChainDefinitions

具体各个bean的一些基本概念Main concepts and components,一个稍微全面点的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<bean id="casClientConfiguration" class="org.pac4j.cas.config.CasConfiguration">
<constructor-arg name="loginUrl" value="${shiro.loginUrl}"/>
<constructor-arg name="prefixUrl" value="${shiro.casServerUrlPrefix}"/>
</bean>

<bean id="casClient" class="org.pac4j.cas.client.CasClient">
<property name="configuration" ref="casClientConfiguration"/>
<property name="callbackUrl" value="${shiro.casService}"/>
</bean>

<bean id="casClients" class=" org.pac4j.core.client.Clients">
<property name="clients">
<util:list>
<ref bean="casClient"/>
</util:list>
</property>
<property name="defaultClient" ref="casClient"/>
</bean>

<bean id="casConfig" class="org.pac4j.core.config.Config">
<property name="clients" ref="casClients"/>
</bean>

<bean name="casCallbackFilter" class="io.buji.pac4j.filter.CallbackFilter">
<property name="config" ref="casConfig"/>
</bean>

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"
p:securityManager-ref="securityManager"
p:filterChainDefinitions="/cas = cas\n/logout = logout" <!-- 省略了配置,其他的自己加上 -->
p:loginUrl="${shiro.loginUrl}"
p:successUrl="${shiro.successUrl}">
<property name="filters">
<util:map>
<!-- ... -->
<entry key="cas" value-ref="casCallbackFilter"/> <!-- 注册filter -->
<!-- ... -->
</util:map>
</property>
</bean>

其他修改地方

  • 登录后 Principal 为 Pac4jPrincipal对象,获取cas传递回来的username,通过:String username = pac4jPrincipal.getProfile().getId();
  • 如果开启了缓存,应重写权限缓存以及认证缓存的key值,在AuthorizingRealm中的getAuthorizationCacheKey以及getAuthenticationCacheKey,推荐使用username来作为缓存key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

@Override
protected Object getAuthorizationCacheKey(PrincipalCollection principals) {
Pac4jPrincipal pac4jPrincipal = (Pac4jPrincipal) principals.getPrimaryPrincipal();
return pac4jPrincipal.getProfile().getId();
}

@Override
protected Object getAuthenticationCacheKey(AuthenticationToken token) {
if (token instanceof Pac4jToken) {
Pac4jToken pac4jToken = (Pac4jToken) token;
Object principal = pac4jToken.getPrincipal();
if (principal instanceof Optional) {
@SuppressWarnings("unchecked") Optional<CasProfile> casProfileOptional = (Optional<CasProfile>) principal;
return casProfileOptional.get().getId();
}
}
return super.getAuthenticationCacheKey(token);
}
  • 通过默认的CallbackFilter登录成功以后,会直接redirectToOriginallyRequestedUrl,但是在pac4j里面没有再去读取被shiro userfilter检测到未登录后存在session中的SavedRequest,而是读取org.pac4j.core.context.Pac4jConstants#REQUESTED_URL,因此重写UserFilter中的saveRequest适配pac4j
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

public class UserFilter extends org.apache.shiro.web.filter.authc.UserFilter {


@Override
protected void saveRequest(ServletRequest request) {
// 还是先执行着shiro自己的方法
super.saveRequest(request);
Session session = SecurityUtils.getSubject().getSession();
session.setAttribute(Pac4jConstants.REQUESTED_URL, toHttp(request).getRequestURI());
}


}

最后

以上配置以后基本就能跑起来了,但是还是有很多细节的地方需要去处理,后面会慢慢写出来,比如如何去配置jasig cas通过ajax来登录,以及相比Spring Securityshiro原生所缺乏的 csrf cors http头部安全 功能通过pac4j都可以去实现…

参考资料

  • https://github.com/bujiio/buji-pac4j
  • http://www.pac4j.org/docs/index.html