窥探cas登录

情景问题


	最近项目用到了cas登录
	于是开始看看开源的单点登录解决方案cas

cas认证服务搭建


	https://github.com/apereo/cas-overlay-template
	下载5.3分支
	构建命令 .\build.cmd run
	修改http协议等操作 
	services\HTTPSandIMAPS-10000001.json
		"serviceId" : "^(https|http|imaps)://.*",
	application.properties
		# 去除https认证
		cas.tgc.secure=false
		cas.serviceRegistry.initFromJson=true
	部署在tomcat容器 修改端口 8099
	访问 http://localhost:8099/cas即可
	

cas客户端搭建


	搭建一个简单springboot应用添加如下依赖
    <dependency>
		<groupId>net.unicon.cas</groupId>
		<artifactId>cas-client-autoconfig-support</artifactId>
		<version>2.1.0-GA</version>
	</dependency>
	
	启动类添加 @EnableCasClient 
	
	添加cas客户端配置文件
	# 认证服务器地址
	cas.server-url-prefix=http://localhost:8099/cas
	# 认证服务器登录地址
	cas.server-login-url=http://localhost:8099/cas/login
	# 重定向客户端地址
	cas.client-host-url=http://client01.cas:8077
	# 认证类型
	cas.validation-type=cas3
	
	添加控制器
	@RestController
	public class TestController {
		@GetMapping("/test1")
		public String test1() {
			# 这里可以获取登录session用户
			return AssertionHolder.getAssertion().getPrincipal().getName();
		}
	}
	
	配置第二个客户端配置端口 8088
	# 重定向客户端地址
	cas.client-host-url=http://client02.cas:8088
	

第一次访问客户端01


client01.cas:8077/test1
可以看到客户端重定向到认证服务了
此时还没有cookie信息

cas客户端第一次访问


后台接口日志

cas客户端第一次访问


登录 casuser/Mellon
请求认证服务器登录接口
第一个请求
http://localhost:8099/cas/login?service=http%3A%2F%2Fclient01.cas%3A8077%2Ftest1
	响应cookie 此时是认证服务器的
		TGC=eyJhbGciOiJIUzUx; Path=/cas/; HttpOnly 
	重定向请求到 客户端 附带属于该客户端票据 ST
		http://client01.cas:8077/test1?ticket=ST-6-vM2IqfujsjTKDiPTA4Xx3Q7ST7ILAPTOP-KU3JFMRQ

认证服务登录


http://client01.cas:8077/test1?ticket=ST-6-vM2IqfujsjTKDiPTA4Xx3Q7ST7ILAPTOP-KU3JFMRQ
客户端携带属于它的票据换session
响应了cookie
	JSESSIONID=9FDD020BB7DED08D1EE3DCB721B96BC7; Path=/; HttpOnly
重定向到客户端原始访问地址
	http://client01.cas:8077/test1;jsessionid=9FDD020BB7DED08D1EE3DCB721B96BC7

客户端换票据


http://client01.cas:8077/test1;jsessionid=9FDD020BB7DED08D1EE3DCB721B96BC7
执行客户端实际请求
附带 cookie信息
	JSESSIONID=9FDD020BB7DED08D1EE3DCB721B96BC7
url也重写了session
	http://client01.cas:8077/test1;jsessionid=9FDD020BB7DED08D1EE3DCB721B96BC7
至此第一次登陆流程完毕

客户端附带session请求


第一次登陆后台日志

第一次登陆后台日志

访问客户端02


http://client02.cas:8088/test1
此时浏览器已经有认证服务登陆信息了
对应新的子系统该如何自动登录呢
首先该子系统没有登录重定向到认证服务器了

客户端02第一次访问


http://localhost:8099/cas/login?service=http://client02.cas:8088/test1
这次访问了认证系统 把之前的认证服务器 cookie信息也带上了
	TGC=eyJhbGciOiJIUzUxMiJ9.ZXlKNmFYQ
并且重定向到了 该客户端要访问的原始地址 附带 属于该系统的票据

客户端02登录


http://client02.cas:8088/test1?ticket=ST-7-ohrRhr01Q-KhKYXqf1jC6Tk5dIMLAPTOP-KU3JFMRQ
调回原来子系统附带上票据
通过票据换取了session信息
然后重定向方式带上session
响应cookie
	JSESSIONID=A7E377525D9DA6E38DE531A901DC3891; Path=/; HttpOnly

