SSO单点登录(二)代码实现篇

服务认证中心

当用户从客户端发起用户认证请求的时候,服务认证中心作为统一的系统授权中心,承担了用户授权验证的作用,由于服务中心的授权操作,才能实现各级站点间的授权登录

客户端系统

客户端主要是承担各个站点的用户验证,他是用户登录的入口,但是他有不具有授权用户的能力(即不对外提供登录注册接口),当用户登录请求后(从服务端授权),客户端则用于与服务端的授权交互调用,并且对用户令牌进行缓存,只对含有缓存或者通过令牌请求登录的请求进行认证授权

  • SSO系统站点的配置策略

站点的概念在SSO系统中由为重要(如阿里巴巴的淘宝和天猫),只有经过我认证的系统站点,才认定为合法,才会对SSO系统下的站点进行授权认证;系统采用了较为简单的XML配置文件手动配置解析,文件结构如下:

<?xml version="1.0" encoding="UTF-8"?>

<webSites>
    <webSite id="1"
             callbackUrl="http://localhost:81/">
    </webSite>
    <webSite id="2"
             callbackUrl="http://localhost:82/">
    </webSite>
</webSites>

webSite为站点的内容,我这里采用通过id的方式来区分站点,callbackUrl即认证成功后的回调地址。


xml文件的解析方式采用了DOM(JAXP Crimson解析器)的解析方式:

/**
 * xml文件解析
 *  */
public class XmlParseUtil {

