【Java】单点登陆

后端码匠
• 阅读 277

一、简介

单点登陆:在多系统中单一位置登录可以实现多系统同时登录的一种技术。常在互联网应用和企业级平台中使用。

第三方登陆:在某系统中使用其他系统的用户实现本系统登录的方式。如,在京东中使用微信登录。解决信息孤岛和用户不对等的实现方案。

单点登陆需要解决的问题:数据跨域、信息共享和安全。

二、跨域解决方案

域的概念

​ 在应用模型中一个完整的,有独立访问路径的功能集合称为一个域。如:百度称为一个应用或系统。百度有若干的域,如:搜索引擎【www.baidu.com】,百度贴吧【tie.baidu.com】,百度知道【zhidao.baidu.com】,百度地图【map.baidu.com】等。域信息,有时也称为多级域名。域的划分:以IP,端口,域名,主机名为标准,实现划分。

跨域概念

客户端请求的时候,请求的服务器,不是同一个IP,端口,域名,主机名的时候,都称为跨域。

如:localhost和127.0.0.1也属于跨域

Session跨域

​ 所谓Session跨域就是摒弃了系统(web容器)提供的Session,而使用自定义的类似Session的机制来保存客户端数据的一种解决方案。如:通过设置cookie的domain来实现cookie的跨域传递。在cookie中传递一个自定义的session_id。这个session_id是客户端的唯一标记。将这个标记作为key,将客户端需要保存的数据作为value,在服务端进行保存。这种机制就是Session的跨域解决。

具体逻辑

<!-- 配置跨域请求 -->
<filter>
  <filter-name>corsFilter</filter-name>
  <filter-class>com.thetransactioncompany.cors.CORSFilter</filter-class>
  <init-param>
    <param-name>cors.allowOrigin</param-name>
    <param-value>*</param-value>
  </init-param>
  <init-param>
    <param-name>cors.supportedMethods</param-name>
    <param-value>GET, POST, HEAD, PUT, DELETE</param-value>
  </init-param>
  <init-param>
    <param-name>cors.supportedHeaders</param-name>
    <param-value>Accept, Origin, X-Requested-With, Content-Type, Last-Modified</param-value>
  </init-param>
  <init-param>
    <param-name>cors.exposedHeaders</param-name>
    <param-value>Set-Cookie</param-value>
  </init-param>
  <init-param>
    <param-name>cors.supportsCredentials</param-name>
    <param-value>true</param-value>
  </init-param>
</filter>
<filter-mapping>
  <filter-name>corsFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 设置token的Servlet -->
<servlet>
  <servlet-name>corssServlet</servlet-name>
  <servlet-class>com.kun.CorssServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>corssServlet</servlet-name>
  <url-pattern>/corss</url-pattern>
</servlet-mapping>
public class CorssServlet extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    Cookie[] cookies = req.getCookies();
    PrintWriter writer = resp.getWriter();
    //如果已经存在Cookie则查看Cookie值
    boolean findAuth = false;
    if(cookies != null) {
      for (Cookie cookie : cookies) {
        if(cookie.getName().equals("Auth")){
          findAuth = true;
          writer.println(cookie.getName());
          writer.println(cookie.getValue());
          writer.println(cookie.getMaxAge());
        }
      }
    }

    //生成cookie,注意domain的域,应该用代码智能切分
    //如www.baidu.com,tie.baidu.com均设置为.baidu.com
    if(!findAuth) {
      String token = UUID.randomUUID().toString().replace("-", "");
      Cookie authCookie = new Cookie("Auth", token);
      authCookie.setPath("/");
      authCookie.setDomain(".kun.com");
      resp.addCookie(authCookie);
    }
  }
}

三、信息共享解决方案

Spring-session

​ spring-session技术是spring提供的用于处理集群会话共享的解决方案。spring-session技术是将用户session数据保存到三方存储容器中,如:mysql,redis等

​ spring-session技术是解决同域名下的多服务器集群session共享问题的。不能直接解决跨域session共享问题。

需要设置其它选项。

第一步:导入依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>版本号</version>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>版本号</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    <version>版本号</version>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session</artifactId>
    <version>版本号</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>版本号</version>
</dependency>

第二步:web.xml配置

<!-- Session设置代理过滤器 -->
<filter>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath*:/spring-redis.xml</param-value>
</context-param>

<servlet>
    <servlet-name>Dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:/spring-mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>Dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

第三步:spring-mvc,spring-redis配置

<!-- spring-mvc配置 -->
<context:component-scan base-package="com.kun.controller" use-default-filters="false" >
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>
<!-- spring-redis配置 -->
<context:component-scan base-package="com.kun" >
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>

<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
    <property name="maxIdle" value="300"/>
    <property name="maxWaitMillis" value="1000"/>
    <property name="testOnBorrow" value="true"/>
