Hello Stranger

  • Home

  • Archives

二:SSO 单点登入

Posted on 2019-06-30

二:SSO 单点登入

欲速则不达

Why 单点登入?

一次登入,到处畅行!

前面我们讲了水平扩容多机部署时Session一致性的解决方案!在我们公司日常开发中一般不止一个应用如:商品 / 订单 / 客户 这些模块都要有登入!我们是不是可以讲登入做成模块形式来统一管理呢??

SSO大概流程模块划分图

通过上图可知我们分为3大块

  • SSO-Client

  • SSO-Service

  • User-Service

SSO-Client

主要做登入拦截,这个是做成Jar包的方式集成进目标应用。

  • 拦截方式

    • javax.servlet.Filter servlet 过滤器方式

    • org.springframework.web.servlet.HandlerInterceptor Spring拦截器方式

SSO-Service

主要用于登入,身份认证!一些常用信息存储等。

User-Service

需要登入的话必然我们就需要用户信息相关操作的!其实是作为一个单独作为一个依赖服务的。


Why 同域和跨域?

这是浏览器为了安全的一种保护行为!**跨域资源调度限制**。你可以试着考虑下,你先登入www.taobao.com,然后再登入 www.baidu.com。 难道你要把淘宝域名下的cookie发送给百度的服务器吗? 

但是我们正常的开发中可能一个公司内部有多个系统多个域名,我们需要打通的各个系统的登入。这时候我们就面临新的挑战了! **跨域**

同源策略:

协议 | 域名 | 端口 都要相同,不然就会产生跨域问题

URL 说明 是否跨域
http://www.taobao.com/item http://www.taobao.com/user 相同协议,域名,端口 否
http://www.taobao.com/item/app.js http://www.taobao.com/user/defult.js 相同协议,域名,端口 不同资源 否
http://www.zk.com:8080 http://www.zk.com:8083 相同协议,域名 不同端口 是
http://www.zk.com:8080 https://www.zk.com:8080 相同域名,端口 不同协议 是
http://127.0.0.1:8080 http://www.zk.com:8080 如果该域名指向的就是该IP 也是跨域了 是
http://www.zk.com:8080 http://user.zk.com:8080 主域和子域 是
http://www.baidu.com http://www.taobao.com 不同域名 是

跨域解决方案:

  • JSONP: 这个方案只支持GET请求,遇到携带信息量较大的话会带来一些性能问题。暂时我没有详细研究,大家感兴趣可以自行研究。优点是支持比较老得浏览器!

  • CORS: 全称 跨域资源共享 注意:这个需要浏览器和服务器同时支持。除了一些微软系列比较老得浏览器,现在市面谷歌,火狐,猎豹啊的都支持的。莫名打了广告哈哈😅。

  • … 其他方案网上给出来的总共8种吧,应用我觉得主要就是以上两种。

    好滴,接下来我们来讲讲 CORS !

两种请求:

浏览器会将CORS请求分为:**简单请求**(simple request) 和  **非简单请求**(not-so-simple request)
  • 简单请求: 需要满足两大条件

    • 请求方法:三种之一就好!

      • HEAD

      • GET

      • POST

    • 请求字段:不超出以下几种字段。

      • Accpet : 代表发送端希望接收的数据类型(媒体类型资源)

      • Accpet-Language:代表客户端希望接收的语言类型

      • Content-Language:代表发送端的语言类型

      • Last-Event-ID:

      • Content-Type: 代表发送端的数据类型(媒体类型资源),只限三个值!

        • application/x-www-form-urlencoded

        • multipart/form-data

        • text/plain

  • 非简单请求: 会在正式请求之前增加一个预检查请求(options 类型)注意需要配置文件web.xml开启这个请求类型!收到请求后才会执行真正的操作。

