本文实现为学习B站艾编程架构师项目实战教学:阿里淘宝天猫单点登录项目实战教学的笔记,如有侵权,请联系我删除
完整代码链接
https://github.com/micro-cloud-fly/cas-sys
cas单点登录的源码实现
本文所示内容,使用springboot简化开发,没有使用cas单点登录server端以及client端的第三方包,旨在通过代码梳理cas单点登录的原理
hosts配置
127.0.0.1 www.sso.com # 统一认证中心
127.0.0.1 www.tb.com # 模拟淘宝客户端
127.0.0.1 www.tm.com # 模拟天猫客户端
项目架构说明
- cas-sys是整个项目的父工程
- cas-server继承于cas-sys,是cas单点登录的统一认证中心
- taobao继承于cas-sys,是cas单点登录的一个客户端
- tmall继承于cas-sys,是cas单点登录的一个客户端
如何运行
项目设计解析
当访问淘宝的 http://www.tb.com:8082/taobao 地址时,由于淘宝设置了全局的拦截器,此时会走到
interceptor包内的LoginInterceptor的preHandler方法,此时会首先检查session中是否存在isLogin
标识,如果存在了,证明已经登录过了,那么拦截器返回true,放行这个请求。但是第一次请求的时候,session
中是肯定不存在登录信息的。在这种情况下,就要去重定向到统一认证中心这个项目,请求的地址是checkLogin
在服务端的checkLogin这个方法中,会判断session里面有没有token信息,如果没有,就会重定向到服务端的
登录页面,这里面的关键点是,淘宝请求到checkLogin的时候,会携带一个参数,告诉服务端我是从哪里来的,
checkLogin这个方法如果检测到没有token信息进行重定向到登录页面的时候,也会携带这个参数redirectUrl,
在登录页面,进行登录的时候时候,在login方法中,也会携带这个参数。
接下来是login方法,在这个方法中,首先会校验用户名和密码,如果验证通过了,则会发放一个token给淘宝这个客户端,
然后重定向到淘宝,淘宝这个客户端到拦截器又拦截了这个请求,此时还是没有登录信息,那么会去校验这个请求里面
是否有token这个参数,此时确实是存在了token,那么他会带着token再去cas统一认证中心去验证,这个token是否
正确,假如正确的话,那么就说明验证通过了,此时会在淘宝客户端记录一个session证明登录成功了,然后放行重定向
的请求。
此时登录天猫的时候,流程同淘宝,但是当天猫的客户端重定向到统一认证中心的时候,会发现这个浏览器已经存在了
一个登录的session,在session中记录了token,于是天猫客户端也拿着token去统一认证中心去校验,此时流程
就和上面介绍的淘宝一样了,因此此实现了免登录即可访问。
原理图解析
代码部分
目录结构
cas-sys
├── cas-server
│ ├── HELP.md
│ ├── cas-server.iml
│ ├── mvnw
│ ├── mvnw.cmd
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java
│ │ │ └── cn
│ │ │ └── juhe
│ │ │ ├── CasServerApplication.java
│ │ │ ├── controller
│ │ │ │ └── ServerController.java
│ │ │ └── db
│ │ │ └── MockDB.java
│ │ └── resources
│ │ ├── application.properties
│ │ └── templates
│ │ ├── index.html
│ │ └── login.html
│ └── test
│ └── java
│ └── cn
│ └── juhe
│ └── CasServerApplicationTests.java
├── cas-sys.iml
├── cas.png
├── pom.xml
├── readme.md
├── taobao
│ ├── HELP.md
│ ├── mvnw
│ ├── mvnw.cmd
│ ├── pom.xml
│ ├── src
│ │ ├── main
│ │ │ ├── java
│ │ │ │ └── cn
│ │ │ │ └── juhe
│ │ │ │ ├── TaobaoApplication.java
│ │ │ │ ├── config
│ │ │ │ │ └── LoginConfig.java
│ │ │ │ ├── controller
│ │ │ │ │ └── IndexController.java
│ │ │ │ └── interceptor
│ │ │ │ └── LoginInterceptor.java
│ │ │ └── resources
│ │ │ ├── application.properties
│ │ │ ├── static
│ │ │ └── templates
│ │ │ ├── index.html
│ │ │ └── taobao.html
│ │ └── test
│ │ └── java
│ │ └── cn
│ │ └── juhe
│ │ └── TaobaoApplicationTests.java
│ └── taobao.iml
└── tmall
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── cn
│ │ │ └── juhe
│ │ │ ├── TmallApplication.java
│ │ │ ├── config
│ │ │ │ └── LoginConfig.java
│ │ │ ├── controller
│ │ │ │ └── IndexController.java
│ │ │ └── interceptor
│ │ │ └── LoginInterceptor.java
│ │ └── resources
│ │ ├── application.properties
│ │ └── templates
│ │ ├── index.html
│ │ └── tmall.html
│ └── test
│ └── java
│ └── cn
│ └── juhe
│ └── TmallApplicationTests.java
└── tmall.iml
cas-server 核心代码
package cn.juhe.controller;
import cn.juhe.db.MockDB;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpSession;
import java.util.UUID;
@Controller
@Slf4j
public class ServerController {
@RequestMapping("/index")
public String index(String redirectUrl, Model model) {
return "login";
}
@RequestMapping("/login")
public String login(String username, String password, String redirectUrl, HttpSession session, Model model) {
//校验用户名和密码是否合法,如果合法,则发送一个令牌给这个用户,这个用户收到令牌之后,再过来请求看看这个令牌是否正确
if ("admin".equals(username) && "admin".equals(password)) {
//发放一个ticket
log.info("用户名密码验证成功。。。");
String token = UUID.randomUUID().toString();
session.setAttribute("token", token);
MockDB.T_TOKEN.add(token);//把这个令牌存到数据库当中
//保存这个用户的session
//如果登录成功了,则需要重定向到来时候到路,并且携带上这个生成到ticket,
model.addAttribute("token", token);
log.info("ticket:{}", token);
System.out.println("redirectUrl:" + redirectUrl);
return "redirect:" + redirectUrl + "?token=" + token;
}
model.addAttribute("redirectUrl", redirectUrl);
return "login";
}
@RequestMapping("checkLogin")
public String checkLogin(String redirectUrl, HttpSession session, Model model) {
String token = (String) session.getAttribute("token");
log.info("checkLogin.token:{}", token);
if (StringUtils.isEmpty(token)) {
//如果token是空,那么这个请求肯定是没有登录成功过的,则引导其去登录页面
model.addAttribute("redirectUrl", redirectUrl);
return "login";
}
model.addAttribute("token", token);
//如果不为空,则这个请求从哪里来,就让他回到哪里去
return "redirect:" + redirectUrl + "?token=" + token;
}
@RequestMapping("/verify")
@ResponseBody
public String verifyToken(String token) {
if (MockDB.T_TOKEN.contains(token)) {
return "true";
}
return "false";
}
}
客户端核心代码
package cn.juhe.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class IndexController {
@RequestMapping("/taobao")
public String index() {
System.out.println("taobao");
return "taobao";
}
}
package cn.juhe.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Value("${cas.server.url.prefix}")
private String casServerUrl;
@Value("${cas.client.url}")
private String casClientUrl;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("casServerUrl:{}", casServerUrl);
log.info("casClientUrl:{}", casClientUrl);
//首先检查,有没有登录过,从session中获取
HttpSession session = request.getSession();
Boolean isLogin = (Boolean) session.getAttribute("loginFlag");
if (isLogin != null && isLogin) {
//如果有session存在,证明已经登录成功了,则可以访问首页
return true;
} else {
//如果没有登录过,但是请求携带了token,那么就判定为是用户认证中心发过来的
String token = request.getParameter("token");
log.info("token:{}", token);
if (!StringUtils.isEmpty(token)) {
//如果用户认证中心传过来了token,那么此时需要再去用户认证中心去验证这个token是否是正确的
RestTemplate restTemplate = new RestTemplate();
String forObject = restTemplate.getForObject(casServerUrl + "verify?token=" + token, String.class);
//如果用户认证中心说这个token是正确的,那么就确定登录成功了,此时设置 session
log.info("forObject:{}", forObject);
if ("true".equals(forObject)) {
session.setAttribute("loginFlag", true);
return true;
}
}
//如果session不存在,则让其跳转到用户认证中心,并且告诉用户认证中心,跳转回来到地址是我淘宝
response.sendRedirect(casServerUrl + "checkLogin?redirectUrl=" + casClientUrl + "/taobao");
return false;
}
}
}