面试权威指南

面试权威指南 查看完整档案

其它编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

人生太苦,只有学习是快乐的.

个人动态

面试权威指南 收藏了文章 · 2020-05-13

Nginx实现JWT验证-基于OpenResty实现

介绍

权限认证是接口开发中不可避免的问题,权限认证包括两个方面

  1. 接口需要知道调用的用户是谁
  2. 接口需要知道该用户是否有权限调用

第1个问题偏向于架构,第2个问题更偏向于业务,因此考虑在架构层解决第1个问题,以达到以下目的

  1. 所有请求被保护的接口保证是合法的(已经认证过的用户)
  2. 接口可以从请求头中获取当前用户信息
  3. 每个请求都有uuid用于标识

JWT(JSON Web Token)目前是应用最广的接口权限方案,具有无状态,跨系统,多语言多平台支持等特点,如果能在网关层实现JWT验证不仅可以避免代码入侵还能为整个后台提供统一的解决方案,目前客户网关使用Nginx,但社区版Nginx中没有JWT模块,自己实现不现实,因此选择OpenResty作为网关层, 据官网介绍,OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。本质上就是一个Nginx+Lua的集成软件.
整体架构如图:
整体架构

实现

环境

[root@docker ~]# cat /etc/redhat-release
CentOS Linux release 7.4.1708 (Core)
[root@docker ~]# more /proc/version
Linux version 3.10.0-693.el7.x86_64 (builder@kbuilder.dev.centos.org) (gcc version 4.8.5 20150623 (Red Hat 4.
8.5-16) (GCC) ) #1 SMP Tue Aug 22 21:09:27 UTC 2017

安装OpenResty

OpenRestry安装很简单,可以在这里找到不同版本操作系统安装文档,本次使用的环境是CentOS Linux release 7.4

[root@docker ~]# yum install yum-utils
[root@docker ~]# yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
[root@docker ~]# yum install openresty
[root@docker ~]# yum install openresty-resty

系统默认安装在/usr/local/openresty/目录下,版本如下

[root@docker openresty]# cd /usr/local/openresty/bin/
[root@docker bin]# ./openresty -v
nginx version: openresty/1.13.6.2

可以将OpenResty目录加到PATH里,方便使用.

修改nginx.conf文件测试是否安装成功

tee /usr/local/openresty/nginx/conf/nginx.conf <<-'EOF'
worker_processes  1;
error_log logs/error.log;
events {
    worker_connections 1024;
}
http {
    server {
        listen 8080;
        location / {
            default_type text/html;
            content_by_lua '
                ngx.say("<p>hello, world</p>")
            ';
        }
    }
}
EOF
[root@docker conf]# openresty -s reload
[root@docker conf]# curl localhost:8080
<p>hello, world</p>

安装JWT模块

这里使用JWT官方推荐Lua实现库,项目地址为https://github.com/SkyLothar/...,有趣的是,这个项目的介绍是这么写的JWT For The Great Openresty,看来是为OpenResty量身定做.github上有安装教程,但一方面有些第三方库的安装文档没有提及,另一方面有些内容没有用到安装的时候可以跳过,这里将完整安装步骤重新整理了下.

  1. 在release页面https://github.com/SkyLothar/...下载项目源码,截止到目前最新版为v0.1.11
  2. 下载hmac源码,截止到目前项目还未release,只能下载项目里的源文件https://github.com/jkeys089/l...
  3. 在服务器创建目录/usr/local/openresty/nginx/jwt-lua/resty,将第1步压缩包中目录lua-resty-jwt-0.1.11/lib/resty/下的所有lua文件和第2步中的hmac.lua文件拷贝到该目录下,文件列表如下.
[root@docker resty]# pwd
/usr/local/openresty/nginx/jwt-lua/resty
[root@docker resty]# ll
total 60
-rwxr-xr-x. 1 root root 11592 Jul 18 10:40 evp.lua
-rw-r--r--. 1 root root  3796 Jul 18 10:40 hmac.lua
-rwxr-xr-x. 1 root root 27222 Jul 18 10:40 jwt.lua
-rwxr-xr-x. 1 root root 15257 Jul 18 10:40 jwt-validators.lua

修改nginx.conf验证是否生效

