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)。
在初学/以前的时候,一般我们就单系统,所有的功能都在同一个系统上。
后来,我们为了合理利用资源和降低耦合性,于是把单系统拆分成多个子系统。
比如阿里系的淘宝和天猫,很明显地我们可以知道这是两个系统,但是你在使用的时候,登录了天猫,淘宝也会自动登录。
简单来说,单点登录就是在多个系统中,用户只需一次登录,各个系统即可感知该用户已经登录,只要在一个业务中退出,所有系统都退出。
2.回顾普通系统登录
众所周知,HTTP是无状态的协议,这意味着服务器无法确认用户的信息。于是乎,W3C就提出了:给每一个用户都发一个通行证,无论谁访问的时候都需要携带通行证,这样服务器就可以从通行证上确认用户的信息。通行证就是Cookie。
如果说Cookie是检查用户身上的”通行证“来确认用户的身份,那么Session就是通过检查服务器上的”客户明细表“来确认用户的身份的。Session相当于在服务器中建立了一份“客户明细表”。
HTTP协议是无状态的,Session不能依据HTTP连接来判断是否为同一个用户。于是乎:服务器向用户浏览器发送了一个名为JESSIONID的Cookie,它的值是Session的id值。其实Session是依据Cookie来识别是否是同一个用户。
所以,一般我们单系统实现登录会这样做:
登录:将用户信息保存在Session对象中
- 如果在Session对象中能查到,说明已经登录
- 如果在Session对象中查不到,说明没登录(或者已经退出了登录)
注销(退出登录):从Session中删除用户的信息
记住我(关闭掉浏览器后,重新打开浏览器还能保持登录状态):配合Cookie来用
3.多系统登录的问题与解决?
3.1.Session不共享问题
单系统登录功能主要是用Session保存用户信息来实现的,但我们清楚的是:多系统即可能有多个Tomcat,而Session是依赖当前系统的Tomcat,所以系统A的Session和系统B的Session是不共享的。
解决系统之间Session不共享问题有一下几种方案:
- Tomcat集群Session全局复制(集群内每个tomcat的session完全同步)【会影响集群的性能呢,不建议】
- 根据请求的IP进行Hash映射到对应的机器上(这就相当于请求的IP一直会访问同一个服务器)【如果服务器宕机了,会丢失了一大部分Session的数据,不建议】
- 把Session数据放在Redis中(使用Redis模拟Session)【建议】
我们可以将登录功能单独抽取出来,做成一个子系统。
总结:
- 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/
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
):
下载完源码,我们可以看到目录结构如下:
3.1 SSO Server中央认证服务
打开xxl-sso-server
目录,可以看到有如下结构:
他们分别表示:
打开xxl-sso-server的配置文件,可以看到需要配置Redis地址,在这里配置好Redis地址:
启动xxl-sso-server
日志文件的位置!
可以看到启动成功:
3.2 SSO Client应用(Cookie形式)
SSO
认证中心已经配置好并打开了,下面我们来看看SSO Client端。
打开samples下的xxl-sso-web-sample-springboot
项目,并配置redis
路径(与认证中心的一致):
在上图可以看到xxl.sso.server
对应的值为:http://xxlssoserver.com:8080/xxl-sso-server
,这里用到了域名,所以要在我们本地localhost
文件里配置域名
启动成功:
浏览器输入:http://xxlssoclient1.com:8081/xxl-sso-web-sample-springboot
可以看到自动跳转到了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
:
并在hosts文件增加配置:
启动复制的项目
好了,可以开始验证了。首先浏览器输入Client1服务地址:http://xxlssoclient1.com:8081/xxl-sso-web-sample-springboot
,会自动跳转到授权中心:
点击登录,可看到登录成功,而且登录成功后的sessionid在地址栏也能看到。
接下来看看Client2是否需要再次登录,浏览器输入:http://xxlssoclient2.com:8083/xxl-sso-web-sample-springboot
可以看到Client2也登录成功了,而且sessionid与Client1的一样。
最后,我们看看浏览器的Cookie信息,观察发现他们的sessionid也是一致的:
clinent1
client2
打开Redis可视化窗口,可以看到Redis服务器有保存SessionId:
4.总结
本文主要讲解了单点登录的相关概念,已经使用xxl-sso框架来做演示。
集成SSO服务
引言
主要讲解了SSO单点登录的一些概念,以及使用国产的XXL-SSO单点登录例子来熟悉了单点登录的整个流程。
本文将把XXL-SSO框架集成到我们的项目中,本文先集成SSO 认证服务。
1. 集成xxl-sso-core
本来我是不打算把xxl-core
集成到电商项目的,阅读文档里也没发现有最新的版本发布到仓库,只是更新了代码。远程maven仓库最新的版本为1.1.0
,而代码最新版本为1.1.1
了,如下图:
所以我打算把xxl-sso-core最新的代码直接复制到我们的项目使用。
首先在电商项目通用模块里添加xxl-core模块:
把xxl-core源码复制过去,包括maven依赖:
复制成功,没报错。
2. 集成xxl-server
在基础设施包里新增xxl-sso-server:
添加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里面的内容:
修改配置文件:
### 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);}
}
浏览器输入地址:http://localhost:8099/
,会自动跳转到认证授权中心登录页面
点击Login,登录成功:
总结
本文主要讲解集成SSO认证服务。
改造SSO登录界面
引言
在上一篇主要讲解了如何集成SSO认证中心,集成成功后,登录界面和登录成功界面如下图所示:
登录
登录成功
但是这个登录和主界面并不是我们想要的,本文先来来讲解如何改造登录界面。
注意:我在hosts文件里添加了如下内容,之后的博客都用这些域名:
1. 效果图
下面先贴上效果图(主界面先暂时替代,涉及其它的知识点,下篇博客继续完善):
登录界面
登录成功界面
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="#">—— 第三方登录 ——</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文件里配置好域名):
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
方式与上面一样:
3. 测试
1.启动Eureka
服务、SSO认证服务、
会员服务、
门户服务、
聚合支付服务`。
2.浏览器访问门户服务(注意:hosts文件已经配置了域名)http://guoranxinxian.com:8080/,浏览器自动跳转到登录界面:
3.输入登录信息,执行登录操作,登录成功,可以看到登录成功后,地址栏的url也发生改变了http://guoranxinxian.com:8080/?xxl_sso_sessionid=27_c11ef89924a4465cbf395bfefcafc63d:
同时,看下cookie信息,也把session id自动写入了浏览器的cookie:
4.访问聚合支付门户http://guoranxinxian.pay.com:8079/,可以看到直接就跳转到了聚合支付的首页了,而且浏览器的Session id
与门户服务的session id
一样:
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";}
<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服务、会员服务、门户服务、聚合支付服务
登录门户,浏览器输入http://guoranxinxian.com:8080,登录成功。
访问聚合支付门户,浏览器输入:http://guoranxinxian.pay.com:8079/,可以看到没走登录直接就进入了。
好的,可以看退出效果的演示了,在门户首页点击退出
点击后,自动跳转到了登录页了:
刷新聚合支付页面,可以看到也自动跳转到了登录页面了:
从上面演示效果可以看出:一端退出,所有端都退出。
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
Redis
总结
本文主要讲解SSO单点退出的功能。
XXL-SSO登录逻辑
1.XXL-SSO登录逻辑
代码逻辑描述
- 访问pro.com,获取pro.com域的cookie(xxl_sso_sessionid,由userId_随机数码组成)为空,从请求参数获取cookie为空;
- 获取用户信息为空,重定向sso服务;
- sso服务,获取sso.com域cook’ie为空,获取用户信息为空,跳转登陆页
- 登录页输入用户名密码登陆,登陆成功,
1、创建用户对象,
2、创建sessionid(userId_user版本号),
3、response设置cookie,
4、radis设置key(xxl_sso_sessionid,#,usrid组成),用户对象, 失效时间
- 重定向pro.com?xxl_sso_sessionid=xxl_sso_sessionid;
- 获取pro.com域的cookie(xxl_sso_sessionid,由userId_随机数码组成)为空,从请求参数获取cookie,根据cookie查询raids获取用户对象;
- 如果当前时间超过刷新时间一半的时候,重新设置radis数据的有效时间;设置pro.com,cookie值
- 跳转请求页面;
- 访问pro1.com,获取pro1.com,cookie以及url参数cookie失败,获取对象失败,重定向sso.com服务
- sso服务,获取sso.com域cook’ie,根据cookie查询raids获取用户对象
- 重定向pro1.com?xxl_sso_sessionid=xxl_sso_sessionid;
- 后面逻辑与6,7,8相同
- 再次访问pro.com,pro1.com,只需要验证本域下的cookie;
2.XXL-SSO注销逻辑
代码逻辑
- 用户注销pro.com,销毁pro.com下的cookie;重定向sso.com,销毁sso.com下的cookie,删除radis下的用户信息,跳转登录页。
- 用户访问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攻击的思想:
从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤:
- 登录受信任网站A,并在本地生成Cookie。
- 在不登出A的情况下,访问危险网站B。
看到这里,你也许会说:“如果我不满足以上两个条件中的一个,我就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生:
-
你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站。
-
你不能保证你关闭浏览器了后,你本地的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。
例子:
-
用户访问某个表单页面。
-
服务端生成一个Token,放在用户的Session中,或者浏览器的Cookie中。
-
在页面表单附带上Token参数。
-
用户提交请求后, 服务端验证表单中的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
以上三个条件中有一个条件不同就会产生跨域问题。
4.跨域流程
参考地址:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
5.解决跨域
配置当次请求允许跨域
解决方法:在网关中定义“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);}
}