单点登录基本流程分为三种情况
- 首次访问应用A,cas未登录:浏览器请求API被cas过滤器(application配置项配置内容)拦截,重定向到cas认证中心登录判断cas是否登录,若cas未登录重定向到cas登录页面。在cas登录页面输入账号密码登录成功后重定向到service参数对应的地址,并通过get的方式挟带了一个ticket,同时在Cookie中设置CASTGC,该Cookie是访问service的cookie,只有访问该地址才会携带这个cookie。向Cookie中添加CASTGC的目的是为了下次访问service_url时,浏览器请求时携带TGC参数,服务器根据该TGC查找对应的TGT,从而判断用户是否登录过了,是否需要展示登录页面。TGT与TGC的关系就像SESSION与Cookie中SESSIONID的关系。后端接收到该ticket会进行验证有效性,校验通过后展示用户相关内容到浏览器上。(首次访问流程结束)
- 再次访问应用A,cas已经登录:用户发起请求经过cas-client,也就是过滤器,因为第一次访问成功之后会在session中记录用户信息,因此这里直接就通过了,不用验证了。浏览器返回正常资源。
- 正在使用应用A且cas已经登录,想去访问应用B(第一次访问):用户发起访问请求到应用B,由于是第一次访问该应用,会重定向到cas认证中心登录,发现cas已经登录(不会展示cas登录页面),认证中心重定向到service参数对应的地址,并通过get的方式挟带了一个ticket,后端接收到该ticket会进行验证有效性,校验通过后展示用户相关内容到浏览器上。
cas认证流程
- 访问服务:由于CAS client和WEB应用部署在一起,当用户访问WEB应用时,CAS client就会处理请求
- 定向认证:CAS client客户端校验HTTP请求中是否包含ST和TGT,如果没有则会重定向到CAS server地址进行用户认证
- 用户认证:用户通过浏览器填写用户信息,提交给CAS Server认证
- 发放票据:CAS Server校验过用户信息后,为CAS client发放ST,并在浏览器cookie中设置TGC,下次访问CAS Server时会根据TGC和TGT验证,判断是否已经登录
- 验证票据:CAS Client拿到ST后,再次请求CAS Server验证ST合法性,验证通过后允许客户端访问
- 传输用户信息:CAS Server校验过ST后,传输用户信息给CAS client
相关名词解释
AS
Authentication Service:认证服务,发放TGT
KDC
Key Distribution Center:密钥发放中心
TGS
Ticket-Granting Service:票据授权服务,索取TGT,发放ST
TGC
ticket-granting cookie:授权的票据证明,由CAS Server通过SSL方式发送给终端用户。该值存在Cookie中,根据TGC可以找到TGT。
TGT
Ticket Granting tieckt:俗称大令牌,或者票根,由KDC和AS发放,获取该票据后,可直接申请其他服务票据ST,不需要提供身份认证信息
ST
Service Ticket:服务票据,由KDC的TGS发放,ST是访问server内部的令牌
CAS Client客户端搭建(与项目代码在一起)
1.先在我们的SpringBoot项目的pom.xml文件中,引入对应的依赖,这边我直接贴上我客户端pom.xml里面的代码
<dependency> <groupId>net.unicon.cas</groupId>
<artifactId>cas-client-autoconfig-support</artifactId>
<version>2.3.0-GA</version> </dependency>
2.然后我们要在这里加上对应配置文件代码,主要就是指明server端的地址
以及当前客户端的地址
#CAS服务器的URL前缀。指定CAS服务器的地址和端口号。
cas.server-url-prefix=http://192.168.3.96:8199/cas
#CAS服务器的登录页面URL。指定CAS登录页面的完整地址。
cas.server-login-url=http://192.168.3.96:8199/cas/login
# CAS客户端的主机URL。指定CAS客户端的地址和端口号。
cas.client-host-url=http://localhost:9003
#:CAS验证URL的模式。指定需要进行CAS认证的URL模式,多个模式之间使用逗号分隔。
cas.authentication-url-patterns=/,/index,/base/login,/index/
#CAS客户端的本地URL。在CAS登录成功后,会跳转回CAS客户端的这个地址。service参数用来指定登录成功后要返回的URL。
local.url=http://192.168.3.96:8199/cas/login?service=http://localhost:9003/cas/index
#应用程序的HTTP端口号。指定应用程序运行的HTTP端口。
server.httpPort= 9003
3.然后在我们的启动类上要加上这个@EnableCasClient
注解
4.接下来就是对CAS过滤器进行配置(该部分使用公司已有的cas过滤器比较合适,下面给出的代码仅为参考提供思路,具体以实际业务需求为准)
package com.xk.common.web;
import com.xk.common.core.user.dao.CapUserRepository;
import com.xk.common.core.user.model.dto.CapUserDto;
import com.xk.common.core.user.model.entity.CapUserEntity;
import com.xk.framework.common.Constants;
import net.unicon.cas.client.configuration.CasClientConfigurationProperties;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
@Controller("indexController")
public class IndexController {
protected Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private CapUserRepository capUserRepository;
@RequestMapping("/")
public String toIndexDefault(HttpServletRequest request) {
CapUserDto capUserDto = (CapUserDto) request.getSession().getAttribute(Constants.XK_SESSION_USER);
// 如果当前没有登录,session中没有会话,代表非法访问
if (capUserDto == null) {
logger.info(">>>>>>>>>>>>>>当前没有登录,session中没有会话,代表非法访问<<<<<<<<<<<<<<<");
return "error/404";
}
CapUserEntity capUser=capUserRepository.xkFindById(capUserDto.getId());
String oldPwd=null;
BCryptPasswordEncoder bc = new BCryptPasswordEncoder(4);
if(null!=capUser && StringUtils.isNotEmpty(capUser.getId())){
oldPwd=capUser.getPassword();
capUser.setPassword(bc.encode("wgs1234"));
}else{
logger.info(">>>>>>>>>>>>>>当前没有登录,session中没有会话,代表非法访问<<<<<<<<<<<<<<<");
return "error/404";
}
UsernamePasswordAuthenticationToken token=new UsernamePasswordAuthenticationToken(capUserDto.getUserId(),"wgs1234");
Authentication authentication=authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
request.getSession().setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
// 从session中获取用户信息,然后根据返回的usertype,跳转到不同的逻辑页面
//CapUserDto wgsUserInfoDto = capUserService.getUserById(capUserDto.getUserId());
// 如果当前登录,但是没有找到匹配的记录
/*if (wgsUserInfoDto == null) {
logger.info(">>>>>>>>>>>>>>当前登录存在session会话,但是没有找到匹配的记录,userid:{}<<<<<<<<<<<<<<<", capUserDto.getUserId());
return "error/404";
}*/
//userService.loadUserByUsername(capUserDto.getUserName());
request.getSession().setAttribute("XK_USER", capUserDto);
capUser.setPassword(oldPwd);
return "admin/home";
}
@GetMapping("/home")
public String toIndex(HttpServletRequest request) {
return toIndexDefault(request);
}
@Autowired
private CasClientConfigurationProperties casClientConfigurationProperties;
/**
* 本地应用(CASClient)退出登录
* @param request
* @return
*/
@GetMapping("/portal/logout")
public String toLogout(HttpServletRequest request) {
//销毁本地应用的Session
request.getSession().invalidate();
//直接跳转至CASServer的注销地址,并带上本地应用的主页地址,便于再次登录后返回至该应用
return "redirect:" + casClientConfigurationProperties.getServerUrlPrefix() + "/logout?service=" + casClientConfigurationProperties.getClientHostUrl() ;
}
}
package com.xk.wgs.cas;
import com.google.common.collect.ImmutableMap;
import com.xk.common.core.organize.model.dto.EmployeeDto;
import com.xk.common.core.user.model.dto.CapRoleDto;
import com.xk.common.core.user.model.dto.CapUserDto;
import com.xk.framework.common.Constants;
import com.xk.platform.core.organize.service.IEmployeeQueryService;
import com.xk.platform.security.user.service.CustomUserDetailsService;
import com.xk.platform.security.user.service.ICapRoleService;
import com.xk.platform.security.user.service.ICapUserService;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.util.AssertionHolder;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
public class CasUserReceivingTicketValidationFilter extends Cas20ProxyReceivingTicketValidationFilter {
public CasUserReceivingTicketValidationFilter() {
super();
}
private ICapUserService capUserService;
private ICapRoleService capRoleQueryService;
private IEmployeeQueryService employeeQueryService;
@Resource
private CustomUserDetailsService userDetailsService;
public CasUserReceivingTicketValidationFilter(ICapUserService capUserService, ICapRoleService capRoleQueryService,IEmployeeQueryService employeeQueryService) {
super();
this.capUserService = capUserService;
this.capRoleQueryService = capRoleQueryService;
this.employeeQueryService = employeeQueryService;
}
@Override
public void onSuccessfulValidation(HttpServletRequest request,
HttpServletResponse response, Assertion assertion) {
AttributePrincipal principal = assertion.getPrincipal();
logger.info(principal.getName() + "--------------------------------");
AssertionHolder.setAssertion(assertion);
//获取用户信息
CapUserDto capUserDto = capUserService.getUserByUserid(principal.getName());
capUserDto.setPassword("");
String roleId = "";
// 获取用户登录信息
// 获取当前用户授予的角色
if (capUserDto == null) {
logger.error(">>>>>>>>>>>>>>>>>>>单点登录成功后的门户无此人信息,登录账号:{}<<<<<<<<<<<<<<", principal.getName());
} else {
// 根据学号/工号,查询所属的员工,再查员工授予的角色
EmployeeDto empByUserId = employeeQueryService.getEmpByUserId(capUserDto.getId());
// 查询员工授予的角色
List<CapRoleDto> capRoleDtos = capRoleQueryService.findRolesByPartyIdAndPartyType(empByUserId.getId(), "EMP");
if (capRoleDtos != null && capRoleDtos.size() > 0) {
roleId = capRoleDtos.get(0).getId();
logger.info(">>>>>>>查询到角色id:{}",roleId);
}
}
// 这里一般只有一个角色,确定;
// 生成令牌
Map<String, Object> map = ImmutableMap.of("userid", principal.getName(), "roleId", roleId);
String jwt = RTokenAuthenticationService.genAuthentication(response,
principal.getName(),
map);
//userDetailsService.loadUserByUsername(capUserDto.getUserId());
request.getSession().setAttribute(Constants.XK_SESSION_USER, capUserDto);
request.getSession().setAttribute("XK_TOKEN", jwt);
request.getSession().setAttribute("XK_USERID", principal.getName());
}
@Override
public void onFailedValidation(HttpServletRequest request, HttpServletResponse response) {
logger.info("Failed to validate cas ticket");
}
}
该部分代码因涉及到token的生成及其他业务流程,不适用于其他项目,仅为提供参考思路。
退出登录
退出登录时通过a标签跳转/portal/logout,重定向到cas销毁地址,实现退出登录。
cas集成完毕,下面是测试阶段内容。
访问应用,跳转到cas登陆页面,输入账号密码后登陆成功。
点击退出登录