├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── encodings.xml ├── jarRepositories.xml ├── misc.xml ├── uiDesigner.xml └── vcs.xml ├── DoubanAPI.iml ├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── yize │ │ └── douban │ │ ├── base │ │ ├── DoubanWebRequest.java │ │ └── SignatureHelper.java │ │ ├── module │ │ ├── CelebrityPhoto.java │ │ └── UrlModule.java │ │ ├── servlet │ │ └── GetPhoto.java │ │ └── util │ │ ├── AesEncrypt.java │ │ ├── Base64.java │ │ ├── HMACHash1.java │ │ ├── HttpRequestHelper.java │ │ └── TextUtils.java └── webapp │ ├── WEB-INF │ └── web.xml │ └── index.jsp └── test └── java └── com └── yize └── test ├── TestDoubanWebRequest.java ├── TestEncrypt.java └── TestRequestCelebrityPhoto.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /target/ -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /../../../../../../../../../:\Users\yize\Downloads\jadx-1.1.0\proj\DoubanAPI\DoubanAPI\.idea/dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /DoubanAPI.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 1、简介 2 | 3 | 目前的主要能力是计算豆瓣API的请求签名 4 | 5 | # 2、注意事项 6 | 7 | API示例 8 | 9 | ```java 10 | https://frodo.douban.com/api/v2/elessar/subject/27260217/photos?count=100&apikey=0dad551ec0f84ed02907ff5c42e8ec70&_sig=gfTX2YbSiADYaG%2FzXJ%2BpNo5IhbI%3D&_ts=1599316450 11 | ``` 12 | 13 | UA 14 | 15 | ```java 16 | api-client/1 com.douban.frodo/6.42.2(194) Android/22 product/shamu vendor/OPPO model/OPPO R11 Plus rom/android network/wifi platform/mobile nd/1 17 | 18 | ``` 19 | 20 | 必须要保证签名和UA正确,才能得到预期响应 21 | 22 | # 3、原理 23 | 24 | 有关如何获取签名的过程已在下面两篇文章作了介绍 25 | 26 | [安卓逆向-豆瓣app签名算法分析与解密(上)](https://blog.csdn.net/qq_23594799/article/details/108445726) 27 | 28 | [安卓逆向-豆瓣app签名算法分析与解密(下)](https://blog.csdn.net/qq_23594799/article/details/108446352) 29 | 30 | # 4、工程导入 31 | 32 | 完整工程是一个使用maven构建的servlet程序,用idea导入即可 33 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | com.yize.douban 8 | DoubanAPI 9 | 1.0-SNAPSHOT 10 | war 11 | 12 | DoubanAPI Maven Webapp 13 | 14 | http://www.example.com 15 | 16 | 17 | UTF-8 18 | 1.7 19 | 1.7 20 | 21 | 22 | 23 | 24 | junit 25 | junit 26 | 4.11 27 | test 28 | 29 | 30 | 31 | javax.servlet 32 | javax.servlet-api 33 | 4.0.1 34 | provided 35 | 36 | 37 | 38 | 39 | 40 | DoubanAPI 41 | 42 | 43 | 44 | maven-clean-plugin 45 | 3.1.0 46 | 47 | 48 | 49 | maven-resources-plugin 50 | 3.0.2 51 | 52 | 53 | maven-compiler-plugin 54 | 3.8.0 55 | 56 | 57 | maven-surefire-plugin 58 | 2.22.1 59 | 60 | 61 | maven-war-plugin 62 | 3.2.2 63 | 64 | 65 | maven-install-plugin 66 | 2.5.2 67 | 68 | 69 | maven-deploy-plugin 70 | 2.8.2 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/main/java/com/yize/douban/base/DoubanWebRequest.java: -------------------------------------------------------------------------------- 1 | package com.yize.douban.base; 2 | 3 | import com.yize.douban.util.HttpRequestHelper; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | public class DoubanWebRequest { 9 | public final static String UA_DOUBAN_ANDROID="api-client/1 com.douban.frodo/6.42.2(194) Android/22 product/shamu vendor/OPPO model/OPPO R11 Plus rom/android network/wifi platform/mobile nd/1"; 10 | 11 | public static String builder(String baseLink,Map paramsMap){ 12 | StringBuilder sb=new StringBuilder(); 13 | sb.append(baseLink+"?"); 14 | 15 | Map verifyMap=SignatureHelper.getVerifyMap(baseLink,"GET",null); 16 | for (String key:paramsMap.keySet()){ 17 | sb.append("&"+key+"="+paramsMap.get(key)); 18 | } 19 | for (String key: verifyMap.keySet()){ 20 | sb.append("&"+key+"="+verifyMap.get(key)); 21 | } 22 | return sb.toString(); 23 | } 24 | 25 | public static String downloadWebSiteUseGet(String baseLink, Map paramsMap,Map headerMap){ 26 | if(headerMap==null){ 27 | headerMap=new HashMap<>(); 28 | headerMap.put("User-Agent",UA_DOUBAN_ANDROID); 29 | } 30 | paramsMap.put("apikey","0dad551ec0f84ed02907ff5c42e8ec70"); 31 | return HttpRequestHelper.downloadWebSiteUseGet(builder(baseLink,paramsMap),headerMap); 32 | } 33 | 34 | public static String downloadWebSiteUseGet(String baseLink, Map paramsMap){ 35 | return downloadWebSiteUseGet(baseLink,paramsMap,null); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/yize/douban/base/SignatureHelper.java: -------------------------------------------------------------------------------- 1 | package com.yize.douban.base; 2 | 3 | import com.yize.douban.util.AesEncrypt; 4 | import com.yize.douban.util.HMACHash1; 5 | import com.yize.douban.util.TextUtils; 6 | 7 | import java.io.UnsupportedEncodingException; 8 | import java.net.URLEncoder; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | public class SignatureHelper { 13 | private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray(); 14 | private static final String DEFAULT_ENCODING = "UTF-8"; 15 | public static final String AMPERSAND = "&"; 16 | private final static int NOT_FOUND = -1; 17 | /** 18 | * 经过android的Base64加密后的豆瓣APP签名信息,每个版本都是固定的 19 | * 20 | * 在安卓里面可以这样获取: 21 | * Application application=(Application)getApplicationContext(); 22 | * PackageInfo packageInfo=application.getPackageManager().getPackageInfo("com.douban.frodo",PackageManager.GET_SIGNATURES); 23 | * String sign=Base64.encodeToString(packageInfo.signatures[0].toByteArray(),0); 24 | * 25 | * 这里直接给出一个版本的豆瓣APP的签名信息 26 | */ 27 | public final static String SIGN="MIICUjCCAbsCBEty1MMwDQYJKoZIhvcNAQEEBQAwcDELMAkGA1UEBhMCemgxEDAOBgNVBAgTB0Jl\n" + 28 | "aWppbmcxEDAOBgNVBAcTB0JlaWppbmcxEzARBgNVBAoTCkRvdWJhbiBJbmMxFDASBgNVBAsTC0Rv\n" + 29 | "dWJhbiBJbmMuMRIwEAYDVQQDEwlCZWFyIFR1bmcwHhcNMTAwMjEwMTU0NjExWhcNMzcwNjI3MTU0\n" + 30 | "NjExWjBwMQswCQYDVQQGEwJ6aDEQMA4GA1UECBMHQmVpamluZzEQMA4GA1UEBxMHQmVpamluZzET\n" + 31 | "MBEGA1UEChMKRG91YmFuIEluYzEUMBIGA1UECxMLRG91YmFuIEluYy4xEjAQBgNVBAMTCUJlYXIg\n" + 32 | "VHVuZzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAg622fxLuwQtC8KLYp5gHk0OmfrFiIisz\n" + 33 | "kzPLBhKPZDHjYS1URhQpzf00T8qg2oEwJPPELjN2Q7YOoax8UINXLhMgFQkyAvMfjdEOSfoKH93p\n" + 34 | "v2d4n/IjQc/TaDKu6yb53DOq76HTUYLcfLKOXaGwGjAp3QqTqP9LnjJjGZCdSvMCAwEAATANBgkq\n" + 35 | "hkiG9w0BAQQFAAOBgQA3MovcB3Hv4bai7OYHU+gZcGQ/8sOLAXGD/roWPX3gm9tyERpGztveH35p\n" + 36 | "aI3BrUWg2Vir0DRjbR48b2HxQidQTVIH/HOJHV0jgYNDviD18/cBwKuLiBvdzc2Fte+zT0nnHXMy\n" + 37 | "E6tVeW3UdHC1UvzyB7Qcxiu4sBiEO1koToQTWw==\n"; 38 | /** 39 | * 反编译豆瓣后得到的一个常量,在豆瓣APP里面固定写死了,具体位置看我的图文解析 40 | */ 41 | public final static String TEXT="bHUvfbiVZUmm2sQRKwiAcw=="; 42 | public static Map getVerifyMap(String str, String str2, String str3) { 43 | Map map=new HashMap<>(); 44 | String decode; 45 | if (TextUtils.isEmpty(str)) { 46 | return null; 47 | } 48 | //String str4= AesEncrypt.encrypt(TEXT,SIGN); 49 | String str4 = "bf7dddc7c9cfe6f7"; 50 | if (TextUtils.isEmpty(str4)) { 51 | return null; 52 | } 53 | StringBuilder sb = new StringBuilder(); 54 | sb.append(str2); 55 | String encodedPath = encodedPath(str); 56 | System.out.println(encodedPath); 57 | if (encodedPath == null || (decode = encodedPath) == null) { 58 | return null; 59 | } 60 | if (decode.endsWith("/")) { 61 | decode = decode.substring(0, decode.length() - 1); 62 | } 63 | sb.append(AMPERSAND); 64 | sb.append(uriEncode(decode,null)); 65 | if (!TextUtils.isEmpty(str3)) { 66 | sb.append(AMPERSAND); 67 | sb.append(str3); 68 | } 69 | long currentTimeMillis = System.currentTimeMillis() / 1000; 70 | sb.append(AMPERSAND); 71 | sb.append(currentTimeMillis); 72 | try { 73 | map.put("_sig", URLEncoder.encode(HMACHash1.a(str4, sb.toString()),"utf-8")); 74 | } catch (Exception e) { 75 | e.printStackTrace(); 76 | } 77 | map.put("_ts",String.valueOf(currentTimeMillis)); 78 | return map; 79 | } 80 | 81 | public static String uriEncode(String s, String allow) { 82 | if (s == null) { 83 | return null; 84 | } 85 | 86 | // Lazily-initialized buffers. 87 | StringBuilder encoded = null; 88 | 89 | int oldLength = s.length(); 90 | 91 | // This loop alternates between copying over allowed characters and 92 | // encoding in chunks. This results in fewer method calls and 93 | // allocations than encoding one character at a time. 94 | int current = 0; 95 | while (current < oldLength) { 96 | // Start in "copying" mode where we copy over allowed chars. 97 | 98 | // Find the next character which needs to be encoded. 99 | int nextToEncode = current; 100 | while (nextToEncode < oldLength 101 | && isAllowed(s.charAt(nextToEncode), allow)) { 102 | nextToEncode++; 103 | } 104 | 105 | // If there's nothing more to encode... 106 | if (nextToEncode == oldLength) { 107 | if (current == 0) { 108 | // We didn't need to encode anything! 109 | return s; 110 | } else { 111 | // Presumably, we've already done some encoding. 112 | encoded.append(s, current, oldLength); 113 | return encoded.toString(); 114 | } 115 | } 116 | 117 | if (encoded == null) { 118 | encoded = new StringBuilder(); 119 | } 120 | 121 | if (nextToEncode > current) { 122 | // Append allowed characters leading up to this point. 123 | encoded.append(s, current, nextToEncode); 124 | } else { 125 | // assert nextToEncode == current 126 | } 127 | 128 | // Switch to "encoding" mode. 129 | 130 | // Find the next allowed character. 131 | current = nextToEncode; 132 | int nextAllowed = current + 1; 133 | while (nextAllowed < oldLength 134 | && !isAllowed(s.charAt(nextAllowed), allow)) { 135 | nextAllowed++; 136 | } 137 | 138 | // Convert the substring to bytes and encode the bytes as 139 | // '%'-escaped octets. 140 | String toEncode = s.substring(current, nextAllowed); 141 | try { 142 | byte[] bytes = toEncode.getBytes(DEFAULT_ENCODING); 143 | int bytesLength = bytes.length; 144 | for (int i = 0; i < bytesLength; i++) { 145 | encoded.append('%'); 146 | encoded.append(HEX_DIGITS[(bytes[i] & 0xf0) >> 4]); 147 | encoded.append(HEX_DIGITS[bytes[i] & 0xf]); 148 | } 149 | } catch (UnsupportedEncodingException e) { 150 | throw new AssertionError(e); 151 | } 152 | 153 | current = nextAllowed; 154 | } 155 | 156 | // Encoded could still be null at this point if s is empty. 157 | return encoded == null ? s : encoded.toString(); 158 | } 159 | 160 | private static boolean isAllowed(char c, String allow) { 161 | return (c >= 'A' && c <= 'Z') 162 | || (c >= 'a' && c <= 'z') 163 | || (c >= '0' && c <= '9') 164 | || "_-!.~'()*".indexOf(c) != NOT_FOUND 165 | || (allow != null && allow.indexOf(c) != NOT_FOUND); 166 | } 167 | 168 | public static String encodedPath(String url) { 169 | String scheme="https"; 170 | int pathStart = url.indexOf('/', scheme.length() + 3); // "://".length() == 3. 171 | int pathEnd = delimiterOffset(url, pathStart, url.length(), "?#"); 172 | return url.substring(pathStart, pathEnd); 173 | } 174 | 175 | public static int delimiterOffset(String input, int pos, int limit, String delimiters) { 176 | for(int i = pos; i < limit; ++i) { 177 | if (delimiters.indexOf(input.charAt(i)) != -1) { 178 | return i; 179 | } 180 | } 181 | return limit; 182 | } 183 | } -------------------------------------------------------------------------------- /src/main/java/com/yize/douban/module/CelebrityPhoto.java: -------------------------------------------------------------------------------- 1 | package com.yize.douban.module; 2 | 3 | import com.yize.douban.base.DoubanWebRequest; 4 | 5 | import java.util.HashMap; 6 | 7 | import static com.yize.douban.module.UrlModule.photoRequestApi; 8 | 9 | public class CelebrityPhoto { 10 | public String requestCelebrityPhoto(String photoId,String start,String count){ 11 | HashMap paramsMap=new HashMap<>(); 12 | paramsMap.put("count",count+""); 13 | paramsMap.put("start",start+""); 14 | String response= DoubanWebRequest.downloadWebSiteUseGet(photoRequestApi.replace("photoid",photoId),paramsMap); 15 | return response; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/yize/douban/module/UrlModule.java: -------------------------------------------------------------------------------- 1 | package com.yize.douban.module; 2 | 3 | public class UrlModule { 4 | //名人的图片example: "https://frodo.douban.com/api/v2/elessar/subject/27260217/photos?count=100&apikey=0dad551ec0f84ed02907ff5c42e8ec70&_sig=gfTX2YbSiADYaG%2FzXJ%2BpNo5IhbI%3D&_ts=1599316450" 5 | public final static String photoRequestApi="https://frodo.douban.com/api/v2/elessar/subject/photoid/photos"; 6 | //电影排行榜 example: "https://frodo.douban.com/api/v2/movie/rank_list?apikey=0dad551ec0f84ed02907ff5c42e8ec70&s=rexxar_new&sugar=46000&loc_id=108288&_sig=ANPs38ZIIxIu5UD%2BiqKsnT%2F5AVA%3D&_ts=1599406040" 7 | public final static String movieRankListApi="https://frodo.douban.com/api/v2/movie/rank_list"; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/yize/douban/servlet/GetPhoto.java: -------------------------------------------------------------------------------- 1 | package com.yize.douban.servlet; 2 | 3 | import com.yize.douban.module.CelebrityPhoto; 4 | 5 | import javax.servlet.ServletException; 6 | import javax.servlet.annotation.WebServlet; 7 | import javax.servlet.http.HttpServlet; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.io.IOException; 11 | import java.io.PrintWriter; 12 | 13 | @WebServlet("/api/photo") 14 | public class GetPhoto extends HttpServlet { 15 | private static CelebrityPhoto celebrityPhoto; 16 | @Override 17 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 18 | doPost(req,resp); 19 | } 20 | 21 | @Override 22 | protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 23 | request.setCharacterEncoding("utf-8"); 24 | response.setCharacterEncoding("utf-8"); 25 | response.setContentType("text/json;charset=utf-8"); 26 | 27 | PrintWriter writer = response.getWriter(); 28 | String photoId = request.getParameter("id"); 29 | String start = request.getParameter("start"); 30 | String count = request.getParameter("count"); 31 | if(celebrityPhoto==null){ 32 | celebrityPhoto=new CelebrityPhoto(); 33 | } 34 | if(photoId==null||start==null||count==null){ 35 | writer.write("{}"); 36 | }else { 37 | writer.write(celebrityPhoto.requestCelebrityPhoto(photoId,start,count)); 38 | } 39 | writer.flush(); 40 | writer.close(); 41 | 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/yize/douban/util/AesEncrypt.java: -------------------------------------------------------------------------------- 1 | package com.yize.douban.util; 2 | 3 | import javax.crypto.Cipher; 4 | import javax.crypto.NoSuchPaddingException; 5 | import javax.crypto.spec.IvParameterSpec; 6 | import javax.crypto.spec.SecretKeySpec; 7 | import java.io.UnsupportedEncodingException; 8 | import java.security.NoSuchAlgorithmException; 9 | 10 | public class AesEncrypt { 11 | public static final String encrypt(String text,String key){ 12 | SecretKeySpec spec=buildSpec(key); 13 | byte[] decode=Base64.decode(text,0); 14 | Cipher instance= null; 15 | try { 16 | instance = Cipher.getInstance("AES/CBC/NoPadding"); 17 | instance.init(2,spec,new IvParameterSpec("DOUBANFRODOAPPIV".getBytes())); 18 | return new String(instance.doFinal(decode)); 19 | } catch (Exception e) { 20 | e.printStackTrace(); 21 | } 22 | return text; 23 | } 24 | 25 | private static SecretKeySpec buildSpec(String key) { 26 | byte[] b; 27 | if(key==null){ 28 | key=""; 29 | } 30 | StringBuilder sb=new StringBuilder(); 31 | sb.append(key); 32 | while (sb.length()<16){ 33 | sb.append("\u0000"); 34 | } 35 | if(sb.length()>16){ 36 | sb.setLength(16); 37 | } 38 | try { 39 | b=sb.toString().getBytes("utf-8"); 40 | } catch (UnsupportedEncodingException e) { 41 | e.printStackTrace(); 42 | b=null; 43 | } 44 | return new SecretKeySpec(b,"AES"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/yize/douban/util/Base64.java: -------------------------------------------------------------------------------- 1 | package com.yize.douban.util; 2 | 3 | /* 4 | * Copyright (C) 2010 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | import java.io.UnsupportedEncodingException; 19 | 20 | /** 21 | * Utilities for encoding and decoding the Base64 representation of 22 | * binary data. See RFCs 2045 and 3548. 25 | */ 26 | public class Base64 { 27 | /** 28 | * Default values for encoder/decoder flags. 29 | */ 30 | public static final int DEFAULT = 0; 31 | 32 | /** 33 | * Encoder flag bit to omit the padding '=' characters at the end 34 | * of the output (if any). 35 | */ 36 | public static final int NO_PADDING = 1; 37 | 38 | /** 39 | * Encoder flag bit to omit all line terminators (i.e., the output 40 | * will be on one long line). 41 | */ 42 | public static final int NO_WRAP = 2; 43 | 44 | /** 45 | * Encoder flag bit to indicate lines should be terminated with a 46 | * CRLF pair instead of just an LF. Has no effect if {@code 47 | * NO_WRAP} is specified as well. 48 | */ 49 | public static final int CRLF = 4; 50 | 51 | /** 52 | * Encoder/decoder flag bit to indicate using the "URL and 53 | * filename safe" variant of Base64 (see RFC 3548 section 4) where 54 | * {@code -} and {@code _} are used in place of {@code +} and 55 | * {@code /}. 56 | */ 57 | public static final int URL_SAFE = 8; 58 | 59 | /** 60 | * Flag to pass to {@link } to indicate that it 61 | * should not close the output stream it is wrapping when it 62 | * itself is closed. 63 | */ 64 | public static final int NO_CLOSE = 16; 65 | 66 | // -------------------------------------------------------- 67 | // shared code 68 | // -------------------------------------------------------- 69 | 70 | /* package */ static abstract class Coder { 71 | public byte[] output; 72 | public int op; 73 | 74 | /** 75 | * Encode/decode another block of input data. this.output is 76 | * provided by the caller, and must be big enough to hold all 77 | * the coded data. On exit, this.opwill be set to the length 78 | * of the coded data. 79 | * 80 | * @param finish true if this is the final call to process for 81 | * this object. Will finalize the coder state and 82 | * include any final bytes in the output. 83 | * 84 | * @return true if the input so far is good; false if some 85 | * error has been detected in the input stream.. 86 | */ 87 | public abstract boolean process(byte[] input, int offset, int len, boolean finish); 88 | 89 | /** 90 | * @return the maximum number of bytes a call to process() 91 | * could produce for the given number of input bytes. This may 92 | * be an overestimate. 93 | */ 94 | public abstract int maxOutputSize(int len); 95 | } 96 | 97 | // -------------------------------------------------------- 98 | // decoding 99 | // -------------------------------------------------------- 100 | 101 | /** 102 | * Decode the Base64-encoded data in input and return the data in 103 | * a new byte array. 104 | * 105 | *

