文章目录
在上一篇文章中已经详细介绍了单点登录的核心原理如下,本文将围绕该原理的流程展开编写
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/271736593d2c7e0180558d1427b6677a.png)
一、本地模拟跨域配置
在Windows系统盘下找到如下路径的文件,修改任意hosts文件
打开任意一个文件添加如下所示本地映射域名
二、项目整体工程创建
建立如下图所示父工程及子模块(创建Maven项目并添加web项目支持)
三个子模块Maven依赖如下
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2.1-b03</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
</dependencies>
web.xml如下,注意配置文件映射地址
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--DispatcherServlet-->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--encodingFilter-->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
application.xml 如下
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 1.开启SpringMVC注解驱动 -->
<mvc:annotation-driven />
<!-- 2.静态资源默认servlet配置-->
<mvc:default-servlet-handler/>
<!-- 3.配置jsp 显示ViewResolver视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
<!-- 4.扫描web相关的bean -->
<context:component-scan base-package="com.ranran" />
</beans>
三、认证中心登录业务创建(sso-server)
1、导入静态资源
2、登录页面(login.jsp)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>SSO 统一登录中心</title>
<link href="${pageContext.request.contextPath}/asserts/css/bootstrap.min.css" rel="stylesheet">
<link href="${pageContext.request.contextPath}/asserts/css/signin.css" rel="stylesheet">
</head>
<body class="text-center">
<form class="form-signin" action="${pageContext.request.contextPath}/login" method="post">
<input type="hidden" name="redirectUrl" value="${redirectUrl}">
<img class="mb-4" src="${pageContext.request.contextPath}/asserts/img/bootstrap-solid.svg" alt="" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal">登录</h1>
<input type="text" class="form-control" placeholder="Username" name="username" required="" autofocus="">
<input type="password" class="form-control" placeholder="Password" name="password" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me"> Remember me
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">登录</button>
<p class="mt-5 mb-3 text-muted">© 2017-2018</p>
<a class="btn btn-sm">中文</a>
<a class="btn btn-sm">English</a>
</form>
</body>
</html>
3、实现登录请求
@Controller
public class SsoServerController {
@RequestMapping("/index")
public String index(){
return "login"; //重定向到login.jsp
}
@RequestMapping("/login")
@ResponseBody
public String login(String username,String password){
return "login ok"; //登录成功页面login OK
}
}
4、配置Tomcat
注意IDEA创建的Maven Web项目不会默认依赖jar包,需要在项目结构下的WEB-INF下创建lib目录并导入所有依赖
导入依赖后启动Tomcat访问localhost:8081/index
或http://www.sso.com:8081/index
可重定向到如下页面
四、客户端1创建(sso-taobao)
1、在之前创建的sso-taobao
模块中新建taobao.jsp
如下
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>淘宝首页</title>
</head>
<body>
<h1>淘宝首页</h1>
<hr>
<p>
当前登录的用户 <span> admin </span>
</p>
</body>
</html>
2、创建LoginController
@Controller
public class LoginController {
@RequestMapping("taobao")
public String index(){
return "taobao";
}
}
3、配置Tomcat并测试(同上一个模块)
测试结果如下
可以看到成功访问了淘宝首页
至此,两个基础模块一完成了,接下来就需要在这两个模块中实现CAS逻辑
五、拦截跳转及验证(核心)
我们以顺序逻辑来进行编写
用户首先访问www.tb.com:8082/taobao
,由于此时还未登陆,不能直接进入首页,因此需要在客户端(sso-taobao)设置一个拦截器将其拦截做权限验证
编写一个拦截器类继承HandlerInterceptor
类,实现preHandle
方法,拦截验证通过返回true,拦截验证不通过返回false,我们思考,要对一个请求进行验证,首先验证其是否存在会话,如果已经存在会话,即已经登录过,即可直接进入首页;其次,在第一次登录验证时,权限验证服务器给客户端返回了一个token,即令牌,客户端收到了令牌,还需要再次到服务端进行查验这个令牌是否有效;最后,如果既没有会话也没有令牌,则表明还未登陆,就需要重定向到登录页输入登录信息;以下为拦截器实际代码
public class SsoClientInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//返回true则通行 false拦截
//1.判断是否存在会话,即已经登录成功后再次访问
HttpSession session = request.getSession();
Boolean isLogin = (Boolean) session.getAttribute("isLogin");
if (isLogin != null && isLogin){
//存在会话
System.out.println("存在会话直接登录");
return true;
}
//2.判断token是否有效,如果不为空则到发送HTTP请求到sso-server验证
//即第一次登录时sso-server给客户端返回的token
String token = request.getParameter("token");
if (!StringUtils.isEmpty(token)){
System.out.println("检测到token信息,需要拿到服务器去校验token"+token);
String httpUrl = SSOClientUtil.SERVER_URL_PREFIX + "/verify";
HashMap<String, String> params = new HashMap<String, String>();
params.put("token",token);
try {
String isVerify = HttpUtil.sendHttpRequest(httpUrl, params);
if ("true".equals(isVerify)){
System.out.println("服务端校验token信息通过");
session.setAttribute("isLogin",true);
return true;
}
}catch (Exception e){
e.printStackTrace();
}
}
//没有登录信息就跳转到登录页面
SSOClientUtil.redirectToSSOURL(request,response);
System.out.println("没有登录信息重定向到登陆页");
return false;
}
}
按照主线,由于此时还未登陆并且没有token,就会走到拦截器的最后一步,即重定向到验证服务端(sso-server)的登录页,但这里涉及到一个核心问题,即我们要让验证服务端知道我们是从哪里来的,即“我从哪里来的”问题,这里我采用了天猫的解决方案,在重定向的时候将起始地址一直携带,这就需要我们来拼接一个新的URL,因此有了如下工具类,首先从本地的配置文件获取到当前服务器的域名和需要去到的验证服务端的域名,然后获取到servlet请求地址,最后将其拼接并重定向到验证服务端/checkLogin
请求去验证是否登录
public class SSOClientUtil {
private static Properties ssoProperties = new Properties();
public static String SERVER_URL_PREFIX; //统一认证中心地址
public static String CLIENT_HOST_URL; //当前客户端地址
//从配置文件获取信息
static {
try {
ssoProperties.load(SSOClientUtil.class.getClassLoader().getResourceAsStream("sso.properties"));
} catch (IOException e) {
e.printStackTrace();
}
SERVER_URL_PREFIX = ssoProperties.getProperty("server-url-prefix");
CLIENT_HOST_URL = ssoProperties.getProperty("client-host-url");
}
/**
* 当客户端请求被拦截,跳往统一认证中心,需要带redirectUrl的参数,统一认证中心登录后回调的地址
*/
public static String getRedirectUrl(HttpServletRequest request){
//获取请求URL
return CLIENT_HOST_URL+request.getServletPath();
}
/**
* 根据request获取跳转到统一认证中心的地址,通过Response跳转到指定的地址
*/
public static void redirectToSSOURL(HttpServletRequest request,HttpServletResponse response) throws IOException {
String redirectUrl = getRedirectUrl(request);
StringBuilder url = new StringBuilder(50)
.append(SERVER_URL_PREFIX)
.append("/checkLogin?redirectUrl=")
.append(redirectUrl);
response.sendRedirect(url.toString());
}
}
这里涉及到的配置sso-properties
文件如下
server-url-prefix=http://www.sso.com:8081
client-host-url=http://www.tb.com:8082
重定向到的/checkLogin
请求如下,其逻辑就是判断是否存在token,如果不存在,就到登录页面,如果存在,就重定向到来时的地方,例如这里就是www.tb.com:8082/taobao
,然后被拦截器二次拦截进行验证
此时我们还未输入过用户名和密码,因此在我们输入用户名和密码提交后就需要进行验证,验证通过后也存在一些列操作,首先需要创建一个全局唯一的令牌,这里使用UUID实现;第二,需要创建一个会话并将token存入;第三,将token存入持久层,例如Redis,我们这里用一个set集合代替;最后,我们需要将token返回给客户端并将页面再次重定向到客户端。如下为第一次登录的逻辑操作
模拟的数据库的set集合如下所示
public class MockDB {
public static Set<String> T_TOKEN = new HashSet<String>();
}
在第一次登录之后,验证服务端返回了一个token给客户端,并再次重定向到客户端,此时会再次被拦截器拦截,与第一次不同的是,这次拦截客户端已经得到了服务端返回的token,就会走以上拦截器的第二步,这次有了token,就需要拿到服务器去校验token,这里的校验需要发送一个请求,携带一个参数,返回一个结果,可以用Java给我们提供的HTTPClient来实现,我们这里可以自己编写一个工具类来实现通信,如下
public class HttpUtil {
/**
* 模拟浏览器的请求
*/
public static String sendHttpRequest(String httpURL,Map<String,String> params) throws Exception{
//建立URL连接对象
URL url = new URL(httpURL);
//创建连接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//设置请求的方式(需要是大写的)
conn.setRequestMethod("POST");
//设置需要输出
conn.setDoOutput(true);
//判断是否有参数.
if(params!=null&¶ms.size()>0){
StringBuilder sb = new StringBuilder();
for(Map.Entry<String,String> entry:params.entrySet()){
sb.append("&").append(entry.getKey()).append("=").append(entry.getValue());
}
//sb.substring(1)去除最前面的&
conn.getOutputStream().write(sb.substring(1).toString().getBytes("utf-8"));
}
//发送请求到服务器
conn.connect();
//获取远程响应的内容.
String responseContent = StreamUtils.copyToString(conn.getInputStream(),Charset.forName("utf-8"));
conn.disconnect();
return responseContent;
}
}
拦截器到入伍的验证token的请求时/verify
拦截器通过上面的通信工具类到/verify
请求下验证token,如果验证返回了true
就证明验证成功,即可成功登陆,下次再请求首页时,由于会话中已经存在isLogin
参数,就可直接进入首页,至此已完成一次登陆操作
我们启动sso-server
和sso-taobao
两个Tomcat,然后访问www.tb.com:8082/taobao
测试,会发现页面会被重定向到http://www.sso.com:8081/checkLogin?redirectUrl=http://www.tb.com:8082/taobao
此时输入密码和用户名即可登录如下
细心的朋友可能会发现我们的/checkLogin
请求的存在令牌的情况在目前是不气任何用处的
而存在token的情况,就是存在多个模块时登录的情况,我们创建一个新的模块为sso-tmall
,将sso-taobao
模块的内容复制一份,注意修改首页请求和本地映射路径等信息。
启动sso-tmall
模块,访问www.tm.com:8083/tmall
请求,能直接进入首页,因为本地已经存在同一份token了,如下注意对比两者URL,可以证明其共用一份token