废话不多说直接上代码咯,什么好处,实现原理相关资料baidu去,基本都一样,就不复制了
controller层代码
package com.ffcs.sso.controller;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import com.ffcs.sso.pojo.User;
import com.ffcs.sso.service.LogoutManagerService;
import com.ffcs.sso.service.TicketManagerService;
import com.ffcs.sso.service.UserManagerService;
import com.ffcs.sso.utils.Constant;
/**
*
* @author damon
*
*/
@Controller
public class SSOController {
private final static Logger LOGGER=LoggerFactory.getLogger(SSOController.class);
@Autowired
private TicketManagerService ticketManagerService;
@Autowired
private UserManagerService userManagerService;
@Autowired
private LogoutManagerService logoutManagerService;
/**
* 主页
* @return
*/
@RequestMapping(value="manager/index",method=RequestMethod.GET)
public String index(){
return "index";
}
/**
* 登陆首页
* @return
*/
@RequestMapping(value="login",method=RequestMethod.GET)
public ModelAndView loginIndex(String queryParams,String targetUrl,ModelAndView modelAndView){
modelAndView.addObject("queryParams", queryParams);
modelAndView.addObject("targetUrl", targetUrl);
modelAndView.setViewName("login");
return modelAndView;
}
@RequestMapping(value="login",method=RequestMethod.POST)
public String login(@RequestParam(required = true) String account, @RequestParam(required = true) String password,
String targetUrl, String queryParams, Model model, HttpSession session) {
User user = userManagerService.getUserInfo(account, password);
if (user != null) {
String homeCookieId = session.getId();
session.setAttribute(Constant.SSO_IS_LOGIN, true);
session.setAttribute(Constant.SSO_LOGIG_INFO, account);
logoutManagerService.saveLoginUserInfo(homeCookieId, user);
if (StringUtils.isNotBlank(targetUrl)) {
// 生成targetURL对应的票
String ticket = ticketManagerService.generateTicket(targetUrl, homeCookieId);
String params = StringUtils.isNotBlank(queryParams) ? "&" + queryParams : "";
LOGGER.info("################### 用户账号:[{}] 对应主站cookieId:[{}] 登陆系统 :[{}]", account, homeCookieId, targetUrl);
return "redirect:" + targetUrl + "?" + Constant.SSO_TICKET + "=" + ticket + params;
}
return "redirect:manager/index";
} else {
model.addAttribute("error", "用户名或密码错误!");
if (StringUtils.isNotBlank(targetUrl)) {
model.addAttribute("targetUrl", targetUrl);
}
return "login";
}
}
/**
* 重定向到子系统并生成票
* @param targetUrl
* @param queryParams
* @param modelAndView
* @param session
* @return
*/
@RequestMapping(value="redirect",method=RequestMethod.GET)
public ModelAndView redirect(@RequestParam(value="targetUrl",required=true)String targetUrl,
String queryParams,ModelAndView modelAndView,HttpSession session) {
if(session.getAttribute(Constant.SSO_IS_LOGIN)==null){
modelAndView.setViewName("redirect:login");
modelAndView.addObject("targetUrl", targetUrl);
modelAndView.addObject("queryParams", queryParams);
//重定向到login方法,带上目标网页地址
}else{
String homeCookieId=session.getId();
String account=(String) session.getAttribute(Constant.SSO_LOGIG_INFO);
//生成targetURL对应的票
String ticket=ticketManagerService.generateTicket(targetUrl,homeCookieId);
String params=StringUtils.isNotBlank(queryParams)?"&"+queryParams:"";
modelAndView.setViewName("redirect:" + targetUrl + "?" + Constant.SSO_TICKET + "=" + ticket + params);
LOGGER.info("############### 用户账号:[{}] 主站cookieId:[{}] 重定向到系统:[{}] 对应ticket:[{}]", account, homeCookieId, targetUrl, ticket);
}
return modelAndView;
}
/**
* 单点注销
* @param session
* @return
*/
@RequestMapping(value="logout",method=RequestMethod.GET)
public String logout(HttpSession session){
if(session.getAttribute(Constant.SSO_IS_LOGIN)!=null){
String cookieId=session.getId();
String account=(String) session.getAttribute(Constant.SSO_LOGIG_INFO);
logoutManagerService.logout(cookieId);
session.invalidate();
LOGGER.info("########### 单点退出用户账号:[{}] 对应主站cookieId为:[{}] ",account,cookieId);
}
return "redirect:login";
}
}
package com.ffcs.sso.service;
import java.util.UUID;
import net.rubyeye.xmemcached.MemcachedClient;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import com.ffcs.sso.exception.SSOException;
import com.ffcs.sso.pojo.TicketInfo;
import com.ffcs.sso.pojo.TicketResponseInfo;
import com.ffcs.sso.pojo.User;
import com.ffcs.sso.utils.Constant;
/**
*
* 票管理
*
*
* damon
*
*/
@Service
public class TicketManagerServiceImpl implements TicketManagerService{
@Autowired
@Qualifier(value="memcachedSSOClient")
private MemcachedClient memcachedClient;
@Autowired
private LogoutManagerService logoutManagerService;
@Override
public String generateTicket(String target,String homeCookieId){
if(StringUtils.isBlank(target)){
throw new IllegalArgumentException("target不可以为空!");
}
if(StringUtils.isBlank(homeCookieId)){
throw new IllegalArgumentException("homeCookieId不可以为空!");
}
String ticket=UUID.randomUUID().toString();
try {
memcachedClient.set(ticket, Constant.MAX_TICKET_INACTIVE_INTERVAL, new TicketInfo(ticket,target,homeCookieId));
return ticket;
} catch (Exception e) {
throw new SSOException(e);
}
}
@Override
public TicketResponseInfo validateTicket(String ticket,String target,
String subCookieId,String subCookieName,String subLogoutPath){
if(StringUtils.isBlank(ticket)){
throw new IllegalArgumentException("ticket不可以为空!");
}
if(StringUtils.isBlank(target)){
throw new IllegalArgumentException("target不可以为空!");
}
if(StringUtils.isBlank(subCookieId)){
throw new IllegalArgumentException("subCookieId不可以为空!");
}
if(StringUtils.isBlank(subCookieName)){
throw new IllegalArgumentException("subCookieName不可以为空!");
}
if(StringUtils.isBlank(subLogoutPath)){
throw new IllegalArgumentException("subLogoutPath不可以为空!");
}
try {
TicketInfo ticketInfo = memcachedClient.get(ticket);
if(ticketInfo==null||!target.equals(ticketInfo.getTargetUrl())){
//返回空验证不通过
return new TicketResponseInfo(false);
}
//删除票保存的临时信息
memcachedClient.delete(ticket);
String homeCookieId=ticketInfo.getHomeCookieId();
//验证后保存登出信息(原本验证和登出信息分开提交,一并提交减少访问次数)
logoutManagerService.saveSubWebsiteLogouInfo(homeCookieId, subLogoutPath, subCookieId, subCookieName);
User user= logoutManagerService.getLoginUserInfo(homeCookieId);
return new TicketResponseInfo(true,user.getAccount(),homeCookieId);
} catch (Exception e) {
throw new SSOException(e);
}
}
}
package com.ffcs.sso.service;
import java.util.Set;
import net.rubyeye.xmemcached.MemcachedClient;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import com.ffcs.sso.exception.SSOException;
import com.ffcs.sso.pojo.ActivationInfo;
import com.ffcs.sso.pojo.LogoutInfo;
import com.ffcs.sso.pojo.User;
import com.ffcs.sso.utils.Constant;
/**
* 单点退出
*
* damon
*/
@Service
public class LogoutManagerServiceImpl implements LogoutManagerService {
private static final Logger LOGGER=LoggerFactory.getLogger(LogoutManagerServiceImpl.class);
private final String LOGOUT_PREFIX="LOGOUT_";
@Autowired
@Qualifier("memcachedSSOClient")
private MemcachedClient memcachedClient;
@Autowired
private HttpClient httpClient;
@Override
public boolean saveSubWebsiteLogouInfo(String homeCookieId, String logoutPath,String subCookieId,String subCookieName) {
if(StringUtils.isBlank(homeCookieId)){
throw new IllegalArgumentException("homeCookieId 不可以为空!");
}
if(StringUtils.isBlank(logoutPath)){
throw new IllegalArgumentException("logoutPath 不可以为空!");
}
if(StringUtils.isBlank(subCookieId)){
throw new IllegalArgumentException("subWebSite 不可以为空!");
}
ActivationInfo info=null;
Set<LogoutInfo> logoutInfos=null;
try {
info=memcachedClient.get(LOGOUT_PREFIX + homeCookieId);
if(info==null){
info=new ActivationInfo();
}
logoutInfos=info.getLogoutInfo();
logoutInfos.add(new LogoutInfo(logoutPath,subCookieId,subCookieName));
info.setLogoutInfo(logoutInfos);
memcachedClient.set(LOGOUT_PREFIX + homeCookieId, Constant.MAX_USERINFO_INACTIVE_INTERVAL, info);
LOGGER.debug("############### 保存子站登出信息 ,子站登出地址 url:{}, 子站cookie:{}, 主站cookie:{}", logoutPath, subCookieId, homeCookieId);
return true;
} catch (Exception e) {
throw new SSOException(e);
}
}
@Override
public void logout(String homeCookieId) {
if(StringUtils.isBlank(homeCookieId)){
throw new IllegalArgumentException("cookieId 不可以为空!");
}
ActivationInfo info=null;
Set<LogoutInfo> logoutInfos=null;
try {
info = memcachedClient.get(LOGOUT_PREFIX+homeCookieId);
memcachedClient.delete(LOGOUT_PREFIX+homeCookieId);
if(info==null|| (logoutInfos=info.getLogoutInfo())==null){
LOGGER.debug("############## 用户 cookieId:[{}] 未登陆任何系统 !",homeCookieId);
return;
}
} catch (Exception e) {
LOGGER.error("############### Memcached获取单点登出信息失败", e);
return;
}
for (LogoutInfo logoutInfo:logoutInfos) {
HttpPost post=null;
try {
post = new HttpPost(logoutInfo.getLogoutPath());
post.setHeader("charset", "UTF-8");
post.setHeader("Connection", "close");
//添加cookie模拟回调时候子系统能找到session
post.setHeader("Cookie",logoutInfo.getSubCookieName()+"="+logoutInfo.getSubCookieId());
HttpResponse response = httpClient.execute(post);
LOGGER.debug("########## 登出子站 :[{}] 主站cookie:[{}] 子站cookie:[{}] 登出返回状态码:[{}] ",
logoutInfo.getLogoutPath(), homeCookieId, logoutInfo.getSubCookieId(), response.getStatusLine().getStatusCode());
} catch (Exception e) {
LOGGER.error("########## 注销子系统失败,子系统信息:{}",logoutInfo.toString(),e);
} finally {
if(post!=null){
post.releaseConnection();
}
}
}
}
@Override
public void saveLoginUserInfo(String homeCookieId,User userInfo){
try {
ActivationInfo info=memcachedClient.get(LOGOUT_PREFIX+homeCookieId);
if(info==null){
info=new ActivationInfo();
}
info.setUserInfo(userInfo);
memcachedClient.set(LOGOUT_PREFIX+homeCookieId, Constant.MAX_USERINFO_INACTIVE_INTERVAL, info);
} catch (Exception e) {
throw new SSOException("############# 保存用户登陆信息失败 ",e);
}
}
@Override
public User getLoginUserInfo(String homeCookieId){
ActivationInfo info = null;
try {
info=memcachedClient.get(LOGOUT_PREFIX+homeCookieId);
if(info!=null){
return info.getUserInfo();
}
throw new RuntimeException("找不到该cookie:["+homeCookieId+"]的用户信息");
} catch (Exception e) {
throw new SSOException("############# 获取登陆用户信息失败 ####",e);
}
}
@Override
public boolean updateUserInfoTimeout(String homeCookieId) {
if(StringUtils.isBlank(homeCookieId)){
throw new IllegalArgumentException("homeCookieId 不可以为空!");
}
try {
return memcachedClient.touch(LOGOUT_PREFIX+homeCookieId, Constant.MAX_USERINFO_INACTIVE_INTERVAL);
} catch (Exception e) {
LOGGER.error("############ 定时更新主站登陆用户信息失败 ",e);
}
return false;
}
}
package com.ffcs.sso.filter;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang.StringUtils;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ffcs.sso.exception.HttpRequestException;
import com.ffcs.sso.pojo.TicketResponseInfo;
import com.ffcs.sso.utils.Constant;
import com.ffcs.sso.utils.HttpClientUtils;
import com.ffcs.sso.utils.JsonUtil;
public class SSOFilter implements Filter {
private final static Logger LOGGER=LoggerFactory.getLogger(SSOFilter.class);
private String ticketValidateURL;
private String redirectLoginURL;
private String updateUserInfoTimeOutURL;
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
HttpSession session=request.getSession();
if(session.getAttribute(Constant.SSO_IS_LOGIN)==null){
String targetUrl = request.getRequestURL().toString();
String ticket = request.getParameter(Constant.SSO_TICKET);
//子站单点退出路径
String subLogoutPath = request.getScheme()+"://"+request.getServerName()+":"
+request.getServerPort()+ request.getContextPath() +"/"+Constant.SSO_LOGOUT_SUFFIX;
TicketResponseInfo responseInfo=new TicketResponseInfo(false);
if(StringUtils.isNotBlank(ticket)){
//校验票如果校验通过同时保存子站的登出信息(减少提交次数,原本验证成功后在提交登出信息),并返回主站的cookieId和主站登陆的账号
responseInfo=validateTicketInfo(ticket,targetUrl,Constant.SSO_SUB_COOKIE_NAME,session.getId(),subLogoutPath);
}
if(!responseInfo.isSuccess()){
String queryParams =this.getQureyParams(request);
String params=StringUtils.isNotBlank(queryParams)?"&queryParams="+URLEncoder.encode(queryParams,"UTF-8"):"";
//重定向到主站判断系统是否登陆过
response.sendRedirect(redirectLoginURL + "?targetUrl=" + targetUrl + params);
LOGGER.debug("############## 重定向到主系统:" + redirectLoginURL + "?targetUrl=" + targetUrl + params);
return;
}
session.setAttribute(Constant.SSO_IS_LOGIN, true);
session.setAttribute(Constant.SSO_ACCOUNT, responseInfo.getAccount());
session.setAttribute(Constant.SSO_HOME_COOKIEID, responseInfo.getHomeCookieId());
}
Long updateInterval = (System.currentTimeMillis() - session.getLastAccessedTime()) / 1000;
if( updateInterval > Constant.SSO_HOME_USERINFO_UPDATE_TIMEOUT){
//更新主站用户信息超时时间
updateUserInfoTimeOut((String)session.getAttribute(Constant.SSO_HOME_COOKIEID));
LOGGER.debug("############## 更新主站用户信息超时时间,间隔{}秒 ########",Constant.SSO_HOME_USERINFO_UPDATE_TIMEOUT);
}
chain.doFilter(request, response);
return;
}
private String getQureyParams(HttpServletRequest request) {
StringBuilder queryParams=new StringBuilder();
Map<String,String[]> map=request.getParameterMap();
Iterator<Entry<String, String[]>> params=map.entrySet().iterator();
boolean tag=false;
while (params.hasNext()) {
Map.Entry<String,String[]> entry = params.next();
if(!entry.getKey().equals(Constant.SSO_TICKET)){
String[] values=entry.getValue();
for(String value:values){
if(tag){
queryParams.append("&");
}
queryParams.append(entry.getKey()).append("=").append(value);
}
tag=true;
}
}
return queryParams.toString();
}
private void updateUserInfoTimeOut(String homeCookieId){
HttpPut put=new HttpPut(updateUserInfoTimeOutURL);
//添加cookie主站那边能够找到session
put.setHeader("Cookie",Constant.SSO_HOME_COOKIE_NAME+"="+homeCookieId);
put.setHeader("accept", "text/plain; charset=UTF-8");
try {
HttpClientUtils.getResponse(put);
} catch (HttpRequestException e) {
LOGGER.error("###################### 定时更新主站用户信息失败 ",e);
}
}
/**
* 根据ticket验证是否正确,验证通过返回主站的cookieId
*
* @param ticket
*
* @param target
*
* @return 返回验证空失败 成功cookieId
*/
private TicketResponseInfo validateTicketInfo(String ticket, String target,String subCookieName,
String subCookieId, String subLogoutPath) {
HttpGet httpget = new HttpGet(ticketValidateURL + "/"+ ticket + "?target=" + target
+"&subCookieId="+subCookieId+"&subCookieName="+subCookieName+"&subLogoutPath="+subLogoutPath);
httpget.setHeader("accept", "application/json; charset=UTF-8");
String result;
try {
result = HttpClientUtils.getResponse(httpget);
return JsonUtil.fromJson(result, TicketResponseInfo.class);
} catch (HttpRequestException e) {
throw new RuntimeException("############### 子站验证ticket失败 ",e);
}
}
@Override
public void init(FilterConfig filterConfig) {
if (filterConfig != null) {
LOGGER.info("####### SSOFilter:Initializing filter");
}
this.ticketValidateURL = filterConfig.getInitParameter("ticketValidateURL");
this.redirectLoginURL=filterConfig.getInitParameter("redirectLoginURL");
this.updateUserInfoTimeOutURL=filterConfig.getInitParameter("updateUserInfoTimeOutURL");
}
}
比较简单,服务端通过httpclient回调这个路口进行退出,服务端必须把客户端登陆的cookie返回过来,不然找不到session
package com.ffcs.sso.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
public class LogoutFilter implements Filter{
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res,FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
request.getSession().invalidate();
return;
}
@Override
public void init(FilterConfig filterConfig) {
}
}
<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"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<filter>
<filter-name>DisableUrlSessionFilterfilter-name>
<filter-class>com.ffcs.sso.filter.DisableUrlSessionFilterfilter-class>
filter>
<filter-mapping>
<filter-name>DisableUrlSessionFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
<filter>
<filter-name>sessionFilterfilter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
filter>
<filter-mapping>
<filter-name>sessionFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
<filter>
<filter-name>LogoutFilterfilter-name>
<filter-class>com.ffcs.sso.filter.LogoutFilterfilter-class>
filter>
<filter-mapping>
<filter-name>LogoutFilterfilter-name>
<url-pattern>/SSO_LOGOUTurl-pattern>
filter-mapping>
<listener>
<description>spring监听器description>
<listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
listener>
<listener>
<listener-class>org.springframework.web.util.IntrospectorCleanupListenerlistener-class>
listener>
<context-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath:spring.xmlparam-value>
context-param>
<filter>
<description>字符集过滤器description>
<filter-name>encodingFilterfilter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
<init-param>
<description>字符集编码description>
<param-name>encodingparam-name>
<param-value>UTF-8param-value>
init-param>
filter>
<filter-mapping>
<filter-name>encodingFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
<filter>
<filter-name>SSOFilterfilter-name>
<filter-class>com.ffcs.sso.filter.SSOFilterfilter-class>
<init-param>
<param-name>ticketValidateURLparam-name>
<param-value>http://127.0.0.1:8080/SSO/webservice/ticketparam-value>
init-param>
<init-param>
<param-name>updateUserInfoTimeOutURLparam-name>
<param-value>http://127.0.0.1:8080/SSO/webservice/userinfo/timeoutparam-value>
init-param>
<init-param>
<param-name>redirectLoginURLparam-name>
<param-value>http://127.0.0.1:8080/SSO/redirectparam-value>
init-param>
filter>
<filter-mapping>
<filter-name>SSOFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
<servlet>
<servlet-name>springMvcservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<init-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath:spring-mvc.xmlparam-value>
init-param>
servlet>
<servlet-mapping>
<servlet-name>springMvcservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
<filter>
<filter-name>HiddenHttpMethodFilterfilter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilterfilter-class>
filter>
<filter-mapping>
<filter-name>HiddenHttpMethodFilterfilter-name>
<servlet-name>springMvcservlet-name>
filter-mapping>
<error-page>
<error-code>404error-code>
<location>/error/404.jsplocation>
error-page>
<error-page>
<error-code>500error-code>
<location>/error/500.jsplocation>
error-page>
web-app>
springMvc
/
HiddenHttpMethodFilter
org.springframework.web.filter.HiddenHttpMethodFilter
HiddenHttpMethodFilter
springMvc
404
/error/404.jsp
500
/error/500.jsp
```