SSO基础

文章目录

  • SSO基础
    • 1.什么是单点登录?
    • 2.回顾普通系统登录
    • 3.多系统登录的问题与解决?
      • 3.1.Session不共享问题
  • XXL-SSO框架基础入门
    • 1.什么是XXL-SSO
    • 2.特性
    • 3. 官方Demo分析
      • 3.1 SSO Server中央认证服务
      • 3.2 SSO Client应用(Cookie形式)
    • 4.总结
  • 集成SSO服务
    • 引言
    • 1. 集成xxl-sso-core
    • 2. 集成xxl-server
    • 总结
  • 改造SSO登录界面
    • 引言
    • 1. 效果图
    • 2. 登录界面代码(前端+后台)
    • 3.总结
  • SSO单点登录(Client端集成)
    • 1.首页门户集成SSO Client
    • 2. 聚合支付门户集成SSO Client
    • 3. 测试
    • 4.显示登录的用户信息
    • 5.总结
  • SSO单点登录(退出登录)
    • 1. 效果演示
    • 2.退出功能实现
    • 总结
  • XXL-SSO登录逻辑
    • 1.XXL-SSO登录逻辑
    • 2.XXL-SSO注销逻辑
  • CSRF攻击
    • 1.CSRF是什么
    • 2.CSRF可以做什么
    • 3.CSRF漏洞现状
    • 4.CSRF的原理
    • 5.CSRF示例
      • 5.1.示例1:
      • 5.2.示例2:
      • 5.3.示例3:
      • 5.4.总结
    • 6.CSRF的防御
      • 6.1. 尽量使用POST,限制GET
      • 6.2.浏览器Cookie策略
      • 6.3.加验证码
      • 6.4.Referer Check
      • 6.5.Anti CSRF Token
      • 6.6.总结
  • 跨域(CORS)
    • 1.引言
    • 2.什么是跨域(CORS)
    • 3.什么情况会跨域(CORS)
    • 4.跨域流程
    • 5.解决跨域

1.什么是单点登录?

单点登录的英文名叫做:Single Sign On(简称SSO)。

在初学/以前的时候,一般我们就单系统,所有的功能都在同一个系统上。
一篇了解SSO单点登录-编程之家
后来,我们为了合理利用资源和降低耦合性,于是把单系统拆分成多个子系统。
一篇了解SSO单点登录-编程之家
比如阿里系的淘宝天猫,很明显地我们可以知道这是两个系统,但是你在使用的时候,登录了天猫,淘宝也会自动登录。
一篇了解SSO单点登录-编程之家

简单来说,单点登录就是在多个系统中,用户只需一次登录,各个系统即可感知该用户已经登录,只要在一个业务中退出,所有系统都退出

2.回顾普通系统登录

众所周知,HTTP是无状态的协议,这意味着服务器无法确认用户的信息。于是乎,W3C就提出了:给每一个用户都发一个通行证,无论谁访问的时候都需要携带通行证,这样服务器就可以从通行证上确认用户的信息。通行证就是Cookie。

如果说Cookie是检查用户身上的”通行证“来确认用户的身份,那么Session就是通过检查服务器上的”客户明细表“来确认用户的身份的。Session相当于在服务器中建立了一份“客户明细表”

HTTP协议是无状态的,Session不能依据HTTP连接来判断是否为同一个用户。于是乎:服务器向用户浏览器发送了一个名为JESSIONID的Cookie,它的值是Session的id值。其实Session是依据Cookie来识别是否是同一个用户

所以,一般我们单系统实现登录会这样做:

登录:将用户信息保存在Session对象中

  • 如果在Session对象中能查到,说明已经登录
  • 如果在Session对象中查不到,说明没登录(或者已经退出了登录)
    注销(退出登录):从Session中删除用户的信息
    记住我(关闭掉浏览器后,重新打开浏览器还能保持登录状态):配合Cookie来用

一篇了解SSO单点登录-编程之家

3.多系统登录的问题与解决?

3.1.Session不共享问题

单系统登录功能主要是用Session保存用户信息来实现的,但我们清楚的是:多系统即可能有多个Tomcat,而Session是依赖当前系统的Tomcat,所以系统A的Session和系统B的Session是不共享的。

一篇了解SSO单点登录-编程之家

解决系统之间Session不共享问题有一下几种方案:

  • Tomcat集群Session全局复制(集群内每个tomcat的session完全同步)【会影响集群的性能呢,不建议
  • 根据请求的IP进行Hash映射到对应的机器上(这就相当于请求的IP一直会访问同一个服务器)【如果服务器宕机了,会丢失了一大部分Session的数据,不建议】
  • 把Session数据放在Redis中(使用Redis模拟Session)【建议】

我们可以将登录功能单独抽取出来,做成一个子系统。
一篇了解SSO单点登录-编程之家
总结:

  • SSO系统生成一个token,并将用户信息存到Redis中,并设置过期时间
  • 其他系统请求SSO系统进行登录,得到SSO返回的token,写到Cookie中
  • 每次请求时,Cookie都会带上,拦截器得到token,判断是否已经登录

