服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - SpringCloud实现SSO 单点登录的示例代码

SpringCloud实现SSO 单点登录的示例代码

2021-07-01 14:51huanzi-qch Java教程

作为分布式项目,单点登录是必不可少的,这篇文章主要介绍了SpringCloud实现SSO 单点登录的示例代码,非常具有实用价值,需要的朋友可以参考下

前言

作为分布式项目,单点登录是必不可少的,文本基于之前的的博客(猛戳:springcloud系列——zuul 动态路由springboot系列——redis)记录zuul配合redis实现一个简单的sso单点登录实例

sso单点登录思路:

1、访问分布式系统的任意请求,被zuul的filter拦截过滤

2、在run方法里实现过滤规则:cookie有令牌accesstoken且作为key存在于redis,或者访问的是登录页面、登录请求则放行

3、否则,将重定向到sso-server的登录页面且原先的请求路径作为一个参数;response.sendredirect("http://localhost:10010/sso-server/sso/loginpage?url=" + url);

4、登录成功,sso-server生成accesstoken,并作为key(用户名+时间戳,这里只是demo,正常项目的令牌应该要更为复杂)存到redis,value值存用户id作为value(或者直接存储可暴露的部分用户信息也行)设置过期时间(我这里设置3分钟);设置cookie:new cookie("accesstoken",accesstoken);,设置maxage(60*3);、path("/");

5、sso-server单点登录服务负责校验用户信息、获取用户信息、操作redis缓存,提供接口,在eureka上注册

代码编写

sso-server

