情景问题
最近项目用到了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客户端第一次访问](https://img-blog.csdnimg.cn/277295107e464da7a99d0385b6142f44.png)
后台接口日志
![cas客户端第一次访问](https://img-blog.csdnimg.cn/c20e11cc64e144e7a36cf713403a990d.png)
登录 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
![认证服务登录](https://img-blog.csdnimg.cn/8aa49cb15af94e1ca4999588dea14acb.png)
http://client01.cas:8077/test1?ticket=ST-6-vM2IqfujsjTKDiPTA4Xx3Q7ST7ILAPTOP-KU3JFMRQ
客户端携带属于它的票据换session
响应了cookie
JSESSIONID=9FDD020BB7DED08D1EE3DCB721B96BC7; Path=/; HttpOnly
重定向到客户端原始访问地址
http://client01.cas:8077/test1;jsessionid=9FDD020BB7DED08D1EE3DCB721B96BC7
![客户端换票据](https://img-blog.csdnimg.cn/fcf17858408f4aacbbed61c1047c7193.png)
http://client01.cas:8077/test1;jsessionid=9FDD020BB7DED08D1EE3DCB721B96BC7
执行客户端实际请求
附带 cookie信息
JSESSIONID=9FDD020BB7DED08D1EE3DCB721B96BC7
url也重写了session
http://client01.cas:8077/test1;jsessionid=9FDD020BB7DED08D1EE3DCB721B96BC7
至此第一次登陆流程完毕
![客户端附带session请求](https://img-blog.csdnimg.cn/e3baed4a62a24f3789d4ef6ec5a15a5d.png)
第一次登陆后台日志
![第一次登陆后台日志](https://img-blog.csdnimg.cn/f2f3c62fc4fa431e821704d011a24da4.png)
访问客户端02
http://client02.cas:8088/test1
此时浏览器已经有认证服务登陆信息了
对应新的子系统该如何自动登录呢
首先该子系统没有登录重定向到认证服务器了
![客户端02第一次访问](https://img-blog.csdnimg.cn/9e5433d045614596af3b078b3cde822c.png)
http://localhost:8099/cas/login?service=http://client02.cas:8088/test1
这次访问了认证系统 把之前的认证服务器 cookie信息也带上了
TGC=eyJhbGciOiJIUzUxMiJ9.ZXlKNmFYQ
并且重定向到了 该客户端要访问的原始地址 附带 属于该系统的票据
![客户端02登录](https://img-blog.csdnimg.cn/9c3880346a6d45f09c64cdbad4a2520b.png)
http://client02.cas:8088/test1?ticket=ST-7-ohrRhr01Q-KhKYXqf1jC6Tk5dIMLAPTOP-KU3JFMRQ
调回原来子系统附带上票据
通过票据换取了session信息
然后重定向方式带上session
响应cookie
JSESSIONID=A7E377525D9DA6E38DE531A901DC3891; Path=/; HttpOnly
![客户端02根据票据换取session](https://img-blog.csdnimg.cn/4e6bb70def8941fdb619136c4d7dc29a.png)
http://client02.cas:8088/test1;jsessionid=A7E377525D9DA6E38DE531A901DC3891
携带cookie请求系统
至此客户端02完成自动登录
![客户端02携带用户信息访问系统](https://img-blog.csdnimg.cn/950e91b98b644a5c9f6a11649d206f02.png)
客户端02后台日志
![客户端02后台日志](https://img-blog.csdnimg.cn/ea00ba6be0bc488197da9f176c7e5eba.png)
手写实现
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;
@Slf4j
@RestController
@RequiredArgsConstructor
public class CasServiceController {
private static final String CASUSER = "casuser";
private static final String TGC = "TGC";
private static final Map<String, String> AMING_CAS_CLIENT = new ConcurrentHashMap<>();
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;
@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");
}
}
@GetMapping("/cas/login")
public ModelAndView routeLoginPage(@RequestParam(value = "service", required = false) String 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) {
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;
}
@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;
}
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)) {
log.debug("认证服务登录成功跳转index");
sendRedirect("http://localhost:8099/index");
return;
} else {
String generateST = generateST(service);
String tgcValue = cookieOf(TGC);
if (tgcValue != null) {
boolean verify = JWTUtil.verify(tgcValue, AMING_CAS_SERVICE_SING_KEY.getBytes());
if (verify) {
log.debug("为service签发票据 并重定向回service: {}", service);
TICKET_SESSION.put(generateST, session);
sendRedirect(service + "?ticket=" + generateST);
} else {
log.error("签名验证失败");
}
} else {
TICKET_SESSION.put(generateST, session);
log.debug("为service签发票据 并重定向回service: {}", service);
sendRedirect(service + "?ticket=" + generateST);
}
}
}
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;
}
private void sendRedirect(String location) {
try {
response.sendRedirect(location);
} catch (IOException e) {
log.error("", e);
}
}
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;
}
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());
}
@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;
}
}
<!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" 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>
$(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;
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;
}
.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;
@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 {
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 可不一致