项目分析 NiterForum(3)第三方服务与缓存

第三方服务

Ali

主要是获取资讯

通过使用 OkHttpClient 进行请求:OkhttpClient的使用详解_workingman_li的博客-CSDN博客

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>3.14.1</version>
</dependency>

BaiduCloud

主要是百度云的内容审核服务:

// 初始化一个AipImageCensor
AipContentCensor client = new AipContentCensor(APP_ID, API_KEY, SECRET_KEY);
JSONObject response = client.textCensorUserDefined(text);

第三方登录

AuthorizeController 里进行配置文件的读取和回调函数的编写

Baidu

要想调用百度服务需要一个百度的access token:

同理,使用 OkHttpClient 发送请求

MediaType mediaType = MediaType.get("application/x-www-form-urlencoded; charset=utf-8"); // 媒体类型,决定浏览器将以什么形式、什么编码对资源进行解析
OkHttpClient client = new OkHttpClient();
String s = "grant_type="+baiduAccessTokenDTO.getGrant_type()+"&code="+baiduAccessTokenDTO.getCode()+"&client_id="+baiduAccessTokenDTO.getClient_id()+"&client_secret="+baiduAccessTokenDTO.getClient_secret()+"&redirect_uri="+baiduAccessTokenDTO.getRedirect_uri(); // baiduAccessTokenDTO含有配置文件中的百度登录API配置信息
RequestBody body = RequestBody.create(mediaType, s); // 准备请求体
Request request = new Request.Builder()
    .url("https://openapi.baidu.com/oauth/2.0/token")
    .post(body)
    .build();
Response response = client.newCall(request).execute(); // 发送请求,返回带有access token的json

之后带着获取的access token去发请求即可:

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
	.url("https://openapi.baidu.com/rest/2.0/passport/users/getInfo?access_token=" + accessToken)
	.build();
Response response = client.newCall(request).execute(); // 返回带有个人信息的BaiduUserDTO对象

Github

与百度类似,但是获取的access token是放在header里去发请求的:

OkHttpClient client = new OkHttpClient();
/*
*使用参数的方式明文传输,并不推荐,即将被Github废弃
Request request = new Request.Builder()
    .url("https://api.github.com/user?access_token=" + accessToken)
    .build();
*/

//作为header中的参数传输,强烈推荐
Request request = new Request.Builder()
    .url("https://api.github.com/user")
    .header("Authorization","token "+accessToken)
    .build();

Qq

先获取access token,然后再获得openID,最终通过access token、openID和qq互联的clientId获取用户信息

开发者可通过openID来获取用户的基本信息。特别需要注意的是,如果开发者拥有多个移动应用、网站应用,可通过获取用户的unionID来区分用户的唯一性,因为只要是同一QQ互联平台下的不同应用,unionID是相同的。换句话说,同一用户,对同一个QQ互联平台下的不同应用,unionID是相同的

public String getOpenID(String accessToken) {
    
    
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url("https://graph.qq.com/oauth2.0/me?access_token=" + accessToken)
                .build();
        try {
    
    
            Response response = client.newCall(request).execute();
            String string = response.body().string();
            String jsonString = string.split(" ")[1].split(" ")[0];
            //System.out.println(jsonString);
            JSONObject obj = JSONObject.parseObject(jsonString);
            String openid = obj.getString("openid");
            //System.out.println(openid);
            return openid;
        } catch (IOException e) {
    
    
        }
        return null;
    }


    public String getUnionId(String accessToken) {
    
    
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url("https://graph.qq.com/oauth2.0/me?access_token="+accessToken+"&unionid=1")
                .build();
        try {
    
    
            Response response = client.newCall(request).execute();
            String string = response.body().string();
            System.out.println("rs:"+string);
            String jsonString = string.split(" ")[1].split(" ")[0];
            System.out.println(jsonString);
            JSONObject obj = JSONObject.parseObject(jsonString);
            String unionid = obj.getString("unionid");
            System.out.println(unionid);
            return unionid;
        } catch (IOException e) {
    
    
        }
        return null;
    }

Weibo

同百度

JiGuang

主要是极光短信服务

用户输入手机号后,手机号和ip、登录token一起送至后端 PhoneController#getPhoneCode 方法

首先 JiGuangProvider#testSendSMSCode 发一个验证码短信(需有配置),然后对发送后的结果进行处理(判定是否发送成功)