客户端02根据票据换取session


http://client02.cas:8088/test1;jsessionid=A7E377525D9DA6E38DE531A901DC3891
携带cookie请求系统
至此客户端02完成自动登录


客户端02携带用户信息访问系统


客户端02后台日志

客户端02后台日志

手写实现

cas-service端

	<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter</artifactId>
			<version>5.8.2</version>
			<scope>compile</scope>
		</dependency>
		<dependency>
			<groupId>cn.hutool</groupId>
			<artifactId>hutool-all</artifactId>
			<version>5.7.22</version>
	</dependency>


package com.aming.cas.service.controller;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import cn.hutool.core.map.MapBuilder;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.ContentType;
import cn.hutool.jwt.JWTUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * @author aming
 * @since 2022-11-01
 */
@Slf4j
@RestController
@RequiredArgsConstructor
public class CasServiceController {
	
	/**
	 * 
	 */
	private static final String CASUSER = "casuser";
	
	/**
	 * 
	 */
	private static final String TGC = "TGC";
	
	/**
	 * 票据和service名映射
	 */
	private static final Map<String, String> AMING_CAS_CLIENT = new ConcurrentHashMap<>();
	
	/**
	 * 票据和session映射
	 */
	private static final Map<String, HttpSession> TICKET_SESSION = new ConcurrentHashMap<>();
	
	/**
	 * 
	 */
	private static final String AMING_CAS_SERVICE_SING_KEY = "aming-cas-service";
	
	private final HttpServletRequest request;
	
	private final HttpServletResponse response;
	
	/**
	 * 首页
	 * 
	 * @param ticket
	 * @return Boolean
	 */
	@GetMapping("/index")
	public void index() {
		if (request.getSession(false) != null) {
			ServletUtil.write(response, "login success !", ContentType.TEXT_HTML.getValue());
		}else {
			sendRedirect("http://localhost:8099/cas/login");
		}
	}
	
	/**
	 * 跳转登录页面
	 * 
	 * @param ticket
	 * @return
	 * @return Boolean
	 */
	@GetMapping("/cas/login")
	public ModelAndView routeLoginPage(@RequestParam(value = "service", required = false) String service) {
		// 情况1 单纯登录认证服务器
		// 情况2 service系统重定向过来判断浏览器cookie的TGC 通过签发该service票据
		String tgcValue = cookieOf(TGC);
		if (tgcValue == null && service == null) {
			return new ModelAndView("login");
		} 
		
		else if (tgcValue == null && service != null) {
			return new ModelAndView("login");
		}
		else if (tgcValue != null && service == null) {
			// 已登录状态
			boolean verify = JWTUtil.verify(tgcValue, AMING_CAS_SERVICE_SING_KEY.getBytes());
			if (verify) {
				sendRedirect("http://localhost:8099/index");
			}
		} else if (tgcValue != null && service != null) {
			boolean verify = JWTUtil.verify(tgcValue, AMING_CAS_SERVICE_SING_KEY.getBytes());
			if (verify) {
				// 为service签发票据 并重定向回service
				String generateST = generateST(service);
				log.debug("为service签发票据 并重定向回service: {}", service);
				HttpSession session = request.getSession();
				session.setAttribute(CASUSER, CASUSER);
				TICKET_SESSION.put(generateST, session);
				sendRedirect(service + "?ticket=" + generateST);
			} else {
				log.error("签名验证失败");
			}
		}
		return null;
	}
	
