SSO单点登录的概述和实现机制

发布于 2022-03-27  1.61k 次阅读


单点登录:多系统,单一位置登录,实现多系统同时登录的一种技术

应用场景:常用于多模块的项目应用和企业级平台,系统分多个子系统通过单点登录完成全系统的授信

三方登录:某系统,使用其他系统的用户,实现本系统登录的方法,如:在京东中使用微信登录

三方登录解决的问题:

  1. 解决信息孤岛
  2. 用户信息不对等

一,身份认证机制

身份认证主要分两类:

  • 传统的Session身份认证
  • Token身份认证

一,传统身份认证机制

Http是一种无状态的协议,也就是它并不知道是谁访问了应用

把用户看成客户端,客户端使用用户名还有密码通过身份认证,不过因为Http的无状态,下一次用户发生请求时还得验证,这就非常麻烦

传统的解决方法:当用户请求登录的时候,如果没有问题,在服务端生成一条记录(也就是session对象),这个session对象可以说明登录的用户是谁,然后把这个session对象的ID号发给客户端,客户端收到以后把这个SessionID保存在Cookie里,下次这个用户再向服务器端发送请求的时候携带者这个Cookie,服务器端会验证这个Cookie里的信息,根据SessionID去匹配服务端的Session对象,如果找到了相应的对象就说明身份验证成功

  • Session+Cookie的方式验证

服务器的Session可能会存储在内存,磁盘,或者数据库中,我们需要定期清理过期的Session对象

传统Session+Cookie认证的缺点:

  1. Session导致服务端内存膨胀:每一次认证用户发起请求,服务器都会创建一个Session对象。当用户量过多,内存开销会不断增大
  2. Session跨域共享问题:在多个服务器中Session是不能共享的,需要通过其他框架
  3. CSRE(跨站请求伪造):如用户在访问银行网站时,他很容易受到跨站请求伪造的攻击,并且能够被利用访问其他网站

二,Token身份认证机制

使用基于Token的身份验证方法,在服务端不需要存储用户的登录信息

使用Token身份效验的过程:

  1. 客户端使用用户名,密码请求登录
  2. 服务端收到请求,去验证用户名,密码
  3. 验证成功后,服务端会签发一个Token,再把这个Token发送给客户端(浏览器)
  4. 客户端收到Token以后可以把它存储起来,比如放在Cookie里或者Local Storage,Session Storage
  5. 客户端每次向服务端请求资源的时候需要携带这个Token(可以提供Cookie或请求头携带)
  6. 服务端收到请求,如何验证客户端请求里面带着的Token如何验证成功就先用户返回数据(服务端效验是从第三方数据库中取数据如redis等,而不是从JVM或者本地内存中取)

注:Local Storage是本地存储空间一个服务端对于一个Local Storage,Session Storage是会话存储空间一次会话对应一个Session Storage,他们都存储在客户端

使用Token验证的优势:

  1. 无状态,可拓展:在客户端存储的Token是无状态的,并且能够被拓展,基于无状态和不存储Session信息,负载均衡器能够将用户信息从一个服务传到其他服务器上
  2. 安全性:请求中发送Token而不再发送Cookie能够防止CSRF(跨域请求伪造),即使在客户端使用Cookie存储token,Cookie也仅仅是一个存储机制而不是用于认证

二,JSON Web Token(JWT)机制

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

  • 紧凑:数据小,可以提供URL,POST/GET参数请求头发送,传输数据快
  • 自包含:使用payload数据块记录用户必要且不隐私的数据,可以有效的减少数据库访问次数

JWT的主要作用:

  • 用户身份验证:用户身份验证,一旦用户登录,每个后续请求都将包含JWT,允许用户访问该令牌允许的路由,服务和资源,单点登录就是使用了JWT做为数据传输和效验,因为它开销小,并且能轻松跨域
  • 数据信息交换:JWT是一种用于非常方便的多方传输(跨站传输)数据的载体,因为其可以使数据前面来保证数据的有效性和安全性

JWT的数据结构:A.B.C(中间由字符点.来分割三部分数据)

  1. A:header头信息
  2. B:payload数据块
  3. C:Signature签名(可以由于加密)

1,Header

Header数据结构(Json格式):{"alg":"加密算法名词","typ":"JWT"}

  • alg:加密算法定义内容,如:HMAC,SHA256或RSA
  • typ:是token类型,这里固定为JWT

2,payload

在payload数据块中一般用于记录实体(通常为用户信息)或其他数据

payload分为三部分:

  1. 已注册信息(registered claims)
  2. 公开数据(public claims)
  3. 私有数据(private claims)

1,已注册信息:iss(发行者),exp(到期时间),sub(主题),aud(受众)等