tee /usr/local/openresty/nginx/conf/nginx.conf <<-'EOF'
worker_processes  1;
error_log logs/error.log info;
events {
    worker_connections 1024;
}
http {
    lua_package_path "/usr/local/openresty/nginx/jwt-lua/?.lua;;";
    server {
        listen 8080;
        default_type text/plain;
        location = / {
            content_by_lua '
                local cjson = require "cjson"
                local jwt = require "resty.jwt"

                local jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" ..
                    ".eyJmb28iOiJiYXIifQ" ..
                    ".VAoRL1IU0nOguxURF2ZcKR0SGKE1gCbqwyh8u2MLAyY"
                local jwt_obj = jwt:verify("lua-resty-jwt", jwt_token)
                ngx.say(cjson.encode(jwt_obj))
            ';
        }
        location = /sign {
            content_by_lua '
                local cjson = require "cjson"
                local jwt = require "resty.jwt"

                local jwt_token = jwt:sign(
                    "lua-resty-jwt",
                    {
                        header={typ="JWT", alg="HS256"},
                        payload={foo="bar"}
                    }
                )
                ngx.say(jwt_token)
            ';
        }
    }
}
EOF
[root@docker resty]# curl localhost:8080
{"signature":"VAoRL1IU0nOguxURF2ZcKR0SGKE1gCbqwyh8u2MLAyY","reason":"everything is awesome~ :p","valid":true,"raw_header":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9","payload":{"foo":"bar"},"header":{"alg":"HS256","typ":"JWT"},"verified":true,"raw_payload":"eyJmb28iOiJiYXIifQ"}

验证通过,jwt模块安装完毕

自定义验证逻辑

上面jwt模块还无法用于生产环境,有几个问题没解决

  1. jwt token目前是写死在配置文件里,生产需要从header Authorization中获取
  2. 验证失败目前返回是200生产需要返回401
  3. 需要配置反向代理并且将用户信息放在代理header上

创建文件/usr/local/openresty/nginx/jwt-lua/resty/nginx-jwt.lua

local jwt = require "resty.jwt"
local cjson = require "cjson"
--your secret
local secret = "5pil6aOO5YaN576O5Lmf5q+U5LiN5LiK5bCP6ZuF55qE56yR"

local M = {}

function M.auth(claim_specs)
    -- require Authorization request header
    local auth_header = ngx.var.http_Authorization

    if auth_header == nil then
        ngx.log(ngx.WARN, "No Authorization header")
        ngx.exit(ngx.HTTP_UNAUTHORIZED)
    end

    ngx.log(ngx.INFO, "Authorization: " .. auth_header)

    -- require Bearer token
    local _, _, token = string.find(auth_header, "Bearer%s+(.+)")

    if token == nil then
        ngx.log(ngx.WARN, "Missing token")
        ngx.exit(ngx.HTTP_UNAUTHORIZED)
    end

    ngx.log(ngx.INFO, "Token: " .. token)

    local jwt_obj = jwt:verify(secret, token)
    if jwt_obj.verified == false then
        ngx.log(ngx.WARN, "Invalid token: ".. jwt_obj.reason)
        ngx.exit(ngx.HTTP_UNAUTHORIZED)
    end

    ngx.log(ngx.INFO, "JWT: " .. cjson.encode(jwt_obj))

    -- write the uid variable
    ngx.var.uid = jwt_obj.payload.sub
end

return M

修改配置文件nginx.conf

worker_processes  1;
error_log logs/error.log info;
events {
    worker_connections 1024;
}
http {
    upstream tomcat{
     server localhost:80;
    }
    lua_package_path "/usr/local/openresty/nginx/jwt-lua/?.lua;;";
    server {
        listen 8080;
        set $uid '';
        location / {
            access_by_lua '
            local jwt = require("resty.nginx-jwt")
            jwt.auth()
        ';
            default_type application/json;
            proxy_set_header uid $uid;
            proxy_pass http://tomcat;
        }
    }
}

这里后台启动了一台tomcat并设置监听端口为80,tomcat上部署了一个示例的war包,代码逻辑较简单,就是输出所有的header,代码如下:

package asan.demo;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import javax.servlet.*;
import javax.servlet.http.*;

public class JWTDemoService extends HttpServlet {
    private static final String CONTENT_TYPE = "text/html; charset=UTF-8";

    public void init(ServletConfig config) throws ServletException {
        super.init(config);
    }

    public void service(HttpServletRequest request,
                        HttpServletResponse response) throws ServletException,
                                                             IOException {
        response.setContentType(CONTENT_TYPE);
        PrintWriter out = response.getWriter();
        Enumeration em=request.getHeaderNames();
        while(em.hasMoreElements()){
            String key=(String)em.nextElement();
            String value=(String)request.getHeaders(key).nextElement();
            out.println(String.format("%s ==> %s", key,value));
        }
        out.close();
    }
}

重启OpenResty测试,如果没有指定jwt token信息返回401

[root@docker conf]# curl http://localhost:8080/jwtdemo/service
<html>
<head><title>401 Authorization Required</title></head>
<body bgcolor="white">
<center><h1>401 Authorization Required</h1></center>
<hr><center>openresty/1.13.6.2</center>
</body>
</html>

指定jwt token

[root@docker conf]# curl -i http://localhost:8080/jwtdemo/ -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ5YXlhIiwiaWF0IjoxNTMxODkyNzE3LCJpc3MiOiJ5YXlhIiwic3ViIjoieWF5YSIsImV4cCI6MTUzMTkyODcxN30.W5UXlwKHSrpUAYbfoF-fTBTS9Enm1wsvCKNQm0yLSfQ'
HTTP/1.1 200
Server: openresty/1.13.6.2
Date: Wed, 18 Jul 2018 05:52:13 GMT
Content-Type: text/html;charset=UTF-8
Content-Length: 298
Connection: keep-alive

uid ==> yaya
host ==> tomcat
connection ==> close
user-agent ==> curl/7.29.0
accept ==> */*
authorization ==> Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ5YXlhIiwiaWF0IjoxNTMxODkyNzE3LCJpc3MiOiJ5YXlhIiwic3ViIjoieWF5YSIsImV4cCI6MTUzMTkyODcxN30.W5UXlwKHSrpUAYbfoF-fTBTS9Enm1wsvCKNQm0yLSfQ

从结果上看,后台服务已经获取到uid这个header
至于请求用到jwt token可以从任意平台生成只要保证secret一样即可,根据官网介绍,该库目前支持到jwt生成算法如图:

uuid生成

为每个请求生成唯一的uuid码可以将网关层上的请求和应用层的请求关联起来,对排查问题,接口统计都非常有用.

创建文件/usr/local/openresty/nginx/jwt-lua/resty/uuid.lua
local M  = {}
local charset = {}  do -- [0-9a-zA-Z]
    for c = 48, 57  do table.insert(charset, string.char(c)) end
    for c = 65, 90  do table.insert(charset, string.char(c)) end
    for c = 97, 122 do table.insert(charset, string.char(c)) end
end
function M.uuid(length)
        local res = ""
        for i = 1, length do
                res = res .. charset[math.random(1, #charset)]
        end
        return res
end
return M
修改配置文件nginx.conf
worker_processes  1;
error_log logs/error.log info;
events {
    worker_connections 1024;
}
http {
    upstream tomcat{
     server localhost:80;
    }
    lua_package_path "/usr/local/openresty/nginx/jwt-lua/?.lua;;";
    server {
        listen 8080;
        set $uid '';
        set $uuid '';
        location / {
            access_by_lua '
            local jwt = require("resty.nginx-jwt")
            jwt.auth()
            local u = require("resty.uuid")
            ngx.var.uuid = u.uuid(64)
        ';
            default_type application/json;
            proxy_set_header uid $uid;
            proxy_set_header uuid $uuid;
            proxy_pass http://tomcat;
        }
    }
}

重启OpenResty,测试

[root@docker conf]# openresty -s reload
[root@docker conf]# curl -i http://localhost:8080/jwtdemo/ -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ5YXlhIiwiaWF0IjoxNTMxODk0MDA3LCJpc3MiOiJ5YXlhIiwic3ViIjoieWF5YSIsImV4cCI6MTUzMTkzMDAwN30.vQvpQpIHCmK5QBgIoRR8jhIGeYlHOMYySIr4gHvoZFE'
HTTP/1.1 200
Server: openresty/1.13.6.2
Date: Wed, 18 Jul 2018 08:05:45 GMT
Content-Type: text/html;charset=UTF-8
Content-Length: 372
Connection: keep-alive

uid ==> yaya
uuid ==> nhak5eLjQZ73yhAyHLTgZnSBeDa8pa1p3pcpBFvJ4Mv1fkY782UgVr8Islheq03l
host ==> tomcat
connection ==> close
user-agent ==> curl/7.29.0
accept ==> */*
authorization ==> Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ5YXlhIiwiaWF0IjoxNTMxODk0MDA3LCJpc3MiOiJ5YXlhIiwic3ViIjoieWF5YSIsImV4cCI6MTUzMTkzMDAwN30.vQvpQpIHCmK5QBgIoRR8jhIGeYlHOMYySIr4gHvoZFE

可以看到,多了一个uuid的请求头

jwt token生成java示例

这里提供一个生成jwt token的java示例

package com.yaya;

import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description:
 * @author: jianfeng.zheng
 * @since: 2018/7/5 下午9:56
 * @history: 1.2018/7/5 created by jianfeng.zheng
 */
public class JWTDemo {
    
    public static final String SECRET="5pil6aOO5YaN576O5Lmf5q+U5LiN5LiK5bCP6ZuF55qE56yR";
    
    public static String createJWT(String uid, long ttlMillis) throws Exception {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        Key signingKey = new SecretKeySpec(SECRET.getBytes(), signatureAlgorithm.getJcaName());

        Map<String,Object> header=new HashMap<String,Object>();
        header.put("typ","JWT");
        header.put("alg","HS256");
        JwtBuilder builder = Jwts.builder().setId(uid)
                .setIssuedAt(now)
                .setIssuer(uid)
                .setSubject(uid)
                .setHeader(header)
                .signWith(signatureAlgorithm, signingKey);
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }
        return builder.compact();
    }

    public static void main(String[]cmd) throws Exception {
        String s=createJWT("yaya",36000000);
        System.out.println("Bearer "+s);
    }

}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.yaya</groupId>
    <artifactId>jwtdemo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.6.0</version>
        </dependency>
    </dependencies>