上硬菜 代码: springBoot项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* web配置
*
* @author zhoukun
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
//授权的访问源
.allowedOrigins("*")
//允许的请求动词
.allowedMethods("POST, GET, OPTIONS, DELETE")
//预检授权的有效期,单位:秒
.maxAge(3600)
//额外允许访问的响应头
.allowedHeaders("x-requested-with")
//是否允许携带
.allowCredentials(true);
}
}

估计你们肯定会想就这么简单吗? 我想说是的。还是扩展性更强的写法。

配置详解:

  • Access-Control-Allow-Origin 必填: 必填 授权访问资源 * 表示所有资源可以访问。你也可以填写一个具体的域名

  • Access-Control-Allow-Methods 必填: 允许跨域的方法 如POST, GET …

  • Access-Control-Max-Age: 预授权(非简单请求的预检查)的有效期,有效期内不用在发送一条检查请求。

  • Access-Control-Expose-Headers: 额外允许访问的响应头 CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。

  • Access-Control-Allow-Credentials: 是否允许携带cookie ,默认不携带,我们在Cookie存储Token肯定是需要的!

我肯定写的比较分散 有点像矮大紧想到哪里,就讲到哪里呢! 大家不要在意这些细节,我收回来。


代码实现SSO!

sso目录结构

SSO-Client: 代码

  1. 新建Spring-Boot 工程!名字sso, 创建子模块sso-client

  2. pom.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependencies>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- http客户端 -->
    <dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.9</version>
    </dependency>
    </dependencies>
  3. 新建拦截器 我这里选用的Interceptor方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* 登入拦截
*
* @author zhoukun
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
private static final String OK = "ok";

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)) {
token = request.getParameter("token");
}
if (StringUtils.isEmpty(token)) {
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("token")) {
token = cookie.getValue();
}
}
}
}
String originUrl = request.getRequestURL().toString();
if (StringUtils.isEmpty(token)) {
response.sendRedirect("http://127.0.0.1:2080?originUrl=" + originUrl);
return false;
}
// if (!StringUtils.isEmpty(token)) {
// Cookie cookie = new Cookie("token", token);
// cookie.setDomain("zk.com");
// response.addCookie(cookie);
// }
Map<String, String> map = new HashMap<>();
map.put("token", token);
String result = httpClientGet(map);
if (OK.equalsIgnoreCase(result)) {
return true;
} else {
response.sendRedirect("http://127.0.0.1:2080?originUrl=" + originUrl);
return false;
}
}


public static String httpClientGet(Map<String, String> map) throws URISyntaxException, IOException {
CloseableHttpClient httpClient = HttpClientBuilder.create().build();

List<NameValuePair> params = new ArrayList<>();
map.forEach((k, v) ->
params.add(new BasicNameValuePair(k, v))
);

URI uri = new URIBuilder().setScheme("http").setHost("localhost")
.setPort(2080).setPath("/auth/token")
.setParameters(params).build();

HttpGet get = new HttpGet(uri);
CloseableHttpResponse response = httpClient.execute(get);

HttpEntity responseEntity = response.getEntity();
System.out.println("响应状态为:" + response.getStatusLine());
String result = EntityUtils.toString(responseEntity);

httpClient.close();
response.close();
return result;

}

}

SSO-Service 代码:

  1. sso 下面新建子工程 sso-servie

  2. 编写jwt 工具类上篇文章已经介绍过了这里就不多讲

  3. pom.xml

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