到这里,其实我们会发现其实就两个变化:

  • 将登陆功能抽取为一个系统(SSO),其他系统请求SSO进行登录
  • 本来将用户信息存到Session,现在将用户信息存到Redis

XXL-SSO框架基础入门

1.什么是XXL-SSO

XXL-SSO 是一个分布式单点登录框架。只需要登录一次就可以访问所有相互信任的应用系统。 拥有"轻量级、分布式、跨域、Cookie+Token均支持、Web+APP均支持"等特性。现已开放源代码,开箱即用。

我们先登录XXL-SSO官网:https://www.xuxueli.com/xxl-sso/

一篇了解SSO单点登录-编程之家

2.特性

1、简洁:API直观简洁,可快速上手
2、轻量级:环境依赖小,部署与接入成本较低
3、单点登录:只需要登录一次就可以访问所有相互信任的应用系统
4、分布式:接入SSO认证中心的应用,支持分布式部署
5、HA:Server端与Client端,均支持集群部署,提高系统可用性
6、跨域:支持跨域应用接入SSO认证中心
7、Cookie+Token均支持:支持基于Cookie基于Token两种接入方式,并均提供Sample项目
8、Web+APP均支持:支持Web和APP接入
9、实时性:系统登陆、注销状态,全部Server与Client端实时共享
10、CS结构:基于CS结构,包括Server"认证中心"Client"受保护应用"
11、记住密码:未记住密码时,关闭浏览器则登录态失效;记住密码时,支持登录态自动延期,在自定义延期时间的基础上,原则上可以无限延期
12、路径排除:支持自定义多个排除路径,支持Ant表达式,用于排除SSO客户端不需要过滤的路径

3. 官方Demo分析