</project>

写在最后

这里只是解决了文章开头提到的第一个问题,接口需要知道是谁调用了接口,第二个问题,用户能不能调接口目前考虑用aop在应用层实现,后续也会继续更新.

查看原文

面试权威指南 赞了文章 · 2020-05-13

Nginx实现JWT验证-基于OpenResty实现

介绍

权限认证是接口开发中不可避免的问题,权限认证包括两个方面

  1. 接口需要知道调用的用户是谁
  2. 接口需要知道该用户是否有权限调用

第1个问题偏向于架构,第2个问题更偏向于业务,因此考虑在架构层解决第1个问题,以达到以下目的

  1. 所有请求被保护的接口保证是合法的(已经认证过的用户)
  2. 接口可以从请求头中获取当前用户信息
  3. 每个请求都有uuid用于标识

JWT(JSON Web Token)目前是应用最广的接口权限方案,具有无状态,跨系统,多语言多平台支持等特点,如果能在网关层实现JWT验证不仅可以避免代码入侵还能为整个后台提供统一的解决方案,目前客户网关使用Nginx,但社区版Nginx中没有JWT模块,自己实现不现实,因此选择OpenResty作为网关层, 据官网介绍,OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。本质上就是一个Nginx+Lua的集成软件.
整体架构如图:
整体架构

实现

环境

[root@docker ~]# cat /etc/redhat-release
CentOS Linux release 7.4.1708 (Core)
[root@docker ~]# more /proc/version
Linux version 3.10.0-693.el7.x86_64 (builder@kbuilder.dev.centos.org) (gcc version 4.8.5 20150623 (Red Hat 4.
8.5-16) (GCC) ) #1 SMP Tue Aug 22 21:09:27 UTC 2017

安装OpenResty

OpenRestry安装很简单,可以在这里找到不同版本操作系统安装文档,本次使用的环境是CentOS Linux release 7.4

[root@docker ~]# yum install yum-utils
[root@docker ~]# yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
[root@docker ~]# yum install openresty
[root@docker ~]# yum install openresty-resty

系统默认安装在/usr/local/openresty/目录下,版本如下

[root@docker openresty]# cd /usr/local/openresty/bin/
[root@docker bin]# ./openresty -v
nginx version: openresty/1.13.6.2

可以将OpenResty目录加到PATH里,方便使用.

修改nginx.conf文件测试是否安装成功

