绝对完全跨域统一单点登录登出
应用场景:多个系统下同属于一个用户,当用户登录了web1系统,那么访问web2,web3. . . . 时候,用户就无需再次登录。如:淘宝与天猫,登出也如此,一个系统登出,其他系统的登录也随之失效,这就是统一单点登录登出。
这里配置三个web系统,一个用户中心系统为栗子
配置hosts实现跨域:
127.0.0.1 ssofront.ljtest.xxxx.com #用户中心 127.0.0.1 my.kuaiji.com #web1 127.0.0.1 my.zikao.com #web2 127.0.0.1 my.xuelxuew.com #web3
nginx 配置用于统一登录页面转发
server { listen 80; server_name ssofront.ljtest.xxxx.com; location /ajax/ { proxy_pass http://ssofront.ljtest.xxxx.com:9092/; } location / { root D:/Java/projects/SSO_Single; index index.html; } }
先看看效果图:
统一登录页面:
所有的web系统登录都重定向到用户中心的统一登录页面
统一登录之后访问各系统
当web1点击登录重定向到用户中心统一登录之后,再访问web2,web3系统都已经是登录状态了。
能够实现这种效果就说明用户中心,3个web系统都分别在自己的域名下成功的把token放到各自的cookie下了,而且4个系统都是跨域的,所以cookie在这四个系统间是不可共享的,所以已经达到跨域统一登录了。
如何实现的??我们先看看流程图
各位看官如果看完流程图还不是很清晰,且听我分析:
当我们第一次访问web1系统时(未登录状态,其他web系统也未登录过),此时经过过滤器,token,action当然为null,就会查找cookie是否存在token,不过是否取得都把这个token拿到用户中心去校验,第一次肯定拿不到,所以校验失败,efftoken有效token为空,重定向到用户中心(重定向由浏览器重新发出可以获取到用户中心的cookie)获取cookie中的token,token去到SSO校验是否失效,第一次访问所以token为空失效,返回token,重定向回web1系统(此时得带上参数:返回的token 和 action=callback(用以标识是用户中心重定向回来的,避免多次重定向)),到这里就完成了一次询问,假如一直未登录,每次访问web1系统都会去用户中心询问是否有别的系统登录过啊,有就返回有效token放入web1系统的session和cookie中,那么下次访问就拿着这个token去用户中心校验判断是否失效。当我们去到用户中心登录了生成token,token会放入用户中心的cookie中,然后重定向回web1系统,经过过滤器时执行上述步骤便可以拿到用户中心的有效token。其中任何一个系统登出都会调用用户中心单点登出,此时token失效,其他系统再次访问时就会再次询问是否失效,失效清空cookie,这样就完成统一登出啦。其他系统同理也是如此。
这么还存在一个小问题就是当从用户中心询问发现没有登录过,带参数重定向回web1系统,浏览器上的url会加上参数 ?token=&action=callback,当我们只是刷新页面没有其他操作,此时过滤器中action值为callback会跳过到用户中心的询问,而token是失效的未登录状态,所以的点击页面刷新,这个问题大家如果想的好的解决办法可以告诉我一下。
主要代码:
web系统:
Login.java
@RequestMapping("/login") public void login(HttpServletRequest request, HttpServletResponse response){ try { //获取上一个URL地址 String previousUrl = request.getHeader("Referer").toString(); response.sendRedirect(LOGIN_URL+previousUrl); }catch (IOException e) { e.printStackTrace(); } }
过滤器
/** * 统一登录过滤器 * Created by Administrator on 2018/4/21 0021. * @author Evan */ public class UnifiedLoginFilter extends OncePerRequestFilter { @Value("${tokenExpired.url}") private String TOKENEXPIRED_URL; @Value("${userCenterToken.url}") private String USERCENTERTOKEN_URL; private final static String ACTION_NAME = "callback"; @Autowired private UserService userService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String action = ServletRequestUtils.getStringParameter(request, "action", null); HttpSession session = request.getSession(); String currentUrl = request.getRequestURL().toString(); //获取用户中心有效token String effectiveToken = null; String currentToken = null; boolean doFilter = true; if(ACTION_NAME.equals(action)) { effectiveToken = ServletRequestUtils.getStringParameter(request, "token", null); } else { currentToken = (String) session.getAttribute(SystemConfig.SESSION_TOKEN_KEY); Cookie[] cookies = request.getCookies(); if (StringUtils.isBlank(currentToken) && cookies != null) { for (int i = 0; i < cookies.length; i++) { Cookie cookie = cookies[i]; if (cookie != null && SystemConfig.COOKIE_TOKEN_NAME.equals(cookie.getName())) { currentToken = cookie.getValue(); break; } } } String result = HttpUtil.doGet4Json(TOKENEXPIRED_URL+currentToken); if(StringUtils.isNotBlank(result)) { JSONObject object = JSONObject.parseObject(result); effectiveToken = object.getString("data"); if(StringUtils.isBlank(effectiveToken)) { doFilter = false; response.sendRedirect(USERCENTERTOKEN_URL+currentUrl); } } } if(doFilter) { //用户中心token有效 if(StringUtils.isNotBlank(effectiveToken)) { if(effectiveToken.equals(currentToken)) { //当前系统token有效,把token存入session session.setAttribute(SystemConfig.SESSION_TOKEN_KEY, effectiveToken); } else { //当前系统token失效,更新token setCookie(effectiveToken, request, response); } } //用户中心token失效 else { removeCookie(request, response); } //重定向去掉url地址参数 if(ACTION_NAME.equals(action) && StringUtils.isNotBlank(effectiveToken)) { response.sendRedirect(currentUrl); } else { filterChain.doFilter(request, response); } } } }
过滤器配置web.xml
<!-- 统一登录过滤 --> <filter> <filter-name>unifiedLoginFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>unifiedLoginFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
使用DelegatingFilterProxy代理过滤器,可以让Spring管理被代理的过滤器,这样就可以在过滤器中访问Spring容器中的bean,属性等。
用户中心:
token校验 UserInfo.java
@ApiOperation(value = "重定向获取用户中心Token") @ApiImplicitParam(name = "redirectUrl", value = "回调地址", required = true, dataType = "String", paramType = "query") @RequestMapping(value = "/userCenterToken", method = RequestMethod.GET) public void userCenterToken(HttpServletRequest request, HttpServletResponse response) { String redirectUrl = ServletRequestUtils.getStringParameter(request, "redirectUrl", null); String schoolId = this.getSchoolId(request); String token = null; Cookie[] cookies = request.getCookies(); if(null != cookies) { for (int i = 0; i < cookies.length; i++) { Cookie cookie = cookies[i]; if (cookie != null && config.getCookieTokenName().equals(cookie.getName())) { token = cookie.getValue(); break; } } } try { String effectiveToken = getEffectiveToken(token,schoolId); response.sendRedirect(redirectUrl+"?token="+effectiveToken+"&action=callback"); } catch (Exception e) { TRACER.error("", e); e.printStackTrace(); } } @ApiOperation(value = "校验Token是否失效") @ApiImplicitParam(name = "token", value = "用户 Token", required = true, dataType = "String", paramType = "query") @RequestMapping(value = "/tokenExpired", method = RequestMethod.GET) @ResponseBody public ResponseEntity<WrappedResponse<String>> tokenExpired(HttpServletRequest request, HttpServletResponse response) { try { String token = ServletRequestUtils.getStringParameter(request, "token", null); String schoolId = this.getSchoolId(request); String effectiveToken = getEffectiveToken(token,schoolId); return this.success(effectiveToken); } catch (Throwable t) { TRACER.error("", t); return this.fail(TransactionStatus.INTERNAL_SERVER_ERROR); } } private String getEffectiveToken(String token, String schoolId) throws Exception { String effectiveToken = ""; HashMap<String, Object> map = new HashMap<>(1); map.put("token", token); HttpPlainResult result = httpConnManager.invoke(HttpMethod.GET, config.getSsoHost()+"/inner/tokenExpired", map,schoolId); TRACER.info(result.getResult()); HttpResultDetail<TokenStatus> entry = HttpResultHandler.handle(result, TokenStatus.class); if (entry.isOK()) { TokenStatus tokenStatus = entry.getResult(); if (!tokenStatus.getExpired()) { effectiveToken = token; } } return effectiveToken; }
统一登录 Login.java
HttpPlainResult result = httpConnManager.invoke(HttpMethod.POST, config.getSsoHost() + "/inner/login", map, schoolId); TRACER.info(result.getResult()); HttpResultDetail<Token> entry = HttpResultHandler.handle(result,Token.class); if(entry.isOK()){ ClientTypeEnum clientTypeEnum = ClientTypeEnum.convertString2ClientType(clientType); if(clientTypeEnum == ClientTypeEnum.WEB){ String token = entry.getResult().getToken(); Cookie token_cookie = new Cookie(config.getCookieTokenName(),token); token_cookie.setMaxAge(config.getCookieTokenTimeout()); token_cookie.setDomain(request.getHeader("Host").split(":")[0]); response.addCookie(token_cookie); return this.success(redirectUrl,token_cookie); }else{ return this.success(entry.getResult(), entry.getResponseStatus()); } }else if(entry.isClientError()){ return this.error(entry.getResponseMessage(), entry.getResponseStatus()); }else if(entry.isServerError()){ return this.fail(entry.getResponseMessage(), entry.getResponseStatus()); }
统一登出 Logout.java
HttpPlainResult result = httpConnManager.invoke(HttpMethod.POST, config.getSsoHost()+"/inner/logout", map,schoolId); TRACER.info(result.getResult()); HttpResultDetail<String> entry = HttpResultHandler.handle(result,String.class); if(entry.isOK()){ Cookie token_cookie = new Cookie(config.getCookieTokenName(),null); token_cookie.setMaxAge(0); token_cookie.setDomain(request.getHeader("Host").split(":")[0]); response.addCookie(token_cookie); return this.success(entry.getResult(), entry.getResponseStatus()); }else if(entry.isClientError()){ return this.error(entry.getResponseMessage(), entry.getResponseStatus()); }else if(entry.isServerError()){ return this.fail(entry.getResponseMessage(), entry.getResponseStatus()); }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!