	/**
	 * post登录 判断携带cookie 没有则生成TGC和ST 并重定向到service附带ticket
	 * 
	 * @param ticket
	 * @return Boolean
	 */
	@PostMapping("/cas/login")
	public void login(@RequestParam(value = "username", required = false) String username, @RequestParam(value = "password", required = false) String password, @RequestParam(value = "service", required = false) String service) {
		if (!"casuser".equals(username) || !"Mellon".equals(password)) {
			log.debug("认证服务登录成功");
			ServletUtil.write(response, "invalid account/password", ContentType.TEXT_PLAIN.getValue());
			return;
		}
		// 用户信息写入session
		HttpSession session = request.getSession();
		
		session.setAttribute(CASUSER, CASUSER);
		Cookie cook = new Cookie(TGC, generateTGC());
		cook.setHttpOnly(true);
		cook.setPath("/cas/");
		cook.setMaxAge(-1);
		response.addCookie(cook);
		
		if (StrUtil.isBlank(service)) {
			// 只单单登录认证系统
			// 不存在cookie 响应 TGC和ST
			log.debug("认证服务登录成功跳转index");
			sendRedirect("http://localhost:8099/index");
			return;
		} else {
			// 判断cookie
			String generateST = generateST(service);
			String tgcValue = cookieOf(TGC);
			if (tgcValue != null) {
				boolean verify = JWTUtil.verify(tgcValue, AMING_CAS_SERVICE_SING_KEY.getBytes());
				if (verify) {
					// 为service签发票据 并重定向回service
					log.debug("为service签发票据 并重定向回service: {}", service);
					TICKET_SESSION.put(generateST, session);
					sendRedirect(service + "?ticket=" + generateST);
				} else {
					log.error("签名验证失败");
				}
			} else {
				// 不存在cookie 响应 TGC和ST
				// 为service签发票据 并重定向回service
				TICKET_SESSION.put(generateST, session);
				log.debug("为service签发票据 并重定向回service: {}", service);
				sendRedirect(service + "?ticket=" + generateST);
			}
		}
		
	}
	
	/**
	 * @param tgc
	 * @return
	 * @return String
	 */
	private String cookieOf(String tgc) {
		Cookie[] cookies = request.getCookies();
		String tgcValue = null;
		if (cookies == null) {
			return tgcValue;
		}
		for (Cookie cookie : cookies) {
			if (cookie.getName().equals(tgc)) {
				tgcValue = cookie.getValue();
			}
		}
		return tgcValue;
	}
	
	/**
	 * @param string
	 * @return void
	 */
	private void sendRedirect(String location) {
		try {
			response.sendRedirect(location);
		} catch (IOException e) {
			log.error("", e);
		}
	}
	
	/**
	 * 
	 * @param service
	 * @return void
	 */
	private String generateST(String service) {
		Map<String, Object> payload = MapBuilder.<String, Object>create().put("method", "api00002").build();
		String st = JWTUtil.createToken(payload, service.getBytes());
		AMING_CAS_CLIENT.put(st, service);
		return st;
	}
	
	/**
	 * 
	 * @return void
	 */
	private String generateTGC() {
		Map<String, Object> payload = MapBuilder.<String, Object>create().put("method", "api00002").build();
		return JWTUtil.createToken(payload, AMING_CAS_SERVICE_SING_KEY.getBytes());
	}
	
	/**
	 * 验证票据
	 * 
	 * @param ticket
	 * @return
	 * @return Boolean
	 */
	@ResponseBody
	@PostMapping("/cas/serviceValidate")
	public String postMethodName(@RequestParam String ticket) {
		log.debug("验证票据开始: {}", ticket);
		String service = AMING_CAS_CLIENT.get(ticket);
		boolean verify = JWTUtil.verify(ticket, service.getBytes());
		if (!verify) {
			return null;
		}
		HttpSession session = TICKET_SESSION.get(ticket);
		if (session != null) {
			String username = (String) session.getAttribute(CASUSER);
			log.debug("用户:{} 验证票据成功: {}", username, ticket);
			return username;
		}
		return null;
	}
	
}


<!--
 * @author: aming
 * @since: 
-->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>登录</title>
</head>
<link rel="stylesheet" href="../css/common.css">
<script type="text/javascript" src="../libs/jquery-3.2.1.min.js"></script>
<style>
  body {
    width: 100%;
    height: 100%;
    background-image: url(../img/login.jpg);
    background-repeat: no-repeat;
    background-size: cover;
  }
</style>

<body>

  <!-- <form id="form" action="/cas/login" method="post">
    <label>用户</label>
    <input type="text" name="username">
    <label>密码</label>
    <input type="password" name="password">
    <button οnclick="login()">登录</button>
   
  </form> -->

  <form id="form" class="logindiv" action="/cas/login" method="post">
    <div class="header">
      <h2>登录</h2>
      <label for="username">
        <span>用户名:</span>
        <input type="text" id="username" name="username">
      </label>
      <br>
      <label for="password">
        <span>密码:</span>
        <input type="password" id="password" name="password">
      </label>
      <br>
      <div class="pwsd">
        <input type="checkbox" id="cbx"><span>记住密码</span>
      </div>
      <div class="del">
        <button onclick="login()">登录</button>
			</button> 
      </div>
    </div>
  </form>
