单点登录 订阅
单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。 展开全文
单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
信息
外文名
Single Sign On
简    称
SSO
使用场景
运维内控审计系统、4A系统
中文名
单点登录
功    能
企业内部登录系统
解    释
企业业务整合的解决方案
单点登录概述
很早期的公司,一家公司可能只有一个Server,慢慢的Server开始变多了。每个Server都要进行注册登录,退出的时候又要一个个退出。用户体验很不好!你可以想象一下,上豆瓣 要登录豆瓣FM、豆瓣读书、豆瓣电影、豆瓣日记......真的会让人崩溃的。我们想要另一种登录体验:一家企业下的服务只要一次注册,登录的时候只要一次登录,退出的时候只要一次退出。怎么做?一次注册。 一次注册不难,想一下是不是只要Server之间同步用户信息就行了?可以,但这样描述不太完整,后续讲用户注册的时候详细说。实际上用户信息的管理才是SSO真正的难点,只是作为初学者,我们的难点在于实现SSO的技术!我们先讨论实现手段。一次登录与一次退出。 回头看看普通商场的故事,什么东西才是保持登录状态关键的东西?记录器(session)?那种叫做cookie的纸张?写在纸张上的ID? 是session里面记录的信息跟那个ID,cookie不只是记录ID的工具而已。客户端持有ID,服务端持有session,两者一起用来保持登录状态。客户端需要用ID来作为凭证,而服务端需要用session来验证ID的有效性(ID可能过期、可能根本就是伪造的找不到对应的信息、ID下对应的客户端还没有进行登录验证等)。但是session这东西一开始是每个server自己独有的,豆瓣FM有自己的session、豆瓣读书有自己的session,而记录ID的cookie又是不能跨域的。所以,我们要实现一次登录一次退出,只需要想办法让各个server的共用一个session的信息,让客户端在各个域名下都能持有这个ID就好了。再进一步讲,只要各个server拿到同一个ID,都能有办法检验出ID的有效性、并且能得到ID对应的用户信息就行了,也就是能检验ID [1]  。
收起全文
精华内容
参与话题
问答
  • 单点登录

    千次阅读 2017-01-12 22:29:41
    1:什么事单点登录: SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的...

    1:什么事单点登录:
    SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制。它是目前比较流行的企业业务整合的解决方案之一。

    2:为什么需要单点登录:
    应为有些公司有好多业务可以使用同一个账户登录,所以需要单点登录

    3:传统的登录流程
    传统登陆流程图

    4:传统流程出现的问题
    部署到集群上会出现问题,主要是session不能共享的问题

    5:解决方法:
    1):可以使用tomcat的共享session,在tomcat里面配置就可以了,每个tomcat服务器都会不断的广播自己的session的,但是集群多的话广播session占用的资源比较多;不能解决分布式工程的session共享问题。例如支付宝和淘宝单点登录的问题
    2):把session保存到redis里面,使用同一个redis服务器来实现session的共享

    6:单点登录流程图
    单点登录流程图

    ps:单点登录需要做成一个webservice来使用

    单点登录一般需要实现如下功能:
    a)数据校验接口
    b)注册接口
    c)登录接口
    d)根据token查询用户接口
    e)安全退出(作业)

    展开全文
  • 什么是单点登录单点登录的三种实现方式

    万次阅读 多人点赞 2018-06-07 21:18:22
    单点登录SSO(Single Sign On)说得简单点就是在一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任。单点登录在大型网站里使用得非常频繁,例如像...

    单点登录SSO(Single Sign On)说得简单点就是在一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任。单点登录在大型网站里使用得非常频繁,例如像阿里巴巴这样的网站,在网站的背后是成百上千的子系统,用户一次操作或交易可能涉及到几十个子系统的协作,如果每个子系统都需要用户认证,不仅用户会疯掉,各子系统也会为这种重复认证授权的逻辑搞疯掉。实现单点登录说到底就是要解决如何产生和存储那个信任,再就是其他系统如何验证这个信任的有效性,因此要点也就以下两个:

    • 存储信任
    • 验证信任

    如果一个系统做到了开头所讲的效果,也就算单点登录,单点登录有不同的实现方式,本文就罗列我开发中所遇见过的实现方式。

    以Cookie作为凭证媒介 
    最简单的单点登录实现方式,是使用cookie作为媒介,存放用户凭证。 
    用户登录父应用之后,应用返回一个加密的cookie,当用户访问子应用的时候,携带上这个cookie,授权应用解密cookie并进行校验,校验通过则登录当前用户。

    这里写图片描述

    不难发现以上方式把信任存储在客户端的Cookie中,这种方式很容易令人质疑:

    • Cookie不安全
    • 不能跨域实现免登

    对于第一个问题,通过加密Cookie可以保证安全性,当然这是在源代码不泄露的前提下。如果Cookie的加密算法泄露,攻击者通过伪造Cookie则可以伪造特定用户身份,这是很危险的。 
    对于第二个问题,更是硬伤。

    通过JSONP实现 
    对于跨域问题,可以使用JSONP实现。 
    用户在父应用中登录后,跟Session匹配的Cookie会存到客户端中,当用户需要登录子应用的时候,授权应用访问父应用提供的JSONP接口,并在请求中带上父应用域名下的Cookie,父应用接收到请求,验证用户的登录状态,返回加密的信息,子应用通过解析返回来的加密信息来验证用户,如果通过验证则登录用户。

    这里写图片描述

    这种方式虽然能解决跨域问题,但是安全性其实跟把信任存储到Cookie是差不多的。如果一旦加密算法泄露了,攻击者可以在本地建立一个实现了登录接口的假冒父应用,通过绑定Host来把子应用发起的请求指向本地的假冒父应用,并作出回应。 
    因为攻击者完全可以按照加密算法来伪造响应请求,子应用接收到这个响应之后一样可以通过验证,并且登录特定用户。

    通过页面重定向的方式 
    最后一种介绍的方式,是通过父应用和子应用来回重定向中进行通信,实现信息的安全传递。 
    父应用提供一个GET方式的登录接口,用户通过子应用重定向连接的方式访问这个接口,如果用户还没有登录,则返回一个的登录页面,用户输入账号密码进行登录。如果用户已经登录了,则生成加密的Token,并且重定向到子应用提供的验证Token的接口,通过解密和校验之后,子应用登录当前用户。

    这里写图片描述

    这种方式较前面两种方式,接解决了上面两种方法暴露出来的安全性问题和跨域的问题,但是并没有前面两种方式方便。 
    安全与方便,本来就是一对矛盾。

    使用独立登录系统 
    一般说来,大型应用会把授权的逻辑与用户信息的相关逻辑独立成一个应用,称为用户中心。 
    用户中心不处理业务逻辑,只是处理用户信息的管理以及授权给第三方应用。第三方应用需要登录的时候,则把用户的登录请求转发给用户中心进行处理,用户处理完毕返回凭证,第三方应用验证凭证,通过后就登录用户。

    本文转载自JC_Huang 
    原文链接:http://www.jianshu.com/p/613e44d4a464

    展开全文
  • 单点登录(SSO)

    万次阅读 多人点赞 2019-10-26 14:27:37
    一、SSO(单点登录)介绍 SSO英文全称Single SignOn,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的...

    一、SSO(单点登录)介绍

    SSO英文全称Single SignOn,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制。它是目前比较流行的企业业务整合的解决方案之一。

    实现机制

    当用户第一次访问应用系统1的时候,因为还没有登录,会被引导到认证系统中进行登录;根据用户提供的登录信息,认证系统进行身份校验,如果通过校验,应该返回给用户一个认证的凭据--ticket;用户再访问别的应用的时候就会将这个ticket带上,作为自己认证的凭据,应用系统接受到请求之后会把ticket送到认证系统进行校验,检查ticket的合法性。如果通过校验,用户就可以在不用再次登录的情况下访问应用系统2和应用系统3了。

    下面对上图简要描述

    1. 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
    2. sso认证中心发现用户未登录,将用户引导至登录页面
    3. 用户输入用户名密码提交登录申请
    4. sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌
    5. sso认证中心带着令牌跳转会最初的请求地址(系统1)
    6. 系统1拿到令牌,去sso认证中心校验令牌是否有效
    7. sso认证中心校验令牌,返回有效,注册系统1
    8. 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
    9. 用户访问系统2的受保护资源
    10. 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
    11. sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
    12. 系统2拿到令牌,去sso认证中心校验令牌是否有效
    13. sso认证中心校验令牌,返回有效,注册系统2
    14. 系统2使用该令牌创建与用户的局部会话,返回受保护资源

      用户登录成功之后,会与sso认证中心及各个子系统建立会话,用户与sso认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心,全局会话与局部会话有如下约束关系

    1. 局部会话存在,全局会话一定存在
    2. 全局会话存在,局部会话不一定存在
    3. 全局会话销毁,局部会话必须销毁

     

    2、注销

      单点登录自然也要单点注销,在一个子系统中注销,所有子系统的会话都将被销毁,用下面的图来说明

    3b139d2e-0b83-4a69-b4f2-316adb8997ce

      sso认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作

      下面对上图简要说明

    1. 用户向系统1发起注销请求
    2. 系统1根据用户与系统1建立的会话id拿到令牌,向sso认证中心发起注销请求
    3. sso认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址
    4. sso认证中心向所有注册系统发起注销请求
    5. 各注册系统接收sso认证中心的注销请求,销毁局部会话
    6. sso认证中心引导用户至登录页面

    总结

    单点登录(SSO)的所有流程都介绍完了,原理大家都清楚了。总结一下单点登录要做的事情:

    • 单点登录(SSO系统)是保障各业务系统的用户资源的安全 。
    • 各个业务系统获得的信息是,这个用户能不能访问我的资源。
    • 单点登录,资源都在各个业务系统这边,不在SSO那一方。 用户在给SSO服务器提供了用户名密码后,作为业务系统并不知道这件事。 SSO随便给业务系统一个ST,那么业务系统是不能确定这个ST是用户伪造的,还是真的有效,所以要拿着这个ST去SSO服务器再问一下,这个用户给我的ST是否有效,是有效的我才能让这个用户访问。
    展开全文
  • 使用JWT实现单点登录(完全跨域方案)

    万次阅读 多人点赞 2018-09-10 15:57:56
    首先介绍一下什么是JSON Web Token(JWT)? 官方文档是这样解释的:JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,可以在各方之间作为JSON对象安全地传输信息。...

    首先介绍一下什么是JSON Web Token(JWT)?

    官方文档是这样解释的:JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,可以在各方之间作为JSON对象安全地传输信息。此信息可以通过数字签名进行验证和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
    虽然JWT可以加密以在各方之间提供保密,但只将专注于签名令牌。签名令牌可以验证其中包含的声明的完整性,而加密令牌则隐藏其他方的声明。当使用公钥/私钥对签署令牌时,签名还证明只有持有私钥的一方是签署私钥的一方。

    通俗来讲,JWT是一个含签名并携带用户相关信息的加密串,页面请求校验登录接口时,请求头中携带JWT串到后端服务,后端通过签名加密串匹配校验,保证信息未被篡改。校验通过则认为是可靠的请求,将正常返回数据。

    什么情况下使用JWT比较适合?

    • 授权:这是最常见的使用场景,解决单点登录问题。因为JWT使用起来轻便,开销小,服务端不用记录用户状态信息(无状态),所以使用比较广泛;
    • 信息交换:JWT是在各个服务之间安全传输信息的好方法。因为JWT可以签名,例如,使用公钥/私钥对儿 - 可以确定请求方是合法的。此外,由于使用标头和有效负载计算签名,还可以验证内容是否未被篡改。

    JWT的结构体是什么样的?

    JWT由三部分组成,分别是头信息、有效载荷、签名,中间以(.)分隔,如下格式:

    xxx.yyy.zzz
    

    header(头信息)
    由两部分组成,令牌类型(即:JWT)、散列算法(HMAC、RSASSA、RSASSA-PSS等),例如:

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    

    然后,这个JSON被编码为Base64Url,形成JWT的第一部分。

    Payload(有效载荷)
    JWT的第二部分是payload,其中包含claims。claims是关于实体(常用的是用户信息)和其他数据的声明,claims有三种类型: registered, public, and private claims。
    Registered claims: 这些是一组预定义的claims,非强制性的,但是推荐使用, iss(发行人), exp(到期时间), sub(主题), aud(观众)等;
    Public claims: 自定义claims,注意不要和JWT注册表中属性冲突,这里可以查看JWT注册表
    Private claims: 这些是自定义的claims,用于在同意使用这些claims的各方之间共享信息,它们既不是Registered claims,也不是Public claims。
    以下是payload示例:

    {
      "sub": "1234567890",
      "name": "John Doe",
      "admin": true
    }
    

    然后,再经过Base64Url编码,形成JWT的第二部分;

    注意:对于签名令牌,此信息虽然可以防止篡改,但任何人都可以读取。除非加密,否则不要将敏感信息放入到Payload或Header元素中。

    Signature
    要创建签名部分,必须采用编码的Header,编码的Payload,秘钥,Header中指定的算法,并对其进行签名。
    例如,如果要使用HMAC SHA256算法,将按以下方式创建签名:

    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      secret)
    

    签名用于验证消息在此过程中未被篡改,并且,在使用私钥签名令牌的情况下,它还可以验证JWT的请求方是否是它所声明的请求方。
    输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递,与SAML等基于XML的标准相比更加紧凑。
    例如:

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
    SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
    

    JWT工作机制?

    在身份验证中,当用户使用其凭据成功登录时,将返回JSON Web Token(即:JWT)。由于令牌是凭证,因此必须非常小心以防止出现安全问题。一般情况下,不应将令牌保留的时间超过要求。理论上超时时间越短越好。

    每当用户想要访问受保护的路由或资源时,用户代理应该使用Bearer模式发送JWT,通常在Authorization header中。标题内容应如下所示:

    Authorization: Bearer <token>
    

    在某些情况下,这可以作为无状态授权机制。服务器的受保护路由将检查Authorization header中的有效JWT ,如果有效,则允许用户访问受保护资源。如果JWT包含必要的数据,则可以减少查询数据库或缓存信息。
    如果在Authorization header中发送令牌,则跨域资源共享(CORS)将不会成为问题,因为它不使用cookie。

    注意:使用签名令牌,虽然他们无法更改,但是令牌中包含的所有信息都会向用户或其他方公开。这意味着不应该在令牌中放置敏感信息。

    使用JWT的好处是什么?

    相比Simple Web Tokens (SWT)(简单Web令牌) and Security Assertion Markup Language Tokens (SAML)(安全断言标记语言令牌)

    • JWT比SAML更简洁,在HTML和HTTP环境中传递更方便;
    • 在安全方面,SWT只能使用HMAC算法通过共享密钥对称签名。但是,JWT和SAML令牌可以使用X.509证书形式的公钥/私钥对进行签名。与签名JSON的简单性相比,使用XML数字签名可能会存在安全漏洞;
    • JSON解析成对象相比XML更流行、方便。

    以下是我实际项目中的应用分析

    首先看一下大致的架构及流程图:
    架构图
    流程图

    主要有以下三步:

    • 项目一开始我先封装了一个JWTHelper工具包(GitHub下载),主要提供了生成JWT、解析JWT以及校验JWT的方法,其他还有一些加密相关操作,稍后我会以代码的形式介绍下代码。工具包写好后我将打包上传到私服,能够随时依赖下载使用;
    • 接下来,我在客户端项目中依赖JWTHelper工具包,并添加Interceptor拦截器,拦截需要校验登录的接口。拦截器中校验JWT有效性,并在response中重新设置JWT的新值;
    • 最后在JWT服务端,依赖JWT工具包,在登录方法中,需要在登录校验成功后调用生成JWT方法,生成一个JWT令牌并且设置到response的header中。

    以下是部分代码分享:

    • JwtHelper工具类:
    /**
     * @Author: Helon
     * @Description: JWT工具类
     * 参考官网:https://jwt.io/
     * JWT的数据结构为:A.B.C三部分数据,由字符点"."分割成三部分数据
     * A-header头信息
     * B-payload 有效负荷 一般包括:已注册信息(registered claims),公开数据(public claims),私有数据(private claims)
     * C-signature 签名信息 是将header和payload进行加密生成的
     * @Data: Created in 2018/7/19 14:11
     * @Modified By:
     */
    public class JwtHelper {
    
        private static Logger logger = LoggerFactory.getLogger(JwtHelper.class);
    
        /**
         * @Author: Helon
         * @Description: 生成JWT字符串
         * 格式:A.B.C
         * A-header头信息
         * B-payload 有效负荷
         * C-signature 签名信息 是将header和payload进行加密生成的
         * @param userId - 用户编号
         * @param userName - 用户名
         * @param identities - 客户端信息(变长参数),目前包含浏览器信息,用于客户端拦截器校验,防止跨域非法访问
         * @Data: 2018/7/28 19:26
         * @Modified By:
         */
        public static String generateJWT(String userId, String userName, String ...identities) {
            //签名算法,选择SHA-256
            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
            //获取当前系统时间
            long nowTimeMillis = System.currentTimeMillis();
            Date now = new Date(nowTimeMillis);
            //将BASE64SECRET常量字符串使用base64解码成字节数组
            byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET);
            //使用HmacSHA256签名算法生成一个HS256的签名秘钥Key
            Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
            //添加构成JWT的参数
            Map<String, Object> headMap = new HashMap<>();
            /*
                Header
                {
                  "alg": "HS256",
                  "typ": "JWT"
                }
             */
            headMap.put("alg", SignatureAlgorithm.HS256.getValue());
            headMap.put("typ", "JWT");
            JwtBuilder builder = Jwts.builder().setHeader(headMap)
                    /*
                        Payload
                        {
                          "userId": "1234567890",
                          "userName": "John Doe",
                        }
                     */
                    //加密后的客户编号
                    .claim("userId", AESSecretUtil.encryptToStr(userId, SecretConstant.DATAKEY))
                    //客户名称
                    .claim("userName", userName)
                    //客户端浏览器信息
                    .claim("userAgent", identities[0])
                    //Signature
                    .signWith(signatureAlgorithm, signingKey);
            //添加Token过期时间
            if (SecretConstant.EXPIRESSECOND >= 0) {
                long expMillis = nowTimeMillis + SecretConstant.EXPIRESSECOND;
                Date expDate = new Date(expMillis);
                builder.setExpiration(expDate).setNotBefore(now);
            }
            return builder.compact();
        }
    
        /**
         * @Author: Helon
         * @Description: 解析JWT
         * 返回Claims对象
         * @param jsonWebToken - JWT
         * @Data: 2018/7/28 19:25
         * @Modified By:
         */
        public static Claims parseJWT(String jsonWebToken) {
            Claims claims = null;
            try {
                if (StringUtils.isNotBlank(jsonWebToken)) {
                    //解析jwt
                    claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET))
                            .parseClaimsJws(jsonWebToken).getBody();
                }else {
                    logger.warn("[JWTHelper]-json web token 为空");
                }
            } catch (Exception e) {
                logger.error("[JWTHelper]-JWT解析异常:可能因为token已经超时或非法token");
            }
            return claims;
        }
    
        /**
         * @Author: Helon
         * @Description: 校验JWT是否有效
         * 返回json字符串的demo:
         * {"freshToken":"A.B.C","userName":"Judy","userId":"123", "userAgent":"xxxx"}
         * freshToken-刷新后的jwt
         * userName-客户名称
         * userId-客户编号
         * userAgent-客户端浏览器信息
         * @param jsonWebToken - JWT
         * @Data: 2018/7/24 15:28
         * @Modified By:
         */
        public static String validateLogin(String jsonWebToken) {
            Map<String, Object> retMap = null;
            Claims claims = parseJWT(jsonWebToken);
            if (claims != null) {
                //解密客户编号
                String decryptUserId = AESSecretUtil.decryptToStr((String)claims.get("userId"), SecretConstant.DATAKEY);
                retMap = new HashMap<>();
                //加密后的客户编号
                retMap.put("userId", decryptUserId);
                //客户名称
                retMap.put("userName", claims.get("userName"));
                //客户端浏览器信息
                retMap.put("userAgent", claims.get("userAgent"));
                //刷新JWT
                retMap.put("freshToken", generateJWT(decryptUserId, (String)claims.get("userName"), (String)claims.get("userAgent"), (String)claims.get("domainName")));
            }else {
                logger.warn("[JWTHelper]-JWT解析出claims为空");
            }
            return retMap!=null?JSONObject.toJSONString(retMap):null;
        }
    
        public static void main(String[] args) {
           String jsonWebKey = generateJWT("123", "Judy",
                   "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36");
           System.out.println(jsonWebKey);
           Claims claims =  parseJWT(jsonWebKey);
            System.out.println(claims);
           System.out.println(validateLogin(jsonWebKey));
        }
    
    • AES加密工具类:
    /**
     * @Author: Helon
     * @Description: AES加密工具类
     * @Data: Created in 2018/7/28 18:38
     * @Modified By:
     */
    public class AESSecretUtil {
    
        /**秘钥的大小*/
        private static final int KEYSIZE = 128;
        
        /**
         * @Author: Helon
         * @Description: AES加密
         * @param data - 待加密内容
         * @param key - 加密秘钥
         * @Data: 2018/7/28 18:42
         * @Modified By:
         */
        public static byte[] encrypt(String data, String key) {
            if(StringUtils.isNotBlank(data)){
                try {
                    KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
                    //选择一种固定算法,为了避免不同java实现的不同算法,生成不同的密钥,而导致解密失败
                    SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
                    random.setSeed(key.getBytes());
                    keyGenerator.init(KEYSIZE, random);
                    SecretKey secretKey = keyGenerator.generateKey();
                    byte[] enCodeFormat = secretKey.getEncoded();
                    SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
                    Cipher cipher = Cipher.getInstance("AES");// 创建密码器
                    byte[] byteContent = data.getBytes("utf-8");
                    cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);// 初始化
                    byte[] result = cipher.doFinal(byteContent);
                    return result; // 加密
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return null;
        }
    
        /**
         * @Author: Helon
         * @Description: AES加密,返回String
         * @param data - 待加密内容
         * @param key - 加密秘钥
         * @Data: 2018/7/28 18:59
         * @Modified By:
         */
        public static String encryptToStr(String data, String key){
    
            return StringUtils.isNotBlank(data)?parseByte2HexStr(encrypt(data, key)):null;
        }
    
    
        /**
         * @Author: Helon
         * @Description: AES解密
         * @param data - 待解密字节数组
         * @param key - 秘钥
         * @Data: 2018/7/28 19:01
         * @Modified By:
         */
        public static byte[] decrypt(byte[] data, String key) {
            if (ArrayUtils.isNotEmpty(data)) {
                try {
                    KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
                    //选择一种固定算法,为了避免不同java实现的不同算法,生成不同的密钥,而导致解密失败
                    SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
                    random.setSeed(key.getBytes());
                    keyGenerator.init(KEYSIZE, random);
                    SecretKey secretKey = keyGenerator.generateKey();
                    byte[] enCodeFormat = secretKey.getEncoded();
                    SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
                    Cipher cipher = Cipher.getInstance("AES");// 创建密码器
                    cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);// 初始化
                    byte[] result = cipher.doFinal(data);
                    return result; // 加密
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return null;
        }
    
        /**
         * @Author: Helon
         * @Description: AES解密,返回String
         * @param enCryptdata - 待解密字节数组
         * @param key - 秘钥
         * @Data: 2018/7/28 19:01
         * @Modified By:
         */
        public static String decryptToStr(String enCryptdata, String key) {
            return StringUtils.isNotBlank(enCryptdata)?new String(decrypt(parseHexStr2Byte(enCryptdata), key)):null;
        }
    
        /**
         * @Author: Helon
         * @Description: 将二进制转换成16进制
         * @param buf - 二进制数组
         * @Data: 2018/7/28 19:12
         * @Modified By:
         */
        public static String parseByte2HexStr(byte buf[]) {
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < buf.length; i++) {
                String hex = Integer.toHexString(buf[i] & 0xFF);
                if (hex.length() == 1) {
                    hex = '0' + hex;
                }
                sb.append(hex.toUpperCase());
            }
            return sb.toString();
        }
    
        /**
         * @Author: Helon
         * @Description: 将16进制转换为二进制
         * @param hexStr - 16进制字符串
         * @Data: 2018/7/28 19:13
         * @Modified By:
         */
        public static byte[] parseHexStr2Byte(String hexStr) {
            if (hexStr.length() < 1)
                return null;
            byte[] result = new byte[hexStr.length()/2];
            for (int i = 0;i< hexStr.length()/2; i++) {
                int high = Integer.parseInt(hexStr.substring(i*2, i*2+1), 16);
                int low = Integer.parseInt(hexStr.substring(i*2+1, i*2+2), 16);
                result[i] = (byte) (high * 16 + low);
            }
            return result;
        }
    
        public static void main(String[] args) {
            String ss = encryptToStr("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMjMiLCJ1c2VyTmFtZSI6Ikp1ZHkiLCJleHAiOjE1MzI3Nzk2MjIsIm5iZiI6MTUzMjc3NzgyMn0.sIw_leDZwG0pJ8ty85Iecd_VXjObYutILNEwPUyeVSo", SecretConstant.DATAKEY);
            System.out.println(ss);
            System.out.println(decryptToStr(ss, SecretConstant.DATAKEY));
        }
    
    • 所使用的常量类:
    /**
     * @Author: Helon
     * @Description: JWT使用常量值
     * @Data: Created in 2018/7/27 14:37
     * @Modified By:
     */
    public class SecretConstant {
    
        //签名秘钥 自定义
        public static final String BASE64SECRET = "***********";
    
        //超时毫秒数(默认30分钟)
        public static final int EXPIRESSECOND = 1800000;
    
        //用于JWT加密的密匙 自定义
        public static final String DATAKEY = "************";
    
    }
    
    • 客户端pom依赖:
            <!--jwt工具类-->
            <dependency>
                <groupId>com.chtwm.component</groupId>
                <artifactId>jwt-helper</artifactId>
                <version>xxx</version>
            </dependency>
    
    • 客户端拦截器:
    /**
     * @Author: Helon
     * @Description: 校验是否登录拦截器
     * @Data: Created in 2018/7/30 14:30
     * @Modified By:
     */
    @Slf4j
    public class ValidateLoginInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
            //首先从请求头中获取jwt串,与页面约定好存放jwt值的请求头属性名为User-Token
            String jwt = httpServletRequest.getHeader("User-Token");
            log.info("[登录校验拦截器]-从header中获取的jwt为:{}", jwt);
            //判断jwt是否有效
            if(StringUtils.isNotBlank(jwt)){
                //校验jwt是否有效,有效则返回json信息,无效则返回空
                String retJson = JwtHelper.validateLogin(jwt);
                log.info("[登录校验拦截器]-校验JWT有效性返回结果:{}", retJson);
                //retJSON为空则说明jwt超时或非法
                if(StringUtils.isNotBlank(retJson)){
                    JSONObject jsonObject = JSONObject.parseObject(retJson);
                    //校验客户端信息
                    String userAgent = httpServletRequest.getHeader("User-Agent");
                    if (userAgent.equals(jsonObject.getString("userAgent"))) {
                        //获取刷新后的jwt值,设置到响应头中
                        httpServletResponse.setHeader("User-Token", jsonObject.getString("freshToken"));
                        //将客户编号设置到session中
                        httpServletRequest.getSession().setAttribute(GlobalConstant.SESSION_CUSTOMER_NO_KEY, jsonObject.getString("userId"));
                        return true;
                    }else{
                        log.warn("[登录校验拦截器]-客户端浏览器信息与JWT中存的浏览器信息不一致,重新登录。当前浏览器信息:{}", userAgent);
                    }
                }else {
                    log.warn("[登录校验拦截器]-JWT非法或已超时,重新登录");
                }
            }
            //输出响应流
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("hmac", "");
            jsonObject.put("status", "");
            jsonObject.put("code", "4007");
            jsonObject.put("msg", "未登录");
            jsonObject.put("data", "");
            httpServletResponse.setCharacterEncoding("UTF-8");
            httpServletResponse.setContentType("application/json; charset=utf-8");
            httpServletResponse.getOutputStream().write(jsonObject.toJSONString().getBytes("UTF-8"));
            return false;
        }
    
        @Override
        public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    
        }
    
        @Override
        public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
    
        }
    }
    
    • 客户端拦截器在XML文件中配置:
    	<!--拦截器配置-->
        <mvc:interceptors>
            <mvc:interceptor>
    	        <!--需拦截url配置-->
                <mvc:exclude-mapping path="/api/aa/bb/**" />
                <mvc:exclude-mapping path="/api/aa/cc/test" />
                <bean id="validateLoginInterceptor" class="com.xxx.ValidateLoginInterceptor" />
            </mvc:interceptor>
        </mvc:interceptors>
    

    到此,后台服务的配置已经完成,下一步就需要前端页面将JWT令牌从response响应头中取出,然后存入localstorage或cookie中。但是遇到跨域场景,处理起来就会比较复杂,因为一旦在浏览器中跨域将获取不到localstorage中的JWT令牌。例如www.a.com域下的JWT,在www.b.com域下是获取不到的,所以我选择了一种页面跨域的方式进行处理,使用iframe+H5的postMessage(参考博文),具体我使用代码分享的方式来分析。

    • 前端页面js代码(服务端):
        /**CURD本地存储信息 start**/
              (function(doc,win){
                  var fn=function(){};
                  fn.prototype={
                      /*本地数据存储 t:cookie有效时间,单位s; domain:cookie存储所属的domain域*/
                      setLocalCookie: function (k, v, t,domain) {
                          //如果当前浏览器不支持localStorage将存储在cookie中
                          typeof window.localStorage !== "undefined" ? localStorage.setItem(k, v) :
                              (function () {
                                  t = t || 365 * 12 * 60 * 60;
                                  domain=domain?domain:".jwtserver.com";
                                  document.cookie = k + "=" + v + ";max-age=" + t+";domain="+domain+";path=/";
                              })()
                      },
                      /*获取本地存储数据*/
                      getLocalCookie: function (k) {
                          k = k || "localDataTemp";
                          return typeof window.localStorage !== "undefined" ? localStorage.getItem(k) :
                              (function () {
                                  var all = document.cookie.split(";");
                                  var cookieData = {};
                                  for (var i = 0, l = all.length; i < l; i++) {
                                      var p = all[i].indexOf("=");
                                      var dataName = all[i].substring(0, p).replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"");
                                      cookieData[dataName] = all[i].substring(p + 1);
                                  }
                                  return cookieData[k]
                              })();
                      },
                      /*删除本地存储数据*/
                      clearLocalData: function (k) {
                          k = k || "localDataTemp";
                          typeof window.localStorage !== "undefined" ? localStorage.removeItem(k) :
                              (function () {
                                  document.cookie = k + "=temp" + ";max-age=0";
                              })()
                      },
                      init:function(){
                          this.bindEvent();
                      },
                      //事件绑定
                      bindEvent:function(){
                          var _this=this;
                          win.addEventListener("message",function(evt){
                              if(win.parent!=evt.source){return}
                              var options=JSON.parse(evt.data);
                              if(options.type=="GET"){
                                  var data=tools.getLocalCookie(options.key);
                                  win.parent.postMessage(data, "*");
                              }
                              options.type=="SET"&&_this.setLocalCookie(options.key,options.value);
                              options.type=="REM"&&_this.clearLocalData(options.key);
                          },false)
                      }
                  };
                  var tools=new fn();
                  tools.init();
              })(document,window);
              /**CURD本地存储信息 end**/
    
    • 前端页面js代码(客户端):
            //页面初始化向iframe域名发送消息
            window.onload = function() {
                console.log('get key value......................')
                window.frames[0].postMessage(JSON.stringify({type:"GET",key:"User-Token"}),'*');
            }
            //监听message信息,接收从iframe域下获取到的token信息,然后存储到localstorage或cookie中
            window.addEventListener('message', function(e) {
                console.log('listen.....');
                var data = e.data;
                console.log(data);
                if(data != null){
                    localStorage.setItem("User-Token", data);
                }
            }, false);
    

    总结:
    优点:在非跨域环境下使用JWT机制是一个非常不错的选择,实现方式简单,操作方便,能够快速实现。由于服务端不存储用户状态信息,因此大用户量,对后台服务也不会造成压力;
    缺点:跨域实现相对比较麻烦,安全性也有待探讨。因为JWT令牌返回到页面中,可以使用js获取到,如果遇到XSS攻击令牌可能会被盗取,在JWT还没超时的情况下,就会被获取到敏感数据信息。

    展开全文
  • 什么是单点登录(原理与实现简介)

    万次阅读 多人点赞 2019-06-12 11:15:30
    一、系统登录机制 1、http无状态协议  web应用采用browser/server架构,http作为通信协议。http是无状态协议,浏览器的每一次请求,服务器会独立处理,不与之前或之后的请求产生关联,这个过程用下图说明,三次...
  • 基础面试1:单点登录 vs 多点登录

    万次阅读 2020-11-04 21:09:39
    原来做过SSO单点登录,但是在某次面试过程中,面试官问到“多点登录”当时没有回答出,遂记录之; SSO(单点登录)的概念:在一个多系统共存的环境下,用户在一处登录后,就不用再其他系统中登录,也就是用户的一次...
  • 单点登录CAS笔记

    万次阅读 2020-09-05 10:27:57
    【5】cas接入REST登录认证 【6】通过Cas Proxy访问其它Cas应用 【7】CAS多属性返回 【1】CAS服务端 #配置hosts C:\Windows\System32\drivers\etc #下载地址 https://github.com/apereo/cas/releases #下载源码后,...

空空如也

1 2 3 4 5 ... 20
收藏数 22,063
精华内容 8,825
关键字:

单点登录