1. “游戏客户端”调用“SDK客户端”的登录功能向“SDK服务端”进行身份认证

  2. 验证通过后,“游戏客户端”可得到用户信息,根据游戏逻辑可将用户信息传给“游戏服务器”进行验证

  3. “游戏服务器”通过客户端传来的用户信息,一般还需要向“SDK服务器”请求验证用户信息

  4. 验证通过后,“游戏服务器”从数据库中查询用户信息,不存在的话直接插入新的用户信息,然后将验证结果和用户信息返回给“游戏客户端”

  5. 如果使用了登录会话管理,用户登录后会生成一个新的会话token字符串,把这个token传给“游戏客户端”即可,使用这个token即可查出对应的用户信息

  6. 在使用第三方账号登录时,需要为每个第三方账号ID建立一个新的玩家ID,使这两个ID产生关联即可

上图是一个典型的登录过程,关键点是游戏服务端如何做第三方玩家账号与游戏中玩家账号的数据库映射。 客户端代码比较简单,可以点此下载完整客户端登录代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void authenticate(final AuthenticationInterface authenticationInterface, 
	final AuthenticationRequest request)
    {
        // save the request
        m_authenticationInterface = authenticationInterface;
        m_request = request;
        
        // call third party login method
        OppoClientHelper m_oppoSdk = OppoClientHelper.getInstance();
        m_oppoSdk.setLoginCallback(new OppoLoginCallback()
        {
            @Override
            public void onSuccess(String userId, String accessToken) {
                doAuthenticate(userId, accessToken);
            }

            @Override
            public void onFailure() {
                m_authenticationInterface.onAuthenticationCancel();
            }
        });
        m_oppoSdk.sdkLogin();
    }

登录服务端采用Java的Servlet技术实现,下面是doPost部分代码。这里主要省略了从数据库中获取用户信息和从第三方SDK获取用户信息的代码。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
public class Authenticate extends HttpServlet
{
    public static class RequiredParameters{
        public String gameId;
        public String deviceId;
        public String userId;
        public String token;
    }
    
    public static class OppoUserInfo
    {
        @JsonIgnoreProperties({ "sex", "profilePictureUrl", "emailStatus", "email" })
        public static class BriefUser
        {
            public String getId() {
                return id;
            }
            public void setId(String id) {
                this.id = id;
            }
            public String getName() {
                return name;
            }
            public void setName(String name) {
                this.name = name;
            }

            private String id;
            private String name;
        }
        
        @JsonProperty("BriefUser")
        public BriefUser getBriefUser() {
            return briefUser;
        }
        public void setBriefUser(BriefUser briefUser) {
            this.briefUser = briefUser;
        }
        
        private BriefUser briefUser;
    }
    
    private static final long serialVersionUID = 1L;
    private static final String authenticateTypeName = "oppo";

    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
    	throws ServletException, IOException
    {    
        // check request parameters
        RequiredParameters parameters = new RequiredParameters();
        Map<String, String[]> parameterMap = request.getParameterMap();
        if (parameterMap.get("userId") == null || parameterMap.get("token") == null){
            AuthenticationUtilities.sendError(response, 
            	HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "internal error");
            AuthenticationUtilities.logWarning(authenticateTypeName, 
            	"wrong parameters:" + parameterMap.toString());
            return;
        }
        else{
            parameters.userId = parameterMap.get("userId").toString();
            parameters.token = parameterMap.get("token").toString();
        }
            
        try {
            // query userInfo from third party SDK
            String gcUserInfo = "{{jsonstring}}";
            
            // check returned JSON string from SDK
            ObjectMapper mapper = new ObjectMapper();
            OppoUserInfo userInfo = mapper.readValue(gcUserInfo, OppoUserInfo.class);
            if (!parameters.userId.equals(userInfo.getBriefUser().getId())){
                AuthenticationUtilities.sendError(response, 
                	HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "internal error");
                AuthenticationUtilities.logWarning(authenticateTypeName, 
                	"verify userId failed:" + parameters.userId.toString());
                return;
            }
            
            try{
                // do with database and get a returned token
                String authenticationToken = "";
                boolean databaseResult = false;
                if (!databaseResult){
                    AuthenticationUtilities.sendError(response, 
                    	HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "internal_error");
                    AuthenticationUtilities.logSevere(authenticateTypeName, 
                    	"database returned R_failure_internal_error.");
                    return;
                }
                
                // if success, send response to client
                response.getWriter().print("authenticationToken=" + authenticationToken);
            }
            catch (final Exception exception){
                AuthenticationUtilities.sendError(response, 
                	HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "internal_error");
                AuthenticationUtilities.logSevere(authenticateTypeName, exception.toString());
                return;
            }
        }
        catch (Exception e) {
            AuthenticationUtilities.sendError(response, 
            	HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "internal error");
            AuthenticationUtilities.logSevere(authenticateTypeName, e.getMessage());
            return;
        }
    }
}