首先我们创建一个单点登录服务sso-server,并在eureka上注册(创建项目请参考之前的springcloud系列博客跟springboot系列——redis

SpringCloud实现SSO 单点登录的示例代码

login.html

我们这里需要用到页面,要先maven引入thymeleaf

?
1
2
3
4
<dependency>
  <groupid>org.springframework.boot</groupid>
  <artifactid>spring-boot-starter-thymeleaf</artifactid>
 </dependency>
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <meta charset="utf-8">
 <title>登录页面</title>
</head>
<body>
 <form action="/sso-server/sso/login" method="post">
  <input name="url" type="hidden" th:value="${url}"/>
  用户名:<input name="username" type="text"/>
  密码:<input name="password" type="password"/>
  <input value="登录" type="submit"/>
 </form>
</body>
</html>

提供如下接口

?
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
@restcontroller
@enableeurekaclient
@springbootapplication
public class ssoserverapplication {
 
 public static void main(string[] args) {
  springapplication.run(ssoserverapplication.class, args);
 }
 
 @autowired
 private stringredistemplate template;
 
 /**
  * 判断key是否存在
  */
 @requestmapping("/redis/haskey/{key}")
 public boolean haskey(@pathvariable("key") string key) {
  try {
   return template.haskey(key);
  } catch (exception e) {
   e.printstacktrace();
   return false;
  }
 }
 
 /**
  * 校验用户名密码,成功则返回通行令牌(这里写死huanzi/123456)
  */
 @requestmapping("/sso/checkusernameandpassword")
 private string checkusernameandpassword(string username, string password) {
  //通行令牌
  string flag = null;
  if ("huanzi".equals(username) && "123456".equals(password)) {
   //用户名+时间戳(这里只是demo,正常项目的令牌应该要更为复杂)
   flag = username + system.currenttimemillis();
   //令牌作为key,存用户id作为value(或者直接存储可暴露的部分用户信息也行)设置过期时间(我这里设置3分钟)
   template.opsforvalue().set(flag, "1", (long) (3 * 60), timeunit.seconds);
  }
  return flag;
 }
 
 /**
  * 跳转登录页面
  */
 @requestmapping("/sso/loginpage")
 private modelandview loginpage(string url) {
  modelandview modelandview = new modelandview("login");
  modelandview.addobject("url", url);
  return modelandview;
 }
 
 /**
  * 页面登录
  */
 @requestmapping("/sso/login")
 private string login(httpservletresponse response, string username, string password, string url) {
  string check = checkusernameandpassword(username, password);
  if (!stringutils.isempty(check)) {
   try {
    cookie cookie = new cookie("accesstoken", check);
    cookie.setmaxage(60 * 3);
    //设置域
//    cookie.setdomain("huanzi.cn");
    //设置访问路径
    cookie.setpath("/");
    response.addcookie(cookie);
    //重定向到原先访问的页面
    response.sendredirect(url);
   } catch (ioexception e) {
    e.printstacktrace();
   }
   return null;
  }
  return "登录失败";
 }
}

zuul-server

引入feign,用于调用sso-server服务

?
1
2
3
4
5
<!-- feign -->
 <dependency>
  <groupid>org.springframework.cloud</groupid>
  <artifactid>spring-cloud-starter-openfeign</artifactid>
 </dependency>

创建ssofeign.java接口

?
1
2
3
4
5
6
7
8
9
@feignclient(name = "sso-server", path = "/")
public interface ssofeign {
 /**
  * 判断key是否存在
  */
 @requestmapping("redis/haskey/{key}")
 public boolean haskey(@pathvariable("key") string key);
 
}

启动类加入@enablefeignclients注解,否则启动会报错,无法注入ssofeign对象

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@enablezuulproxy
@enableeurekaclient
@enablefeignclients
@springbootapplication
public class zuulserverapplication {
 
 public static void main(string[] args) {
  springapplication.run(zuulserverapplication.class, args);
 }
 
 @bean
 public accessfilter accessfilter() {
  return new accessfilter();
 }
}

修改accessfilter过滤逻辑,注入feign接口,用于调用sso-server检查redis,修改run方法的过滤逻辑

?
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
/**
 * zuul过滤器,实现了路由检查
 */
public class accessfilter extends zuulfilter {
 
 @autowired
 private ssofeign ssofeign;
 
 /**
  * 通过int值来定义过滤器的执行顺序
  */
 @override
 public int filterorder() {
  // predecoration之前运行
  return pre_decoration_filter_order - 1;
 }
 
 /**
  * 过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型:
  * public static final string error_type = "error";
  * public static final string post_type = "post";
  * public static final string pre_type = "pre";
  * public static final string route_type = "route";
  */
 @override
 public string filtertype() {
  return pre_type;
 }
 
 /**
  * 过滤器的具体逻辑
  */
 @override
 public object run() {
  requestcontext ctx = requestcontext.getcurrentcontext();
  httpservletrequest request = ctx.getrequest();
  httpservletresponse response = ctx.getresponse();
 
  //访问路径
  string url = request.getrequesturl().tostring();
 
  //从cookie里面取值(zuul丢失cookie的解决方案:https://blog.csdn.net/lindan1984/article/details/79308396)
  string accesstoken = request.getparameter("accesstoken");
  for (cookie cookie : request.getcookies()) {
   if ("accesstoken".equals(cookie.getname())) {
    accesstoken = cookie.getvalue();
   }
  }
  //过滤规则:cookie有令牌且存在于redis,或者访问的是登录页面、登录请求则放行
  if (url.contains("sso-server/sso/loginpage") || url.contains("sso-server/sso/login") || (!stringutils.isempty(accesstoken) && ssofeign.haskey(accesstoken))) {
   ctx.setsendzuulresponse(true);
   ctx.setresponsestatuscode(200);
   return null;
  } else {
   ctx.setsendzuulresponse(false);
   ctx.setresponsestatuscode(401);
   //重定向到登录页面
   try {
    response.sendredirect("http://localhost:10010/sso-server/sso/loginpage?url=" + url);
   } catch (ioexception e) {
    e.printstacktrace();
   }
   return null;
  }
 }
 
 /**
  * 返回一个boolean类型来判断该过滤器是否要执行
  */
 @override
 public boolean shouldfilter() {
  return true;
 }
}

修改配置文件,映射sso-server代理路径,超时时间与丢失cookie的解决

?
1
2
3
4
5
6
7
8
zuul.routes.sso-server.path=/sso-server/**
zuul.routes.sso-server.service-id=sso-server
 
 
zuul.host.socket-timeout-millis=60000
zuul.host.connect-timeout-millis=10000
#zuul丢失cookie的解决方案:https://blog.csdn.net/lindan1984/article/details/79308396
zuul.sensitive-headers=

测试效果

启动eureka、zuul-server、sso-server、config-server、myspringboot、springdatajpa(由两个应用组成,实现了ribbon负载均衡),记得启动我们的rabbitmq服务和redis服务!

SpringCloud实现SSO 单点登录的示例代码

刚开始,没有cookie且无redis的情况下,浏览器访问http://localhost:10010/myspringboot/feign/ribbon,被zuul-server拦截重定向到sso-server登录页面

SpringCloud实现SSO 单点登录的示例代码

SpringCloud实现SSO 单点登录的示例代码

开始登录校验,为了方便演示,我将密码的type改成text

登录失败,返回提示语

SpringCloud实现SSO 单点登录的示例代码

登录成功,重定向到之前的请求

SpringCloud实现SSO 单点登录的示例代码

cookie的值,以及过期时间

SpringCloud实现SSO 单点登录的示例代码

3分钟后我们再次访问http://localhost:10010/myspringboot/feign/ribbon,cookie、redis失效,需要从新登录

SpringCloud实现SSO 单点登录的示例代码

SpringCloud实现SSO 单点登录的示例代码

SpringCloud实现SSO 单点登录的示例代码

后记

sso单点登录就记录到这里,这里只是实现了单机版的sso,以后在进行升级吧。

问题报错:我们在sso-server设置cookie后,在zuul-server的run方法里获取不到设置的cookie,去浏览器查看,cookie没有设置成功,zuul丢失cookie

解决方案

我们是使用spring cloud zuul作为api-gateway实践中,发现默认zuul会过滤掉cookie等header信息,有些业务场景需要传递这些信息该怎么处理呢?

处理方式   在api-gateway的application.properties文件中添加 zuul.sensitive-headers=  

问题原因

负责根据serviceid来路由的ribbonroutingfilter在route之前会调用proxyrequesthelper的buildzuulrequestheaders(request)来重新组装一个新的header。

在buildzuulrequestheaders方法中会对requsetheader中的每一项调用isincludedheader(name)来判断当前项是否应该留在新的header中,如下图,如果当前项在ignored_headers(需要忽略的信息)中,就不会在新header中保留。

predecorationfilter过滤器会调用proxyrequesthelper的addignoredheaders方法把敏感信息(zuulproperties的sensitiveheaders属性)添加到请求上下文的ignored_headers中

sensitiveheaders的默认值初始值是"cookie", "set-cookie", "authorization"这三项,可以看到cookie被列为了敏感信息,所以不会放到新header中

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:http://www.cnblogs.com/huanzi-qch/p/10249227.html

延伸 · 阅读

精彩推荐