前言

    现在由于单点登录的普及,Cas和Oauth2都能完成,最简单的方法是将Cas的依赖导入后,添加上Cas的过滤器,当用户访问系统时就会被拦截,然后就可以重定向到SSO单点登录界面进行单点登录了,登录完成后就会将用户信息存储到session中,用户下一次再访问资源就不用再被拦截了,同时还会在cookie设置上一个域为Cas服务器的CASTGC,那么只要在同一个浏览器中,当用户访问同样集成了Cas客户端的应用的时候,Cas拦截器就会自动带着cookie中的CASTGC去拿到ticket,然后校验ticket,最后将用户信息同样存储到session中,那么用户访问其他应用也不需要再次登录了,实现了单点登录。

在这里插入图片描述

一、整合前后端分离遇到的问题

    前后端系统遇到最多的问题肯定就是302重定向的问题了,即使已经进行了单点登录,请求也会被Cas客户端的过滤器所拦截,这是由于Cas无法感知到已经登录所造成的。
    前后端分离架构中通常使用Ajax请求,而Ajax请求对于tomcat来说都是一个新的请求,就都会创建一个新的JSESSIONID,但我们的用户信息是存储在第一次登录成功的CAS客户端的session上的,这样Cas客户端自然就不能Ajax请求的JSESSIONID查找到用户信息了,就自然被拦截了。
    这时有人会说一个集成了CAS客户端的新的应用系统第一次访问不也是一个新的JSESSIONID吗,那怎么也能实现单点登录,其实这是上述所说的CASTGC这个cookie发挥的巨大作用,当新的应用系统第一次访问时,CAS客户端会将这个cookie拿到CAS服务器获取ticket票据返回,然后CAS客户端再拿着ticket票据到CAS服务器进行校验,校验通过则获得用户信息并将用户信息存储到session中,这样新的应用系统也能实现单点登录了,测试方法是当cookie有CASTGC这个参数时,可以将应用系统的cookie下的JSESSIONID删除,再刷新被CAS客户端拦截的请求,就可以发现上面的流程被执行了。
    很可惜的是Ajax请求同样不具备这个能力,因此请求自然就被CAS客户端拦截,然后重定向到CAS服务端的单点登录界面了。

二、CAS整合前后端分离项目实战

    整合CAS应用系统通常是为了对接第三方提供的CAS服务器,因此应用系统很可能已经有了一套自己的登录逻辑,而整合CAS的目的也明确,就是为了获取CAS服务器上用户的用户名等信息,从而在自己的应用系统上实现登录,为了应用系统的登录逻辑保持不变,二次登录是更好的选择,做法是先CAS服务器上的用户名等信息然后再调用应用系统的登录逻辑完成登录。下面就来真正整合一下项目,我这里采用的是springboot项目。

第一步、引入CAS客户端依赖

<!--cas客户端-->
<dependency>
    <groupId>com.cas</groupId>
    <artifactId>cas-client-core</artifactId>
    <version>3.2.1</version>
</dependency>

需要注意,这个依赖是一个演示,每个CAS服务器对应的依赖版本都可能不同,具体看实际情况进行依赖的选取。

第二步、编写Cas经典拦截器的配置类

@Configuration
public class CasConfig {


    /**
     * 用于实现单点登出功能
     */
    @Bean
    public FilterRegistrationBean<SingleSignOutFilter> logOutFilter() {
        FilterRegistrationBean<SingleSignOutFilter> authenticationFilter = new FilterRegistrationBean();
        authenticationFilter.setFilter(new SingleSignOutFilter());
        authenticationFilter.addUrlPatterns("/*");
        authenticationFilter.setOrder(1);
        return authenticationFilter;
    }


    //配置认证Filter
    @Bean
    public FilterRegistrationBean authenticationFilterRegistrationBean() {
        FilterRegistrationBean authenticationFilter = new FilterRegistrationBean();
        authenticationFilter.setFilter(new AuthenticationFilter());
        Map<String, String> initParameters = new HashMap<String, String>();
        initParameters.put("casServerLoginUrl", "https://authserver.hainanu.edu.cn/authserver/login");
        initParameters.put("serverName", "http://f27714p111.imdo.co/");
        //CAS过滤器白名单的设置,不同版本名称不同,可点进AuthenticationFilter进行查看
		initParameters.put("casWhiteUrl","/MissionController/searchMissionStatusCount/*.*,/casLogout");
        authenticationFilter.setInitParameters(initParameters);
        authenticationFilter.setOrder(2);
        List<String> urlPatterns = new ArrayList<String>();
        urlPatterns.add("/*");// 设置匹配的url
        authenticationFilter.setUrlPatterns(urlPatterns);
        return authenticationFilter;
    }