tee /usr/local/openresty/nginx/conf/nginx.conf <<-'EOF'
worker_processes  1;
error_log logs/error.log;
events {
    worker_connections 1024;
}
http {
    server {
        listen 8080;
        location / {
            default_type text/html;
            content_by_lua '
                ngx.say("<p>hello, world</p>")
            ';
        }
    }
}
EOF
[root@docker conf]# openresty -s reload
[root@docker conf]# curl localhost:8080
<p>hello, world</p>

安装JWT模块

这里使用JWT官方推荐Lua实现库,项目地址为https://github.com/SkyLothar/...,有趣的是,这个项目的介绍是这么写的JWT For The Great Openresty,看来是为OpenResty量身定做.github上有安装教程,但一方面有些第三方库的安装文档没有提及,另一方面有些内容没有用到安装的时候可以跳过,这里将完整安装步骤重新整理了下.

  1. 在release页面https://github.com/SkyLothar/...下载项目源码,截止到目前最新版为v0.1.11
  2. 下载hmac源码,截止到目前项目还未release,只能下载项目里的源文件https://github.com/jkeys089/l...
  3. 在服务器创建目录/usr/local/openresty/nginx/jwt-lua/resty,将第1步压缩包中目录lua-resty-jwt-0.1.11/lib/resty/下的所有lua文件和第2步中的hmac.lua文件拷贝到该目录下,文件列表如下.
[root@docker resty]# pwd
/usr/local/openresty/nginx/jwt-lua/resty
[root@docker resty]# ll
total 60
-rwxr-xr-x. 1 root root 11592 Jul 18 10:40 evp.lua
-rw-r--r--. 1 root root  3796 Jul 18 10:40 hmac.lua
-rwxr-xr-x. 1 root root 27222 Jul 18 10:40 jwt.lua
-rwxr-xr-x. 1 root root 15257 Jul 18 10:40 jwt-validators.lua

修改nginx.conf验证是否生效