2,公开数据和私有数据:一般是在JWT注册表中增加定义,避免和已注册信息冲突

其中公开数据和私有数据可以由开发者自定义

注:即使JWT有签名加密机制,但是payload内容都是明文记录,除非记录是加密数据,否则不排除泄露隐私数据的可能,不推荐payload中记录任何敏感数据

3,Signature

签名信息,这是一个由开发者提供的信息,是服务器验证的传递数据是否安全有效的标准,在生成JWT最终数据之前,先使用header中定义的加密算法,将header和payload进行加密,并使用点进行连接,如:加密后的head和payload再使用相同的加密算法对加密后的数据和签名进行加密得到最终结果

JWT的执行流程:

三,基于JWT的登录案例

这个案例使用SSM来完成的,环境的搭建就不详细写了(案例地址)

结构:

依赖:

<!--servlet依赖-->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.0</version>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>
<dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>javax.servlet.jsp-api</artifactId>
    <version>2.3.3</version>
</dependency>
<!--spring依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>4.3.7.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>4.3.7.RELEASE</version>
</dependency>

<!--json依赖-->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.0</version>
</dependency>

<!--JWT核心依赖-->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.14.0</version>
</dependency>

<!--java开发JWT的依赖jar包-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

①JWTResponseData:返回给客户端的对象

//返回给客户端的对象
public class JWTResponseData {

    private  Integer Code;//状态码

    private Object data;//业务数据

    private String msg;//返回描述

    private String token;//身份标识
//get和set省略
}

②JWTResult:结果对象

public class JWTResult {

    //状态码
    private int Code;

    //是否成功
    private boolean success;

    //验证过程的PyeLoad数据
    private Claims claims;
//get和set省略
}

③JWTSubject:用户实体

//做为Subject数据使用,也就是PyeLoad数据中的Claims
public class JWTSubject {

    private String username;
//get和set,有参和无参省略
}

④JWTUsers:使用Map集合模拟数据库用户并进行用户判断

//模拟数据库用户
public class JWTUsers {
    //使用map模拟数据库
    private static final Map<String,String> USERS = new HashMap<String, String>();

    //初始化USERS增加10个用户
    static {
        for(int i=1;i<=10;i++){
            USERS.put("UserAdmin"+i,"PassWord"+i);
        }}

    public static boolean isLogin(String username,String password){

        //用户为空
        if(username.length()<=0){
            return false;
        }
        String passwords = USERS.get(username);

        //判断密码是否正确
        if(passwords!=null){
            if (passwords.equals(password)){
            return true;
            }
        }

        return false;
    }
}

⑤JWTUtils:核心部分

//JWT工具类(核心部分)
public class JWTUtils {


    //服务器的key,用于做加解密的key数据,如果可以使用客户端生成的key当前定义可以不使用
    private static final String JWT_SECERT = "test_jwt_secert";


    //ObjectMapper用于java对象和json转化的集合
    private static final ObjectMapper MAPPER = new ObjectMapper();


    //自定义状态码
    private static final int JWT_ERRCODE_EXPRRE = 1000;//Token过期
    private static final int JWT_ERRCODE_FAIL = 1001;//验证不通过