</bean>

<context:component-scan base-package="org.springframework.web.filter"/>

<!-- 添加RedisHttpSessionConfiguration用于session共享 -->
<bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
    <property name="maxInactiveIntervalInSeconds" value="18000" />
    <property name="cookieSerializer" ref="defaultCookieSerializer"/>
</bean>

<!-- 设置Cookie domain的名称,如果不设置将无法跨域 -->
<bean id="defaultCookieSerializer" class="org.springframework.session.web.http.DefaultCookieSerializer">
    <property name="domainName" value=".kun.com"/>
    <property name="cookieName" value="SESSION"/>
    <property name="cookiePath" value="/" />
</bean>

<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
    <property name="hostName" value="192.168.1.158"/>
    <property name="port" value="6379"/>
    <property name="poolConfig" ref="poolConfig"/>
    <property name="usePool" value="true"/>
    <property name="timeout" value="3000"/>
</bean>

Spring-session解决跨域修改web容器方案

​ 默认session存放在当前域下,如访问www.kun.com的session的domain为www.kun.com,访问pps.kun.com的session的domain为pps.kun.com。session因为domain不同所以session并不能跨域。

​ 解决方案:在web目录下,创建和WEB-INF同级的META-INF目录,并创建context.xml文件,将以下内容写入即可改变session的domain实现跨域访问。

<?xml version="1.0" encoding="UTF-8"?>
<!-- 设置主域名与子域名sessionid一致 -->
<Context  sessionCookiePath="/" sessionCookieDomain=".kun.com"/>

Nginx Session共享

​ nginx中的ip_hash技术能够将某个ip的请求定向到同一台后端,这样一来这个ip下的某个客户端和某个后端就能建立起稳固的session,ip_hash是在upstream配置中定义的,如下示例

# 设置为ip_hash策略,或者自定义hash策略【经过代理的不可设为ip_hash策略】
upstream nginx.app.com
{
    server www.kun.com:8080;
    server pps.kun.com:8080;
    ip_hash;
    # hash $remote_add; 使用自定义hash策略
}

server
{
    listen 80;
    location /
    {
        proxy_pass http://nginx.app.com;
        proxy_set_header Host  $http_host;
        proxy_set_header Cookie $http_cookie;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        client_max_body_size  100m;
    }
}

四、Token单点登陆机制

传统身份认证【基于cookie】

客户端使用用户名还有密码通过了身份验证,不过下回这个客户端再发送请求时候因为HTTP 是一种没有状态的协议,还得再验证一下。

解决的方法是,当用户请求登录的时候,如果没有问题,我们在服务端生成一条记录,这个记录里存储了登录的用户信息,然后把这条记录的ID号返给客户端。客户端收到以后把这个 ID 号存储在 Cookie 里,下次这个用户再向服务端发送请求的时候,可以带着这个 Cookie ,这样服务端会验证一个这个 Cookie 里的信息,看看能不能在服务端这里找到对应的记录,如果可以,说明用户已经通过了身份验证,就把用户请求的数据返回给客户端。

这种认证中出现的问题是

  • 会话存储:每次认证用户发起请求时,服务器需要去创建一个记录来存储信息。当越来越多的用户发请求时,内存的开销也会不断增加。

  • 可扩展性:如果在服务端的内存【不借助redis等】中存储登录信息,伴随而来的是可扩展性问题。如Token需要同步。

  • CORS(跨域资源共享):如果让数据跨多台移动设备上使用时,跨域资源的共享是个问题。在使用Ajax抓取另一个域的资源,就可以会出现禁止请求的情况。

  • CSRF(跨站请求伪造):用户在访问银行网站时,可能被利用其访问其他的网站。

Token身份认证

使用基于 Token 的身份验证方法,在服务端需要存储用户的登录记录。大致流程如下:

  1. 客户端使用用户名、密码请求登录
  2. 服务端收到请求,验证用户名、密码
  3. 验证成功后,服务端会签发一个Token,再把这个Token发送给客户端
  4. 客户端收到 Token 以后可以把它存储起来,比如放在Cookie里或者Local Storage或Session Storage里
  5. 客户端每次向服务端请求资源时请求头需要带着服务端签发的Token
  6. 服务端收到请求后验证客户端请求里面带着的Token,如果验证成功,就向客户端返回请求的数据

使用Token验证的优势:

无状态、可扩展。在客户端存储的Tokens是无状态的,并且能够被扩展。基于这种无状态和服务端不存储Session信息,负载负载均衡器能够将用户信息从一个服务传到其他服务器上。

安全性。请求中发送token而不再是发送cookie能够防止CSRF(跨站请求伪造)。即使在客户端使用cookie存储token,cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对session操作。

五、JSON Web Token(JWT)机制