tee /usr/local/openresty/nginx/conf/nginx.conf <<-'EOF'
worker_processes  1;
error_log logs/error.log info;
events {
    worker_connections 1024;
}
http {
    lua_package_path "/usr/local/openresty/nginx/jwt-lua/?.lua;;";
    server {
        listen 8080;
        default_type text/plain;
        location = / {
            content_by_lua '
                local cjson = require "cjson"
                local jwt = require "resty.jwt"

                local jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" ..
                    ".eyJmb28iOiJiYXIifQ" ..
                    ".VAoRL1IU0nOguxURF2ZcKR0SGKE1gCbqwyh8u2MLAyY"
                local jwt_obj = jwt:verify("lua-resty-jwt", jwt_token)
                ngx.say(cjson.encode(jwt_obj))
            ';
        }
        location = /sign {
            content_by_lua '
                local cjson = require "cjson"
                local jwt = require "resty.jwt"

                local jwt_token = jwt:sign(
                    "lua-resty-jwt",
                    {
                        header={typ="JWT", alg="HS256"},
                        payload={foo="bar"}
                    }
                )
                ngx.say(jwt_token)
            ';
        }
    }
}
EOF
[root@docker resty]# curl localhost:8080
{"signature":"VAoRL1IU0nOguxURF2ZcKR0SGKE1gCbqwyh8u2MLAyY","reason":"everything is awesome~ :p","valid":true,"raw_header":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9","payload":{"foo":"bar"},"header":{"alg":"HS256","typ":"JWT"},"verified":true,"raw_payload":"eyJmb28iOiJiYXIifQ"}

验证通过,jwt模块安装完毕

自定义验证逻辑

上面jwt模块还无法用于生产环境,有几个问题没解决

  1. jwt token目前是写死在配置文件里,生产需要从header Authorization中获取
  2. 验证失败目前返回是200生产需要返回401
  3. 需要配置反向代理并且将用户信息放在代理header上

创建文件/usr/local/openresty/nginx/jwt-lua/resty/nginx-jwt.lua

local jwt = require "resty.jwt"
local cjson = require "cjson"
--your secret
local secret = "5pil6aOO5YaN576O5Lmf5q+U5LiN5LiK5bCP6ZuF55qE56yR"

local M = {}

function M.auth(claim_specs)
    -- require Authorization request header
    local auth_header = ngx.var.http_Authorization

    if auth_header == nil then
        ngx.log(ngx.WARN, "No Authorization header")
        ngx.exit(ngx.HTTP_UNAUTHORIZED)
    end

    ngx.log(ngx.INFO, "Authorization: " .. auth_header)

    -- require Bearer token
    local _, _, token = string.find(auth_header, "Bearer%s+(.+)")

    if token == nil then
        ngx.log(ngx.WARN, "Missing token")
        ngx.exit(ngx.HTTP_UNAUTHORIZED)
    end

    ngx.log(ngx.INFO, "Token: " .. token)

    local jwt_obj = jwt:verify(secret, token)
    if jwt_obj.verified == false then
        ngx.log(ngx.WARN, "Invalid token: ".. jwt_obj.reason)
        ngx.exit(ngx.HTTP_UNAUTHORIZED)
    end

    ngx.log(ngx.INFO, "JWT: " .. cjson.encode(jwt_obj))

    -- write the uid variable
    ngx.var.uid = jwt_obj.payload.sub
end

return M

修改配置文件nginx.conf

worker_processes  1;
error_log logs/error.log info;
events {
    worker_connections 1024;
}
http {
    upstream tomcat{
     server localhost:80;
    }
    lua_package_path "/usr/local/openresty/nginx/jwt-lua/?.lua;;";
    server {
        listen 8080;
        set $uid '';
        location / {
            access_by_lua '
            local jwt = require("resty.nginx-jwt")
            jwt.auth()
        ';
            default_type application/json;
            proxy_set_header uid $uid;
            proxy_pass http://tomcat;
        }
    }
}

这里后台启动了一台tomcat并设置监听端口为80,tomcat上部署了一个示例的war包,代码逻辑较简单,就是输出所有的header,代码如下:

package asan.demo;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import javax.servlet.*;
import javax.servlet.http.*;

public class JWTDemoService extends HttpServlet {
    private static final String CONTENT_TYPE = "text/html; charset=UTF-8";

    public void init(ServletConfig config) throws ServletException {
        super.init(config);
    }

    public void service(HttpServletRequest request,
                        HttpServletResponse response) throws ServletException,
                                                             IOException {
        response.setContentType(CONTENT_TYPE);
        PrintWriter out = response.getWriter();
        Enumeration em=request.getHeaderNames();
        while(em.hasMoreElements()){
            String key=(String)em.nextElement();
            String value=(String)request.getHeaders(key).nextElement();
            out.println(String.format("%s ==> %s", key,value));
        }
        out.close();
    }
}

重启OpenResty测试,如果没有指定jwt token信息返回401

[root@docker conf]# curl http://localhost:8080/jwtdemo/service
<html>
<head><title>401 Authorization Required</title></head>
<body bgcolor="white">
<center><h1>401 Authorization Required</h1></center>
<hr><center>openresty/1.13.6.2</center>
</body>
</html>

指定jwt token

[root@docker conf]# curl -i http://localhost:8080/jwtdemo/ -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ5YXlhIiwiaWF0IjoxNTMxODkyNzE3LCJpc3MiOiJ5YXlhIiwic3ViIjoieWF5YSIsImV4cCI6MTUzMTkyODcxN30.W5UXlwKHSrpUAYbfoF-fTBTS9Enm1wsvCKNQm0yLSfQ'
HTTP/1.1 200
Server: openresty/1.13.6.2
Date: Wed, 18 Jul 2018 05:52:13 GMT
Content-Type: text/html;charset=UTF-8
Content-Length: 298
Connection: keep-alive

uid ==> yaya
host ==> tomcat
connection ==> close
user-agent ==> curl/7.29.0
accept ==> */*
authorization ==> Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ5YXlhIiwiaWF0IjoxNTMxODkyNzE3LCJpc3MiOiJ5YXlhIiwic3ViIjoieWF5YSIsImV4cCI6MTUzMTkyODcxN30.W5UXlwKHSrpUAYbfoF-fTBTS9Enm1wsvCKNQm0yLSfQ

从结果上看,后台服务已经获取到uid这个header
至于请求用到jwt token可以从任意平台生成只要保证secret一样即可,根据官网介绍,该库目前支持到jwt生成算法如图:

uuid生成

为每个请求生成唯一的uuid码可以将网关层上的请求和应用层的请求关联起来,对排查问题,接口统计都非常有用.

创建文件/usr/local/openresty/nginx/jwt-lua/resty/uuid.lua
local M  = {}
local charset = {}  do -- [0-9a-zA-Z]
    for c = 48, 57  do table.insert(charset, string.char(c)) end
    for c = 65, 90  do table.insert(charset, string.char(c)) end
    for c = 97, 122 do table.insert(charset, string.char(c)) end
end
function M.uuid(length)
        local res = ""
        for i = 1, length do
                res = res .. charset[math.random(1, #charset)]
        end
        return res
end
return M
修改配置文件nginx.conf
worker_processes  1;
error_log logs/error.log info;
events {
    worker_connections 1024;
}
http {
    upstream tomcat{
     server localhost:80;
    }
    lua_package_path "/usr/local/openresty/nginx/jwt-lua/?.lua;;";
    server {
        listen 8080;
        set $uid '';
        set $uuid '';
        location / {
            access_by_lua '
            local jwt = require("resty.nginx-jwt")
            jwt.auth()
            local u = require("resty.uuid")
            ngx.var.uuid = u.uuid(64)
        ';
            default_type application/json;
            proxy_set_header uid $uid;
            proxy_set_header uuid $uuid;
            proxy_pass http://tomcat;
        }
    }
}

重启OpenResty,测试

[root@docker conf]# openresty -s reload
[root@docker conf]# curl -i http://localhost:8080/jwtdemo/ -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ5YXlhIiwiaWF0IjoxNTMxODk0MDA3LCJpc3MiOiJ5YXlhIiwic3ViIjoieWF5YSIsImV4cCI6MTUzMTkzMDAwN30.vQvpQpIHCmK5QBgIoRR8jhIGeYlHOMYySIr4gHvoZFE'
HTTP/1.1 200
Server: openresty/1.13.6.2
Date: Wed, 18 Jul 2018 08:05:45 GMT
Content-Type: text/html;charset=UTF-8
Content-Length: 372
Connection: keep-alive

uid ==> yaya
uuid ==> nhak5eLjQZ73yhAyHLTgZnSBeDa8pa1p3pcpBFvJ4Mv1fkY782UgVr8Islheq03l
host ==> tomcat
connection ==> close
user-agent ==> curl/7.29.0
accept ==> */*
authorization ==> Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ5YXlhIiwiaWF0IjoxNTMxODk0MDA3LCJpc3MiOiJ5YXlhIiwic3ViIjoieWF5YSIsImV4cCI6MTUzMTkzMDAwN30.vQvpQpIHCmK5QBgIoRR8jhIGeYlHOMYySIr4gHvoZFE

可以看到,多了一个uuid的请求头

jwt token生成java示例

这里提供一个生成jwt token的java示例

package com.yaya;

import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description:
 * @author: jianfeng.zheng
 * @since: 2018/7/5 下午9:56
 * @history: 1.2018/7/5 created by jianfeng.zheng
 */
public class JWTDemo {
    
    public static final String SECRET="5pil6aOO5YaN576O5Lmf5q+U5LiN5LiK5bCP6ZuF55qE56yR";
    
    public static String createJWT(String uid, long ttlMillis) throws Exception {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        Key signingKey = new SecretKeySpec(SECRET.getBytes(), signatureAlgorithm.getJcaName());

        Map<String,Object> header=new HashMap<String,Object>();
        header.put("typ","JWT");
        header.put("alg","HS256");
        JwtBuilder builder = Jwts.builder().setId(uid)
                .setIssuedAt(now)
                .setIssuer(uid)
                .setSubject(uid)
                .setHeader(header)
                .signWith(signatureAlgorithm, signingKey);
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }
        return builder.compact();
    }

    public static void main(String[]cmd) throws Exception {
        String s=createJWT("yaya",36000000);
        System.out.println("Bearer "+s);
    }

}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.yaya</groupId>
    <artifactId>jwtdemo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.6.0</version>
        </dependency>
    </dependencies>