之后用户提交验证码,进入 PhoneController#validCode 方法,其调用 JiGuangProvider#testSendValidSMSCode 方法,发送验证的请求 smsclient.sendValidSMSCode(msg_id, code),获得结果后判断、处理用户提交验证码状态的状态(绑定手机或注册登录)

当用户选择极光一键登录时,进入 PhoneController#loginTokenVerify 方法,其调用 JiGuangProvider#loginTokenVerify,该方法发送http请求调用极光的一键登录api,将前端传入的login token发过去,api验证后返回加密的手机号码,然后调用JiGuangProvider#decrypt 对其解密,其使用PKCS8EncodedKeySpec

JAVA——RSA加密【X509EncodedKeySpec、PKCS8EncodedKeySpec、RSAPublicKeySpec、RSAPrivateKeySpec】_Starzkg的博客-CSDN博客

QCloud

腾讯云服务

COS

上传图片时调用:

String url = qCloudProvider.upload(inputStream,contentType,user,contentLength);

upload 内又调用 uploadtoBucket 把文件存入 Bucket :

public String uploadtoBucket(InputStream inputStream, String fileType, String contentType, UserDTO user, String fileName, Long contentLength){
    
    

    // 1 初始化用户身份信息(secretId, secretKey)
    COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
    // 2 设置bucket的区域, COS地域的简称请参照 https://www.qcloud.com/document/product/436/6224
    ClientConfig clientConfig = new ClientConfig(new Region(region));
    // 3 生成cos客户端
    COSClient cosclient = new COSClient(cred, clientConfig);

    String key = "upload/user/"+user.getId()+"/"+fileType+"/"+fileName; // bucket中路径

    ObjectMetadata objectMetadata = new ObjectMetadata();
    // 从输入流上传必须制定content length, 否则http客户端可能会缓存所有数据,存在内存OOM的情况
    objectMetadata.setContentLength(contentLength);
    // 默认下载时根据cos路径key的后缀返回响应的contenttype, 上传时设置contenttype会覆盖默认值
    objectMetadata.setContentType("contentType");

    PutObjectRequest putObjectRequest =
    	new PutObjectRequest(bucketName, key, inputStream, objectMetadata);
    // 设置存储类型, 默认是标准(Standard), 低频(standard_ia)
    putObjectRequest.setStorageClass(StorageClass.Standard);
    try {
    
    
        PutObjectResult putObjectResult = cosclient.putObject(putObjectRequest);
        // putobjectResult会返回文件的etag
        String etag = putObjectResult.getETag();
        //System.out.println(etag);
    } catch (CosServiceException e) {
    
    
        //e.printStackTrace();
        log.error("upload error,{}", key, e);
        throw new CustomizeException(CustomizeErrorCode.FILE_UPLOAD_FAIL);
    } catch (CosClientException e) {
    
    
        //e.printStackTrace();
        log.error("upload error,{}", key, e);
        throw new CustomizeException(CustomizeErrorCode.FILE_UPLOAD_FAIL);
    }
    // 关闭客户端
    cosclient.shutdown();
    return objecturl+key;
}

腾讯短信