4.编写 登入类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* 登入页面
*
* @author zhoukun
*/
@Controller
public class LoginController {

@Autowired
private UserService userService;

@GetMapping("/")
public String form(HttpServletRequest request, HttpServletResponse response) {
String originUrl = request.getParameter("originUrl");
request.getSession().setAttribute("originUrl", originUrl);
return "login";
}

@GetMapping("/login")
@ResponseBody
public String login(HttpServletRequest request, HttpServletResponse response,
@RequestParam("username") String username, @RequestParam("password") String password) throws IOException {
if (StringUtils.isEmpty(username)) {
return "username not null";
}
if (StringUtils.isEmpty(password)) {
return "password not null";
}

UserDo userDo = userService.verifyUser(username, password);
if (userDo == null) {
return "密码或者账号错误!";
}
Map<String, String> claims = new HashMap<>();
claims.put("id", String.valueOf(userDo.getId()));
claims.put("username", userDo.getUserName());
LocalDateTime exprieLocalDateTime = LocalDateTime.now().plusMinutes(10L);
ZonedDateTime zonedDateTime = exprieLocalDateTime.atZone(ZoneId.systemDefault());
String token = JwtUtil.generateToken(claims, Date.from(zonedDateTime.toInstant()));
Cookie cookie = new Cookie("token", token);
response.addCookie(cookie);
response.setHeader("token", token);
String originUrl = (String) request.getSession().getAttribute("originUrl");
response.sendRedirect(originUrl + "?token=" + token);
return token;
}
}
  1. 认证类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 认证Token
*
* @author zhoukun
*/
@RestController
@RequestMapping("auth")
public class AuthTokenController {
public static final String OK = "OK";
public static final String FAIL = "FAIL";


@GetMapping("token")
public String authToken(HttpServletRequest request, HttpServletResponse response) {
String token = request.getParameter("token");
try {
Map<String, String> map = JwtUtil.verifyToken(token);
return OK;
} catch (Exception e) {
e.printStackTrace();
return FAIL;
}
}


}

小叙:

这个是一个比较简单的实现很多地方都不不是特别验证,比如跨域携带, jwtToken实时刷新等等问题,都没有解决 这个这是打个样,我们什么都是从简单到深入,踩很多坑和雷的。接下的文章会更加深入探讨这些问题的。

一:SSO 基础篇

Posted on 2019-06-23

SSO知识概要 初始篇

前言

受惠于人,授惠于人。

我是在16年4月正式踏进了互联网开发这个行业,作为一个统招大专生和手拿自考本科的实习生来说,在上海入职第一家公司实属幸运! 记得16年的末上海房价疯涨,一天一个价。 金融出现了牛市,闭着眼睛投都能赚钱。表面看起来都是一切美好的开始,18年开始特朗普贸易战,互联网行业开始贩卖焦虑,大数据,区块链,算法,AI,996。互联网科技在这两年的发展速度前所未有。 

快,快点,再快点! 

知识更新换代快!怎么办? 只能每天抽出时间学习。其实我是一个有拖延症的人,属于投入了可以坚持很久一旦松懈就会玩手机到睡觉。 工作还是平常都会在网上找大神的文章,萌生了自己写文章的想法!这个想法从18年开始到现在才开始付出行动,说来惭愧!鉴于网上很多文章将的不是特别详细! 我今天就立个flag。

产出的文章是**通俗易懂**

HTTP 和 Session 、Cookic

HTTP介绍

详细我就不讲了大家可以点进链接查看!我主要讲几点主要的。

  • 基于TCP应用层协议 :是可靠的 3次握手4次挥手

  • 无状态 :最初的时候设计http只是来返回静态页面 所以并不需要维护连接状态

举个例子: 小明 去 商店 买东西

小明 问 服务员 有泡泡糖吗? 服务员 说 有。

小明 问 服务员 多少钱?  服务员懵逼了???

服务员 不记得发生了什么!  

当服务员需要知道每次访问的是谁?上次干了什么?session、cookie这时站了出来!

Session 、Cookie

session/cookic

  • Session:存在服务器端 维护了客户端每次请求的一些信息

  • Cookie:存储在客户端(浏览器)存储量比较小 而且不同的浏览器有不同的限制

    • javax.servlet.http.Cookie

      • name:名称

      • value:值

      • maxAge:单位秒 缓存失效时间 -1000附属直接失效

      • domain:cookic属于域名 (zk.com) 跨域支持

      • path:cookic属于的路径 /就是全路径 默认当前路径(localhost:8080/get_cookie)

      • secure: true只接受 https

      • httpOnly:true cookie只在http中传输

以上 domain maxAge 是后面操作必备的

现在我们在来演示一遍