</project>

写在最后

这里只是解决了文章开头提到的第一个问题,接口需要知道是谁调用了接口,第二个问题,用户能不能调接口目前考虑用aop在应用层实现,后续也会继续更新.

查看原文

赞 14 收藏 8 评论 1

面试权威指南 赞了文章 · 2020-05-12

常用的分布式事务解决方案介绍有多少种?

关于分布式事务,工程领域主要讨论的是强一致性和最终一致性的解决方案。典型方案包括:

  • 两阶段提交(2PC, Two-phase Commit)方案
  • eBay 事件队列方案
  • TCC 补偿模式
  • 缓存数据最终一致性

一、一致性理论

分布式事务的目的是保障分库数据一致性,而跨库事务会遇到各种不可控制的问题,如个别节点永久性宕机,像单机事务一样的ACID是无法奢望的。另外,业界著名的CAP理论也告诉我们,对分布式系统,需要将数据一致性和系统可用性、分区容忍性放在天平上一起考虑。

两阶段提交协议(简称2PC)是实现分布式事务较为经典的方案,但2PC 的可扩展性很差,在分布式架构下应用代价较大,eBay 架构师Dan Pritchett 提出了BASE 理论,用于解决大规模分布式系统下的数据一致性问题。BASE 理论告诉我们:可以通过放弃系统在每个时刻的强一致性来换取系统的可扩展性。

1、CAP理论

在分布式系统中:

  • 一致性(Consistency)
  • 可用性(Availability)
  • 分区容忍性(Partition Tolerance)

3个要素最多只能同时满足两个,不可兼得。其中,分区容忍性又是不可或缺的。

  • 一致性:分布式环境下多个节点的数据是否强一致。
  • 可用性:分布式服务能一直保证可用状态。当用户发出一个请求后,服务能在有限时间内返回结果。
  • 分区容忍性:特指对网络分区的容忍性。

举例:Cassandra、Dynamo 等,默认优先选择AP,弱化C;HBase、MongoDB 等,默认优先选择CP,弱化A。

2、BASE 理论

核心思想:

  • 基本可用(BasicallyAvailable):指分布式系统在出现故障时,允许损失部分的可用性来保证核心可用。
  • 软状态(SoftState):指允许分布式系统存在中间状态,该中间状态不会影响到系统的整体可用性。
  • 最终一致性(EventualConsistency):指分布式系统中的所有副本数据经过一定时间后,最终能够达到一致的状态。

二、一致性模型

数据的一致性模型可以分成以下 3 类:

  • 强一致性:数据更新成功后,任意时刻所有副本中的数据都是一致的,一般采用同步的方式实现
  • 弱一致性:数据更新成功后,系统不承诺立即可以读到最新写入的值,也不承诺具体多久之后可以读到
  • 最终一致性:弱一致性的一种形式,数据更新成功后,系统不承诺立即可以返回最新写入的值,但是保证最终会返回上一次更新操作的值

分布式系统数据的强一致性、弱一致性和最终一致性可以通过Quorum NRW算法分析。

https://en.wikipedia.org/wiki/Quorum_(distributed_computing))

三、分布式事务解决方案

1、2PC方案——强一致性

2PC的核心原理是通过提交分阶段和记日志的方式,记录下事务提交所处的阶段状态,在组件宕机重启后,可通过日志恢复事务提交的阶段状态,并在这个状态节点重试,如Coordinator重启后,通过日志可以确定提交处于Prepare还是PrepareAll状态,若是前者,说明有节点可能没有Prepare成功,或所有节点Prepare成功但还没有下发Commit,状态恢复后给所有节点下发RollBack;若是PrepareAll状态,需要给所有节点下发Commit,数据库节点需要保证Commit幂等。

2PC方案的问题:

  • 同步阻塞
  • 数据不一致
  • 单点问题

升级的3PC方案旨在解决这些问题,主要有两个改进:

  • 增加超时机制
  • 两阶段之间插入准备阶段

但三阶段提交也存在一些缺陷,要彻底从协议层面避免数据不一致,可以采用Paxos或者Raft 算法。

Paxos:https://en.wikipedia.org/wiki/Paxos_(computer_science))

Raft:https://raft.github.io/

2、eBay 事件队列方案——最终一致性

eBay 的架构师Dan Pritchett,曾在一篇解释BASE 原理的论文《Base:An Acid Alternative》中提到一个eBay 分布式系统一致性问题的解决方案。它的核心思想是将需要分布式处理的任务通过消息或者日志的方式来异步执行,消息或日志可以存到本地文件、数据库或消息队列,再通过业务规则进行失败重试,它要求各服务的接口是幂等的。

Base:An Acid Alternative:https://link.zhihu.com/?target=http%3A//queue.acm.org/detail.cfm%3Fid%3D1394128