    public static SecretKey generalKey(){
        try{
            //将服务器的key转化为byte数组
            byte[] encodeKey = JWT_SECERT.getBytes("UTF-8");
            //通过服务端key进行加密获取加密的密匙,AES是加密方式
            SecretKeySpec aes = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES");
            return aes;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }
    /*
    * 签发JWT,创建token的方法
    * id:jwt的唯一身份标识,主要用于一次性token,从而回避攻击
    * iss:jwt签发者
    * subject:jwt所面向的用户,pyeload中记录的public Claims 当前环境就是用户登录名
    * ttlMillis:token的有效期(单位:毫秒)
    * token:token是一次性的,是为一个用户的有效登录期准备的一个token,用户退出或者超时,token失效
    * */
    public static String CreateJWT(String id,String iss,String subject,long ttlMillis){
        //加密算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //当前时间
        long nowMillis = System.currentTimeMillis();
        //当前时间的日期对象
        Date date = new Date(nowMillis);
        //获取服务端加密的密钥
        SecretKey secretKey = generalKey();
        //创建JWT的构建器,就是使用指定信息和加密算法,生成Token令牌
        JwtBuilder builder = Jwts.builder()
                .setId(id)
                .setIssuer(iss)
                .setSubject(subject)
                .setIssuedAt(date)//token的生成时间
                .signWith(signatureAlgorithm,secretKey);//设置密钥和算法
        if(ttlMillis>=0){
            long expMillis = nowMillis + ttlMillis;//当前时间+失效时间
            Date date1 = new Date(expMillis);
            builder.setExpiration(date1);//token的失效时间
        }
        return  builder.compact();//生成token
    }


    //效验Token
    public static JWTResult validataJWT(String jwsStr){
        JWTResult result  = new JWTResult();
        Claims claims = null;
        try {
            claims = parseJWT(jwsStr);
            result.setCode(200);
            result.setSuccess(true);
            result.setClaims(claims);
        }catch (ExpiredJwtException e){//token超时异常
            result.setCode(JWT_ERRCODE_EXPRRE);
            result.setSuccess(false);
        }catch (SignatureException e){//效验异常
            result.setSuccess(false);
            result.setCode(JWT_ERRCODE_FAIL);
        }catch (Exception e){//其他异常,JWT提供了很多异常,我这里只写了两个
            result.setSuccess(false);
            result.setCode(JWT_ERRCODE_FAIL);
        }
        return result;
    }


    //解析JWT字符串
    public static Claims parseJWT(String jwt){
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();//获取Token中的PyeLoad数据
    }


    //生成subject信息(subObj是要转化的对象 java对象 -> JSON字符串)
    public static String generalSubject(Object subObj){
        try {
            return MAPPER.writeValueAsString(subObj);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
}

⑥Controller

@Controller
public class JWTController {

    @RequestMapping("/test")
    @ResponseBody
    public Object test(HttpServletRequest request){
        //获取请求头中携带的Token
        String Token = request.getHeader("Authorization");
        //效验Token
        JWTResult result = JWTUtils.validataJWT(Token);

        JWTResponseData responseData = new JWTResponseData();

        //效验成功
        if(result.isSuccess()){
            responseData.setCode(200);
            responseData.setData(result.getClaims().getSubject());
            //更新Token,超时时间为1分钟
            String jwt = JWTUtils.CreateJWT(result.getClaims().getId(), "WQL_SSO", result.getClaims().getSubject(), 1 * 60 * 1000);
            responseData.setToken(jwt);
            responseData.setMsg("效验成功!");
            return responseData;
        }else {
            responseData.setCode(500);
            responseData.setMsg("用户未登录!");
            return request;
        }
    }


    @RequestMapping("/login")
    @ResponseBody
    public Object login(String username,String password){
        JWTResponseData responseData;
        System.out.print(username+"\n"+password);
        if(JWTUsers.isLogin(username,password)){
            //Subject转化
            JWTSubject jwtSubject = new JWTSubject(username);
            //创建一个Token
            String jwtToken = JWTUtils.CreateJWT(UUID.randomUUID().toString(), "WQL_SSO", JWTUtils.generalSubject(jwtSubject), 1 * 60 * 1000);
            responseData = new JWTResponseData();
            responseData.setCode(200);
            responseData.setData(null);
            responseData.setMsg("登录成功");
            responseData.setToken(jwtToken);
        }else {
            responseData = new JWTResponseData();
            responseData.setCode(500);
            responseData.setData(null);
            responseData.setMsg("登录失败");
            responseData.setToken(null);
        }
        return responseData;
    }
}

⑦ 前端

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>$Title$</title>
  </head>
  <body>
<table>
    <tr>
     用户: <input type="text" name="username" id="username"/>
    </tr>

    <tr>
      密码:<input type="text" name="password" id="password"/>
    </tr>

    <tr>
      <td style="text-align: right;padding-right: 5px" colspan="2"></td>
      <input type="button" value="登录" onclick="login()"/>
    </tr>
  </table>
  <input type="button" value="测试是否有登录信息" onclick="testLocalStorage()">
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script type="application/javascript">

  function login() {
    var name = $("#username").val();
    var word = $("#password").val();
    var data = {username:name,password:word};
    $.ajax({
      url:"${pageContext.request.contextPath}/login",
      type:"GET",
      dataType:"json",
      data:data,
      success:function (datas) {
      if(datas.code==200){
        var token = datas.token;
        var localStorage = window.localStorage;//获取localStorage对象用于存储token
        localStorage.token=token;//存储token
      alert("登录成功")
      }else {
        alert("登录失败")
      }}})}

    function testLocalStorage() {
      $.ajax({
        url:"${pageContext.request.contextPath}/test",
        success:function (data) {
          if(data.code==200){
            window.localStorage.token = data.token;
            alert(data.data)
          }else {
            alert(data.msg)
          }},
        beforeSend:function (request) {//请求时头携带token
          request.setRequestHeader("Authorization",window.localStorage.token);
        }
      })};
</script>
</body>
</html>
测试:

① 登录成功后本地的LocalStorage有token

② 请求时Header会携带Authorization

 

 


路漫漫其修远兮,吾将上下而求索