小明 去 商店 买东西

小明 问 服务员 有泡泡糖吗? 服务员分配一个001编号 并在小本本上记录001的行为然后 服务员 说 有 并且把编号交给小明。

小明 问 服务员 多少钱 并且把编号告诉 服务员! 服务员根据编码知道了小明之前问的泡泡糖 服务员告诉小明泡泡糖价格!

我们把 小明当成浏览器 服务员当前服务器 小本本为Seesion 编号为cookie的信息 我们就不难理解这样设计的初衷了!

Session一致性

首先我们来看一张图

war部署

部署和扩容

左边在我们业务量较小的时候基本的部署模式

右边在业务量增长到单机无法支撑的时候我们就需要扩容(多机部署)

  • 垂直扩容: 升级内存 带宽 主要做硬件升级

  • 水平扩容: 常见的就是增加机器 当一台机器QPS500 两台机器就是1000

  • 增加机器 就会带来数据一致性问题!如数据、缓存、session

互联网开发公司常用的就是水平扩容基本所有服务就是多机部署的!在老东家管易云双十一的时候服务都是20台以上,平常数据迁移也是11台机器部署服务的。老东家大搜车汽车零售软件其实也是双机部署的。但是机器是昂贵的我们还要充分分析系统的瓶颈,提升单机性能,再考虑加机器吧。

多机部署的登入问题

假设现在有个小网站www.ts.com 小明打开电脑打开Chrome浏览器登入

session一致性问题

1.0 小明登入 www.ts.com

1.1 通过负载均衡 路由到0.3机器

1.2 验证密码账号成功 设置isLogin=true

1.3 携带jsession并返回

2.0 再次访问 进入的是0.4这台机器 这个台机器的seesion是没有islogin这个状态的 需要重新登入

要解决seesion一致性问题引出了以下几种方案:

  • 粘性session

  • session复制

  • session统一存储

  • token方式

一:粘性session

何为粘性session? 这个切入点在负载均衡这里。 负载均衡分为两种:

  • 硬件负载均衡: 代表有F5

  • 软件负载均衡:代表有nginx、lvs

    这里只说软件负载,我们正常应用会挂个nginx,通过它我们可以设置转发规则。 客户端IP取hash值%机器数量 = 1 那么下一次请求 IP为112.0.0.1结果还是1, 每次路由都是相同机器。

缺点:如果192.0.0.3 和 192.0.0.4两台机器其中192.0.0.3宕掉,产生单点故障。

二:session复制

session复制

这是Tomcat提供的解决方案,具体是两台tomcat服务器之间进行session数据同步!

缺点: 多台服务器之间数据同步造成性能问题!

三:session统一存储

session统一存储

为了减轻服务器之间同步压力,既然是每台服务器都存储session为什么不统一存储起来,统一的访问呢?

这就是第三种方案session统一存储,存储的介质很多:

  • Mysql

  • Redis

比较多的公司都是通过Redis来实现的,常用的方式我们可以借助spring-session轻松实现统一储存。

spring-boot 工程打个样:官方文档

  1. POM引入坐标:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>


<!--有点坑 需要引入这两包 依赖了内部的api 如下-->
<!--SpringSessionRememberMeServices-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
  1. propertis 文件新增配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#session 存储方式
spring.session.store-type=redis
# 会话超时。如果未指定持续时间后缀,则使用秒。
server.servlet.session.timeout=3600
#sessions flush mode。
spring.session.redis.flush-mode=on-save
#用于存储会话的密钥的命名空间。
spring.session.redis.namespace = spring:session:ts

