概述
- 最近工作遇到多个系统需要进行单点登录认证,所以研究了一下CAS
- 单点登录主要是为了避免各个协作办公系统之间需要重复登录的繁琐操作
- 下面主要记录下基于cas+springboot+vue整合遇到的问题
CAS认证中心
采用的是可覆盖的开源CAS:cas-overlay-template,可以到github去下载:
https://github.com/apereo/cas-overlay-template/tree/5.3
https://github.com/apereo/cas-overlay-template/tree/5.2 (本人使用版本)
启动、部署遇到问题,如下:
1)本地tomcat8.0.36 部署可以正常启动,k8s 部署启动却异常,如何排除spring-web依赖冲突
Caused by: java.lang.IllegalArgumentException: More than one fragment with the name [spring_web]
初步判断: tomcat版本不一致, 不兼容jar冲突,无法部署成功;
如何排除jar冲突待解决
解决办法:
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-jdbc</artifactId>
<version>5.2.0</version>
<exclusions>
<exclusion>
<artifactId>spring-web</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
</dependency>
子系统+VUE前端集成
- 如何解决VUE前端跨域或者说未登录状态下的接口异步请求异常却无法跳转到认证中心界面问题?
下面会提到。
- 下面的解决方案中,VUE前端明明配置了跨域代理,为什么还需要进行会话jsessionid重写?
一开始感觉没有必要重写,明明都已经代理了,那浏览器直接请求后端接口返回的sessionid和Vue代理请求的后端接口所返回的sessionId应该都是一样。为什么前端不重写jsessionId,就死循环的不断重定向页面。
其实,这里就需要了解下Vue的proxyTable代理原理:
浏览器是禁止跨域的,但是服务端不禁止,在本地运行npm run dev等命令时实际上是用node运行了一个服务器,因此proxyTable实际上是将请求发给自己的服务器,再由服务器转发给后台服务器,做了亦曾代理,因为不会出现跨域问题。由此可见, 直接访问后台接口和后台和经过代理访问后台接口各自会产生不同的sessionId。
得出结论:浏览器直接访问后端接口所产生的sessionId和vue本地开发代理产生的sessionId是不一致的。这个也是为什么前端需要重写sessionId的原因。
解决办法:
1)仅仅后端接口/loginRedirect由cas授权过滤器校验; 其他子系统后台接口不做cas认证过滤,交由原子系统拦截器拦截校验,子系统拦截发现未登录,返回认证中心登录url(包含service=/loginRedirect),前端检测并跳转认证中心登录页面。登陆认证通过后重定向到后台接口由后台接口进行二次免密登录后,重定向到前端首页。
/**
* 授权过滤器
* @return
*/
@Bean
public FilterRegistrationBean filterAuthenticationRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new AuthenticationFilter());
// 设定匹配的路径
//1、如何解决跨域不友好问题:
//1.1、仅仅后台/loginRedirect由授权过滤器校验;
//1.2、其他后台接口和前端页面不做拦截,交由原拦截器拦截校验,
//1.3、前端跳转认证中心登录页面,认证通过后重定向到后台接口由后台接口进行二次免密登录后,重定向到前端首页
registration.addUrlPatterns("/loginRedirect");
Map<String,String> initParameters = new HashMap<String, String>();
initParameters.put("casServerLoginUrl", serverUrlPrefix);
initParameters.put("serverName", clientHostUrl);
registration.setInitParameters(initParameters);
// 设定加载的顺序
registration.setOrder(4);
return registration;
}
2)子系统接口拦截器
/*
/**
* 权限拦截
*
* @author xuxueli 2015-12-12 18:09:04
*/
@Component
public class PermissionInterceptor extends HandlerInterceptorAdapter {
//http://192.168.0.166:8096/cas/login
@Value("${cas.server-login-url}")
private String serverLoginUrl;
//http://192.168.0.109:30522/loginRedirect
@Value("${casClientLoginRedirectUrl}")
private String casClientLoginRedirectUrl;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return super.preHandle(request, response, handler);
}
// if need login
boolean needLogin = true;
boolean needAdminUser = false;
HandlerMethod method = (HandlerMethod)handler;
PermissionLimit permission = method.getMethodAnnotation(PermissionLimit.class);
if (permission!=null) {
needLogin = permission.limit();
needAdminUser = permission.adminuser();
}
if (needLogin) {
//引入单点登录,登录校验需要改造
XxlJobUser loginUser = AppContextHelper.getSysUser();
if (loginUser == null) {
throw new UserLoginException(serverLoginUrl + "?service=" + casClientLoginRedirectUrl);
}
if (needAdminUser && loginUser.getRole()!=1) {
throw new RuntimeException(I18nUtil.getString("system_permission_limit"));
}
}
return super.preHandle(request, response, handler);
}
}
3)比较重要的接口:
getCurrentUser:前端首页进入,首先访问的接口; 前端获取用户登录信息
loginRedirect: cas认证过滤器检验接口,不通过跳转到认证中心页面, 认证通过后返回sessionID给前端,前端重写jsessionid,可以解决前后端分离开发调试的跨域sessionId不一致问题; 如果前后端不分离开发、部署,前端不需要重写sessionID。
/*
/**
* 首页
* index controller
* @author xuxueli 2015-12-19 16:13:16
*/
@Controller
public class IndexController {
@Resource
private XxlJobService xxlJobService;
@Resource
private LoginService loginService;
//单点登录中心退出链接 cas/logout
@Value("${casClientLogoutUrl}")
private String clientLogoutUrl;
//前端首页链接
@Value("${casClientFrontUrl}")
private String casClientFrontUrl;
/**
* 前端首页进入,最先访问的接口,获取用户信息
* @return
*/
@RequestMapping(value="getCurrentUser", method=RequestMethod.GET)
@ResponseBody
public ReturnT<XxlJobUser> getCurrentUser(){
XxlJobUser loginUser = AppContextHelper.getSysUser();
return new ReturnT<>(loginUser);
}
/**
* 做二次登录验证, 然后登录重定向到前端页面
* @return
*/
@RequestMapping("/loginRedirect")
@PermissionLimit(limit=false)
public String loginRedirect(HttpServletRequest request){
//改造原来的登录方式,进行免密的二次登录验证
loginService.login(request, AssertionHolder.getAssertion().getPrincipal().getName());
//返回sessionId, 由前端重写jsessionid,保持前后端分离部署,sessionId不一致问题
String jsessionid = AppContextHelper.getSession().getId();
return "redirect:"+ casClientFrontUrl + "?jsessionid=" + jsessionid;
}
@RequestMapping(value="logout")
@ResponseBody
@PermissionLimit(limit=false)
public ReturnT<String> logout(){
loginService.logout();
return new ReturnT<>(clientLogoutUrl);
}
}