CAS方式实现单点登录SSO

1. CAS介绍

CAS(Central Authentication Service)中心认证服务
下面这张图来自官网,清晰简单的介绍了CAS的继续交互过程
在这里插入图片描述

2. CAS具体实现

首先需要分别搭建CAS-server和CAS-client服务,
这两个服务分别在2台机器上,官方地址如下:

https://github.com/apereo/java-cas-client

2.1 搭建CAS-server

这一步就不详细阐述了,许多公司内部都已经搭建好了CAS server,我们只需要把我们的域名注册到CAS server即可。

2. 搭建CAS-client

在我们自己的项目中,我们首先需要导入依赖

        <dependency>
            <groupId>org.jasig.cas.client</groupId>
            <artifactId>cas-client-core</artifactId>
            <version>3.6.4</version>
        </dependency>

根据这个库的实现,我们还需要写2个Filter,分别为Filter1_CasAuthenticationFilterFilter2_CasTicketValidationFilter
具体实现如下:
Filter1_CasAuthenticationFilter

package com.vip.data.unific.server.config;

import lombok.extern.slf4j.Slf4j;
import org.jasig.cas.client.authentication.AuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Arrays;


@WebFilter(urlPatterns = "/*")
@Slf4j
public class Filter1_CasAuthenticationFilter implements Filter {

    private AuthenticationFilter authentication;
    @Autowired
    private CasProperties casProperties;
    public Filter1_CasAuthenticationFilter() {
        super();
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

        this.authentication = new AuthenticationFilter();
        this.authentication.setIgnoreInitConfiguration(true);
        this.authentication.setServerName(casProperties.getServerName());
        this.authentication.setCasServerLoginUrl(casProperties.getCasUrlPrefix() + "/login");
        authentication.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        if (Boolean.TRUE.equals(casProperties.getSkipFilter())) {
            chain.doFilter(request, response);
            return ;
        }
        // 不需要 CAS 单点登录的页面直接跳过
        if (Arrays.stream(casProperties.getIgnorePaths()).filter(p -> request.getRequestURI().matches(p)).count() > 0) {
//            log.info(LogMsgKit.of("doFilter").p("uri", request.getRequestURI()).end("跳过cas认证"));
            chain.doFilter(request, response);
            return;
        }
        this.authentication.doFilter(request, response, chain);
    }
    @Override
    public void destroy() {
        this.authentication.destroy();
    }
}

Filter2_CasTicketValidationFilter如下

package com.vip.data.unific.server.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;

@WebFilter(urlPatterns = "/*")
@Slf4j
public class Filter2_CasTicketValidationFilter implements Filter  {

    private static String casServerUrlPrefix = "casServerUrlPrefix";

    private static String serverName = "serverName";

    private static String encoding = "encoding";

    private final TicketValidationWrapper ticketValidation;

    @Autowired
    private CasProperties casProperties;

    public Filter2_CasTicketValidationFilter() {
        super();
        this.ticketValidation = new TicketValidationWrapper();
    }

    @Override
    public void init(final FilterConfig filterConfig) throws ServletException {
        ticketValidation.init(new FilterConfig() {
            @Override
            public String getFilterName() {
                return filterConfig.getFilterName();
            }

            @Override
            public ServletContext getServletContext() {
                return filterConfig.getServletContext();
            }

            @Override
            public String getInitParameter(String name) {
                String value = null;
                if (casServerUrlPrefix.equals(name)) {
                    value = casProperties.getCasUrlPrefix();
                } else if (serverName.equals(name)) {
                    value = casProperties.getServerName();
                } else if(encoding.equals(name)){
                    value = casProperties.getEncoding();
                }
                if (value == null) {
                    value = filterConfig.getInitParameter(name);
                }
                return value;
            }

            @Override
            public Enumeration<String> getInitParameterNames() {
                Enumeration<String> name = filterConfig.getInitParameterNames();
                Set<String> set = new HashSet<>();
                while (name.hasMoreElements()) {
                    set.add(name.nextElement());
                }
                set.add(casServerUrlPrefix);
                set.add(serverName);
                set.add(encoding);
                final Iterator<String> iterator = set.iterator();
                set = null;
                return new Enumeration<String>() {
                    @Override
                    public boolean hasMoreElements() {
                        return iterator.hasNext();
                    }
                    @Override
                    public String nextElement() {
                        return iterator.next();
                    }
                };
            }
        });
        ticketValidation.setRedirectAfterValidation(false);//取消重定向,自定义重定向
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
        if (Boolean.TRUE.equals(casProperties.getSkipFilter())) {
            chain.doFilter(request, response);
            return ;
        }
        // 不需要 CAS 单点登录的页面直接跳过
        if (Arrays.stream(casProperties.getIgnorePaths()).filter(p -> request.getRequestURI().matches(p)).count() > 0) {
//            log.info(LogMsgKit.of("doFilter").p("uri", request.getRequestURI()).end("跳过cas认证"));
            chain.doFilter(request, response);
            return;
        }
        ticketValidation.doFilter(request, response, chain);
    }