​ JWT是一种紧凑且自包含的,用于在多方传递JSON对象的技术。传递的数据可以使用数字签名增加其安全行。可以使用HMAC加密算法或RSA公钥/私钥加密方式。

紧凑:数据小,可以通过URL,POST参数,请求头发送。且数据小代表传输速度快。

自包含:使用payload数据块记录用户必要且不隐私的数据,可以有效的减少数据库访问次数,提高代码性能。


JWT一般用于处理用户身份验证或数据信息交换。

用户身份验证:一旦用户登录,每个后续请求都将包含JWT,允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小,并且能够轻松地跨不同域使用。

数据信息交换:JWT是一种非常方便的多方传递数据的载体,因为可以保证数据的有效性和安全性。

JWT数据结构

JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:

  • Header

    header由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等),如

    {
      "alg": "HS256",
      "type": "JWT"
    }
  • Payload

    包含声明,声明有三种类型: registered, public 和 private。

    • Registered claims : 这里有一组预定义的声明,它们不是强制的。如:iss (发行者), exp (过期时间), sub (主题), aud (受众)等。
    • Public claims : 可以随意定义。一般都会在JWT注册表中增加定义。避免和已注册信息冲突。
    • Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。
    {
        "sub": "user Login",
        "name": "kun"
        "admin": true
    }
  • Signature

    签名算法是header中指定的,用于将Header和Payload加密判断是否更改,具体操作步骤为:

    加密算法(base64URLEncoder(header) + "." + base64URLEncoder(payload), secret)

    签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。

token保存位置

  • webstorage

    webstorage可保存的数据容量为5M分为localStorage和sessionStorage。且只能存储字符串数据。

    • localStorage的生命周期是永久的,关闭页面或浏览器之后localStorage中的数据也不会消失。localStorage除非主动删除数据,否则数据永远不会消失。

    • sessionStorage是会话相关的本地存储单元,生命周期是在仅在当前会话下有效。sessionStorage引入了一个“浏览器窗口”的概念,sessionStorage是在同源的窗口中始终存在的数据。只要这个浏览器窗口没有关闭,即使刷新页面或者进入同源另一个页面,数据依然存在。但是sessionStorage在关闭了浏览器窗口后就会被销毁。同时独立的打开同一个窗口同一个页面,sessionStorage也是不一样的。

  • Cookie

    使用Cookie存储,使用请求头发送可以避免CSRF(跨站请求伪造),且方便跨域请求。

JWT执行流程

【Java】单点登陆

JWT应用示例

JWT工具类,用于生成JWT和解析JWT

public class JwtUtil {

  // 生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取
  // 切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。
  // 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
  private static final String secret = "com.kun.secret";

  /**
    * 用户登录成功后生成Jwt
    * 使用HS256算法  私匙使用用户密码
    */
  public static String createJWT(long ttlMillis, User user) {
    // 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
    SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 生成JWT的时间
    long nowMillis = System.currentTimeMillis();

    // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
    Map<String, Object> claims = new HashMap<String, Object>();
    claims.put("userId", user.getId());
    claims.put("userName", user.getUserName());
    claims.put("userAge", user.getUserAge());

    // 生成签发人
    String subject = user.getUserName();

    // 下面就是在为payload添加各种标准声明和私有声明了
    // 这里其实就是new一个JwtBuilder,设置jwt的body
    JwtBuilder builder = Jwts.builder()
      // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值
      // 一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
      .setClaims(claims)
      // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值
      // 主要用来作为一次性token,从而回避重放攻击。
      .setId(UUID.randomUUID().toString())
      // iat: jwt的签发时间
      .setIssuedAt(new Date(nowMillis))
      // 代表这个JWT的主体,即它的所有人
      // 这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
      .setSubject(subject)
      // 设置过期时间
      .setExpiration(new Date(nowMillis + ttlMillis))
      // 设置签名使用的签名算法和签名使用的秘钥
      .signWith(signatureAlgorithm, secret);
    return builder.compact();
  }


  /**
    * Token的解密, parseClaimsJws步骤会校验
    */
  public static Claims parseJWT(String token) {
    // 签名秘钥,和生成的签名的秘钥一模一样
    // 得到DefaultJwtParser
    Claims claims = Jwts.parser()
      //设置签名的秘钥
      .setSigningKey(secret)
      //设置需要解析的jwt
      .parseClaimsJws(token).getBody();
    return claims;
  }
}

JWT测试

public class JWTTest {

  private static User user = new User(1001, "kun", "123456", 19);

  // 创建
  @Test
  public void create() {
    String token = JwtUtil.createJWT(60000, user);
    System.out.println(token);
  }