    public static HashMap<Long, GxAppSite> siteXmlParse(File file) {
        //创建DocumentBuilderFactory对象
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        try {
            //解析xml文件并对象化
            DocumentBuilder documentBuilder = factory.newDocumentBuilder();
            Document document = documentBuilder.parse(file);
            NodeList webSites = document.getElementsByTagName("webSite");
            //解析站点列表
            HashMap<Long, GxAppSite> appSiteHashMap = pareseToAppSite(webSites);

            return appSiteHashMap;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static HashMap<Long, GxAppSite> pareseToAppSite(NodeList webSites) {
        HashMap<Long, GxAppSite> appSiteHashMap = new HashMap<>(16);

        for (int i = 0; i < webSites.getLength(); i++) {
            Node node = webSites.item(i);

            //获取AppSite字段
            Long id = Long.valueOf(node.getAttributes().getNamedItem("id").getTextContent());
            String callbackUrl = node.getAttributes().getNamedItem("callbackUrl").getNodeValue();

            AppSite appSite = new AppSite(id, callbackUrl);
            appSiteHashMap.put(id, appSite);
        }

        return appSiteHashMap;
    }
}
  • SSO服务用户令牌生成策略(JWT)

sso系统一定是区别于传统的session会话的模式的,他所采用的是需要满足Http的无状态的性质,因此就需要采用一种合理的方式去解决Http协议的无状态的性质。
而JWT则为一种加密验证模式,他是在用户登录授权成功后,返回给用户一个带有服务器秘钥的用户令牌(token),而在之后对有用户登录状态验证的服务请求时,用户携带这个经加密处理后的用户令牌,服务端通过解密操作完成用户的认证

/**
     * 创建令牌
     *
     * @param appId  应用站点ID
     * @return GxSessionInfo
     */
    public synchronized static SessionInfo createToken(SessionInfo sessionInfo, Long appId) throws UnsupportedEncodingException {
        Long systemTime = System.currentTimeMillis();
        Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
        //JWT签名头部
        Map<String, Object> header = new HashMap<>(2);
        header.put("typ", TYPE);
        header.put("alg", ALGORITHM);

        //生成用户总token
        Map<String, String> tokenMap = sessionInfo.getTokenMap();

        if (tokenMap == null){
            tokenMap = new HashMap<>(16);
        }

        //服务器端生成sso-server token
        String split = "_";
        if (tokenMap.get(USER_ID + split + sessionInfo.gUserId()) == null){
            String serverToken = JWT.create()
                    .withClaim(USER_ID, sessionInfo.gUserId())//payload携带用户信息
                    .withClaim(USER_NAME,sessionInfo.gUserName())
                    .withHeader(header)
                    .withIssuer(ISSUER)//颁发令牌的用户
                    .withIssuedAt(new Date(systemTime))//记录生成时间
                    .sign(algorithm);//服务端秘钥
            tokenMap.put(USER_ID + split + sessionInfo.gUserId(), serverToken);
        }

        //客户端端生成client-server token
        if (appId != null && appId != 0L
                && tokenMap.get(APP_ID + split + appId) == null) {
            //生成站点token,并保留其余站点token信息
            String appToken = JWT.create()
                    .withClaim(USER_ID, sessionInfo.gUserId())
                    .withClaim(USER_NAME,sessionInfo.gUserName())
                    .withClaim(APP_ID, appId)
                    .withHeader(header)
                    .withIssuer(ISSUER)
                    .withIssuedAt(new Date(systemTime))
                    .sign(algorithm);
            tokenMap.put(APP_ID + split + appId, appToken);
        }

        sessionInfo.setTokenMap(tokenMap);
        return sessionInfo;
    }

    /**
     * 验证令牌是否合法
     *
     * @param token 用户令牌
     */
    public synchronized static long verifyToken(String token) throws UnsupportedEncodingException {
        Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
        JWTVerifier verifier = JWT.require(algorithm)
                .withIssuer(ISSUER)
                .build();
        DecodedJWT jwt = verifier.verify(token);
        Claim claim = jwt.getClaim(USER_ID);
        if (!claim.isNull()) {
            return claim.asLong();
        }
        return 0;
    }

    /**
     * 根据标签反解令牌信息
     */
    public static  long decodeToken(String token, String name) {
        if (StringUtils.isNotBlank(token)) {
            try {
                DecodedJWT jwt = JWT.decode(token);
                Claim claim = jwt.getClaim(name);
                if (!claim.isNull()) {
                    return claim.asLong();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return 0L;
    }

    /**
     * 根据标签反解令牌信息
     */
    public static String decodePayLoadToken(String token, String name) {
        if (StringUtils.isNotBlank(token)) {
            try {
                DecodedJWT jwt = JWT.decode(token);
                Claim claim = jwt.getClaim(name);
                if (!claim.isNull()) {
                    return claim.asString();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return "";
    }
  • SSO服务端认证流程

用户首先通过客户端的登录入口,提交用户名密码,客户端发现用户未进行登录(客户端无token缓存或checkToken请求时失效),前端请求直接跳到sso-server的login接口请求:


public SsoInfo login(String username, String password, Long appId) throws Exception {
        //动态表名登录用户
        Example example = new Example(UserEntity.class);
        example.and().andEqualTo("username", username);
        example.setTableName(DynamicTableConfig.tableName);
        List<GxUserEntity> userEntities = userMapper.selectByExample(example);

        if (userEntities.size() == 0) {
            throw new InvalidFieldException(null, "username", username, "用户不存在!");
        }

        if (userEntities.size() > 1) {
            throw new InvalidFieldException(null, "username", username, "用户名解析异常!");
        }
        UserEntity user = userEntities.get(0);

        if (!MD5Encrypt.valid(password, user.getPassword())) {
            throw new InvalidFieldException(null, "password", password, "用户账号或登录密码错误!");
        }

        HashMap<Long, GxAppSite> appSiteHashMap = null;
        if (appId != null && appId > 0) {
            // 验证应用站点是否为配置站点
            File file = ResourceUtils.getFile(fileUrl);
            appSiteHashMap = XmlParseUtil.siteXmlParse(file);
            if (appSiteHashMap!=null && !appSiteHashMap.containsKey(appId)) {
                //appId不存在则站点非法
                throw new InvalidFieldException(null, "appId", String.valueOf(appId), "应用站点不合法!");
            }
        }

        //建立缓存对象
        SessionInfo sessionInfo = new SessionInfo();
        //设置用户信息
        sessionInfo.sUserName(user.getUsername());
        sessionInfo.sUserId(user.getId());
        sessionInfo.setLoginTime(System.currentTimeMillis());

        //生成或者更新token
        TokenHelper.createToken(sessionInfo, appId);

        //生成服务端缓存
        getSessionUser().put(user.getId(), sessionInfo);

        //构造token返回对象
        SsoInfo ssoInfo = getSsoInfoResult(sessionInfo, appId, appSiteHashMap);

        //登录成功后定制化业务
        Map<String, AbstarctAuthListener> listenerManager = ListenerBeanContainer.getListenerManager();
        if (listenerManager.size() > 0) {
            AuthUser authUser = new AuthUser(sessionInfo.gUserId(),sessionInfo.gUserName());
            for (Map.Entry<String, AbstarctAuthListener> entry : listenerManager.entrySet()) {
                entry.getValue().afterLogin(authUser);
            }
        }

        logger.info("用户:" + user.getId() + ",于:" + DateUtil.getNow() + "登录认证中心成功!");
        return ssoInfo;
    }
    

认证中心完成授权登录后,将服务端生成的serverToken和clientToken一并返回给用户,并在服务端缓存(缓存策略可增加redis实现),客户端在获取到用户令牌后,前端发送验证返回token是否合法(增加token认证安全性,并将clientToken缓存到浏览器),之后的用户请求则只与站点交互,降低认证中心压力


 public SsoInfo loginToken(String token, Long appId) throws Exception {

        if (StringUtils.isBlank(token)) {
            throw new GxMissingFieldException(null, "token");
        }

        //判断缓存是否含有token
        SessionInfo sessionInfo = getSessionUser().check(token);
        if (sessionInfo == null) {
            throw new InvalidFieldException(null, "token", token, "用户认证失效!");
        }

        HashMap<Long, GxAppSite> appSiteHashMap = new HashMap<>(16);
        if (appId != null && appId > 0) {
            appSiteHashMap = checkAppInfo(appId);
        }

        //生成新的token或刷新token,刷新时效
        SessionInfo sessionInfo = GxTokenHelper.createToken(sessionInfo, appId);
        getSessionUser().put(sessionInfo.gUserId(), gxSessionInfo);
        return getSsoInfoResult(gxSessionInfo, appId, appSiteHashMap);
        
    }

客户端请求过loginToken验证令牌合法性时,通过远程接口调用去请求服务端的checkToken接口(只从缓存中拿取是否有当前token)

    @Override
    public AuthUser checkToken(String token) throws Exception {
        SessionInfo sessionInfo = getSessionUser().check(token);
        if (sessionInfo == null) {
            throw new InvalidFieldException(null, "token", token, "用户认证失效!");
        }
        return sessionInfo.getUser();
    }

这样就完成了一次用户登录授权请求。
当下一个站点B登录时,浏览器发现已经有认证中心的serverToken的令牌时,便携带serverToken与站点的AppId去请求服务端的loginToken接口,生成一个新的clientTokenB返回客户端,站点B则再去请求客户端loginToken接口来缓存clientTokenB到站点客户端
这个过程会有些繁琐,容易让人迷失,需要去细细品味整个授权登录的流程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值