实战指南:Shiro、CAS打造完美单点登录体验

打印 上一主题 下一主题

主题 1053|帖子 1053|积分 3159

弁言

想象一下,在一样平常工作中,我们常常必要举行体系认证和授权。当用户实验登录一个网站时,他们必要提供用户名和密码,网站会查抄这些信息,确认用户是谁。这就是认证的过程
一旦用户被认证,他们可能会实验访问网站的某些部分,好比他们的个人资料页面,在这个时候,网站必要确定用户有权访问这个页面。这就是授权的过程
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

  1. public class SsoCasRealm extends CachingRealm implements Authorizer {
  2.    ...
  3.     // 1、通过ticket获取认证的用户信息
  4.     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  5.         // 通过 ticket 从 cas 获取用户信息
  6.         String ticket = (String) casToken.getCredentials();
  7.         String userId = casServer.ticketValidateAndGet(ticket, service);
  8.         if (null == userId) {
  9.             throw new UnsupportedTokenException("用户名或密码错误");
  10.         }
  11.         User user = new SimpleUser(userId);
  12.         Session session = SecurityUtils.getSubject().getSession();
  13.         session.setAttribute("user", user);
  14.         return new SimpleAuthenticationInfo(user, user.getExt("ticket"), getName());
  15.     }
  16.    
  17.    
  18.     // 2、通过认证通过后的凭证,获取用户权限信息
  19.     protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
  20.         if (principals == null) {
  21.             return null;
  22.         }
  23.         AuthorizationInfo info = null;
  24.         // 首先尝试从缓存取
  25.         Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
  26.         if (cache != null) {
  27.             Object key = getAuthorizationCacheKey(principals);
  28.             info = cache.get(key);
  29.             if (log.isTraceEnabled()) {
  30.                 if (info == null) {
  31.                     log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]");
  32.                 } else {
  33.                     log.trace("AuthorizationInfo found in cache for principals [" + principals + "]");
  34.                 }
  35.             }
  36.         }
  37.         // 如果缓存没有
  38.         if (info == null) {
  39.             // 从权限系统获取权限信息
  40.             // 如 RolePermissions rolePermissions = authorization.authorizate(user);
  41.             info = doGetAuthorizationInfo(principals);
  42.             
  43.             if (info != null && cache != null) {
  44.                 // 缓存权限信息
  45.                 Object key = getAuthorizationCacheKey(principals);
  46.                 cache.put(key, info);
  47.             }
  48.         }
  49.         return info;
  50.     }   
  51. ...
复制代码

  • 认证信息:通过 ticket 从 CAS 调换用户信息
  • 权限信息:先实验从缓存获取,假如缓存没有就直接从权限体系获取
在 Authorizer 接口中界说了 isPermitted,我们可以重写它,通过SsoCasRealm.getAuthorizationInfo 获取权限列表:
  1.     public boolean[] isPermitted(PrincipalCollection principals, List<Permission> permissions) {
  2.         AuthorizationInfo info = getAuthorizationInfo(principals);
  3.         return isPermitted(permissions, info);
  4.     }
复制代码
这样上层拦截器就可以直接通过该方法判断是否有权限了。
2、Filter

  1. public abstract class AuthenticatingFilter extends AuthenticationFilter {
  2.   
  3.     ...
  4.    
  5.     // 当访问失败时,执行 login 逻辑
  6.     @Override
  7.     protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
  8.         boolean isAccessed = executeLogin(request, response);
  9.         return isAccessed;
  10.     }
  11.    
  12.     // 执行登录逻辑
  13.     @Override
  14.     protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
  15.         AuthenticationToken token = createToken(request, response);
  16.         if (token == null) {
  17.             /**
  18.              * 直接跳转
  19.              */
  20.             redirectToLogin(request, response);
  21.             return false;
  22.         }
  23.         try {
  24.             Subject subject = getSubject(request, response);
  25.             subject.login(token);
  26.             return onLoginSuccess(token, subject, request, response);
  27.         } catch (AuthenticationException e) {
  28.             return onLoginFailure(token, e, request, response);
  29.         }
  30.     }
  31.     // 换取 ticket 包装实体
  32.     @Override
  33.     protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
  34.         HttpServletRequest httpRequest = (HttpServletRequest) request;
  35.         String ticket = httpRequest.getParameter("ticket");
  36.         if (ticket == null) return null;
  37.         Map<String, String> attributes = new HashMap<>();
  38.         Enumeration<String> paramNames = request.getParameterNames();
  39.         while (paramNames.hasMoreElements()) {
  40.             String paramName = paramNames.nextElement();
  41.             if (paramName.equals("ticket")) continue;
  42.             attributes.put(paramName, request.getParameter(paramName));
  43.         }
  44.         return new SessionCasToken(token);
  45.     }
  46.    
  47.     ...
  48.    
复制代码
值得注意的是:当我们通过 subject.login(token) 登录时,底层就会使用到 SsoCasRealm.doGetAuthenticationInfo 从 CAS 去获取用户信息。
在 AuthenticationFilter 以及其父类中界说了拦截控制:
  1.     protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
  2.         Subject subject = this.getSubject(request, response);
  3.         return subject.isAuthenticated();
  4.     }
复制代码

  • 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实现跨应用的单点登录。
相干参考:

# Apache Shiro 核心原理及实践
# 一站式登录:揭秘CAS单点登录的原理与流程

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

杀鸡焉用牛刀

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表