    //配置ticket验证Filter
    @Bean
    public FilterRegistrationBean ValidationFilterRegistrationBean(){
        FilterRegistrationBean authenticationFilter = new FilterRegistrationBean();
        authenticationFilter.setFilter(new Cas20ProxyReceivingTicketValidationFilter());
        Map<String, String> initParameters = new HashMap<String, String>();
        initParameters.put("casServerUrlPrefix", "https://authserver.hainanu.edu.cn/authserver");
        initParameters.put("serverName", "http://f27714p111.imdo.co/");
        authenticationFilter.setInitParameters(initParameters);
        authenticationFilter.setOrder(1);
        List<String> urlPatterns = new ArrayList<String>();
        urlPatterns.add("/*");// 设置匹配的url
        authenticationFilter.setUrlPatterns(urlPatterns);
        return authenticationFilter;
    }


    //配置获取用户信息的Filter
    //request.getRemoteUser()
    @Bean
    public FilterRegistrationBean casHttpServletRequestWrapperFilter(){
        FilterRegistrationBean authenticationFilter = new FilterRegistrationBean();
        authenticationFilter.setFilter(new HttpServletRequestWrapperFilter());
        authenticationFilter.setOrder(3);
        List<String> urlPatterns = new ArrayList<String>();
        urlPatterns.add("/*");// 设置匹配的url
        authenticationFilter.setUrlPatterns(urlPatterns);
        return authenticationFilter;
    }

}

到这里为止,CAS的客户端整合就算是完成了。

第三步、编写一个CasController,专门负责Cas的业务

第一步、编写cas登录方法

@RequestMapping("/casLogin")
public void casLogin(HttpServletRequest request,HttpServletResponse response) throws IOException {

    String username = request.getRemoteUser();

    String sessionId = request.getSession().getId();

    //返回给前端,这里先采用百度进行测试,然后前端还是走以前的登录逻辑,只不过是变成自动登录了,让前端将sessionId放到cookie中带过来即可
    response.sendRedirect("http://www.baidu.com?username="+username+"&sessionId="+sessionId);

}

    为了进行登录,用户必须首先访问/casLogin这个地址,然后被Cas的认证拦截器拦截,重定向到Cas服务器的单点登录界面完成登录,登录完成后回到/casLogin这个Controller,获取到用户名以及存储了用户信息的sessionId,最后重定向给前端的登录界面,用户名和sessionId是必须带上的,这里先用百度进行测试。
在这里插入图片描述
这样登录完成后前端就可以获取到用户名和sessionId了,同时CASTGC也会被设置到cookie中。

第二步、测试cas整合是否成功
    因为还没有整合前端,也用spring提供的resttemplates来发送Ajax请求,和前端发送Ajax请求效果是一样的。

@RestController
public class TestController {


    @RequestMapping("/test")
    public Result test(){
        return new Result(200,"123","456");
    }


    @RequestMapping("/test2")
    public Result test2(HttpServletRequest request, HttpSession session){
        System.out.println("进入test2方法");
        RestTemplate restTemplate = new RestTemplate();

        // 构建请求头
        HttpHeaders headers = new HttpHeaders();
		//携带JSESSIONID的cookie
        List<String> cookies = new ArrayList<>();
        cookies.add("JSESSIONID=" + request.getSession().getId());
        headers.put(HttpHeaders.COOKIE,cookies);

        String url = "http://localhost:2638/test";

        //输出session的值
        Enumeration<String> attributeNames = session.getAttributeNames();
        while (attributeNames.hasMoreElements()){
            String element = attributeNames.nextElement();
            System.out.println(element+": "+session.getAttribute(element));
        }
		//由于用户信息存储在session中,因此可以从session中获取到存储用户信息的AssertionImpl对象从而获取用户信息
        AssertionImpl assertion= (AssertionImpl) session.getAttribute("_const_cas_assertion_");
        AttributePrincipal principal = assertion.getPrincipal();
        String name = principal.getName();
        Map<String, Object> attributes = principal.getAttributes();
        System.out.println(name);
        attributes.forEach((k,v)-> System.out.println(k+": "+v));

        HttpEntity<String> entity = new HttpEntity<>(headers);

        ResponseEntity<Result> response = restTemplate.exchange(
                url,
                HttpMethod.GET,
                entity,
                Result.class);

        return new Result(200,response.getBody(),"456");
    }

}

我是采用了自定义的Result来测试的,这个成功的测试Controller,在浏览器访问/test2路径,因为带了JSESSIONID,Cas客户端就能找到对应的用户信息,判断已经登录,允许访问。
在这里插入图片描述
接下来在test2方法中注释掉携带JSESSIONID的代码,重启项目,再次访问/test2路径