public String sendSms(String session, String phone){
    
    

    try {
    
    
        /* 必要步骤:
        * 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretId 和 secretKey
        * 本示例采用从环境变量读取的方式,需要预先在环境变量中设置这两个值
        * CAM 密钥查询:https://console.cloud.tencent.com/cam/capi*/
        Credential cred = new Credential(secretId, secretKey);

        /* 非必要步骤:
        * 实例化一个客户端配置对象,可以指定超时时间等配置 */
        HttpProfile httpProfile = new HttpProfile();
        ClientProfile clientProfile = new ClientProfile();
        
        /* SDK 默认用 TC3-HMAC-SHA256 进行签名
        * 非必要请不要修改该字段 */
        clientProfile.setSignMethod("HmacSHA256");
        clientProfile.setHttpProfile(httpProfile);
        
        /* 实例化 SMS 的 client 对象
        * 第二个参数是地域信息,可以直接填写字符串 ap-guangzhou,或者引用预设的常量 */
        SmsClient client = new SmsClient(cred, "",clientProfile);
        
        /* 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数
        * 您可以直接查询 SDK 源码确定接口有哪些属性可以设置
        * 属性可能是基本类型,也可能引用了另一个数据结构*/
        SendSmsRequest req = new SendSmsRequest();

        /* 填充请求参数,这里 request 对象的成员变量即对应接口的入参
        * 您可以通过官网接口文档或跳转到 request 对象的定义处查看请求参数的定义
        * 基本类型的设置:
        * 帮助链接:
        * 短信控制台:https://console.cloud.tencent.com/smsv2
        * sms helper:https://cloud.tencent.com/document/product/382/3773 */

        /* 短信应用 ID: 在 [短信控制台] 添加应用后生成的实际 SDKAppID,例如1400006666 */
        //String appid = "1400009099";
        req.setSmsSdkAppid(smsAppId);

        /* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,可登录 [短信控制台] 查看签名信息 */
        //String sign = "签名内容";
        req.setSign(smsSign);

        /* 用户的 session 内容: 可以携带用户侧 ID 等上下文信息,server 会原样返回 */
        //String session = "xxx";
        req.setSessionContext(session);

        /* 模板 ID: 必须填写已审核通过的模板 ID,可登录 [短信控制台] 查看模板 ID */
        //String templateID = "400000";
        req.setTemplateID(smsTempId);

        /* 下发手机号码,采用 e.164 标准,+[国家或地区码][手机号]
        * 例如+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号*/
        String[] phoneNumbers = {
    
    "+86"+phone};
        req.setPhoneNumberSet(phoneNumbers);

        /* 模板参数: 若无模板参数,则设置为空*/
        String code = String.valueOf(new Random().nextInt(899999) + 100000);
        String[] templateParams = {
    
    smsSign+session,code,"5"};
        req.setTemplateParamSet(templateParams);

        /* 通过 client 对象调用 SendSms 方法发起请求。注意请求方法名与请求对象是对应的
        * 返回的 res 是一个 SendSmsResponse 类的实例,与请求对象对应 */
        SendSmsResponse res = client.SendSms(req);

        // 输出 JSON 格式的字符串回包
        System.out.println(SendSmsResponse.toJsonString(res));
        SendStatus[] sendStatusSet = res.getSendStatusSet();
        for (SendStatus sendStatus : sendStatusSet) {
    
    
            if("Ok".equals(sendStatus.getCode())){
    
    
                temporaryCache.putPhoneCode(
                    sendStatus.getSerialNo(), sendStatus.getPhoneNumber()+code);
            }
        return SendSmsResponse.toJsonString(sendStatus);
        }

        // 可以取出单个值,您可以通过官网接口文档或跳转到 response 对象的定义处查看返回字段的定义
        //System.out.println(res.getRequestId());

    } catch (TencentCloudSDKException e) {
    
    
    	e.printStackTrace();
    }
    return null;
}

Tinify

<dependency>
  <groupId>com.tinify</groupId>
  <artifactId>tinify</artifactId>
  <version>RELEASE</version>
</dependency>

Tinify API允许您压缩和优化JPEG与PNG图像。 它是基于Rest服务设计的(轻量级web服务)。我们维护各种语言的客户端库,使其与Tinify API进行交互变得非常简单。

Tinify Java API中文参考文档_弃天笑的博客-CSDN博客

在这里,定义了一个定时任务TinifyPngTasks#tinifyPngSchedule用于定时检查 TinifyPngCache 中的 List<TinifyPngDTO> images 里是否有元素。

关于 TinifyPngCache,如果不开启图片压缩,那就没用,开启时会在每次把图片上传到COS桶后把图片url存入 images 里。

TinifyPngTasks#tinifyPngSchedule每过一段时间就把images里的元素(图片url)通过Tinify API压缩并转化为InputStream:

@Value("${tinify.key}")
private String tinifyKey;

public InputStream getStreamfromUrl(String url){
    
    
    Tinify.setKey(tinifyKey);
    InputStream inputStream=null;
    try {
    
    
        byte[] resultData = Tinify.fromUrl(url).toBuffer(); // toBuffer()压缩图像
        inputStream = new ByteArrayInputStream(resultData);
    } catch (IOException e) {
    
    
    	e.printStackTrace();
    }
    return inputStream;
}

之后再调用uploadtoBucket将已压缩的图片文件存入COS,等到将images里的全部上传,就清除

Vaptcha

人机检验

ValidateController#post方法中调用VaptchaProvider.getValidateResult(token,scene,ip)向vaptcha服务器发请求(OkHttpClient),返回的是包含是否通过的json字符串。