描述的场景为,有用户表user 和交易表transaction,用户表存储用户信息、总销售额和总购买额,交易表存储每一笔交易的流水号、买家信息、卖家信息和交易金额。如果产生了一笔交易,需要在交易表增加记录,同时还要修改用户表的金额

论文中提出的解决方法是将更新交易表记录和用户表更新消息放在一个本地事务来完成,为了避免重复消费用户表更新消息带来的问题,增加一个操作记录表updates_applied来记录已经完成的交易相关的信息。

这个方案的核心在于第二阶段的重试和幂等执行。失败后重试,这是一种补偿机制,它是能保证系统最终一致的关键流程。

3、TCC (Try-Confirm-Cancel)补偿模式——最终一致性

某业务模型如图,由服务 A、服务B、服务C、服务D 共同组成的一个微服务架构系统。服务A 需要依次调用服务B、服务C 和服务D 共同完成一个操作。当服务A 调用服务D 失败时,若要保证整个系统数据的一致性,就要对服务B 和服务C 的invoke 操作进行回滚,执行反向的revert 操作。回滚成功后,整个微服务系统是数据一致的。

实现关键要素:

  • 服务调用链必须被记录下来
  • 每个服务提供者都需要提供一组业务逻辑相反的操作,互为补偿,同时回滚操作要保证幂等
  • 必须按失败原因执行不同的回滚策略

4、缓存数据最终一致性

在我们的业务系统中,缓存(Redis 或者Memcached)通常被用在数据库前面,作为数据读取的缓冲,使得I/O 操作不至于直接落在数据库上。以商品详情页为例,假如卖家修改了商品信息,并写回到数据库,但是这时候用户从商品详情页看到的信息还是从缓存中拿到的过时数据,这就出现了缓存系统和数据库系统中的数据不一致的现象。

要解决该场景下缓存和数据库数据不一致的问题我们有以下两种解决方案:

为缓存数据设置过期时间。当缓存中数据过期后,业务系统会从数据库中获取数据,并将新值放入缓存。这个过期时间就是系统可以达到最终一致的容忍时间。更新数据库数据后同时清除缓存数据。数据库数据更新后,同步删除缓存中数据,使得下次对商品详情的获取直接从数据库中获取,并同步到缓存。

四、选择建议

在面临数据一致性问题的时候,首先要从业务需求的角度出发,确定我们对于3 种一致性模型的接受程度,再通过具体场景来决定解决方案。

从应用角度看,分布式事务的现实场景常常无法规避,在有能力给出其他解决方案前,2PC也是一个不错的选择。

对购物转账等电商和金融业务,中间件层的2PC最大问题在于业务不可见,一旦出现不可抗力或意想不到的一致性破坏,如数据节点永久性宕机,业务难以根据2PC的日志进行补偿。金融场景下,数据一致性是命根,业务需要对数据有百分之百的掌控力,建议使用TCC这类分布式事务模型,或基于消息队列的柔性事务框架,这两种方案都在业务层实现,业务开发者具有足够掌控力,可以结合SOA框架来架构,包括Dubbo、Spring Cloud等(题主的标签写了Dubbo)。

利益相关:

以上内容,基本都摘自网易云基础服务架构团队著《云原生应用架构实践》

出处:网易云
地址:https://www.zhihu.com/question/64921387

Contact

关注公众号-搜云库

查看原文

赞 11 收藏 18 评论 0

面试权威指南 赞了问题 · 2020-05-12

php 处理10w+20w数据

用户10万 订单20万
通过用户去匹配订单最后处理完成

1 从mysql循环查询,(cpu撑不住不可靠)
2 查询所有会员,订单,再匹配 (php内存溢出)
3 批次会员2000个,订单分3份 去匹配,处理速度慢
比如循环2000会员,去匹配第1批订单[无],
再从mysql取出第二批[有,缓存下来供后面会员使用]

现在不知道该怎么处理速度快,不会消耗很多服务器资源,

可能数据不止这些,目前遇到的就是 内存溢出 处理速度慢 占用系统资源高

关注 7 回答 6

面试权威指南 关注了标签 · 2020-05-04

docker

an open source project to pack, ship and run any application as a lightweight container ! By Lock !

关注 36518

面试权威指南 回答了问题 · 2020-05-04

订单回调思路

收到支付的回调后, 不要直接去更新DB里订单
应该

1.校验请求来源是否安全可靠

如果你的回调接口被暴露了,可能会被恶意请求,随便来个请求就把你的订单数据修改了,造成的后果,你懂得.

2.根据订单编号 查询订单是否真实存在

3.检查订单状态 是否已支付

因为回调受网络影响,可能会重试多次,避免重复处理.

4. 如果订单存在,那么回调给你的支付成功的和数据库里的订单金额是否完全一致

注意 这是一个很容易被忽略的点
创建订单后,可以通过抓包工具,修改金额,然后进行支付,订单号没变,但实际支付的金额缺减少了.

有其他疑问,请在评论区交流.

关注 2 回答 1

面试权威指南 回答了问题 · 2020-05-04

php 处理10w+20w数据

用PCNTL扩展,自己模拟多进程实现.
10万条数据 切割成10份.
10个进程 每个进程处理1W条数据.
切割成多少份,随意 20也行.

sql中使用主键id,会使用到覆盖索引,没有SQL性能问题.