//携带JSESSIONID的cookie
/*List<String> cookies = new ArrayList<>();
cookies.add("JSESSIONID=" + request.getSession().getId());
headers.put(HttpHeaders.COOKIE,cookies);*/

在这里插入图片描述
在这里插入图片描述
此时浏览器会报500错误,并且控制台也会说没有合适的转换器将Html页面转换为Result,原因是test2方法中的restTemplate请求已经被Cas客户端拦截了,因为Cas客户端识别为未登录状态,因此Ajax请求携带上JSESSIONID就能成功请求了。

第三步、接收前端传来的值,真正开始应用系统的登录逻辑方法

@RequestMapping("/login")
public Result login(@RequestBody(required = false) LoginDto loginDto,HttpSession session) throws IOException {

    //由于用户信息存储在session中,因此可以从session中获取到存储用户信息的AssertionImpl对象从而获取用户信息
    AssertionImpl assertion= (AssertionImpl) session.getAttribute("_const_cas_assertion_");
    AttributePrincipal principal = assertion.getPrincipal();
    String username = principal.getName();
    Map<String, Object> attributes = principal.getAttributes();
    System.out.println(username);
    attributes.forEach((k,v)-> System.out.println(k+": "+v));

    //安全判断,传过来的username和session中的username相同才认为用户已经到CAS服务器上进行登录了
    if (!loginDto.getUsername().equals(username)){
        return new Result(HttpServletResponse.SC_FORBIDDEN,null,"需要先到Cas服务器进行登录");
    }

    //获取到前端传来的用户账号等信息后,就可以执行原先应用系统的登录逻辑了
    RestTemplate restTemplate = new RestTemplate();
    String url = "http://localhost:2640/login/casLogin";

    ResponseEntity<Result> result = restTemplate.postForEntity(url, loginDto, Result.class);

    //处理返回结果
    if (result.getStatusCode()!= HttpStatus.OK || !result.getBody().getCode().equals(HttpStatus.OK.value())){
        return new Result(HttpServletResponse.SC_UNAUTHORIZED,null,"登录失败");
    }

    //将原本系统登录成功的返回值返回给前端就实现了二次登录了
    return result.getBody();
}

第四步、进行完整测试

首先,用户在浏览器访问应用系统的casLogin方法,然后进入cas登录界面,并且url后面会自动带上回调地址,就是/casLogin。
在这里插入图片描述
在Cas服务器的单点登录界面输入用户名和密码进行登录,登录成功,就会执行casLogin方法的逻辑,还是采用百度来模拟前端接收用户数据。
在这里插入图片描述
这样就模拟前端接收到了username以及sessionId,接下来前端带着username以及sessionId来访问login方法,执行原先系统的登录逻辑,这里使用postman进行演示。
在这里插入图片描述
在这里插入图片描述
发送请求
在这里插入图片描述
这样前端就获取到原先系统的登录信息了,需要注意的是如果只有一个后端项目tomcat的话,前端一直带着sessionId,那么Cas就一直能认定是登录状态了,Cas单点登录就算是完成了。

单点退出功能

@RequestMapping("/casLogout")
public void casLogout(HttpSession session,HttpServletResponse response) throws IOException {

    //先执行业务系统的退出功能,这里省略了

    //再执行Cas的单点退出功能
    //注销session
    session.invalidate();
    // ids的退出地址,ids6.wisedu.com为ids的域名  authserver为ids的上下文,logout为固定值
    String casLogoutURL = "https://authserver.hainanu.edu.cn/authserver/logout";
    // service后面带的参数为应用的访问地址,需要使用URLEncoder进行编码
    String redirectURL = casLogoutURL + "?service=" + URLEncoder.encode("http://f27714p111.imdo.co/casLogin");

    response.sendRedirect(casLogoutURL);

}

在浏览器直接访问Cas应用系统的/casLogout地址就可以了,退出成功就会回调到casLogin方法,Cas过滤器发现没有登录,就又会跳转到Cas服务器的单点登录页面了。

总结

    上述的单点登录过程其实是针对单个tomcat项目的单点登录,如果项目是分很多模块的,需要部署多个tomcat,那么上述做法就只对其中的一个tomcat项目有效,因为每个tomcat对应的session都是不一样的,要想做到更好的效果,最好的办法是实现分布式的共享session,保证每个tomcat项目的session数据都一致,就能很好的实现上述效果,但是很多时候,如果项目本身没有使用session的话,这样做成本就比较高了,当然只实现其中的一个tomcat项目也能完成单点登录,只是单点退出做不到而已。
    看得出来cas是很依赖session的,要怎么样集成还是要根据实际项目来完成才行。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