public static String getValidateResult(String token,int scene,String ip){
    
    
    MediaType mediaType = MediaType.get("application/x-www-form-urlencoded; charset=utf-8");
    OkHttpClient client = new OkHttpClient();
    String s = "id="+vid+"&secretkey="+key+"&token="+token+"&scene="+scene+"&ip="+ip;
    RequestBody body = RequestBody.create(mediaType, s);
    Request request = new Request.Builder()
        .url("http://0.vaptcha.com/verify")
        .post(body)
        .build();
    try (Response response = client.newCall(request).execute()) {
    
    
        String string = response.body().string();
        return string;
    } catch (Exception e) {
    
    
        e.printStackTrace();
    }
    return null;
}

缓存

AppUserCache

ExpiringMap<String,String> map = ExpiringMap.builder()
    .maxSize(100)//最大容量,防止恶意注入
    .expiration(1, TimeUnit.MINUTES)//过期时间1分钟
    .expirationPolicy(ExpirationPolicy.ACCESSED)
    .variableExpiration()
    .build();

用于缓存在手机端登录的用户,进而在扫码登录的过程中进行登录检验。

HotTagCache

private List<String> hots = new ArrayList<>();

类里有一个方法用于更新缓存,用到了优先队列PriorityQueue

Java PriorityQueue(优先队列)_2Tree的博客-CSDN博客

  • PriorityQueue是一个无限制的队列,并且动态增长。默认初始容量'11'可以使用相应构造函数中的initialCapacity参数覆盖。
  • 它不允许NULL对象。
  • 添加到PriorityQueue的对象必须具有可比性。
  • 默认情况下,优先级队列的对象按自然顺序排序
  • 比较器可用于队列中对象的自定义排序。
  • 优先级队列的头部是基于自然排序或基于比较器的排序的最小元素。当我们轮询队列时,它从队列中返回头对象。
  • 如果存在多个具有相同优先级的对象,则它可以随机轮询其中任何一个。
  • PriorityQueue 不是线程安全的PriorityBlockingQueue在并发环境中使用。
  • 它为add和poll方法提供了**O(log(n))**时间。

DTO实现Comparable接口,进而可以指定放入优先队列时的比较策略:

@Data
public class HotTagDTO implements Comparable {
    
    
    private String name;
    private Integer priority;

    @Override
    public int compareTo(Object o) {
    
    
        return this.getPriority() - ((HotTagDTO) o).getPriority();
    }
}

先将Map tags存入优先队列,然后依次取出加入一个新List,之后指定hots指向新List(此处运用了写入时拷贝的思想)。

同样,通过定时任务的方式调用更新缓存的方法

IpLimitCache

ExpiringMap<String,String> interval = ExpiringMap.builder()
    .maxSize(20)//设置最大容量,增大攻击难度,值越大存储的可疑ip越多,过大会占用额外资源
    .expiration(30, TimeUnit.SECONDS)//过期时间30秒
    .expirationPolicy(ExpirationPolicy.CREATED)//每次访问重置过期时间
    .variableExpiration()
    .build();

ExpiringMap<String,Integer> ipScores = ExpiringMap.builder()
    .maxSize(100)//设置最大容量,增大攻击难度,值越大存储的可疑ip越多,过大会占用额外资源
    .expiration(1, TimeUnit.DAYS)//过期时间1天
    .expirationPolicy(ExpirationPolicy.CREATED)//每次更新重置过期时间
    .variableExpiration()
    .build();

interval<ip, token>是一个ip的黑名单,当然这个黑名单是有过期时间的,在规定时间内(30s),同一ip不可重复验证

ipScores<ip, score>是一个ip的计分表,每次用户发来操作,后端都在interval里找到了它,或者一个ip的分数超过了100,那么会加分并且返回异常

LoginUserCache

private List<User> loginUsers = new ArrayList<>();

ExpiringMap<Long,Long> loginUserMap = ExpiringMap.builder()
    .maxSize(16)//最大容量,防止恶意注入
    .expiration(11, TimeUnit.MINUTES)//过期时间10分钟
    .expirationPolicy(ExpirationPolicy.CREATED)
    .variableExpiration()
    .build();

在说说篇有相关分析

TagCache

静态方法生成一个包含所有预设tag的List

TemporaryCache

两个ExpiringMap分别用于缓存电话、邮件验证码

TinifyPngCache

见第三方服务篇Tinify

猜你喜欢

转载自blog.csdn.net/Falling_Asteroid/article/details/130121488