引言
想象一下,在日常工作中,我们经常需要进行系统认证
和授权
。当用户尝试登录一个网站时,他们需要提供用户名和密码,网站会检查这些信息,确认用户是谁。这就是认证的过程。
一旦用户被认证,他们可能会尝试访问网站的某些部分,比如他们的个人资料页面,在这个时候,网站需要确定用户有权访问这个页面。这就是授权的过程。
Shiro 与 CAS
认证、授权如何做?能否抽象、通用、一次登录跨系统访问?
当然可以,Shiro 和 CAS 就是一种常见的组合解决方案,也是我们今天要详细讨论的方案。
Shiro 是一个 Java 安全框架,主要用于认证和授权。CAS 是一个单点登录解决方案,主要用于认证。
结合 Shiro 和 CAS,是一种能力组合,我们可以实现一个既安全又便捷的认证和授权方案。
两者的关系?简单理解 Shiro 将认证、授权能力进行抽象,而 CAS 则是【认证】的一种实现方案。授权的实现和具体业务相关联,你可以扩展思考下。
为什么需要单点登录?
在大型企业环境中,我们常常需要在后台系统A、后台系统B、用户中心、配置中心 … 等多个系统之间进行切换。
如果每次切换都需要重新输入账号密码,这无疑会大大降低工作效率,同时还可能增加账号密码泄露的风险。为了解决这个问题,我们引入了单点登录(Single Sign-On)技术。
通过单点登录,用户只需进行一次身份验证,就可以在所有互相信任的系统中自由切换,无需再次进行身份验证。
这不仅提高了工作效率,也大大降低了安全风险。
当然,如果你的系统只有一个应用,那么可能不需要使用 CAS。用户可以直接在这个应用上进行登录,然后就可以使用这个应用的所有功能了。
实践
整体流程
我们先看看整体的认证/授权流程:
我们一般选择在网关统一做认证、授权逻辑:
- 在网关利用 Shiro 的过滤器机制,校验用户是否认证,如果没有则重定向登录页
- 用户输入账号密码,走 CAS 认证
- 认证通过,返回 ticket 票据
- 客户端携带 ticket 票据重新访问业务系统
- 网关 Shiro 过滤器从 param 参数检测到 ticket 信息,走 Shiro 的 login 登录,最终通过 Realm 请求 CAS 换取用户信息。
- 认证成功,并获取到用户信息后,将其存到业务 session 中。
- Shiro 的授权 filter 继续拦截判断权限信息,如果缓存(可选)没有,尝试从权限系统获取权限列表
- 拿到权限后,判断有权限,则继续业务操作。
- 响应业务结果
注意:与 CAS 的交互会还有更多的必要信息,以及可能多次重新定向操作,这里只简单列出了最主要的信息以及流程。
实践
核心步骤:
- Realm:定义 SsoCasRealm 用来与 CAS 服务、权限系统交互
- Filter:定义 SsoFilter 用来拦截判断认证、授权信息
本文只讲核心逻辑,各种包引入、配置等不赘述!
1、Realm
public class SsoCasRealm extends CachingRealm implements Authorizer {
...
// 1、通过ticket获取认证的用户信息
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 通过 ticket 从 cas 获取用户信息
String ticket = (String) casToken.getCredentials();
String userId = casServer.ticketValidateAndGet(ticket, service);
if (null == userId) {
throw new UnsupportedTokenException("用户名或密码错误");
}
User user = new SimpleUser(userId);
Session session = SecurityUtils.getSubject().getSession();
session.setAttribute("user", user);
return new SimpleAuthenticationInfo(user, user.getExt("ticket"), getName());
}
// 2、通过认证通过后的凭证,获取用户权限信息
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
if (principals == null) {
return null;
}
AuthorizationInfo info = null;
// 首先尝试从缓存取
Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
if (cache != null) {
Object key = getAuthorizationCacheKey(principals);
info = cache.get(key);
if (log.isTraceEnabled()) {
if (info == null) {
log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]");
} else {
log.trace("AuthorizationInfo found in cache for principals [" + principals + "]");
}
}
}
// 如果缓存没有
if (info == null) {
// 从权限系统获取权限信息
// 如 RolePermissions rolePermissions = authorization.authorizate(user);
info = doGetAuthorizationInfo(principals);
if (info != null && cache != null) {
// 缓存权限信息
Object key = getAuthorizationCacheKey(principals);
cache.put(key, info);
}
}
return info;
}
...
- 认证信息:通过 ticket 从 CAS 换取用户信息
- 权限信息:先尝试从缓存获取,如果缓存没有就直接从权限系统获取
在 Authorizer 接口中定义了 isPermitted
,我们可以重写它,通过SsoCasRealm.getAuthorizationInfo 获取权限列表:
public boolean[] isPermitted(PrincipalCollection principals, List<Permission> permissions) {
AuthorizationInfo info = getAuthorizationInfo(principals);
return isPermitted(permissions, info);
}
这样上层拦截器就可以直接通过该方法判断是否有权限了。
2、Filter
public abstract class AuthenticatingFilter extends AuthenticationFilter {
...
// 当访问失败时,执行 login 逻辑
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
boolean isAccessed = executeLogin(request, response);
return isAccessed;
}
// 执行登录逻辑
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = createToken(request, response);
if (token == null) {
/**
* 直接跳转
*/
redirectToLogin(request, response);
return false;
}
try {
Subject subject = getSubject(request, response);
subject.login(token);
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}
// 换取 ticket 包装实体
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String ticket = httpRequest.getParameter("ticket");
if (ticket == null) return null;
Map<String, String> attributes = new HashMap<>();
Enumeration<String> paramNames = request.getParameterNames();
while (paramNames.hasMoreElements()) {
String paramName = paramNames.nextElement();
if (paramName.equals("ticket")) continue;
attributes.put(paramName, request.getParameter(paramName));
}
return new SessionCasToken(token);
}
...
值得注意的是:当我们通过 subject.login(token)
登录时,底层就会使用到 SsoCasRealm.doGetAuthenticationInfo 从 CAS 去获取用户信息。
在 AuthenticationFilter 以及其父类中定义了拦截控制:
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = this.getSubject(request, response);
return subject.isAuthenticated();
}
- true:则继续往下走
- false:则走重定向登录逻辑
关于 subject.login
在 Apache Shiro 中,login 方法的主要任务是执行认证操作,也就是验证用户提供的身份(如用户名)和凭证(如密码)是否匹配。
当你调用 Subject.login(token) 方法时,Shiro 会将这个 token 传递给安全管理器(SecurityManager),然后由安全管理器协调相应的认证器(Authenticator)和认证策略进行认证。
在认证过程中,Shiro 会从相应的 Realm 中获取用户的认证信息,然后与 token 中的信息进行比较。如果匹配,认证成功;否则,认证失败,会抛出相应的 AuthenticationException。
一旦认证成功,Shiro 会将用户的认证信息(如用户的身份、角色、权限等)保存到用户的 session 中,以便后续的授权操作。这样,当用户在后续的请求中需要进行授权操作时,Shiro 可以直接从 session 中获取用户的认证信息,而无需再次进行认证。
简单理解:login 方法的主要任务是将 Realm 获取的认证信息存储到 session 中。
结论
在日常工作中,我们通常需要进行系统认证和授权。认证是验证用户身份的过程,而授权则是确定用户权限的过程。
Shiro是一个Java安全框架,主要用于认证和授权。它通过收集用户的身份和凭证进行认证,成功后返回已认证的身份。在授权方面,Shiro提供了基于角色和基于权限的访问控制,满足不同的授权需求。
CAS(Central Authentication Service)是一个单点登录解决方案,主要用于认证。用户在CAS中进行一次认证后,就可以访问所有与CAS集成的应用,无需再次认证。
结合Shiro和CAS,我们可以实现一个既安全又便捷的认证和授权方案:使用Shiro进行应用内的认证和授权,使用CAS实现跨应用的单点登录。