需求背景:随着公司的发展,公司内部使用的系统越来越多。但是对于使用系统的员工来说不是个好事情。
1.每个系统都需要记住对应的账号和密码,很多员工都是每个系统的账户和密码都一样的。
2.如果同时要使用CRM系统、WMS系统、OA系统,用户需要登录三次,如果10个系统需要登录分别登录十次,非常繁琐。
3.如果不使用了,还需要分别在三个系统中依次的注销。
需求:
1.后台用户通过SSO系统实现统一登录,并在SSO系统中点击其他系统并跳转到该系统,无需再次登录。
2.后台用户未在SSO系统登录时,其他系统无权访问,需统一跳转至SSO系统登录界面。
3.同步退出机制,即不管在SSO系统中退出,还是在其他系统中退出,实现统一退出,即单点注销。
技术分析:
原理:
相比于单系统登录,sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,sso认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。
sso认证中心与sso客户端通信方式有多种,这里以简单好用的HttpURLConnection为例,webService、rpc、restful api都可以
会话机制:
1.第一次访问前,浏览器本地没有cookie,服务器也没有对应的session。
2.第一次访问的时候,服务器会创建session对象,每个session对象都会有id,也就是JSESSIONID。
3.服务器会把JSESSIONID通过cookie的方式写到浏览器中。
4.第二次以后的请求,都会在请求服务器的时候,把JSESSIONID带上,通过JSESSIONID就可以找到服务器对应的session对象。
5.不同域名下的系统session对象是不一样的(跨域)。
SSO单点登录系统 JSESSIONID D7591D0622CB525F165E08C498543457
CRM客户管理系统 JSESSIONID 84C481030AA27C4F2AE2C955CB33EBE5
WMS进销存系统 JSESSIONID 9C757F7AEAD769146317E006DDCA66AB
实现步骤:
修改C:\Windows\System32\drivers\etc\host文件,添加如下配置:
127.0.0.1 www.sso.com
127.0.0.1 www.crm.com
127.0.0.1 www.wms.com
客户端操作(CRM系统、WMS系统、CMS系统…)流程图:
客户端(CRM系统、WMS系统、CMS系统…)项目结构图:
代码:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.wolfcode.sso</groupId>
<artifactId>client-crm</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.2.3.RELEASE</version>
</dependency>
</dependencies>
<build>
<finalName>client-crm</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
package cn.wolfcode.sso.util;
import org.springframework.util.StreamUtils;
import sun.net.www.protocol.http.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Map;
public class HttpUtil {
/**
* 模拟浏览器的请求
* @param httpURL 发送请求的地址
* @param params 请求参数
* @return
*/
public static String sendHttpRequest(String httpURL, Map<String,String> params) throws Exception {
//建立URL连接对象
URL url = new URL(httpURL);
//创建连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
//设置请求的方式 需要大写
connection.setRequestMethod("POST");
//设置需要输出
connection.setDoOutput(true);
//判断是否有参数
if( params != null && params.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)去掉最前面的&
connection.getOutputStream().write(sb.substring(1).toString().getBytes("utf-8"));
}
//发送请求到服务器
connection.connect();
//获取远程响应的内容
String responseContent = StreamUtils.copyToString(connection.getInputStream(), Charset.forName("utf-8"));
connection.disconnect();
return responseContent;
}
/**
* 模拟浏览器的请求
* @param httpURL 发生请求的地址
* @param jessionId 会话 Id
*
*/
public static void sendHttpRequest(String httpURL, String jessionId) throws Exception {
//建立URL连接对象
URL url = new URL(httpURL);
//创建连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
//设置请求的方式 需要大写
connection.setRequestMethod("POST");
//设置需要输出
connection.setDoOutput(true);
connection.addRequestProperty("Cookie","JSESSIONID="+jessionId);
//发送请求到服务期
connection.connect();
connection.getInputStream();
connection.disconnect();
}
}
package cn.wolfcode.sso.util;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Properties;
public class SSOClientUtil {
private static Properties properties = new Properties();
public static String SERVER_URL_PREFIX; //统一认证中心地址;http://www.sso.com:8443,在sso.properties配置
public static String CLIENT_HOST_URL;//当前客户端地址; http://www.crm.com:8088 ,在sso.properties配置
static {
try {
properties.load(SSOClientUtil.class.getClassLoader().getResourceAsStream("sso.properties"));
}catch (Exception e){
e.printStackTrace();
}
SERVER_URL_PREFIX = properties.getProperty("server-url-prefix");
CLIENT_HOST_URL = properties.getProperty("client-host-url");
}
/**
* 当客户端请求被拦截, 跳往统一认证中心,需要带redirectUrl 的参数,统一认证中心登录后回调的地址
* 通过Request获得这次请求的地址 http://www.crm.com:8088/main
* @param request
* @return
*/
public static String getRedirectUrl(HttpServletRequest request){
//获取请求URL
return CLIENT_HOST_URL + request.getServletPath();
}
/**
* 根据request 获取跳转到统一认证中心的地址 http://www.sso.com:8443//checkLogin?redirectUrl=
* http://www.crm.com:8088/main 通过Response 跳转到指定的地址
* @param request
* @param response
* @throws IOException
*/
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());
}
/**
* 获取客户端的完整登出地址 http://www.crm.com:8088/logOut
* @return
*/
public static String getClientLogOutUrl(){
return CLIENT_HOST_URL + "/logOut";
}
/**
* 获取认证中心的登出地址 http://www.sso.com:8443/logOut
* @return
*/
public static String getServerLogOutUrl(){
return SERVER_URL_PREFIX + "/logOut";
}
}
package cn.wolfcode.sso.filter;
import cn.wolfcode.sso.util.HttpUtil;
import cn.wolfcode.sso.util.SSOClientUtil;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class SSOClientFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String path = request.getRequestURI();
System.out.println("crm:"+path);
HttpSession httpSession = request.getSession();
//1.判断是否有局部的会话
Boolean isLogin = (Boolean) httpSession.getAttribute("isLogin");
if(isLogin != null && isLogin){
//有局部会话,直接放行
filterChain.doFilter(request,response);
return;
}
// 判断地址栏中是否有携带token参数
String token = request.getParameter("token");
if(StringUtils.isNoneBlank(token)){
// token信息不为null, 说明地址中包含了token,拥有令牌。
//判断token信息是否有认证中心产生的
String httpURL = SSOClientUtil.SERVER_URL_PREFIX+"/verify";
Map<String,String> params = new HashMap<>();
params.put("token",token);
params.put("clientUrl", SSOClientUtil.getClientLogOutUrl());
params.put("jessionid",httpSession.getId());
try {
String isVerify = HttpUtil.sendHttpRequest(httpURL,params);
if("true".equals(isVerify)){
//如果返回的字符串是true,说明这个token是由统一认证中心产生的
//创建局部的会话
httpSession.setAttribute("isLogin",true);
//放行该次的请求
filterChain.doFilter(request,response);
return;
}
}catch (Exception e){
e.printStackTrace();
}
}
//没有局部的会话,重定向到统一认证中心,检查是否有其他的系统已经登录过
// http://www.sso.com:8443/checkLogin?redirectUrl=http://www.crm.com:8088
SSOClientUtil.redirectToSSOURL(request,response);
}
@Override
public void destroy() {
}
}
package cn.wolfcode.sso.controller;
import cn.wolfcode.sso.util.SSOClientUtil;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name="mainServlet",urlPatterns = "/main")
public class MainServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)throws ServletException, IOException {
this.doPost(req,res);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res)throws ServletException, IOException {
req.setAttribute("serverLogOutUrl", SSOClientUtil.getServerLogOutUrl());
req.getRequestDispatcher("/WEB-INF/views/main.jsp").forward(req,res);
}
}
package cn.wolfcode.sso.controller;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name="LogOutServlet.java", urlPatterns = "/logOut")
public class LogOutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)throws ServletException , IOException {
doPost(req,res);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res)throws ServletException , IOException {
req.getSession().invalidate();
}
}
package cn.wolfcode.sso.controller;
import cn.wolfcode.sso.util.SSOClientUtil;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name="mainServlet",urlPatterns = "/main")
public class MainServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)throws ServletException, IOException {
this.doPost(req,res);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res)throws ServletException, IOException {
req.setAttribute("serverLogOutUrl", SSOClientUtil.getServerLogOutUrl());
req.getRequestDispatcher("/WEB-INF/views/main.jsp").forward(req,res);
}
}
sso.properties
crm 和wms的 properties 的 client-host-url 不一样
server-url-prefix=http://www.sso.com:8080
client-host-url=http://www.crm.com:8088
main.jsp
<%--
Created by IntelliJ IDEA.
User: Administrator
Date: 2019/6/21
Time: 15:59
To change this template use File | Settings | File Templates.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>客户关系管理系统</title>
</head>
<body>
<h1>这是客户管理系统的首页</h1>
<div style="text-align: right;width: 25%;">
<a href="${serverLogOutUrl}">退出系统</a>
</div>
</body>
</html>
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
<filter>
<filter-name>SSOClientFilter</filter-name>
<filter-class>cn.wolfcode.sso.filter.SSOClientFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>SSOClientFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
服务端(SSO系统)操作流程图:
/checkLogin
1.检测客户端在服务端是否已经登录了
1.1获取session中的token.
1.2如果token不为空,说明服务端已经登录过了,此时重定向到客户端的地址,并把token带上
1.3如果token为空,跳转到统一认证中心的的登录页面,并把redirectUrl放入到request域中.
/user_login
2.统一认证中心的登录方法
2.1判断用户提交的账号密码是否正确.
2.2如果正确
2.2.1创建token(可以使用UUID,保证唯一就可以)
2.2.2把token放入到session中.
2.2.3这个token要知道有哪些客户端登陆了,创建Map<String,List<String[]> clientMap;(为单点注销做准备)
SSOUtil.clientMap.put(token,new ArrayList());(把这些数据放入到数据库中也是可以的,我们就做比较简单的,模拟一下.)
2.2.4转发到redirectUrl地址,把token带上.
2.3如果错误
转发到login.jsp,还需要把redirectUrl参数放入到request域中.
/verify
3.统一认证中心认证token方法
3.1如果SSOUtil.clientMap.get(token)有数据clientList,说明token是有效的.
3.1.1clientList把客户端传入的客户端登出地址(clientLogOutUrl)和会话ID(jsessionid)保存到集合中.
3.1.2返回true字符串.
3.1如果SSOUtil.clientMap.get(token)为null,说明token是无效的,返回false字符串.
项目结构图:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.wolfcode.sso</groupId>
<artifactId>sso-server</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>server Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.6</version>
</dependency>
</dependencies>
<build>
<finalName>server</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
package cn.wolfcode.sso.vo;
import lombok.Data;
@Data
public class ClientInfoVo {
private String clientUrl;
private String jsessionId;
}
package cn.wolfcode.sso.util;
import cn.wolfcode.sso.vo.ClientInfoVo;
import java.util.*;
/**
* 模拟数据库,保存子系统的session对象信息
*/
public class MockDatabaseUtil {
public static Set<String> T_TOKEN = new HashSet<>();
public static Map<String, List<ClientInfoVo>> T_CLIENT_INFO = new HashMap<>();
}
package cn.wolfcode.sso.util;
import org.springframework.util.StreamUtils;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Map;
public class HttpUtil {
public static String sendHttpRequest(String httpURL, Map<String, String> params) throws Exception {
//建立URL连接对象
URL url = new URL(httpURL);
//建立连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
//设置请求的方式 需要是大写的
connection.setRequestMethod("POST");
//设置需要输出
connection.setDoOutput(true);
if (params != null && params.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)去除最前面的&
connection.getOutputStream().write(sb.substring(1).toString().getBytes("utf-8"));
}
//发送请求到服务器
connection.connect();
//获取远程响应的内容
String responseContent = StreamUtils.copyToString(connection.getInputStream(), Charset.forName("utf-8"));
connection.disconnect();
return responseContent;
}
/**
* 模拟浏览器的请求
* @param httpURL
* @param jsessionId
* @return
*/
public static void sendHttpRequest(String httpURL, String jsessionId) throws Exception {
//建立URL 连接对象
URL url = new URL(httpURL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.addRequestProperty("Cookie","JSESSION="+jsessionId);
connection.connect();
connection.getInputStream();
connection.disconnect();
}
public static void sendHttpRequest(String httpUrl) throws Exception {
URL url = new URL(httpUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.connect();
connection.getInputStream();
connection.disconnect();
}
}
package cn.wolfcode.sso.listener;
import cn.wolfcode.sso.util.HttpUtil;
import cn.wolfcode.sso.util.MockDatabaseUtil;
import cn.wolfcode.sso.vo.ClientInfoVo;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import java.util.List;
public class MySessionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent httpSessionEvent) {
}
@Override
public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
HttpSession session = httpSessionEvent.getSession();
String token = (String) session.getAttribute("token");
//删除 t_token 表中的数据
MockDatabaseUtil.T_TOKEN.remove(token);
List<ClientInfoVo> clientInfoVoList = MockDatabaseUtil.T_CLIENT_INFO.remove(token);
try {
if (clientInfoVoList != null) {
for (ClientInfoVo vo : clientInfoVoList) {
//获取出注册的子系统,依次调用子系统的登出方法
HttpUtil.sendHttpRequest(vo.getClientUrl(),vo.getJsessionId());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
package cn.wolfcode.sso.filter;
import com.sun.deploy.net.HttpResponse;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* @category :登录拦截器
*
*/
public class LoginFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//如果是登录有关操作的,不拦截
String path = request.getRequestURI();
//1.session还有效
HttpSession session = request.getSession();
String token = (String) session.getAttribute("token");
if (token != null) {
//放行
filterChain.doFilter(request,response);
return;
}
if (path.contains("login") || path.endsWith(".css") || path.endsWith(".js")) {
//放行
filterChain.doFilter(request, response);
return;
} else {
System.out.println("path:"+path);
if (path.endsWith("verify")) {
//放行
filterChain.doFilter(request,response);
return;
}else {
response.sendRedirect("login");
}
}
}
@Override
public void destroy() {
}
}
package cn.wolfcode.sso.controller;
import cn.wolfcode.sso.util.HttpUtil;
import cn.wolfcode.sso.util.MockDatabaseUtil;
import cn.wolfcode.sso.vo.ClientInfoVo;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpSession;
import java.util.*;
@Controller
public class SSOServerController {
@RequestMapping("/")
public String index() {
return "index";
}
@RequestMapping("/login")
public String login(){
return "login";
}
@RequestMapping("/test")
public String test() {
return "test";
}
@RequestMapping("/checkLogin")
public String checkLogin(String redirectUrl, HttpSession session, Model model) {
//1.判断是否有全局的会话
String token = (String) session.getAttribute("token");
if (StringUtils.isEmpty(token)) {
//表示没有全局会话
//跳转到统一认证中心的登录页面
model.addAttribute("redirectUrl",redirectUrl);
return "login";
} else {
//有全局会话
//取出令牌信息,重定向到redirectUrl,把令牌带上 http://www.wms.com:8089/main?token
model.addAttribute("token",token);
return "redirect:"+redirectUrl;
}
}
/**
* 登录功能
* @return
*/
@RequestMapping("/user_login")
public String login(String username, String password, String redirectUrl, HttpSession httpSession, Model model){
if ("admin".equals(username)&&"123456".equals(password)) {
//账号密码匹配
//1.创建令牌信息
String token = UUID.randomUUID().toString();
//2.创建全局的会话,把令牌信息放入会话中,
httpSession.setAttribute("token",token);
//3.需要把令牌信息放到数据库中。
MockDatabaseUtil.T_TOKEN.add(token);
if (StringUtils.isEmpty(redirectUrl)) {
return "redirect:/";
}
//4.重定向到redirectUrl,把令牌信息带上。 http://www.crm.com:8088/main?token=
model.addAttribute("token",token);
return "redirect:"+redirectUrl;
}
//如果账号密码有误
if(!StringUtils.isEmpty(redirectUrl)){
model.addAttribute("redirectUrl",redirectUrl);
}
return "redirect:login";
}
@RequestMapping("/verify")
@ResponseBody
public String verifyToken(String token, String clientUrl, String jsessionId) {
if (MockDatabaseUtil.T_TOKEN.contains(token)) {
//把客户端的登出地址记录
List<ClientInfoVo> clientInfoVoList = MockDatabaseUtil.T_CLIENT_INFO.get(token);
if (clientInfoVoList == null) {
clientInfoVoList = new ArrayList<ClientInfoVo>();
MockDatabaseUtil.T_CLIENT_INFO.put(token,clientInfoVoList);
}
ClientInfoVo vo = new ClientInfoVo();
vo.setClientUrl(clientUrl);
vo.setJsessionId(jsessionId);
clientInfoVoList.add(vo);
//说明令牌有效,返回true
return "true";
}
return "false";
}
@RequestMapping("/logOut")
public String logOut(HttpSession session){
//销毁全局会话
session.invalidate();
return "redirect:login";
}
}
applicationContext.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 http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 开启注解扫描-->
<context:component-scan base-package="cn.wolfcode.sso"/>
<!-- mvc注解驱动支持-->
<mvc:annotation-driven/>
<!--静态资源处理-->
<mvc:default-servlet-handler/>
<!--视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>sso单点登录系统</title>
</head>
<body>
<h1>这是sso单点登录的首页</h1>
<div style="text-align:center;">
<a href="http://www.crm.com:8088/main" target="_blank">crm系统</a>        <a href="http://www.wms.com:8089/main" target="_blank">wms系统</a>       
<a href="/logOut">退出系统</a>
</div>
</body>
</html>
login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>统一认证中心</title>
<link rel="stylesheet" type="text/css" href="/static/css/style.css" />
<script type="text/javascript" src="/static/js/jquery-latest.min.js"></script>
<script type="text/javascript" src="/static/js/placeholder.js"></script>
</head>
<body>
${errorMsg}
<form id="slick-login" method="post" action="/user_login">
<input type="hidden" name="redirectUrl" value="${redirectUrl}">
<label>username</label><input type="text" name="username" class="placeholder" placeholder="账号">
<label>password</label><input type="password" name="password" class="placeholder" placeholder="密码">
<input type="submit" value="登录">
</form>
</body>
</html>
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">
<display-name>Archetype created web application</display-name>
<filter>
<filter-name>LoginFilter</filter-name>
<filter-class>cn.wolfcode.sso.filter.LoginFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>LoginFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<listener>
<listener-class>cn.wolfcode.sso.listener.MySessionListener</listener-class>
</listener>
</web-app>
单点退出操作:
单点退出流程:用户在SSO系统登录后创建token,保存token到数据库t_token表中,当子系统登录时,将该token的子系统信息:退出url和jsessionid信息保存到t_client_info表,一对多关系。当用户点击退出时,调用SSO系统/logOut方法,销毁sesseion,并在web.xml中注册session监听器,当有销毁操作时,调出t_client_info表对象信息,并依次调用子系统的/logOut方法,根据jsessionid销毁子系统中session对象信息,从而实现全部退出
整体效果图:
输入账号密码登录后:
在SSO系统中分别进入CRM系统、WMS系统
三个系统中不管哪个系统点击退出系统,全部退出,并再次跳转到SSO系统登录界面。根据不同单点登录需求,修改代码,这里针对后台所有系统统一登录管理。