下面是使用到的日志功能代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class AuthenticationUtilities 
{
    private static final Logger ms_logger = Logger.getLogger("spacetime-authentication");
    
    public static void sendError(final HttpServletResponse response, final int responseCode, 
    	final String errorMessage) throws IOException{
        response.setStatus(responseCode);
        response.getWriter().print(errorMessage);
    }

    public static void logInfo(final String authenticationTypeName, final String message){
        ms_logger.info(authenticationTypeName + ":" + message);
    }
    
    public static void logWarning(final String authenticationTypeName, final String message){
        ms_logger.warning(authenticationTypeName + ":" + message);
    }
    
    public static void logSevere(final String authenticationTypeName, final String message){
        ms_logger.severe(authenticationTypeName + ":" + message);
    }
}

这里对数据库的操作也很重要,下面是使用postgresql中用到的部分Sql代码。 如果用户第一次登录,还需要在数据库中建立新用户,然后产生会话token。如果可以查到用户信息,直接产生token并返回结果。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
CREATE TABLE authentication_token
(
    token UUID NOT NULL,
    account_id UUID NOT NULL,
    authentication_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
    PRIMARY KEY (token)
);

CREATE INDEX authentication_token_index_account_id ON authentication_token (account_id);
CREATE INDEX authentication_token_index_authentication_time ON authentication_token (authentication_time);

-- --------------------------------------------------------------------

CREATE TABLE auth_account
(
    account_id UUID NOT NULL,
    creation_time TIMESTAMP DEFAULT now() NOT NULL,
    PRIMARY KEY (account_id)
);

ALTER TABLE authentication_token ADD CONSTRAINT authentication_token_account_id_fkey FOREIGN KEY (account_id) REFERENCES auth_account (account_id) ON UPDATE CASCADE ON DELETE CASCADE;

-- ----------------------------------------------------------------------

CREATE TABLE spacetime_account_oppo
(
    oppo_account_id VARCHAR(64) NOT NULL,
    account_id UUID NOT NULL,
    creation_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
    PRIMARY KEY(oppo_account_id)
);

ALTER TABLE spacetime_account_oppo ADD CONSTRAINT spacetime_account_oppo_account_id_fkey FOREIGN KEY (account_id) REFERENCES auth_account (account_id) ON UPDATE CASCADE ON DELETE NO ACTION;
ALTER TABLE spacetime_account_oppo ADD CONSTRAINT spacetime_account_oppo_account_id_unique UNIQUE(account_id);

-- --------------------------------------------------------------------

CREATE OR REPLACE FUNCTION get_new_auth_account_id_0007
(
    v_account_id OUT UUID
)
RETURNS UUID
AS $$
BEGIN
    <<retry>>
    LOOP
        v_account_id := public.uuid_generate_v4();
        BEGIN
            INSERT INTO auth_account (account_id) VALUES (v_account_id);
            EXIT retry;
        EXCEPTION
            WHEN UNIQUE_VIOLATION THEN
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql; 

-- ----------------------------------------------------------------------

CREATE FUNCTION cleanup_expired_authentication_tokens_0001
(
)
RETURNS VOID
AS $$
BEGIN

    DELETE FROM authentication_token WHERE authentication_time < now() - interval '10 minutes';
    
END;
$$
LANGUAGE plpgsql;

-- --------------------------------------------------------------------

CREATE FUNCTION create_authentication_token_0001
(
    v_account_id IN UUID,
    v_token OUT UUID
)
RETURNS UUID
AS $$
BEGIN
    PERFORM cleanup_expired_authentication_tokens_0001();
    v_token := public.uuid_generate_v4();
    UPDATE
        authentication_token 
    SET
        token = v_token, 
        authentication_time = now()
    WHERE 
        account_id = v_account_id;
    IF NOT FOUND THEN
        BEGIN
            INSERT INTO authentication_token 
                (token, account_id) 
            VALUES 
                (v_token, v_account_id);
        EXCEPTION
            WHEN UNIQUE_VIOLATION THEN
                UPDATE 
                    authentication_token 
                SET 
                    token = v_token, 
                    authentication_time = now()
                WHERE 
                    account_id = v_account_id;
        END;
    END IF;

END;
$$
LANGUAGE plpgsql;

-- ----------------------------------------------------------------------

CREATE OR REPLACE FUNCTION get_authentication_token_oppo_0008
(
    v_oppoId IN VARCHAR,
    v_result OUT INTEGER,
    v_token OUT UUID
)
RETURNS RECORD
AS $$
DECLARE
    t_account_id UUID;
BEGIN

    SELECT account_id INTO t_account_id FROM spacetime_account_oppo WHERE oppo_account_id = v_oppoId;
    IF NOT FOUND THEN
        t_account_id := get_new_auth_account_id_0007();
        BEGIN
            INSERT INTO spacetime_account_oppo
                (oppo_account_id, account_id)
            VALUES
                (v_oppoId, t_account_id);
        EXCEPTION
            WHEN UNIQUE_VIOLATION THEN
                BEGIN
                    RAISE EXCEPTION 'spacetime_account_oppo: oppo_account_id [%] or account_id [%] already exist', v_oppoId, t_account_id;
                END;
        END;
    END IF;

    v_result := 0; -- R_success
    v_token := (SELECT f.v_token FROM create_authentication_token_0001(t_account_id) f);

END;
$$
LANGUAGE plpgsql;

-- --------------------------------------------------------------------