</body>
<script type="text/javascript" src="../js/login.js"></script>

</html>


/*
 * @author: aming
 * @since: 2022-11-01
 */
$(function () {
	// 登录
	window.login = function login() {

    $("#form").attr('action',window.location.href)

    $("#form").submit()
	};
});


a {
	text-decoration: none;
	color: #fff;
}

body {
	width: 100%;
	height: 100%;
	background-image: url(./img/RE53r3l.jfif);
	background-repeat: no-repeat;
	background-size: cover;
}

.header {
	width: 400px;
	height: 450px;
	background: rgba(0, 0, 0, .2);
	border-radius: 14px;
	display: flex;
	flex-direction: column;
	/* margin: 100px 0 0 200px; */
	padding: 20px;
}

h2 {
	font-size: 24px;
	color: #fff;
}

label {
	margin-top: 40px;
	width: 350px;
	display: flex;
	align-items: center;
	justify-content: space-between;
}

label>span {
	font-size: 24px;
	color: #fff;
}

label>input {
	border-radius: 20px;
	border: 1px solid #ccc;
	padding: 0 20px;
	background-color: rgba(255, 255, 255, .6);
	box-sizing: border-box;
	outline: none;
	width: 240px;
	height: 30px;
	font-size: 18px;
}

.del {
	margin-top: 30px;
	display: flex;
	justify-content: space-around;
	/* width: 325px; */
}

.pwsd {
	display: flex;
	margin-top: 45px;
}

.pwsd>input {
	width: 24px;
	height: 24px;
}

.pwsd>span {
	font-size: 18px;
	color: #fff;
	margin-left: 20px;
}

button {
	width: 100px;
	height: 40px;
	background: rgba(0, 0, 0, .6);
	border: none;
	border-radius: 12px;
	font-size: 18px;
	color: #fff;
}

.logindiv{
  display: flex;
    align-items: center;
    justify-content: center;
    height: 100vh;
    position: fixed;
    width: 100vw;
}

cas-client端

package com.aming.cas.client.filter;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import lombok.extern.slf4j.Slf4j;

/**
 * @author aming
 * @since 2022-11-01
 */
@Slf4j
@Component
@Order(Integer.MIN_VALUE + 1)
public class CasAuthenticationFilter extends OncePerRequestFilter {
	
	/**
	 * 
	 */
	private static final String CAS_SERVICE_VALIDATE = "http://localhost:8099/cas/serviceValidate?";
	/**
	 * 
	 */
	private static final String CAS_ASSERTION = "_const_cas_assertion_";
	
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
		log.debug("casAuthenticationFilter exec");
		// 已登录放行
		HttpSession session = request.getSession(false);
		if (session != null && session.getAttribute(CAS_ASSERTION) != null) {
			log.debug("用户: {} 已登录", session.getAttribute(CAS_ASSERTION));
			filterChain.doFilter(request, response);
		} else {
			// 有参数ticket去认证服务器校验
			String st = request.getParameter("ticket");
			if (st != null) {
				String body = HttpUtil.createPost(CAS_SERVICE_VALIDATE + "ticket=" + st).execute().body();
				if (StrUtil.isNotBlank(body)) {
					request.getSession().setAttribute(CAS_ASSERTION, body);
					// 登录成功回调原始请求
					String encodeURL = response.encodeURL(request.getRequestURL().toString());
					log.debug("登录成功回调原始请求: {}", encodeURL);
					response.sendRedirect(encodeURL);
					return;
				} else {
					log.error("未能识别票据: {}", st);
					return;
				}
			} else {
				// 未登录重定向认证服务器
				String queryString = request.getQueryString();
				StringBuffer buffer = request.getRequestURL();
				if (queryString != null) {
					buffer.append("?").append(request.getQueryString());
				}
				String redirectURL = "http://localhost:8099/cas/login?" + "service=" + urlEncode(buffer.toString());
				log.debug("未登录重定向认证服务器: {}", redirectURL);
				response.sendRedirect(redirectURL);
			}
		}
	}
	
	public static String urlEncode(final String value) {
		try {
			return URLEncoder.encode(value, "UTF-8");
		} catch (final UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		}
	}
	
}

总结


手写实现简单cas统一认证 完全模拟开源cas 
心得
	利用认证服务生成根票据 给service签发票据 完成service 认证
	加深cookie 和 session 认识 浏览器重定向
	两端 sessionid 可不一致

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值