The padding '=' characters at the end are considered optional, but 106 | * if any are present, there must be the correct number of them. 107 | * 108 | * @param str the input String to decode, which is converted to 109 | * bytes using the default charset 110 | * @param flags controls certain features of the decoded output. 111 | * Pass {@code DEFAULT} to decode standard Base64. 112 | * 113 | * @throws IllegalArgumentException if the input contains 114 | * incorrect padding 115 | */ 116 | public static byte[] decode(String str, int flags) { 117 | return decode(str.getBytes(), flags); 118 | } 119 | 120 | /** 121 | * Decode the Base64-encoded data in input and return the data in 122 | * a new byte array. 123 | * 124 | *

The padding '=' characters at the end are considered optional, but 125 | * if any are present, there must be the correct number of them. 126 | * 127 | * @param input the input array to decode 128 | * @param flags controls certain features of the decoded output. 129 | * Pass {@code DEFAULT} to decode standard Base64. 130 | * 131 | * @throws IllegalArgumentException if the input contains 132 | * incorrect padding 133 | */ 134 | public static byte[] decode(byte[] input, int flags) { 135 | return decode(input, 0, input.length, flags); 136 | } 137 | 138 | /** 139 | * Decode the Base64-encoded data in input and return the data in 140 | * a new byte array. 141 | * 142 | *

The padding '=' characters at the end are considered optional, but 143 | * if any are present, there must be the correct number of them. 144 | * 145 | * @param input the data to decode 146 | * @param offset the position within the input array at which to start 147 | * @param len the number of bytes of input to decode 148 | * @param flags controls certain features of the decoded output. 149 | * Pass {@code DEFAULT} to decode standard Base64. 150 | * 151 | * @throws IllegalArgumentException if the input contains 152 | * incorrect padding 153 | */ 154 | public static byte[] decode(byte[] input, int offset, int len, int flags) { 155 | // Allocate space for the most data the input could represent. 156 | // (It could contain less if it contains whitespace, etc.) 157 | Decoder decoder = new Decoder(flags, new byte[len*3/4]); 158 | 159 | if (!decoder.process(input, offset, len, true)) { 160 | throw new IllegalArgumentException("bad base-64"); 161 | } 162 | 163 | // Maybe we got lucky and allocated exactly enough output space. 164 | if (decoder.op == decoder.output.length) { 165 | return decoder.output; 166 | } 167 | 168 | // Need to shorten the array, so allocate a new one of the 169 | // right size and copy. 170 | byte[] temp = new byte[decoder.op]; 171 | System.arraycopy(decoder.output, 0, temp, 0, decoder.op); 172 | return temp; 173 | } 174 | 175 | /* package */ static class Decoder extends Coder { 176 | /** 177 | * Lookup table for turning bytes into their position in the 178 | * Base64 alphabet. 179 | */ 180 | private static final int DECODE[] = { 181 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 182 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 183 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 184 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, 185 | -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 186 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, 187 | -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 188 | 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, 189 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 190 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 191 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 192 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 193 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 194 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 195 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 196 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 197 | }; 198 | 199 | /** 200 | * Decode lookup table for the "web safe" variant (RFC 3548 201 | * sec. 4) where - and _ replace + and /. 202 | */ 203 | private static final int DECODE_WEBSAFE[] = { 204 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 205 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 206 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, 207 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, 208 | -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 209 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, 210 | -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 211 | 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, 212 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 213 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 214 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 215 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 216 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 217 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 218 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 219 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 220 | }; 221 | 222 | /** Non-data values in the DECODE arrays. */ 223 | private static final int SKIP = -1; 224 | private static final int EQUALS = -2; 225 | 226 | /** 227 | * States 0-3 are reading through the next input tuple. 228 | * State 4 is having read one '=' and expecting exactly 229 | * one more. 230 | * State 5 is expecting no more data or padding characters 231 | * in the input. 232 | * State 6 is the error state; an error has been detected 233 | * in the input and no future input can "fix" it. 234 | */ 235 | private int state; // state number (0 to 6) 236 | private int value; 237 | 238 | final private int[] alphabet; 239 | 240 | public Decoder(int flags, byte[] output) { 241 | this.output = output; 242 | 243 | alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE; 244 | state = 0; 245 | value = 0; 246 | } 247 | 248 | /** 249 | * @return an overestimate for the number of bytes {@code 250 | * len} bytes could decode to. 251 | */ 252 | public int maxOutputSize(int len) { 253 | return len * 3/4 + 10; 254 | } 255 | 256 | /** 257 | * Decode another block of input data. 258 | * 259 | * @return true if the state machine is still healthy. false if 260 | * bad base-64 data has been detected in the input stream. 261 | */ 262 | public boolean process(byte[] input, int offset, int len, boolean finish) { 263 | if (this.state == 6) return false; 264 | 265 | int p = offset; 266 | len += offset; 267 | 268 | // Using local variables makes the decoder about 12% 269 | // faster than if we manipulate the member variables in 270 | // the loop. (Even alphabet makes a measurable 271 | // difference, which is somewhat surprising to me since 272 | // the member variable is final.) 273 | int state = this.state; 274 | int value = this.value; 275 | int op = 0; 276 | final byte[] output = this.output; 277 | final int[] alphabet = this.alphabet; 278 | 279 | while (p < len) { 280 | // Try the fast path: we're starting a new tuple and the 281 | // next four bytes of the input stream are all data 282 | // bytes. This corresponds to going through states 283 | // 0-1-2-3-0. We expect to use this method for most of 284 | // the data. 285 | // 286 | // If any of the next four bytes of input are non-data 287 | // (whitespace, etc.), value will end up negative. (All 288 | // the non-data values in decode are small negative 289 | // numbers, so shifting any of them up and or'ing them 290 | // together will result in a value with its top bit set.) 291 | // 292 | // You can remove this whole block and the output should 293 | // be the same, just slower. 294 | if (state == 0) { 295 | while (p+4 <= len && 296 | (value = ((alphabet[input[p] & 0xff] << 18) | 297 | (alphabet[input[p+1] & 0xff] << 12) | 298 | (alphabet[input[p+2] & 0xff] << 6) | 299 | (alphabet[input[p+3] & 0xff]))) >= 0) { 300 | output[op+2] = (byte) value; 301 | output[op+1] = (byte) (value >> 8); 302 | output[op] = (byte) (value >> 16); 303 | op += 3; 304 | p += 4; 305 | } 306 | if (p >= len) break; 307 | } 308 | 309 | // The fast path isn't available -- either we've read a 310 | // partial tuple, or the next four input bytes aren't all 311 | // data, or whatever. Fall back to the slower state 312 | // machine implementation. 313 | 314 | int d = alphabet[input[p++] & 0xff]; 315 | 316 | switch (state) { 317 | case 0: 318 | if (d >= 0) { 319 | value = d; 320 | ++state; 321 | } else if (d != SKIP) { 322 | this.state = 6; 323 | return false; 324 | } 325 | break; 326 | 327 | case 1: 328 | if (d >= 0) { 329 | value = (value << 6) | d; 330 | ++state; 331 | } else if (d != SKIP) { 332 | this.state = 6; 333 | return false; 334 | } 335 | break; 336 | 337 | case 2: 338 | if (d >= 0) { 339 | value = (value << 6) | d; 340 | ++state; 341 | } else if (d == EQUALS) { 342 | // Emit the last (partial) output tuple; 343 | // expect exactly one more padding character. 344 | output[op++] = (byte) (value >> 4); 345 | state = 4; 346 | } else if (d != SKIP) { 347 | this.state = 6; 348 | return false; 349 | } 350 | break; 351 | 352 | case 3: 353 | if (d >= 0) { 354 | // Emit the output triple and return to state 0. 355 | value = (value << 6) | d; 356 | output[op+2] = (byte) value; 357 | output[op+1] = (byte) (value >> 8); 358 | output[op] = (byte) (value >> 16); 359 | op += 3; 360 | state = 0; 361 | } else if (d == EQUALS) { 362 | // Emit the last (partial) output tuple; 363 | // expect no further data or padding characters. 364 | output[op+1] = (byte) (value >> 2); 365 | output[op] = (byte) (value >> 10); 366 | op += 2; 367 | state = 5; 368 | } else if (d != SKIP) { 369 | this.state = 6; 370 | return false; 371 | } 372 | break; 373 | 374 | case 4: 375 | if (d == EQUALS) { 376 | ++state; 377 | } else if (d != SKIP) { 378 | this.state = 6; 379 | return false; 380 | } 381 | break; 382 | 383 | case 5: 384 | if (d != SKIP) { 385 | this.state = 6; 386 | return false; 387 | } 388 | break; 389 | } 390 | } 391 | 392 | if (!finish) { 393 | // We're out of input, but a future call could provide 394 | // more. 395 | this.state = state; 396 | this.value = value; 397 | this.op = op; 398 | return true; 399 | } 400 | 401 | // Done reading input. Now figure out where we are left in 402 | // the state machine and finish up. 403 | 404 | switch (state) { 405 | case 0: 406 | // Output length is a multiple of three. Fine. 407 | break; 408 | case 1: 409 | // Read one extra input byte, which isn't enough to 410 | // make another output byte. Illegal. 411 | this.state = 6; 412 | return false; 413 | case 2: 414 | // Read two extra input bytes, enough to emit 1 more 415 | // output byte. Fine. 416 | output[op++] = (byte) (value >> 4); 417 | break; 418 | case 3: 419 | // Read three extra input bytes, enough to emit 2 more 420 | // output bytes. Fine. 421 | output[op++] = (byte) (value >> 10); 422 | output[op++] = (byte) (value >> 2); 423 | break; 424 | case 4: 425 | // Read one padding '=' when we expected 2. Illegal. 426 | this.state = 6; 427 | return false; 428 | case 5: 429 | // Read all the padding '='s we expected and no more. 430 | // Fine. 431 | break; 432 | } 433 | 434 | this.state = state; 435 | this.op = op; 436 | return true; 437 | } 438 | } 439 | 440 | // -------------------------------------------------------- 441 | // encoding 442 | // -------------------------------------------------------- 443 | 444 | /** 445 | * Base64-encode the given data and return a newly allocated 446 | * String with the result. 447 | * 448 | * @param input the data to encode 449 | * @param flags controls certain features of the encoded output. 450 | * Passing {@code DEFAULT} results in output that 451 | * adheres to RFC 2045. 452 | */ 453 | public static String encodeToString(byte[] input, int flags) { 454 | try { 455 | return new String(encode(input, flags), "US-ASCII"); 456 | } catch (UnsupportedEncodingException e) { 457 | // US-ASCII is guaranteed to be available. 458 | throw new AssertionError(e); 459 | } 460 | } 461 | 462 | /** 463 | * Base64-encode the given data and return a newly allocated 464 | * String with the result. 465 | * 466 | * @param input the data to encode 467 | * @param offset the position within the input array at which to 468 | * start 469 | * @param len the number of bytes of input to encode 470 | * @param flags controls certain features of the encoded output. 471 | * Passing {@code DEFAULT} results in output that 472 | * adheres to RFC 2045. 473 | */ 474 | public static String encodeToString(byte[] input, int offset, int len, int flags) { 475 | try { 476 | return new String(encode(input, offset, len, flags), "US-ASCII"); 477 | } catch (UnsupportedEncodingException e) { 478 | // US-ASCII is guaranteed to be available. 479 | throw new AssertionError(e); 480 | } 481 | } 482 | 483 | /** 484 | * Base64-encode the given data and return a newly allocated 485 | * byte[] with the result. 486 | * 487 | * @param input the data to encode 488 | * @param flags controls certain features of the encoded output. 489 | * Passing {@code DEFAULT} results in output that 490 | * adheres to RFC 2045. 491 | */ 492 | public static byte[] encode(byte[] input, int flags) { 493 | return encode(input, 0, input.length, flags); 494 | } 495 | 496 | /** 497 | * Base64-encode the given data and return a newly allocated 498 | * byte[] with the result. 499 | * 500 | * @param input the data to encode 501 | * @param offset the position within the input array at which to 502 | * start 503 | * @param len the number of bytes of input to encode 504 | * @param flags controls certain features of the encoded output. 505 | * Passing {@code DEFAULT} results in output that 506 | * adheres to RFC 2045. 507 | */ 508 | public static byte[] encode(byte[] input, int offset, int len, int flags) { 509 | Encoder encoder = new Encoder(flags, null); 510 | 511 | // Compute the exact length of the array we will produce. 512 | int output_len = len / 3 * 4; 513 | 514 | // Account for the tail of the data and the padding bytes, if any. 515 | if (encoder.do_padding) { 516 | if (len % 3 > 0) { 517 | output_len += 4; 518 | } 519 | } else { 520 | switch (len % 3) { 521 | case 0: break; 522 | case 1: output_len += 2; break; 523 | case 2: output_len += 3; break; 524 | } 525 | } 526 | 527 | // Account for the newlines, if any. 528 | if (encoder.do_newline && len > 0) { 529 | output_len += (((len-1) / (3 * Encoder.LINE_GROUPS)) + 1) * 530 | (encoder.do_cr ? 2 : 1); 531 | } 532 | 533 | encoder.output = new byte[output_len]; 534 | encoder.process(input, offset, len, true); 535 | 536 | assert encoder.op == output_len; 537 | 538 | return encoder.output; 539 | } 540 | 541 | /* package */ static class Encoder extends Coder { 542 | /** 543 | * Emit a new line every this many output tuples. Corresponds to 544 | * a 76-character line length (the maximum allowable according to 545 | * RFC 2045). 546 | */ 547 | public static final int LINE_GROUPS = 19; 548 | 549 | /** 550 | * Lookup table for turning Base64 alphabet positions (6 bits) 551 | * into output bytes. 552 | */ 553 | private static final byte ENCODE[] = { 554 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 555 | 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 556 | 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 557 | 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', 558 | }; 559 | 560 | /** 561 | * Lookup table for turning Base64 alphabet positions (6 bits) 562 | * into output bytes. 563 | */ 564 | private static final byte ENCODE_WEBSAFE[] = { 565 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 566 | 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 567 | 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 568 | 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_', 569 | }; 570 | 571 | final private byte[] tail; 572 | /* package */ int tailLen; 573 | private int count; 574 | 575 | final public boolean do_padding; 576 | final public boolean do_newline; 577 | final public boolean do_cr; 578 | final private byte[] alphabet; 579 | 580 | public Encoder(int flags, byte[] output) { 581 | this.output = output; 582 | 583 | do_padding = (flags & NO_PADDING) == 0; 584 | do_newline = (flags & NO_WRAP) == 0; 585 | do_cr = (flags & CRLF) != 0; 586 | alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE; 587 | 588 | tail = new byte[2]; 589 | tailLen = 0; 590 | 591 | count = do_newline ? LINE_GROUPS : -1; 592 | } 593 | 594 | /** 595 | * @return an overestimate for the number of bytes {@code 596 | * len} bytes could encode to. 597 | */ 598 | public int maxOutputSize(int len) { 599 | return len * 8/5 + 10; 600 | } 601 | 602 | public boolean process(byte[] input, int offset, int len, boolean finish) { 603 | // Using local variables makes the encoder about 9% faster. 604 | final byte[] alphabet = this.alphabet; 605 | final byte[] output = this.output; 606 | int op = 0; 607 | int count = this.count; 608 | 609 | int p = offset; 610 | len += offset; 611 | int v = -1; 612 | 613 | // First we need to concatenate the tail of the previous call 614 | // with any input bytes available now and see if we can empty 615 | // the tail. 616 | 617 | switch (tailLen) { 618 | case 0: 619 | // There was no tail. 620 | break; 621 | 622 | case 1: 623 | if (p+2 <= len) { 624 | // A 1-byte tail with at least 2 bytes of 625 | // input available now. 626 | v = ((tail[0] & 0xff) << 16) | 627 | ((input[p++] & 0xff) << 8) | 628 | (input[p++] & 0xff); 629 | tailLen = 0; 630 | }; 631 | break; 632 | 633 | case 2: 634 | if (p+1 <= len) { 635 | // A 2-byte tail with at least 1 byte of input. 636 | v = ((tail[0] & 0xff) << 16) | 637 | ((tail[1] & 0xff) << 8) | 638 | (input[p++] & 0xff); 639 | tailLen = 0; 640 | } 641 | break; 642 | } 643 | 644 | if (v != -1) { 645 | output[op++] = alphabet[(v >> 18) & 0x3f]; 646 | output[op++] = alphabet[(v >> 12) & 0x3f]; 647 | output[op++] = alphabet[(v >> 6) & 0x3f]; 648 | output[op++] = alphabet[v & 0x3f]; 649 | if (--count == 0) { 650 | if (do_cr) output[op++] = '\r'; 651 | output[op++] = '\n'; 652 | count = LINE_GROUPS; 653 | } 654 | } 655 | 656 | // At this point either there is no tail, or there are fewer 657 | // than 3 bytes of input available. 658 | 659 | // The main loop, turning 3 input bytes into 4 output bytes on 660 | // each iteration. 661 | while (p+3 <= len) { 662 | v = ((input[p] & 0xff) << 16) | 663 | ((input[p+1] & 0xff) << 8) | 664 | (input[p+2] & 0xff); 665 | output[op] = alphabet[(v >> 18) & 0x3f]; 666 | output[op+1] = alphabet[(v >> 12) & 0x3f]; 667 | output[op+2] = alphabet[(v >> 6) & 0x3f]; 668 | output[op+3] = alphabet[v & 0x3f]; 669 | p += 3; 670 | op += 4; 671 | if (--count == 0) { 672 | if (do_cr) output[op++] = '\r'; 673 | output[op++] = '\n'; 674 | count = LINE_GROUPS; 675 | } 676 | } 677 | 678 | if (finish) { 679 | // Finish up the tail of the input. Note that we need to 680 | // consume any bytes in tail before any bytes 681 | // remaining in input; there should be at most two bytes 682 | // total. 683 | 684 | if (p-tailLen == len-1) { 685 | int t = 0; 686 | v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4; 687 | tailLen -= t; 688 | output[op++] = alphabet[(v >> 6) & 0x3f]; 689 | output[op++] = alphabet[v & 0x3f]; 690 | if (do_padding) { 691 | output[op++] = '='; 692 | output[op++] = '='; 693 | } 694 | if (do_newline) { 695 | if (do_cr) output[op++] = '\r'; 696 | output[op++] = '\n'; 697 | } 698 | } else if (p-tailLen == len-2) { 699 | int t = 0; 700 | v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) | 701 | (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2); 702 | tailLen -= t; 703 | output[op++] = alphabet[(v >> 12) & 0x3f]; 704 | output[op++] = alphabet[(v >> 6) & 0x3f]; 705 | output[op++] = alphabet[v & 0x3f]; 706 | if (do_padding) { 707 | output[op++] = '='; 708 | } 709 | if (do_newline) { 710 | if (do_cr) output[op++] = '\r'; 711 | output[op++] = '\n'; 712 | } 713 | } else if (do_newline && op > 0 && count != LINE_GROUPS) { 714 | if (do_cr) output[op++] = '\r'; 715 | output[op++] = '\n'; 716 | } 717 | 718 | assert tailLen == 0; 719 | assert p == len; 720 | } else { 721 | // Save the leftovers in tail to be consumed on the next 722 | // call to encodeInternal. 723 | 724 | if (p == len-1) { 725 | tail[tailLen++] = input[p]; 726 | } else if (p == len-2) { 727 | tail[tailLen++] = input[p]; 728 | tail[tailLen++] = input[p+1]; 729 | } 730 | } 731 | 732 | this.op = op; 733 | this.count = count; 734 | 735 | return true; 736 | } 737 | } 738 | 739 | private Base64() { } // don't instantiate 740 | } 741 | -------------------------------------------------------------------------------- /src/main/java/com/yize/douban/util/HMACHash1.java: -------------------------------------------------------------------------------- 1 | package com.yize.douban.util; 2 | 3 | import com.yize.douban.util.Base64; 4 | 5 | import javax.crypto.Mac; 6 | import javax.crypto.spec.SecretKeySpec; 7 | 8 | 9 | public class HMACHash1 { 10 | public static final String HMAC_SHA1 = "HmacSHA1"; 11 | public static final String a(String str, String str2) { 12 | try { 13 | SecretKeySpec secretKeySpec = new SecretKeySpec(str.getBytes(),HMAC_SHA1); 14 | Mac instance = Mac.getInstance(HMAC_SHA1); 15 | instance.init(secretKeySpec); 16 | return Base64.encodeToString(instance.doFinal(str2.getBytes()), 2); 17 | } catch (Exception e) { 18 | e.printStackTrace(); 19 | return null; 20 | } 21 | } 22 | 23 | 24 | } -------------------------------------------------------------------------------- /src/main/java/com/yize/douban/util/HttpRequestHelper.java: -------------------------------------------------------------------------------- 1 | package com.yize.douban.util; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.InputStreamReader; 5 | import java.io.PrintWriter; 6 | import java.net.HttpURLConnection; 7 | import java.net.URL; 8 | import java.util.Map; 9 | 10 | public class HttpRequestHelper { 11 | public static String downloadWebSiteUseGet(String link, Map headerMap){ 12 | StringBuilder sb=new StringBuilder(); 13 | try { 14 | URL url=new URL(link); 15 | HttpURLConnection conn=(HttpURLConnection)url.openConnection(); 16 | conn.setRequestMethod("GET"); 17 | conn.setReadTimeout(10000); 18 | conn.setConnectTimeout(10000); 19 | if(headerMap!=null){ 20 | for (String key:headerMap.keySet()){ 21 | conn.addRequestProperty(key,headerMap.get(key)); 22 | } 23 | } 24 | BufferedReader reader=new BufferedReader(new InputStreamReader(conn.getInputStream())); 25 | String line; 26 | while ((line=reader.readLine())!=null){ 27 | sb.append(line); 28 | } 29 | reader.close(); 30 | conn.disconnect(); 31 | } catch (Exception e) { 32 | e.printStackTrace(); 33 | } 34 | return sb.toString(); 35 | } 36 | /** 37 | * 做POST请求 38 | * @param link 请求地址 39 | * @param params 请求体,类似于keyword=十年&num=100这样的格式 40 | * @param headerMap 41 | * @return 42 | */ 43 | public static String downloadWebSiteUsePost(String link,String params, Map headerMap){ 44 | String response=null; 45 | try { 46 | URL url=new URL(link); 47 | HttpURLConnection conn=(HttpURLConnection)url.openConnection(); 48 | conn.setRequestMethod("POST"); 49 | conn.setDoInput(true); 50 | conn.setDoOutput(true); 51 | conn.setConnectTimeout(10000); 52 | conn.setReadTimeout(10000); 53 | if((headerMap!=null&&headerMap.keySet().size()>0)){ 54 | for(String key:headerMap.keySet()){ 55 | conn.setRequestProperty(key,headerMap.get(key)); 56 | } 57 | } 58 | PrintWriter writer=new PrintWriter(conn.getOutputStream()); 59 | writer.print(params); 60 | writer.flush(); 61 | BufferedReader reader=new BufferedReader(new InputStreamReader(conn.getInputStream(),"utf-8")); 62 | String line; 63 | StringBuilder sb=new StringBuilder(); 64 | while ((line=reader.readLine())!=null){ 65 | sb.append(line); 66 | } 67 | writer.close(); 68 | reader.close(); 69 | conn.disconnect(); 70 | response=sb.toString(); 71 | }catch (Exception ignored){ 72 | 73 | } 74 | return response; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/yize/douban/util/TextUtils.java: -------------------------------------------------------------------------------- 1 | package com.yize.douban.util; 2 | 3 | public class TextUtils { 4 | public static boolean isEmpty(String str) { 5 | return str==null||str.length()==0; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | Archetype Created Web Application 7 | 8 | -------------------------------------------------------------------------------- /src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Hello World!

4 | 5 | 6 | -------------------------------------------------------------------------------- /src/test/java/com/yize/test/TestDoubanWebRequest.java: -------------------------------------------------------------------------------- 1 | package com.yize.test; 2 | 3 | import com.yize.douban.base.DoubanWebRequest; 4 | import org.junit.Test; 5 | 6 | import java.util.HashMap; 7 | 8 | public class TestDoubanWebRequest { 9 | @Test 10 | public void testWebRequest(){ 11 | HashMap paramsMap=new HashMap<>(); 12 | paramsMap.put("count","100"); 13 | String response=DoubanWebRequest.downloadWebSiteUseGet("https://frodo.douban.com/api/v2/elessar/subject/27260217/photos",paramsMap); 14 | System.out.println(response); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/com/yize/test/TestEncrypt.java: -------------------------------------------------------------------------------- 1 | package com.yize.test; 2 | 3 | import com.yize.douban.util.AesEncrypt; 4 | import org.junit.Test; 5 | 6 | public class TestEncrypt { 7 | 8 | /** 9 | * 经过android的Base64加密后的豆瓣APP签名信息,每个版本都是固定的 10 | * 11 | * 在安卓里面可以这样获取: 12 | * Application application=(Application)getApplicationContext(); 13 | * PackageInfo packageInfo=application.getPackageManager().getPackageInfo("com.douban.frodo",PackageManager.GET_SIGNATURES); 14 | * String sign=Base64.encodeToString(packageInfo.signatures[0].toByteArray(),0); 15 | * 16 | * 这里直接给出一个版本的豆瓣APP的签名信息 17 | */ 18 | public final static String SIGN="MIICUjCCAbsCBEty1MMwDQYJKoZIhvcNAQEEBQAwcDELMAkGA1UEBhMCemgxEDAOBgNVBAgTB0Jl\n" + 19 | "aWppbmcxEDAOBgNVBAcTB0JlaWppbmcxEzARBgNVBAoTCkRvdWJhbiBJbmMxFDASBgNVBAsTC0Rv\n" + 20 | "dWJhbiBJbmMuMRIwEAYDVQQDEwlCZWFyIFR1bmcwHhcNMTAwMjEwMTU0NjExWhcNMzcwNjI3MTU0\n" + 21 | "NjExWjBwMQswCQYDVQQGEwJ6aDEQMA4GA1UECBMHQmVpamluZzEQMA4GA1UEBxMHQmVpamluZzET\n" + 22 | "MBEGA1UEChMKRG91YmFuIEluYzEUMBIGA1UECxMLRG91YmFuIEluYy4xEjAQBgNVBAMTCUJlYXIg\n" + 23 | "VHVuZzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAg622fxLuwQtC8KLYp5gHk0OmfrFiIisz\n" + 24 | "kzPLBhKPZDHjYS1URhQpzf00T8qg2oEwJPPELjN2Q7YOoax8UINXLhMgFQkyAvMfjdEOSfoKH93p\n" + 25 | "v2d4n/IjQc/TaDKu6yb53DOq76HTUYLcfLKOXaGwGjAp3QqTqP9LnjJjGZCdSvMCAwEAATANBgkq\n" + 26 | "hkiG9w0BAQQFAAOBgQA3MovcB3Hv4bai7OYHU+gZcGQ/8sOLAXGD/roWPX3gm9tyERpGztveH35p\n" + 27 | "aI3BrUWg2Vir0DRjbR48b2HxQidQTVIH/HOJHV0jgYNDviD18/cBwKuLiBvdzc2Fte+zT0nnHXMy\n" + 28 | "E6tVeW3UdHC1UvzyB7Qcxiu4sBiEO1koToQTWw==\n"; 29 | /** 30 | * 反编译豆瓣后得到的一个常量,在豆瓣APP里面固定写死了,具体位置看我的图文解析 31 | */ 32 | public final static String TEXT="bHUvfbiVZUmm2sQRKwiAcw=="; 33 | 34 | @Test 35 | public void testEncrypt(){ 36 | String hmacKey= AesEncrypt.encrypt(TEXT,SIGN); 37 | System.out.println(hmacKey);//输出结果是:bf7dddc7c9cfe6f7 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/yize/test/TestRequestCelebrityPhoto.java: -------------------------------------------------------------------------------- 1 | package com.yize.test; 2 | 3 | import com.yize.douban.module.CelebrityPhoto; 4 | import org.junit.Test; 5 | 6 | public class TestRequestCelebrityPhoto { 7 | @Test 8 | public void test(){ 9 | CelebrityPhoto celebrityPhoto=new CelebrityPhoto(); 10 | String response=celebrityPhoto.requestCelebrityPhoto("27260217","0","50"); 11 | System.out.println(response); 12 | } 13 | } 14 | --------------------------------------------------------------------------------