首先我们从Github 克隆XXL-SSO的源码到本地(https://github.com/xuxueli/xxl-sso.git):
一篇了解SSO单点登录-编程之家
下载完源码,我们可以看到目录结构如下:
一篇了解SSO单点登录-编程之家

3.1 SSO Server中央认证服务

打开xxl-sso-server目录,可以看到有如下结构:
一篇了解SSO单点登录-编程之家
他们分别表示:
一篇了解SSO单点登录-编程之家
打开xxl-sso-server的配置文件,可以看到需要配置Redis地址,在这里配置好Redis地址:
一篇了解SSO单点登录-编程之家
启动xxl-sso-server
一篇了解SSO单点登录-编程之家

日志文件的位置!

可以看到启动成功:
一篇了解SSO单点登录-编程之家

3.2 SSO Client应用(Cookie形式)

SSO 认证中心已经配置好并打开了,下面我们来看看SSO Client端。

打开samples下的xxl-sso-web-sample-springboot项目,并配置redis路径(与认证中心的一致):
一篇了解SSO单点登录-编程之家
在上图可以看到xxl.sso.server对应的值为:http://xxlssoserver.com:8080/xxl-sso-server,这里用到了域名,所以要在我们本地localhost文件里配置域名
一篇了解SSO单点登录-编程之家

启动成功:
一篇了解SSO单点登录-编程之家
浏览器输入:http://xxlssoclient1.com:8081/xxl-sso-web-sample-springboot
一篇了解SSO单点登录-编程之家
可以看到自动跳转到了SSO 认证服务中心的登录页面了,url地址变为如下,可以看到携带了一个redirect_url,指的就是登录成功后重定向的地址:

http://xxlssoserver.com:8080/xxl-sso-server/login?redirect_url=http://xxlssoclient1.com:8081/xxl-sso-web-sample-springboot/

为了更好的验证单点登录,我们复制xxl-sso-web-sample-springboot项目命名为xxl-sso-web-sample-springboot8083,并设置端口号为8083
一篇了解SSO单点登录-编程之家

并在hosts文件增加配置:

一篇了解SSO单点登录-编程之家启动复制的项目

好了,可以开始验证了。首先浏览器输入Client1服务地址:http://xxlssoclient1.com:8081/xxl-sso-web-sample-springboot,会自动跳转到授权中心:
一篇了解SSO单点登录-编程之家
点击登录,可看到登录成功,而且登录成功后的sessionid在地址栏也能看到。
一篇了解SSO单点登录-编程之家
接下来看看Client2是否需要再次登录,浏览器输入:http://xxlssoclient2.com:8083/xxl-sso-web-sample-springboot

可以看到Client2也登录成功了,而且sessionid与Client1的一样。

最后,我们看看浏览器的Cookie信息,观察发现他们的sessionid也是一致的:

clinent1
一篇了解SSO单点登录-编程之家
client2
一篇了解SSO单点登录-编程之家
打开Redis可视化窗口,可以看到Redis服务器有保存SessionId:
一篇了解SSO单点登录-编程之家

4.总结

本文主要讲解了单点登录的相关概念,已经使用xxl-sso框架来做演示。

集成SSO服务

引言

主要讲解了SSO单点登录的一些概念,以及使用国产的XXL-SSO单点登录例子来熟悉了单点登录的整个流程。

本文将把XXL-SSO框架集成到我们的项目中,本文先集成SSO 认证服务。

1. 集成xxl-sso-core

本来我是不打算把xxl-core集成到电商项目的,阅读文档里也没发现有最新的版本发布到仓库,只是更新了代码。远程maven仓库最新的版本为1.1.0,而代码最新版本为1.1.1了,如下图:
一篇了解SSO单点登录-编程之家
所以我打算把xxl-sso-core最新的代码直接复制到我们的项目使用。

首先在电商项目通用模块里添加xxl-core模块:
一篇了解SSO单点登录-编程之家
把xxl-core源码复制过去,包括maven依赖:
一篇了解SSO单点登录-编程之家
复制成功,没报错。

2. 集成xxl-server

在基础设施包里新增xxl-sso-server:
一篇了解SSO单点登录-编程之家
添加xxl-core的maven依赖:

<dependency><groupId>com.guoranxinxian</groupId><artifactId>guoranxinxian-shop-common-xxlsso-core</artifactId><version>1.0-SNAPSHOT</version>
</dependency><!-- freemarker --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId></dependency>

复制代码和resources里面的内容:
一篇了解SSO单点登录-编程之家
修改配置文件:

### web
server.port=8099
#server.servlet.context-path=/xxl-sso-server### resources
spring.mvc.servlet.load-on-startup=0
spring.mvc.static-path-pattern=/static/**
spring.resources.static-locations=classpath:/static/### freemarker
spring.freemarker.templateLoaderPath=classpath:/templates/
spring.freemarker.suffix=.ftl
spring.freemarker.charset=UTF-8
spring.freemarker.request-context-attribute=request
spring.freemarker.settings.number_format=0.############# xxl-sso
xxl.sso.redis.address=redis://127.0.0.1:6379
xxl.sso.redis.expire.minute=1440
eureka.client.service-url.defaultZone=http://127.0.0.1:8080/eurekaspring.application.name=guoranxinxian-shop-basics-xxlsso-server

启动类增加@EnableEurekaClient注解,启动注册中心,和SSO Server:

package com.xxl.sso.server;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@SpringBootApplication
@EnableEurekaClient
public class XxlSsoServerApplication {public static void main(String[] args) {SpringApplication.run(XxlSsoServerApplication.class, args);}
}

一篇了解SSO单点登录-编程之家
浏览器输入地址:http://localhost:8099/,会自动跳转到认证授权中心登录页面

一篇了解SSO单点登录-编程之家
点击Login,登录成功:
一篇了解SSO单点登录-编程之家

总结

本文主要讲解集成SSO认证服务。

改造SSO登录界面

引言

在上一篇主要讲解了如何集成SSO认证中心,集成成功后,登录界面和登录成功界面如下图所示:

登录
一篇了解SSO单点登录-编程之家
登录成功
一篇了解SSO单点登录-编程之家
但是这个登录和主界面并不是我们想要的,本文先来来讲解如何改造登录界面。

注意:我在hosts文件里添加了如下内容,之后的博客都用这些域名:
一篇了解SSO单点登录-编程之家

1. 效果图

下面先贴上效果图(主界面先暂时替代,涉及其它的知识点,下篇博客继续完善):

登录界面
一篇了解SSO单点登录-编程之家

登录成功界面
一篇了解SSO单点登录-编程之家

2. 登录界面代码(前端+后台)

先贴上前端代码(核心代码,注意里面携带了redirect_url,隐藏起来了),改造原来自带的登录页面

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta http-equiv="X-UA-Compatible" content="ie=edge" /><link rel="shortcut icon" href="/static/img/page-common/favicon.ico" type="image/x-icon" /><title>果然新鲜 - 登录</title><link rel="stylesheet" type="text/css" href="/static/css/page-common.css" /><link rel="stylesheet" type="text/css" href="/static/css/page-login.css" /><link rel="stylesheet" type="text/css" href="/static/css/page-login-header.css" />
</head><body>
<!-- 网页头部开始 -->
<script type="text/javascript" src="/static/js/page-login-header.js" charset="UTF-8"></script>
<!-- 网页头部结束 --><!-- 网页主体开始 -->
<div class="fresh-main-fluid" style="width: 100%;height:100%;background:#2663b6;"><div class="fresh-main fresh-center fresh-clearfix"><div class="fresh-body-1"><div class="fresh-img"> <img src="/static/img/page-login/bg.png" /> </div><div class="fresh-loginbox"><h2>账号登录<span style="color: red">${error!''}</span></h2><form action="doLogin" method="post"><div class="fresh-loginbox-text"> <p>手机号</p><div> <img src="/static/img/page-login/denglu.png" /><input type="text" name="mobile" value="${(loginVo.mobile)!''}" id="mobile" placeholder="请输入手机号码" /></div></div><div class="fresh-loginbox-text"> <p>密码</p><div> <img src="/static/img/page-login/mima.png" /><input type="password" name="password" id="password" value="${(loginVo.password)!''}" placeholder="请输入密码" /></div></div><div class="fresh-loginbox-text"> <p>验证码</p><div> <img src="/static/img/page-login/mima.png" /><input type="text" name="graphicCode" id="graphicCode" placeholder="请输入验证码" /><img src="/getVerify" style="width: 80px;" id="getverification" onclick="getVerify(this);"/></div></div><div class="fresh-login-forget"> <a href="forget.html">忘记密码</a> </div><div class="fresh-login-submit"><input type="hidden" name="redirect_url" value="${RequestParameters['redirect_url']!''}" /><input type="submit" value="登录" /></div><div class="fresh-login-thirdlogin"> <a href="#">——&nbsp;&nbsp;第三方登录&nbsp;&nbsp; ——</a> </div><div class="fresh-login-loginmode"><div> <a href="/qqAuth"> <img src="/static/img/page-login/qq.png" /> </a><a href="#"> <img src="/static/img/page-login/weixin.png" /> </a><a href="#"> <img src="/static/img/page-login/weibo.png" /> </a></div></div><div class="fresh-login-Register"> <a href="register.html">立即注册</a> </div></form></div></div></div>
</div>
<!-- 网站主体结束 --><!-- 网页底部开始 -->
<script type="text/javascript" src="/static/js/page-footer.js" charset="UTF-8"></script>
<!-- 网页底部结束 --><script type="text/javascript" src="/static/plugins/jquery/jquery-1.12.4.min.js"></script><script>//获取验证码function getVerify(obj) {obj.src = "getVerify?" + Math.random();}</script></body>
</html>

WebController层代码(现在业务系统查询用户是否存在,然后使用XXL-SSO框架登录):

package com.xxl.sso.server.controller;import com.guoranxinxian.api.BaseResponse;
import com.guoranxinxian.common.base.BaseWebController;
import com.guoranxinxian.common.util.RandomValidateCodeUtil;
import com.guoranxinxian.common.util.WebBeanUtils;
import com.guoranxinxian.constants.Constants;
import com.guoranxinxian.member.dto.input.UserLoginInDTO;
import com.guoranxinxian.member.dto.output.UserLoginInOutDTO;
import com.xxl.sso.core.conf.Conf;
import com.xxl.sso.core.login.SsoWebLoginHelper;
import com.xxl.sso.core.store.SsoLoginStore;
import com.xxl.sso.core.store.SsoSessionIdHelper;
import com.xxl.sso.core.user.XxlSsoUser;
import com.xxl.sso.server.controller.req.vo.LoginVo;
import com.xxl.sso.server.feign.MemberLoginServiceFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.UUID;@Controller
public class WebController extends BaseWebController {/*** 跳转到登陆页面页面*/private static final String MB_LOGIN_FTL = "login";@Autowiredprivate MemberLoginServiceFeign memberLoginServiceFeign;/*** 重定向到首页*/private static final String REDIRECT_INDEX = "redirect:/";@RequestMapping("/")public String index(Model model, HttpServletRequest request, HttpServletResponse response) {XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(request, response);if (xxlUser == null) {return "redirect:/login";} else {model.addAttribute("xxlUser", xxlUser);return "index";}}@RequestMapping(Conf.SSO_LOGIN)public String login(Model model, HttpServletRequest request, HttpServletResponse response) {// login checkXxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(request, response);if (xxlUser != null) {// success redirectString redirectUrl = request.getParameter(Conf.REDIRECT_URL);if (redirectUrl!=null && redirectUrl.trim().length()>0) {String sessionId = SsoWebLoginHelper.getSessionIdByCookie(request);String redirectUrlFinal = redirectUrl + "?" + Conf.SSO_SESSIONID + "=" + sessionId;;return "redirect:" + redirectUrlFinal;} else {return "redirect:/";}}model.addAttribute("errorMsg", request.getParameter("errorMsg"));model.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL));return "login";}/*** 接受请求参数** @return*/@PostMapping("/doLogin")public String postLogin(@ModelAttribute("loginVo") @Validated LoginVo loginVo,BindingResult bindingResult, Model model, RedirectAttributes redirectAttributes, HttpServletRequest request,HttpServletResponse response, HttpSession httpSession, String ifRemember) {if (bindingResult.hasErrors()) {// 如果参数有错误的话// 获取第一个错误!String errorMsg = bindingResult.getFieldError().getDefaultMessage();setErrorMsg(model, errorMsg);return MB_LOGIN_FTL;}// 1.图形验证码判断String graphicCode = loginVo.getGraphicCode();if (!RandomValidateCodeUtil.checkVerify(graphicCode, httpSession)) {setErrorMsg(model, "图形验证码不正确!");return MB_LOGIN_FTL;}// 2.将vo转换dto调用会员登陆接口UserLoginInDTO userLoginInpDTO = WebBeanUtils.voToDto(loginVo, UserLoginInDTO.class);userLoginInpDTO.setLoginType(Constants.MEMBER_LOGIN_TYPE_PC);String info = webBrowserInfo(request);userLoginInpDTO.setDeviceInfor(info);BaseResponse<UserLoginInOutDTO> login = memberLoginServiceFeign.ssoLogin(userLoginInpDTO);if (!isSuccess(login)) {setErrorMsg(model, login.getMsg());return MB_LOGIN_FTL;}UserLoginInOutDTO data = login.getData();XxlSsoUser xxlUser = new XxlSsoUser();xxlUser.setUserid(data.getToken());xxlUser.setUsername(data.getUserName());xxlUser.setVersion(UUID.randomUUID().toString().replaceAll("-", ""));xxlUser.setExpireMinute(SsoLoginStore.getRedisExpireMinute());xxlUser.setExpireFreshTime(System.currentTimeMillis());// 设置sessionidString sessionId = SsoSessionIdHelper.makeSessionId(xxlUser);// 认证服务登录boolean ifRem = (ifRemember != null && "on".equals(ifRemember)) ? true : false;SsoWebLoginHelper.login(response, sessionId, xxlUser, ifRem);// 4、return, redirect sessionIdString redirectUrl = request.getParameter(Conf.REDIRECT_URL);if (redirectUrl != null && redirectUrl.trim().length() > 0) {String redirectUrlFinal = redirectUrl + "?" + Conf.SSO_SESSIONID + "=" + sessionId;return "redirect:" + redirectUrlFinal;} else {return "redirect:/";}}@RequestMapping(Conf.SSO_LOGOUT)public String logout(HttpServletRequest request, HttpServletResponse response, RedirectAttributes redirectAttributes) {// logoutSsoWebLoginHelper.logout(request, response);redirectAttributes.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL));return "redirect:/login";}}

3.总结

本文主要讲解了XXL-SSO认证服务的登录界面改造。

SSO单点登录(Client端集成)

1.首页门户集成SSO Client

1.Maven添加xxl-sso-core模块:

<dependency><artifactId>guoranxinxian-shop-common-xxlsso-core</artifactId><groupId>com.guoranxinxian</groupId><version>1.0-SNAPSHOT</version>
</dependency>

2.配置applicatoin.yml,完整内容如下(注意要在hosts文件里配置好域名):
一篇了解SSO单点登录-编程之家

3.添加配置文件

spring.redis.hostName=127.0.0.1
spring.redis.port=6379xxl.sso.logout.path=/logout
xxl.sso.server=http://guoranxinxian.ssoserver.com:8099
xxl-sso.excluded.paths=
package com.guoranxinxian.config;import com.xxl.sso.core.conf.Conf;
import com.xxl.sso.core.filter.XxlSsoWebFilter;
import com.xxl.sso.core.util.JedisUtil;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class XxlSsoConfig implements DisposableBean {@Value("${xxl.sso.server}")private String xxlSsoServer;@Value("${xxl.sso.logout.path}")private String xxlSsoLogoutPath;@Value("${xxl-sso.excluded.paths}")private String xxlSsoExcludedPaths;@Value("${spring.redis.host}")private String redisHost;@Value("${spring.redis.port}")private String port;@Beanpublic FilterRegistrationBean xxlSsoFilterRegistration() {// xxl-sso, redis initJedisUtil.init(String.format("redis://%s:%s", redisHost, port));// xxl-sso, filter initFilterRegistrationBean registration = new FilterRegistrationBean();registration.setName("XxlSsoWebFilter");registration.setOrder(1);registration.addUrlPatterns("/*");registration.setFilter(new XxlSsoWebFilter());registration.addInitParameter(Conf.SSO_SERVER, xxlSsoServer);registration.addInitParameter(Conf.SSO_LOGOUT_PATH, xxlSsoLogoutPath);registration.addInitParameter(Conf.SSO_EXCLUDED_PATHS, xxlSsoExcludedPaths);return registration;}@Overridepublic void destroy() throws Exception {// xxl-sso, redis closeJedisUtil.close();}}

2. 聚合支付门户集成SSO Client

创建聚合支付门户模块guoranxinxian-shop-portal-pay-web,具体的代码不再详述,可以clone代码下来看,SSO Client方式与上面一样:

一篇了解SSO单点登录-编程之家

3. 测试

1.启动Eureka服务、SSO认证服务、会员服务门户服务聚合支付服务`。
一篇了解SSO单点登录-编程之家
2.浏览器访问门户服务(注意:hosts文件已经配置了域名)http://guoranxinxian.com:8080/,浏览器自动跳转到登录界面:
一篇了解SSO单点登录-编程之家
3.输入登录信息,执行登录操作,登录成功,可以看到登录成功后,地址栏的url也发生改变了http://guoranxinxian.com:8080/?xxl_sso_sessionid=27_c11ef89924a4465cbf395bfefcafc63d:

一篇了解SSO单点登录-编程之家

同时,看下cookie信息,也把session id自动写入了浏览器的cookie:
一篇了解SSO单点登录-编程之家

4.访问聚合支付门户http://guoranxinxian.pay.com:8079/,可以看到直接就跳转到了聚合支付的首页了,而且浏览器的Session id与门户服务的session id一样:
一篇了解SSO单点登录-编程之家

4.显示登录的用户信息

     @GetMapping("/")public String index(HttpServletRequest request, HttpServletResponse response, Model model){XxlSsoUser xxlUser = (XxlSsoUser) request.getAttribute(Conf.SSO_USER);if (xxlUser != null && StringUtils.isNotEmpty(xxlUser.getUserid())) {DataResults<Users> results = usersFeign.getByUserId(Long.valueOf(xxlUser.getUserid()));if(results.getData()!=null){String mobile = results.getData().getMobile();// 对手机号码实现脱敏String desensMobile = mobile.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");model.addAttribute("desensMobile", desensMobile);}}model.addAttribute("goods_fresh_fruits",itemServiceFeign.findGoodsByCategory1Id(1001).getData()); // 新鲜水果 1001model.addAttribute("goods_fresh_fish",itemServiceFeign.findGoodsByCategory1Id(1038).getData()); // 海鲜水产 1038List<Content> content_top= (List<Content>) redisTemplate.opsForValue().get("redis_content_top");if(content_top==null||content_top.size()==0){content_top=contentServiceFeign.findContentBycategoryId(1).getData();redisTemplate.opsForValue().set("redis_content_top",content_top,3, TimeUnit.MINUTES);  //3分刷新缓存}model.addAttribute("content_top",content_top); // 轮播图model.addAttribute("content_fresh_fruits",contentServiceFeign.findContentBycategoryId(3).getData()); // 新鲜水果主体return "index";}

一篇了解SSO单点登录-编程之家

<li th:if="${desensMobile==null}"><a href="login.html">您好,请登录</a></li><li th:if="${desensMobile!=null}"><a href="login.html" th:text="|您好,${desensMobile}|">您好,请登录</a></li>
<li>
<a href="register.html">免费注册</a></li><li><a href="home-order.html">我的订单</a></li><li th:if="${desensMobile!=null}"><a href="javascript:void(0);" onclick="logout();">退出</a></li><li><a href="home-person-footprint.html">我的足迹</a></li>

5.总结

本文主要讲解SSO Client集成与测试。

SSO单点登录(退出登录)

1. 效果演示

首先启动Eureka注册中心、SSO服务、会员服务、门户服务、聚合支付服务
一篇了解SSO单点登录-编程之家
登录门户,浏览器输入http://guoranxinxian.com:8080,登录成功。
一篇了解SSO单点登录-编程之家
访问聚合支付门户,浏览器输入:http://guoranxinxian.pay.com:8079/,可以看到没走登录直接就进入了。
一篇了解SSO单点登录-编程之家
好的,可以看退出效果的演示了,在门户首页点击退出
一篇了解SSO单点登录-编程之家
点击后,自动跳转到了登录页了:
一篇了解SSO单点登录-编程之家

刷新聚合支付页面,可以看到也自动跳转到了登录页面了:
一篇了解SSO单点登录-编程之家

从上面演示效果可以看出:一端退出,所有端都退出。

2.退出功能实现

前端代码(核心代码):

 <!--引入JQuery--><script type="text/javascript" src="plugins/jquery/jquery-1.12.4.min.js"></script><script type="text/javascript">function logout() {if(confirm("确定退出吗?")){$.ajax({type: "delete",//url: "exit",url: "ssoExit",contentType: "application/json",dataType: "json",success: function (result) {if(result.code==200){window.location.href = "/";}},error: function (result) {}});}}</script>

Controller层代码:

@RestController
public class LogoutController {@DeleteMapping("/ssoExit")@ResponseBodypublic DataResults logout(HttpServletRequest request, HttpServletResponse response, Model model) {// logoutXxlSsoUser xxlUser = (XxlSsoUser) request.getAttribute(Conf.SSO_USER);SsoWebLoginHelper.logout(request, response);return DataResults.success(ResultCode.SUCCESS);}
}

退出成功后,可以看到浏览器Cookie信息为空,Redis保存的内容也移除了,数据库更新为未登录。

Cookie
一篇了解SSO单点登录-编程之家

Redis
一篇了解SSO单点登录-编程之家

总结

本文主要讲解SSO单点退出的功能。

XXL-SSO登录逻辑

1.XXL-SSO登录逻辑

一篇了解SSO单点登录-编程之家
代码逻辑描述

  1. 访问pro.com,获取pro.com域的cookie(xxl_sso_sessionid,由userId_随机数码组成)为空,从请求参数获取cookie为空;
  2. 获取用户信息为空,重定向sso服务;
  3. sso服务,获取sso.com域cook’ie为空,获取用户信息为空,跳转登陆页
  4. 登录页输入用户名密码登陆,登陆成功,
1、创建用户对象,
2、创建sessionid(userId_user版本号),
3、response设置cookie,
4、radis设置key(xxl_sso_sessionid,#,usrid组成),用户对象, 失效时间
  1. 重定向pro.com?xxl_sso_sessionid=xxl_sso_sessionid;
  2. 获取pro.com域的cookie(xxl_sso_sessionid,由userId_随机数码组成)为空,从请求参数获取cookie,根据cookie查询raids获取用户对象;
  3. 如果当前时间超过刷新时间一半的时候,重新设置radis数据的有效时间;设置pro.com,cookie值
  4. 跳转请求页面;
  5. 访问pro1.com,获取pro1.com,cookie以及url参数cookie失败,获取对象失败,重定向sso.com服务
  6. sso服务,获取sso.com域cook’ie,根据cookie查询raids获取用户对象
  7. 重定向pro1.com?xxl_sso_sessionid=xxl_sso_sessionid;
  8. 后面逻辑与6,7,8相同
  9. 再次访问pro.com,pro1.com,只需要验证本域下的cookie;

2.XXL-SSO注销逻辑

一篇了解SSO单点登录-编程之家

代码逻辑

  1. 用户注销pro.com,销毁pro.com下的cookie;重定向sso.com,销毁sso.com下的cookie,删除radis下的用户信息,跳转登录页。
  2. 用户访问pro1.com,从pro1.com下获取cookie,从raids查询用户信息失败,无法返回用户信息登陆失败,重定向sso.com服务,获取sso.com域下cookie失败,从raids查询用户信息失败,跳转登陆页。

cookie可能会受到防跨站请求伪造(CSRF)攻击,token可以解决这个问题

举个CSRF攻击的例子,在网页中有这样的一个链接
(http://bank.com?withdraw=1000&to=tom),假设你已经通过银行的验证并且cookie中存在验证信息,同时银行网站没有CSRF保护。一旦用户点了这个图片,就很有可能从银行向tom这个人转1000块钱。

但是如果银行网站使用了token作为验证手段,攻击者将无法通过上面的链接转走你的钱。(因为攻击者无法获取正确的token)

CSRF攻击

1.CSRF是什么

CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。

2.CSRF可以做什么

你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账…造成的问题包括:个人隐私泄露以及财产安全。

3.CSRF漏洞现状

CSRF这种攻击方式在2000年已经被国外的安全人员提出,但在国内,直到06年才开始被关注,08年,国内外的多个大型社区和交互网站分别爆出CSRF漏洞,如:NYTimes.com(纽约时报)、Metafilter(一个大型的BLOG网站),YouTube和百度HI…而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为“沉睡的巨人”。

4.CSRF的原理

下图简单阐述了CSRF攻击的思想:
一篇了解SSO单点登录-编程之家
从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤:

  1. 登录受信任网站A,并在本地生成Cookie。
  2. 在不登出A的情况下,访问危险网站B。

看到这里,你也许会说:“如果我不满足以上两个条件中的一个,我就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生:

  1. 你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站。

  2. 你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。(事实上,关闭浏览器不能结束一个会话,但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了…)

5.CSRF示例

5.1.示例1:

银行网站A,它以GET请求来完成银行转账的操作,如:http://www.mybank.com/Transfer.php?toBankId=11&money=1000

危险网站B,它里面有一段HTML的代码如下:

<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000>

首先,你登录了银行网站A,然后访问危险网站B,噢,这时你会发现你的银行账户少了1000块…

为什么会这样呢?

原因是银行网站A违反了HTTP规范,使用GET请求更新资源。在访问危险网站B的之前你已经登录了银行网站A,而B中的<img/>以GET的方式请求第三方资源(这里的第三方就是指银行网站了,原本这是一个合法的请求,但这里被不法分子利用了),所以你的浏览器会带上你的银行网站A的Cookie发出Get请求,去获取资源“http://www.mybank.com/Transfer.php?toBankId=11&money=1000”,结果银行网站服务器收到请求后,认为这是一个更新资源操作(转账操作),所以就立刻进行转账操作…

5.2.示例2:

为了杜绝上面的问题,银行决定改用POST请求完成转账操作。

银行网站A的WEB表单如下:

<form action="Transfer.php" method="POST"><p>ToBankId: <input type="text" name="toBankId" /></p><p>Money: <input type="text" name="money" /></p><p><input type="submit" value="Transfer" /></p>
</form>

后台处理页面Transfer.php如下:

<?php    
session_start();    
if (isset($_REQUEST['toBankId'] && isset($_REQUEST['money'])) {       buy_stocks($_REQUEST['toBankId'], $_REQUEST['money']);    } 
?>

危险网站B,仍然只是包含那句HTML代码:

<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000>
和示例1中的操作一样,你首先登录了银行网站A,然后访问危险网站B,结果.....和示例1一样,你再次没了1000块~T_T,这次事故的原因是:银行后台使用了$_REQUEST去获取请求的数据,而$_REQUEST既可以获取GET请求的数据,也可以获取POST请求的数据,这就造成了在后台处理程序无法区分这到底是GET请求的数据还是POST请求的数据。在PHP中,可以使用$_GET和$_POST分别获取GET请求和POST请求的数据。在JAVA中,用于获取请求数据request一样存在不能区分GET请求数据和POST数据的问题。

5.3.示例3:

经过前面2个惨痛的教训,银行决定把获取请求数据的方法也改了,改用$_POST,只获取POST请求的数据,后台处理页面Transfer.php代码如下:

<?phpsession_start();if (isset($_POST['toBankId'] && isset($_POST['money'])){buy_stocks($_POST['toBankId'], $_POST['money']);}?>

然而,危险网站B与时俱进,它改了一下代码:

<html><head>
<script type="text/javascript">function steal(){iframe = document.frames["steal"];iframe.document.Submit("transfer");}</script></head><body onload="steal()"><iframe name="steal" display="none"><form method="POST" name="transfer" action="http://www.myBank.com/Transfer.php"><input type="hidden" name="toBankId" value="11"><input type="hidden" name="money" value="1000"></form></iframe></body>
</html>

如果用户仍是继续上面的操作,很不幸,结果将会是再次不见1000块…因为这里危险网站B暗地里发送了POST请求到银行!

5.4.总结

上面3个例子,CSRF主要的攻击模式基本上是以上的3种,其中以第1,2种最为严重,因为触发条件很简单,一个<img>就可以了,而第3种比较麻烦,需要使用JavaScript,所以使用的机会会比前面的少很多,但无论是哪种情况,只要触发了CSRF攻击,后果都有可能很严重。

CSRF攻击的本质原因

CSRF攻击是源于Web的隐式身份验证机制!Web的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的。CSRF攻击的一般是由服务端解决。

6.CSRF的防御

6.1. 尽量使用POST,限制GET

GET接口太容易被拿来做CSRF攻击,看第一个示例就知道,只要构造一个img标签,而img标签又是不能过滤的数据。接口最好限制为POST使用,GET则无效,降低攻击风险。

当然POST并不是万无一失,攻击者只要构造一个form就可以,但需要在第三方页面做,这样就增加暴露的可能性。

6.2.浏览器Cookie策略

IE6、7、8、Safari会默认拦截第三方本地Cookie(Third-party Cookie)的发送。但是Firefox2、3、Opera、Chrome、Android等不会拦截,所以通过浏览器Cookie策略来防御CSRF攻击不靠谱,只能说是降低了风险。

PS:Cookie分为两种,Session Cookie(在浏览器关闭后,就会失效,保存到内存里),Third-party Cookie(即只有到了Exprie时间后才会失效的Cookie,这种Cookie会保存到本地)。

6.3.加验证码

验证码,强制用户必须与应用进行交互,才能完成最终请求。在通常情况下,验证码能很好遏制CSRF攻击。但是出于用户体验考虑,网站不能给所有的操作都加上验证码。因此验证码只能作为一种辅助手段,不能作为主要解决方案。

6.4.Referer Check

Referer Check在Web最常见的应用就是“防止图片盗链”。同理,Referer Check也可以被用于检查请求是否来自合法的“源”(Referer值是否是指定页面,或者网站的域),如果都不是,那么就极可能是CSRF攻击。

但是因为服务器并不是什么时候都能取到Referer,所以也无法作为CSRF防御的主要手段。但是用Referer Check来监控CSRF攻击的发生,倒是一种可行的方法。

6.5.Anti CSRF Token

现在业界对CSRF的防御,一致的做法是使用一个Token。
例子:

  1. 用户访问某个表单页面。

  2. 服务端生成一个Token,放在用户的Session中,或者浏览器的Cookie中。

  3. 在页面表单附带上Token参数。

  4. 用户提交请求后, 服务端验证表单中的Token是否与用户Session(或Cookies)中的Token一致,一致为合法请求,不是则非法请求。

这个Token的值必须是随机的,不可预测的。由于Token的存在,攻击者无法再构造一个带有合法Token的请求实施CSRF攻击。另外使用Token时应注意Token的保密性,尽量把敏感操作由GET改为POST,以form或AJAX形式提交,避免Token泄露。

6.6.总结

CSRF攻击是攻击者利用用户的身份操作用户帐户的一种攻击方式,通常使用Anti CSRF Token来防御CSRF攻击,同时要注意Token的保密性和随机性。

跨域(CORS)

1.引言

我们在开发过程中经常会遇到前后端分离而导致的跨域问题,导致无法获取返回结果。跨域就像分离前端和后端的一道鸿沟,君在这边,她在那边,两两不能往来.

2.什么是跨域(CORS)

跨域(CORS)是指不同域名之间相互访问。跨域,指的是浏览器不能执行其他网站的脚本,它是由浏览器的同源策略所造成的,是浏览器对于JavaScript所定义的安全限制策略。

3.什么情况会跨域(CORS)

  • 同一协议, 如http或https
  • 同一IP地址, 如127.0.0.1
  • 同一端口, 如8080

以上三个条件中有一个条件不同就会产生跨域问题。

一篇了解SSO单点登录-编程之家

4.跨域流程

一篇了解SSO单点登录-编程之家

参考地址:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS

5.解决跨域

配置当次请求允许跨域
一篇了解SSO单点登录-编程之家

解决方法:在网关中定义“CorsConfig”类,该类用来做过滤,允许所有的请求跨域。

package com.microservice.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;//配置过滤器,解决跨域问题
@Configuration
public class CorsConfig {private CorsConfiguration buildConfig() {CorsConfiguration corsConfiguration = new CorsConfiguration();corsConfiguration.addAllowedOrigin("*"); //允许任何域名使用corsConfiguration.addAllowedHeader("*"); //允许任何头corsConfiguration.addAllowedMethod("*"); //允许任何方法(post、get等)return corsConfiguration;}@Beanpublic CorsFilter corsFilter() {UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", buildConfig());return new CorsFilter(source);}
}