单点登录(SSO)是目前比较流行的企业业务整合的解决方案之一,它的机制是在企业网络用户访问企业网站时作一次身份认证,随后就可以对所有被授权的网络资源进行无缝的访问,而不需要多次输入自己的认证信息.Web服务具有松散耦合、语言中立、平台无关性、开放性的特性,通过对集中单点登录模型的分析和比较,提出了基于Web服务和token-id的单点登录管理框架.介绍了该系统的体系机构及关键部分的工作原理.

本项目主要是采用spring boot +mysql 实现。重点描述如何重代码结构上实现 Web服务统一身份认证协议设计与实现功能。

主要功能如下

  1. token生成与管理
  2. 系统接入token 实现sso 单点登录
  3. 统一身份token 认证与校验
  4. 分布式系统接入sso模式
项目整体结构

统一身份认证系统开发教程 统一身份认证平台设计_服务器

一:sso 管理

原理,利用http 头部信息保存token 匹配服务器的session信息,进行校验。当用户首次登录系统,注册sso 信息

(1):注册sso token

public ResultPo register(String sessionId, String token, long uid) {
        String session = getSession(uid, token);
        if (session != null && session.equals(sessionId)) {
            ResultPo result = ResultPo.create(200, "sso token exist");
            SSoPo po = redisService.getPo(SSoPo.class, new MongoPrimaryKey("session_id", session));
            if (po != null) {
                po.setMtime(TimeUtils.Now());
                redisService.updatePo(po);
                result.appendData("sso", po);
                return result;
            }
        } else if (session != null) {
            this.removeSession(session, token);
        }

        SSoPo sso = new SSoPo();
        sso.setUid(uid);
        sso.setSessionId(sessionId);
        sso.setToken(token);
        sso.setMtime(TimeUtils.Now());
        SSoPo po = redisService.getPo(SSoPo.class, sso.getPrimaryKeys());
        if (po != null) {
            po.setMtime(TimeUtils.Now());
            redisService.updatePo(po);
            ResultPo result = ResultPo.create(200, "sso token exist");
            result.appendData("sso", sso);
            return result;
        }
        redisService.savePo(sso);
        ResultPo result = ResultPo.create(200, "sso register successful");
        result.appendData("sso", sso);
        redisService.hsetString(getSessionPool(token), uid + "", sessionId, sso.getClass().getAnnotation(Cache.class).timestamp());
        return result;
    }

    /**
     * token 生成器
     *
     * @return
     */
    public String generateTokeCode() {
        String value = System.currentTimeMillis() + new Random().nextInt() + "";
        System.out.println(value);

        long currentTime = System.currentTimeMillis();
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy年-MM月dd日-HH时mm分ss秒");
        Date date = new Date(currentTime);
        System.out.println(formatter.format(date));

        //获取数据指纹,指纹是唯一的
        try {
            MessageDigest md = MessageDigest.getInstance("md5");
            byte[] b = md.digest(value.getBytes());//产生数据的指纹
            //Base64编码
            BASE64Encoder be = new BASE64Encoder();
            be.encode(b);
            System.out.println(be.encode(b));
            return be.encode(b);//制定一个编码
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

把信息保存到中心服务中,把用户uid sessionid token 三者关系建立起来。

(2):校验sso

public ResultPo validateSession(String sessionId, String token) {
        SSoPo sso = redisService.getPo(SSoPo.class, new MongoPrimaryKey("session_id", sessionId));
        if (sso == null) {
            return ResultPo.create(ErrorCode.SESSION_ERROR, "session not found");
        }
        //过期
        boolean expr = TimeUtils.Now() > (sso.getMtime() + sso.getClass().getAnnotation(Cache.class).timestamp());
        if (expr) {
            removeSession(sso.getSessionId(), token);
            return ResultPo.create(ErrorCode.SESSION_ERROR, "session long old");
        }
        ResultPo po = ResultPo.create(200, "session validate successful");
        po.appendData("sso", sso);
        return po;
    }

用户在分布式系统中请求都携带此token 中,不用在此进行登录或者用户信息的校验,只需要通过token 和application所颁发的token 进行校验。

/**
     * 验证web 应用是否合法
     *
     * @param serverId
     * @param token
     * @return
     */
    public ResultPo validateApp(long serverId, String token) {
        HashMap<String, Object> params = new HashMap<>();
        params.put("serverId", serverId);
        params.put("token", token);
        String json = HttpUtil.getInstance(token).post("", this.url + "/sso/appValidate", params);
        return BaseUtils.getResultPo(json);
    }

(3):token 销毁

@Override
    public ResultPo removeSession(String sessionId, String token) {
        SSoPo sso = redisService.getPo(SSoPo.class, new MongoPrimaryKey("session_id", sessionId));
        if (sso == null) {
            return ResultPo.create(200, "");
        }
        redisService.deletePo(SSoPo.class, new MongoPrimaryKey("session_id", sessionId));
        redisService.deleteHField(getSessionPool(token), sso.getUid() + "");
        return ResultPo.create(200, "successful");
    }

二:服务器节点接入

服务节点的介入,任何系统授信信息都需要通过中心服务器颁发的token 与appid 进行注册校验,提供application的注册信息。颁发给每个系统。

(1):系统节点接入

应用节点定义:

@Document(collection = "server_node")
@Cache(cache = true)
public class ServerNode implements IMongoPo {

    @Field("server_id")
    @PrimaryKey(name = "server_id")
    @Indexed(name = "server_id", unique = true)  //索引  name为索引名称,unique=true,唯一索引
    private int serverId;
    @Field("type")
    private String type;
    @Field("name")
    private String name;
    @Field("url")
    private String url;
    @Field("token")
    private String token;
    @Field("port")
    private int port;
    @Field("weight")
    private int weight;
    @Field("status")
    private int status;

应用接入:

@Override
    public ResultPo register(ServerNode serverNode) {
        boolean successful = serverDao.add(serverNode);
        if (successful) {
            return ResultPo.create();
        }
        return ResultPo.create(500, "create app error");
    }

分系统接入
定义一个服务的主要信息

"sso": [
    {
      "id": 1,
      "url": "http://127.0.0.1",
      "port": 8524,
      "weight": 1,
      "token": "123456"
    }
  ],

应用校验

应用系统进行网络通讯的时候,首先会进行application校验。

@RequestMapping("appValidate")
    public ResultPo appValidate(HttpServletRequest request, long serverId, String token) {
        ResultPo resultpo = serverService.get(serverId);
        Object obj = resultpo.get("data");
        if (obj == null) {
            resultpo.setCode(500);
            resultpo.setMessage("application validate error!");
            return resultpo;
        }
        ServerNode node = (ServerNode) obj;
        if (!node.getToken().equals(token)) {
            resultpo.setCode(500);
            resultpo.setMessage("application validate error! token not march");
            return resultpo;
        }
        resultpo.setCode(200);
        resultpo.setMessage("application validate successful");
        return resultpo;
    }
package com.graduation.online.proxy;

import com.graduation.online.base.BaseUtils;
import com.graduation.online.base.enums.ServerType;
import com.graduation.online.base.model.ResultPo;
import com.graduation.online.base.service.ServerNodeService;
import com.graduation.online.base.utils.HttpUtil;

import java.util.HashMap;

public class SSoAgentServer extends BaseAgent {

    public SSoAgentServer() {
        super(ServerNodeService.getInstance().getServerNode(ServerType.SSO));
    }

    public ResultPo register(String sessionId, long uid) {
        HashMap<String, Object> params = new HashMap<>();
        params.put("sessionId", sessionId);
        params.put("token", token);
        params.put("uid", uid);
        String json = HttpUtil.getInstance(token).post(sessionId, this.url + "/sso/register", params);
        return BaseUtils.getResultPo(json);
    }


    /**
     * 验证系统用户session 同一身份
     *
     * @param sessionId
     * @return
     */
    public ResultPo validateSession(String sessionId) {
        HashMap<String, Object> params = new HashMap<>();
        params.put("sessionId", sessionId);
        params.put("token", token);
        String json = HttpUtil.getInstance(token).post(sessionId, this.url + "/sso/validate", params);
        return BaseUtils.getResultPo(json);
    }

    /**
     * 验证web 应用是否合法
     *
     * @param serverId
     * @param token
     * @return
     */
    public ResultPo validateApp(long serverId, String token) {
        HashMap<String, Object> params = new HashMap<>();
        params.put("serverId", serverId);
        params.put("token", token);
        String json = HttpUtil.getInstance(token).post("", this.url + "/sso/appValidate", params);
        return BaseUtils.getResultPo(json);
    }

    public long getSSoUId(String sessionId) {
        HashMap<String, Object> params = new HashMap<>();
        params.put("sessionId", sessionId);
        params.put("token", token);
        String json = HttpUtil.getInstance(token).post(sessionId, this.url + "/sso/uid", params);
        ResultPo result = BaseUtils.getResultPo(json);
        if (result.get("uid") == null) {
            return -1;
        }
        return Long.parseLong(result.get("uid").toString());
    }

    public String getSession(long uid) {
        HashMap<String, Object> params = new HashMap<>();
        params.put("uid", uid);
        params.put("token", token);
        String json = HttpUtil.getInstance(token).post(null, this.url + "/sso/getSession", params);
        ResultPo result = BaseUtils.getResultPo(json);
        if (result.get("sessionId") == null) {
            return "";
        }
        return result.get("sessionId").toString();
    }

    public ResultPo removeSession(String sessionId) {
        HashMap<String, Object> params = new HashMap<>();
        params.put("sessionId", sessionId);
        params.put("token", token);
        String json = HttpUtil.getInstance(token).post(sessionId, this.url + "/sso/remove", params);
        return BaseUtils.getResultPo(json);
    }
}

分系统用户进行登录时候,只需要在登录授权一次,既可以在任何系统进行数据读取,

三:简单演示

统一身份认证系统开发教程 统一身份认证平台设计_java_02

当用户登录系统后

统一身份认证系统开发教程 统一身份认证平台设计_服务器_03

后台sso 系统接收到用户认证信息

用户授权后,进入到系统

系统为应用授权

统一身份认证系统开发教程 统一身份认证平台设计_服务器_04

总结:本系统采用web 的sso 统一身份认知,利用一个简单的,实现web 认证管理,加密认证,分布式系统应用授权认证。

代码地址:git@github.com:twjitm/graduation-online-server-code.git