请看伪代码

        
        //子进程最大数量
        $pMax  = 10;
        //获取数据最大id  数据可能id不连续,使用max(id) 比使用count(*) 更合理
        $sql   = "SELECT MAX(id) as count FROM table ";
        $count = $db->fetchOne($sql, \Phalcon\Db::FETCH_ASSOC);
        $count = $count['count'];
        var_dump("数据总数:".$count);
        $limit_count = ceil($count / $pMax);
        var_dump("每个进程处理数据条数:".$limit_count);

        //$sql = " SELECT id FROM `table`  WHERE id < {$id}  ORDER BY id DESC limit {$limit} ";
        //echo "生产者sql:" . PHP_EOL;
        //echo $sql . PHP_EOL;
        //$data = $db->fetchAll($sql, \Phalcon\Db::FETCH_ASSOC);


        // 10个子进程处理任务
        $start = self::getMicroTime();
        for ($p = 1; $p <= $pMax; $p++) {

            sleep(1);
            $pid = pcntl_fork();

            if ($pid == -1) {
                die("could not fork");

            } elseif ($pid) {
                echo "父进程: $pid\n";
                //pcntl_wait($status,WNOHANG);

            } else {
                // 子进程处理
                //$func = $args[0].'Action';
                $func = '子进程执行函数名称';

                $params = [];

                //子进程参数1-当前进程需要处理的数据最大id
                $params[1] = $p*$limit_count;

                //子进程参数0-进程编号
                $params[0] = ($p-1)*$limit_count;
                //子进程参数2-进程id
                $params[2] = $p;
                //子进程参数3-数据内容
                $params[3] = $data;

                $this->$func($params);
                // 一定要注意退出子进程,否则pcntl_fork() 会被子进程再fork,带来处理上的影响。
                exit;
            }
        }
        // 等待子进程执行结束
        while (pcntl_waitpid(0, $status) != -1) {
            $status = pcntl_wexitstatus($status);
            echo "子进程 $status 完成\n";
        }

看看是否满足你的需求

多进程比较好理解.

如果追求更高性能,可以用Swoole来写,不过还需要额外安装PHP扩展.

有其他疑问,请在评论区交流.

关注 7 回答 6

面试权威指南 回答了问题 · 2020-05-04

这些扩展哪些是laravel能用到的?

这些扩展其实和laravel无关, 具体需要哪些扩展,和你的业务功能有关
举例子

  1. 你需要从mysql查询数据 那么需要pdo mysqli扩展
  2. 你需要把数据放到内存中,那么需要memcached 或者 Redis扩展
  3. 你需要对图片进行,裁剪,压缩等处理,那么需要imageMagic扩展
    以此类推,明白了吧?
  4. 你列出的这些扩展 大部分都是PHP安装后就自带的了. 不需要额外安装

还有其他疑问,请在评论里追加.

关注 3 回答 2

面试权威指南 发布了文章 · 2020-05-03

PHP 百万日活五千万PV的系统架构

流量

日活

100万

PV

5千万

每个用户打开10个页面 每个页面5次API请求

秒QPS

流量高峰 18-22点 按照4小时计算
5千万/4/3600 = 3400

单机承载最大QPS (8C8G)

8G内存预留2G给系统缓冲 6GB分配给PHP-FPM
单个PHP-FPM进程 占用大约25-40M内存, 以40M计算
6 X 1024 / 40 = 150
单个机器每秒可并发处理150个请求
单个请求假设耗时0.25秒(250ms)
则单机最大秒QPS为150 X 4 = 600

需要6台机器

3400 / 600 ≈ 6
预留一台做缓冲 比如重启项目时,轮询重启,保证可提供服务的恒定有6台.
这里的6台只是nginx+php-fpm的应用服务器 数据库和缓存之类的另做计算

3台机器

Swoole4.5 + EasySwoole 重写.

架构图

QQ截图20200503225351.png

欢迎友好讨论,以上为理论值.

查看原文

赞 3 收藏 2 评论 2

面试权威指南 发布了文章 · 2020-05-02

我常用的Linux命令-持续更新

实时查看日志

tail -f 日志名称

以json方式查看日志

需要安装jq软件
tail -f test.log | jq

统计目录内文件总大小

du -sh .

统计目录内文件大小 按照文件从大到小排序

查看磁盘已用/可用空间

df -h

查看某个端口是否在使用

查看linux哪些端口允许被访问
iptables -L -n --line-number
ACCEPT 表示允许

查看某个程序是否在运行

ps -ef | grep php-fpm

统计某个程序的运行数量

ps -ef | grep php-fpm | wc -l

上传单个文件

rz 选择一个文件

下载单个文件

sz 文件名称

Docker命令

安装mysql

docker run -p 3306:3306 --name wiki-mysql -v /mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456  -d mysql:5.7

参数说明:

-name:指定容器名  这个例子的是 wiki-mysql

-p:映射宿主主机端口

-v:挂载宿主目录到容器目录

-e:设置环境变量,此处指定root密码

-d:后台运行容器


进入docker镜像
docker exec -it wiki-mysql /bin/bash

访问mysql
mysql -h172.17.0.1 -p3306 -uroot -p123456

修改mysql访问权限 修改root 可以通过任何客户端连接
ALTER  USER  'root'@'%'  IDENTIFIED  WITH mysql_native_password BY  '123456';

查看正在运行的docker容器实例

以下两个命令都可以,输出的结果是一样的
docker ps

docker container ls

立刻停止指定的容器运行

 docker container kill [containerID]
 dicker kill [containerID]

**容器停止运行之后,物理文件还在,用下面的命令删除容器文件。
删除指定的容器文件**

docker container rm [containerID]
docker rm [containerID]

启动/停止 指定的容器实例

docker container start [containerID]

docker container stop [containerID]

查看指定容器实例的日志信息

docker container logs [containerID]

进入某个正在运行的容器实例内部

docker exec -it [containerID] /bin/bash
docker container exec -it [containerID] /bin/bash

从正在运行的 Docker 容器里面,将文件拷贝到本机。
下面是拷贝到当前目录的写法。

docker cp [containID]:[/path/to/file] .
查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 8 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-07-29
个人主页被 482 人浏览