  // 解析&校验
  @Test
  public void parse() {
    String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJrdW4iLCJ1c2VyTmFtZSI6Imt1biIsImV4cCI6MTU2MDMyNzcxOSwidXNlcklkIjoxMDAxLCJpYXQiOjE1NjAzMjc2NTksInVzZXJBZ2UiOjE5LCJqdGkiOiIwNGE1MDQxMi1jY2Q5LTRhYzQtODYwZC02NzcyYTAzMWVjMmEifQ.T1Q7JF6HyJrA1R0XJd5QFpFBAGpepJwPNNOeuFFEbux";
    Claims claims = JwtUtil.parseJWT(token);
    System.out.println(claims.get("userId"));
    System.out.println(claims.get("userName"));
    System.out.println(claims.get("userAge"));
  }
}

JWT单点登陆使用注意点

  • 使用Cookie时需要解决跨域问题(设置domain),Cookie只是存储手段,需要使用Http请求头发送
  • 服务器需要开启允许跨域请求,否则无法发起跨域请求
  • JWT的过期时间需要在每次请求之后进行修改
点赞
收藏
评论区
推荐文章
【实践篇】基于CAS的单点登录实践之路
上个月我负责的系统SSO升级,对接京东ERP系统,这也让我想起了之前我做过一个单点登录的项目。想来单点登录有很多实现方案,不过最主流的还是基于CAS的方案,所以我也就分享一下我的CAS实践之路。
Easter79 Easter79
3年前
SpringBoot集成SpringSecurity+CAS
1、简介本文主要讲述如何通过SpringSecurityCAS在springboot项目中实现单点登录和单点注销的功能。参考内容有SpringSecurity官方文档中的1.5\.JavaConfiguration(https://www.oschina.net/action/GoToLink?urlhttps%3
Wesley13 Wesley13
3年前
ubuntu 18.04使用root用户登录ssh
ubuntu系统默认root用户是不能登录的,密码也是空的。如果要使用root用户登录,必须先为root用户设置密码打开终端,输入:sudopasswdroot然后按回车此时会提示你输入密码,在password:后输入你现在登录的用户的密码在ubuntu系统中,默认是不开启ssh使用root用户登陆的,在/etc/ssh/sshd\_c
Easter79 Easter79
3年前
TP、PHP同域不同子级域名共享Session、单点登录
目的:为了部署同个域名下不同子级域名共享会话,从而实现单点登录的问题,一处登录,同域处处子系统即可以实现自动登录。PHP支持通过设置cookie使得同域不同子域共享SESSION1\.通过在执行PHP的入口文件中设置如下代码:
Stella981 Stella981
3年前
CAS 实现站内单点登录及实现第三方 OAuth、OpenId 登录(一)
一、CAS介绍    CAS是Yale大学发起的一个开源项目,旨在为Web应用系统提供一种可靠的单点登录方法,CAS在2004年12月正式成为JASIG的一个项目。CAS具有以下特点:开源的企业级单点登录解决方案CASServer为需要独立部署的Web应用CASClient支持非
Wesley13 Wesley13
3年前
CAS 4.1.x 单点登出(退出登录)的原理解析
  我们在项目中使用了cas作为单点登录的解决方案,当在集成shiro做统一权限控制的时候,发现单点退出登录有坑,所以啃了一下CAS的单点登出的源码,在此分享一下。1、回顾单点登录中一些关键事件  在解析CAS单点登出的原理之前,我们先回顾一下在单点登录过程中,CAS服务器和CAS客户端都做了一些什么事,这些事
Stella981 Stella981
3年前
Spring+ Spring cloud + SSO单点登录应用认证
之前的文章中有介绍springcloudsso集成的方案,也做过springjwtredis的解决方案,不同系统的无缝隙集成,统一的sso单点登录界面的管理、每个应用集成的权限认证,白名单等都是我们需要考虑的,现在针对于以上的问题我们做了sso单点登录应用认证平台,设计如下:1\.数据库设计:Java代码!复制代码(http
Wesley13 Wesley13
3年前
CAS单点登录(一):单点登录与CAS理论介绍
一、什么是单点登录(SSO)  单点登录主要用于多系统集成,即在多个系统中,用户只需要到一个中央服务器登录一次即可访问这些系统中的任何一个,无须多次登录。  单点登录(SingleSignOn),简称为SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所
Stella981 Stella981
3年前
Django学习之JWT
JWTJsonWebToken,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC7519),被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。单点登录(SingleSignOn,以下简称SSO),是指在多系统应用群中登录一个系统,便可在该应用群其他所有系统中得到授权而无需再次登录。
LeeFJ LeeFJ
1年前
Foxnic-Web 实现单点登录(SSO)
<aname"RPMAi"</a概述所谓单点登录(SingleSignOn),简称为SSO,就是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。<br/任何系统接入SSO前需要完成两个步骤,<b
后端码匠
后端码匠
Lv1
我们都不再联系忘了过去的一点一滴
文章
1
粉丝
0
获赞
0
热门文章

暂无数据