#Redis服务器主机。
spring.redis.host = 129.28.195.236
#redis服务器的登录密码。
spring.redis.password =
#Redis服务器端口。
spring.redis.port = 6379
  1. 登入web

  2. 我们打开redis客户端可以看到

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> keys *
1) "trojan2"
2) "spring\xc3\xaf\xc2\xbc\xc2\x9asession:sessions:expires:320b113f-820f-470c-8982-5eed9afbdddb"
3) "spring\xc3\xaf\xc2\xbc\xc2\x9asession:sessions:320b113f-820f-470c-8982-5eed9afbdddb"
4) "trojan1"
5) "Back2"
6) "Back3"
7) "Back1"
8) "spring\xc3\xaf\xc2\xbc\xc2\x9asession:expirations:1561205820000"
127.0.0.1:6379>
  1. 看到这里我们已经使用spring-session搭建了一个使用redis统一存储session应用

四:Token方式

我们先考虑个几个问题

  1. 如果浏览器禁用cookie,无法获取jessionid怎么?

  2. 当我们面对app这种平台如何进行登入管理?

  3. 跨域cookie无法传送的怎么办?

衍生了另一种方式 Token 常用方式,它既可以当作请求参数传入也可以放入cookie中!Token就是一串唯一身份标识符。常用的Token一般识用账户唯一信息+加密的key通过加密算法(HSA/MD5)等方式生成一串唯一标识符。

接下来我来介绍一种封装Token框架 JWT(Json Web Token)。

jwt

通过这张官网的图我们可以简单得知 它分为三块:

  • HEADER:头 包含了 加密算法 和 加密类型

  • PAYLOAD:载体 包含了 用户信息(用户id)和 jwt预设的信息(失效时间)

  • VERIFY SIGNATURE:签名 这个是后台验证的关键

需要的注意 HEADER,PAYLOAD 都只是BSAEURL64编码了一下 所以很容易被人破解,请不要存储敏感信息。

不讲虚的我们来点干货,上代码->

  1. 引入Jar包
1
2
3
4
5
6
 <!--jwt 认证-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.1</version>
</dependency>
  1. 编写工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* jwt TOKEN 生成 解析
*
* @author zhoukun
*/
@Slf4j
public class JwtUtil {

private JwtUtil() {

}

/**
* 加密Key
*/
public static final String SECRET = "c61070db-450b-419e-8ff9-e45fae4f6e3c";
/**
* 发行人
*/
private static String ISSUER = "JackKun";


/**
* 生成Token
*
* @param claims 主体
* @param expireDate 过期时间
* @return
*/
public static String generateToken(Map<String, String> claims, Date expireDate) {
try {
//申明加密算法
Algorithm algorithm = Algorithm.HMAC256(SECRET);

//创建JWT
JWTCreator.Builder builder = JWT.create()
.withIssuer(ISSUER)
.withExpiresAt(expireDate);

//设置主体参数
claims.forEach((k, v) -> {
builder.withClaim(k, v);
});

//返回加密签名 token
return builder.sign(algorithm);
} catch (IllegalArgumentException e) {
log.error("生成Token 非法参数异常! errMsg:{}", e);
throw new RuntimeException(e);

} catch (JWTCreationException e) {
log.error("生成Token 创建JWT异常! errMsg:{}", e);
throw new RuntimeException(e);
}
}

/**
* 验证Token
*
* @return
*/
public static Map<String, String> verifyToken(String token) {
//申明加密算法
Algorithm algorithm = Algorithm.HMAC256(SECRET);

JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(ISSUER)
.build();
DecodedJWT jwt = verifier.verify(token);

Map<String, String> resultMap = new HashMap<>();
Map<String, Claim> claimMap = jwt.getClaims();
claimMap.forEach((k, v) -> resultMap.put(k, v.asString()));
return resultMap;
}
}

这个工具类稍微比较简陋,朋友可以自行研究研究!具体通过它实现SSO我们先放到后面再讲,这里就不在展开讲了!

引子

上面讲了这么多只是讲了一些基础 后面会讲讲什么是单点登入?切入点?我们怎么来实现?跨域如何解决?

Hello World

Posted on 2019-06-23

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

拔剑四顾心茫然

视其所以 观其所由 察其所安

3 posts
© 2019 拔剑四顾心茫然
Powered by Hexo v3.8.0
|
Theme – NexT.Muse v6.7.0