    @Override
    public void destroy() {
        ticketValidation.destroy();
    }
}

这2个Filter基本就是固定写法,顺序最好不要交换(虽然交换了也能执行成功),

3、常见问题讨论

常见问题

如果我有多台服务器,如何实现分布式session共享?

单点登录其本质就是分布式session共享的一种解决方案,也就是集中管理session,所以单点登录已经解决了session共享问题

用户访问/api/xxx路径,登录成功后跳转到/api/aaa路径,如何修改重定向路径?

修改重定向有2个方法,一个是修改response如下,但是cas-client中是直接response.sendRedirect();所以这种方法不管用

response.setHeader(“Location”,“/index”);
response.setStatus(302);
第二种方法,继承Cas20ProxyReceivingTicketValidationFilter,然后重写onSuccessfulValidation方法,自己定义重定向地址

public class TicketValidationWrapper extends Cas20ProxyReceivingTicketValidationFilter {
 
//    @Value("${cas.redirectURL}") //filter启动先于spring bean初始化
    public static final String redirectURL="/";
    @Override
    protected void onSuccessfulValidation(final HttpServletRequest request, final HttpServletResponse response,
                                          final Assertion assertion) {
        try {
            response.sendRedirect(redirectURL);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

为什么需要2个Filter,能写在一起吗?

2个Filter负责的职责不同,理论上可以写在一起,但是分开写逻辑更加清楚

这两个Filter的顺序能交换吗?

通过实验测试,发现2个Filter交换顺序并不会影响登录,用户体验结果是一样,但是F2放在F1前面的话会多走1此Filter,建议Filter1 auth,Filter2 ticket

Filter1_CasAuthenticationFilter
这个主要用来判断用户是否登录,没有登录则重定向到登录界面进行登录,登录完成继续重定向到原始路径

Filter2_CasTicketValidationFilter
这个主要用来验证是否有ST-ticket(ticket是一次性使用的)

session到底存储在哪?

登录成功后,CAS-server有一个session,当用户请求的cookie中携带TGT-xxx访问CAS-server时,可以得到一个ST-ticket

自己的app中也会存储一个session,当用户请求的cookie中携带jsessionid时,则判断为登录成功

也就是CAS-server和自己的app都存储一份session,这两个session是不一样的

编码时可能出现的问题?

注册Filter时,使用2个注解即可,否则会多次注册Filer,导致一个请求执行多次Filter

@ServletComponentScan 加在springboot启动main函数上
@WebFilter(urlPatterns = “/*”) 加在Filter上

Filter上不用加@Component,加了可能会报错或者filter多次注册
Filer执行多次原因还有可能是浏览器默认请求了favicon.ico这个文件,可能检查网络请求中是否有

参考链接:https://blog.csdn.net/chaijunkun/article/details/7646338

在Filter进行注入时要注意,无法使用@Value注入,因为Filter启动时,springbean还没初始化?(可能)

如何控制Filter执行顺序

@order注解不管用,默认是按照类名,所以建议以Filer1xxx,Filter2xxx命名

参考链接:https://www.cnblogs.com/tfgzs/p/4571137.html

下面是一些调研

分布式session解决方案
方案1:Tomcat集群Session全局复制(集群内每个tomcat的session完全同步)【会影响集群的性能】

方案2:根据请求的IP进行Hash映射到对应的机器上【如果服务器宕机了,会丢失Session的数据,实现最简单】

方案3:引入中间件Redis,把Session数据放在Redis中,已经实现的框架有Spring session 使用Spring Session和Redis解决分布式Session共享【有一定的侵入性,实现难度中等,】

方案4:JWT方式,user信息保存在token中,每次请求都携带token【可能存在安全问题,网络开销大一点】

(单点登录其实就属于方案1和3的结合,CAS-server保存登录状态,每个app中也保存的单独的session)

共同点(单点登录的核心)
问题:单点登录的问题是session是各个系统所独自拥有的,各个系统不知道用户是否登录,无法共享用户的登录状态,
目标/切入点:目标/切入点是 “一定要让所有的系统就都可以知道现在用户登录没有”,只要能够实现这个目标/切入点,就可以作为方案,所以 Tomcat集群Session全局复制、请求的IP一直会访问同一个服务器、引入中间件Redis 都是方案。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
# sso-shiro-cas spring下使用shiro+cas配置单点登录,多个系统之间的访问,每次只需要登录一次 ## 系统模块说明 1. cas单点登录模块,这里直接拿的是cas的项目改了点样式而已 2. doc: 文档目录,里面有数据库生成语句,采用的是MySQL5.0,数据库名为db_test 3. spring-node-1: 应用1 4. spring-node-2: 应用2 其中node1跟node2都是采用spring + springMVC + mybatis 框架,使用maven做项目管理 ## cas集成说明 1.首先采用的是查数据库的方式来校验用户身份的,在cas/WEB-INF/deployerConfigContext.xml中第135行构建了这个类型 ``` xml ``` 其中QueryDatabaseAuthenticationHandler这个类是自定义构建的,在cas/WEB-INF/lib/cas-jdbc-1.0.0.jar里面,有兴趣的同学可以发编译看下,关于几个属性的说明 1. dataSource: 数据源,配置MySQL的连接信息 2. passwordEncoder: 加密方式,这里用的是MD5 3. sql: sql查询语句,这个语句就是根据用户输入的账号查询其密码 #### 以上就是单点登录管理的主要配置 ## 应用系统的配置node1 1. 应用系统采用shiro做权限控制,并且跟cas集成 2. 在/spring-node-1/src/main/resources/conf/shiro.properties 文件中 ``` properties shiro.loginUrl=http://127.0.0.1:8080/cas/login?service=http://127.0.0.1:8081/node1/shiro-cas shiro.logoutUrl=http://127.0.0.1:8080/cas/logout?service=http://127.0.0.1:8081/node1/shiro-cas shiro.cas.serverUrlPrefix=http://127.0.0.1:8080/cas shiro.cas.service=http://127.0.0.1:8081/node1/shiro-cas shiro.failureUrl=/users/loginSuccess shiro.successUrl=/users/loginSuccess ``` 其中shiro.loginUrl 跟 shiro.logoutUrl的前面是cas验证的地址,后面的是我们应用系统的地址,这样配置的方式是为了在访问我们的应用系统的时候,先到cas进行验证,如果验证成功了,cas将重定向到shiro.successUrl 所表示的地址 3.在/spring-node-1/src/main/resources/conf/shiro.xml 文件中 ``` xml /shiro-cas = casFilter /logout = logoutFilter /users/** = user ``` > 其中shiroFilter这个类主要用于需要拦截的url请求,需要注意的是这个是shiro的拦截,我们还需要配置cas的过滤配置casFilter > casRealm这个类是需要我们自己实现的,主要用于shiro的权限验证,里面的属性说明如下 1. defaultRoles: 默认的角色 2. casServerUrlPrefix: cas地址 3. casService: 系统应用地址 最后我们还需要在/spring-node-1/src/main/webapp/WEB-INF/web.xml 文件中配置相关的过滤器拦截全部请求 ``` xml shiroFilter org.springframework.web.filter.DelegatingFilterProxy targetFilterLifecycle true shiroFilter /* ``` ## 系统运行 1. 端口说明,cas:8080,node1:8081,node2:8082,大家可以采用maven提供的tomcat7插件,配置如下: ``` xml org.apache.tomcat.maven tomcat7-maven-plugin 2.1 8081 UTF-8 tomcat7 /node1 ``` 这样的配置,我们甚至都不需要配置tomcat服务器了,建议这种方式 2.各个模块的访问地址 > cas:http://127.0.0.1:8080/cas > node1:http://127.0.0.1:8081/node1 > node2:http://127.0.0.1:8082/node2 3.访问系统 > 输入 http://127.0.0.1:8081/node1/shiro-cas ,进入cas验证 > 输入用户名 admin,密码 admin@2015,验证成功后将会重定向到http://127.0.0.1:8081/node1//users/loginSuccess ,也就是node1系统的主页,里面的节点2代表的是node2系统的主页,你会发现我们不需要登录到node2系统就能访问其中的系统了

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值