Cas单点登录集成前后端分离项目
文章目录前言一、整合前后端分离遇到的问题二、CAS整合前后端分离项目实战第一步、引入CAS客户端依赖第二步、编写Cas经典拦截器的配置类第三步、编写一个CasController,专门负责Cas的业务第四步、进行完整测试单点退出功能总结前言 现在由于单点登录的普及,Cas和Oauth2都能完成,最简单的方法是将Cas的依赖导入后,添加上Cas的过滤器,
文章目录
前言
现在由于单点登录的普及,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的,要怎么样集成还是要根据实际项目来完成才行。
更多推荐
所有评论(0)