Redis는 Key & Values로 이루어 진 구조이며, values의 데이터타입은 String, Hashes, Lists, Sets, Sorted Sets 5가지가 있다.




String (문자열 데이터)


- String 값으로 저장 할 뿐 아니라, String의 Binary 데이터도 저장할 수 있다.


Key & Values



Key : user:1:data


value: {

               "id": "user00",

               "name":"kim"

           }





Hashes (해쉬 데이터)


Key & Values



Key : user:1:data


value:  필드 - 값으로 이루어진 해쉬 데이터


필드

 

 id

user00 

 name

kim 



-> 새로운 데이터를 추가할 경우 필드-값의 데이터를 추가하면 된다.

ex) age - 15

 



Lists



Key & Values



Key : user:1:data


value:  요소




요소


user00 - kim


나이가 15인 요소 추가


: user00 - kim - 15






sets


Key & Values



Key : user:1:data


value:  요소



 user00

 kim

 15

 true


요소 추가

 user00

 kim

 15

 true


 korea

 







Sorted sets




Key & Values



Key : user:1:data


value:  가중치 - 값



 가중치

 

 22

 user00

 24

 kim


 

23의 가중치와 15의 값을 갖는 데이터 요소 추가


가중치

 값

 22

 user00

 23

 15

 24

 kim

  







'Cloud & NoSQL & Middleware > Redis' 카테고리의 다른 글

Java에서 Redis 연동 테스트 (jedis)  (0) 2018.07.01
MacOS에서 Redis 설치 및 실행  (0) 2018.07.01
Redis 특징(3) - Redis Persistance  (0) 2018.06.29
Redis 특징  (2) 2018.06.29
레디스(Redis) 란?  (1) 2018.06.29

특징


1. String, lists, sets, hashes, sorted sets  과 같은 자료구조를 지원한다.

2. 디스크가 아닌 메모리 기반의 데이터 저장소이다. 

3. DataBase로도 사용할 수 있고 Cashe로도 사용할 수 있다. (NoSQL & Cashe)

   - 캐쉬로 사용할 경우 저장소 기능을 off해야 함.

4. Redis Sentinel을 통해 높은 가용성을 갖을 수 있고, Redis Cluster를 통해 Master-slave의 클러스터링을 수행할 수 있다. 

5. 성능은 환경에 따라 다르겠지만 초당 2만~10만회 수행할 수 있다.





IMDB(in-memory DB)


메모리 기반 저장소 VS 디스크 기반 저장소


속도                                     >

내구성                                 <





Redis


Remote Dictionary Server 의 약자이며  데이터베이스, 캐시 및 메시지 브로커로 사용되는 오픈소스  메모리 데이터 구조 저장소이다. (in-memory data structure sotre)


Redis는 ANSI C로 작성되었으며 Linux, * BSD, OS X와 ​​같은 대부분의 POSIX 시스템에서 외부 종속성없이 작동한다.


Redis는 페이스북, 인스타그램, 네이버 LINE 서비스, StackOverflow, 블리자드 등 대형 서비스 업체들이 사용자들의 대규모 메세지를 실시간으로 처리하기 위하여 사용하고 있다.



Apns JWT Format



Heder


{

    "alg": "ES256",            // 알고리즘

    "kid": "ABC123DEFG“   // key 8글자

}



계정 인증키는 아래에서 확인 할 수 있다. 주의 할 점은 함부로 노출되선 안되며 발급은 최초 한번 밖에 안되니 따로 저장해둘 것.






PayLoad



{

    "iss": "DEF123GHIJ", //  10-character Team ID

    "iat": 1437179036       // in terms of the number of seconds since Epoch, in UTC

 }





Signature

{

      base64_header + "." + base64_payload,

       secret

) base64_endcoding



-> 서명 형식에서 중요한 것은 secret 값이다 . 위의 계정 인증 키와 함께 .p8 인증서 파일을 다운 받을 수 있는데 파일안에 data를 읽어서 String으로 설정하면 된다.





Apple은 JWT를 사용하는 데 있어서 다음과 같은 사항을 요구하고 권장한다.


- APN은 ES256 알고리즘으로 서명 된 공급자 인증 토큰 만 지원합니다.

- 보안되지 않은 JWT 또는 다른 알고리즘으로 서명 된JWT는 거부되고 공급자는 InvalidProviderToken(403) 응답을 받습니다 .


- 보안을 보장하기 위해 APN은 주기적으로 새 토큰을 생성해야합니다.


- 새 토큰에는 토큰이 생성 된 시간을 나타내는 claim 키에서 업데이트 된 발급 항목이 있습니다. 



- 만일 토큰 문제가 발생하고 토큰 문제에 대한 타임 스탬프가 지난 1 시간 이내에 있지 않으면 APN은 후속 푸시 메시지를 거부하고 ExpiredProviderToken (403) 오류를 반환합니다.


-제공자 토큰 서명 키가 유출 된 것으로 의심되면 개발자 계정에서 해지 할 수 있습니다. 새 키 쌍을 발행 할 수 있으며 새 개인 키를 사용하여 새 토큰을 생성 할 수 있습니다. 보안을 최대화하려면 지금 폐기 된 키로 서명 된 토큰을 사용했던 APN에 대한 모든 연결을 닫고 새 키로 서명 된 토큰을 사용하기 전에 다시 연결하십시오.






=> 사용자로 하여금 지속적으로 토큰을 수정하게 하도록하고, 또한 앞서 단점이라고 생각했던 보안에 있어서 완벽하다고 할 순 없지만 apple에서는 커버하고자 하려 했던 것 같다.


=> JWT는 현재 java, python, c 등등 라이브러리를 지원하고 있다. 라이브러리를 사용하는 것보단 직접 구현을 통해 JWT를 구현해보고자 한다.




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
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
 
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
 
public class JsonWebToken {
    
    private String token;
    
    
    public String createToken() throws InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, SignatureException {
        long nowMillis = System.currentTimeMillis() / 1000;
        
        
        //json
        String jwt_header = "{\"alg\" : \"ES256\" , \"kid\":\"KA1KG44A\"}";
        //json
        String jwt_payload = "{\"iss\" : \"TEAMID1234\" , \"iat\":" + nowMillis + "}";
        
        
        String KEY_PATH = JsonWebToken.class.getResource("").getPath() + "../../lib/AuthKey.p8";
 
 
 
        String base64_header = new String(Base64.encode(jwt_header.getBytes(StandardCharsets.UTF_8)));
        String base64_payload = new String(Base64.encode(jwt_payload.getBytes(StandardCharsets.UTF_8)));
        String part1 = base64_header + "." + base64_payload;
 
        BufferedReader br = null;
        String secret = "";
        try {
            String currentLine;
            br = new BufferedReader(new FileReader(KEY_PATH));
            while ((currentLine = br.readLine()) != null) {
                secret += currentLine;
            }
  br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        
        token = base64_header + "." + base64_payload + "." + ES256(secret, part1);
    
        System.err.println(token);
        
        return token;
        
    }
    
    public String ES256(final String secret, final String data) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException {
 
            KeyFactory kf;
    
            kf = KeyFactory.getInstance("EC");
            KeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(secret));
//            KeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(secret));
            PrivateKey key;
            key = kf.generatePrivate(keySpec);
 
            final Signature sha256withECDSA = Signature.getInstance("SHA256withECDSA");
            sha256withECDSA.initSign(key);
 
            sha256withECDSA.update(data.getBytes(StandardCharsets.UTF_8));
            final byte[] signed = sha256withECDSA.sign();
            
            return Base64.encode(signed).toString();
    
                    
    }
}
cs







 






장점


(1)사용이 쉽다.

(2)서버 개발 부담을 덜어 준다.

(3)보안

- 애플리케이션의 보안을 높일 수 있지만 무조건 위험을 벗어나는 것은 아니다.


(4) 모바일 환경에 적합하다.

- (웹에서 쿠키와같은 데이터로 인증안해도됨)


(5)Stateless 서버 (무상태)

- 즉 연결 유지를 하지 않아도 데이터를 가지고 올 수 있다.

- 토큰 값만 알고 있다면, 서버로부터 토큰값과 함께 데이터를 요청할 수 있다.







단점


(1)길이

- Claim에 넣는 데이터가 많아질 수록, JWT 토큰의 길이가 길어진다.

- API 호출 시 매 호출마다 헤더에 붙어서 가야하기 때문에, 길이가 길다는 것은 그만큼 네트워크 대역폭 낭비가 심하다는 의미이다.


(2) 한번 발급된 토큰은 값을 수정하거나 폐기가 불가

- JWT는 토큰 내에 모든 정보를 다 가지고 있기 때문에, 한번 발급된 토큰에 대한 변경은 서버에서는 더 이상 불가능하다. 예를 들어 토큰을 잘못 발행해서 삭제하고 싶더라도, Signature만 맞으면 맞는 토큰으로 인식을 하기 때문에, 서버에서는 한번 발급된 토큰의 정보를 바꾸는 일등이 불가능하다.

그래서 만약에 JWT를 쓴다면, Expire time을 꼭 명시적으로 두도록 하고, refresh token등을 이용해서, 중간중간 토큰을 재발행하도록 해야 한다.



(3)보안

JWT는 기본적으로 Claim에 대한 정보를 암호화 하지 않는다. 단순히 BASE64인코딩만 하기 때문에, 중간에 패킷을 가로채거나, 기타 방법으로 토큰을 취득했으면 토큰 내부 정보를 통해서 사용자 정보가 누출 될 수 있는 가능성이 있다


특히 자바스크립트 기반의 웹 클라이언트의 경우 브라우져상의 디버거등을 통해서 토큰이 노출될 가능성이 높다.


그래서, 이를 보완하는 방법으로는 토큰 자체를 암호화 하는 방법이 있다. JSON을 암호화 하기 위한 스펙으로는 JWE(JSON Web Encryption)





=> 위와 같은 단점에도 불구하고 폐쇄성이 높은 APPLE에서 JWT를 사용한다는 것은 위의 단점을 모두 커버했을 뿐 아니라 적절하게 이용되고 있을 것이라고 생각하다. 











APNS 공급자 인증


HTTP/2 프로토콜을 이용하여 APNS Provider API를 사용할 경우, JWT(Json Web Token) 을 지원하며 이를 통해 공급자 인증을 수행할 수 있다.
또한 APNS 개발자 계정의 고유한 key 값을 통해 JWT가 만들어지며 모든 앱에 Notification을 수행할 수 있는 강력한 방법이다.

HTTP/2 프로토콜을 이용하여 JWT를 사용하지 않고, 인증서를 통해 공급자 인증을 수행한다면 당연하게도 하나의 앱에서 Notificaiton을 수행한다.

공급자 인증을 수행하기 전에 JWT에 대해 자세히 알아봐야 한다.






JWT


- JWT JSON 객체를 전달할 수 있고 여기에 서명하거나 암호화 할 수 있다.

- JWT 는 보통 2가지 경우에서 사용한다.


(1)인증

(2)정보 교류


- 구조 


aaaaa.bbbb.cccccc

                                              header       payload      signature    



(1) JWT Header


- Typ : 토큰의 타입 

- Alg : 알고리즘



{

  "typ": "JWT",

  "alg": "HS256“

 }







(2) JWT Payload



payload 부분에는 토큰에 담을 정보가 들어있다
- payload에 담는 정보의 한 ‘조각’ 을 클레임(claim) 이라고 부르고, 이는 name / value 의 한 쌍으로 이뤄져있다. 
- 토큰에는 여러개의 클레임 들을 넣을 수 있습니다.
- 클레임 의 종류는 다음과 같이 크게 세 분류로 나뉘어져있다.

 등록된 (registered) 클레임, 공개 (public) 클레임, 비공개 (private) 클레임



{

  "iss": "velopert.com",  //등록된 클레임

   "exp": "1485270000000",  //등록된 클레임

   "https://velopert.com/jwt_claims/is_admin": true,  //공개된 클레임

   "username": "velopert    //비공개 클레임

}




- 클레임의 종류 


iss: 토큰 발급자 (issuer)

sub: 토큰 제목 (subject)

aud: 토큰 대상자 (audience)

exp: 토큰의 만료시간 (expiraton), 시간은 NumericDate 형식으로 되어있어야 하며 (: 1480849147370) 언제나 현재 시간보다 이후로 설정


nbf: Not Before 를 의미하며, 토큰의 활성 날짜와 비슷한 개념. 여기에도 NumericDate 형식으로 날짜를 지정하며, 이 날짜가 지나기 전까지는 토큰이 처리되지 않는다.


iat: 토큰이 발급된 시간 (issued at), 이 값을 사용하여 토큰의 age 가 얼마나 되었는지 판단 가능하다.

jti: JWT의 고유 식별자로서, 주로 중복적인 처리를 방지하기 위하여 사용됩니다. 일회용 토큰에 사용하면 유용하다.








(3) JWT Signature


- 서명 부분을 만들려면
인코딩 된 헤더, 인코딩 된 페이로드, 암호, 헤더에 지정된 알고리즘을 서명해야한다.

- 예를 들어 HMAC SHA256 알고리즘을 사용하려면 다음과 같은 방법으로 서명을 만든다.


{

  base64_header + "." + base64_payload,

  secret

) base64_endcoding















참고: https://jwt.io/





Payload 는 다음과 같은 형태로 보낸다.


Json Data


{

"aps":{

"alert":{

"title":"title",

"body":"Hi"

},

"badge":3,

"sound":"default"

}

}





Frame 


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
public class Frame {
    private final int COMMAND_NOTIFICATION = 2;
 
    private final short ITEM_ID_DEVICE_TOKEN = 1;
    private final short ITEM_ID_PAYLOAD = 2;
    private final short ITEM_ID_NOTIFICATION_ID = 3;
    private final short ITEM_ID_EXPIRATION_DATE = 4;
    private final short ITEM_ID_PRIORITY = 5;
 
    private final int MAX_PAYLOAD_BYTES = 2048/// 2048 크기 이상 pay 로드에 싣을수 없음.
 
    private String device_token;
    private int frame_length;
 
    private byte[] frame_data;
 
    public Frame(String device_token) {
        this.device_token = device_token;
    }
 
    public byte[] getFrame_data() {
 
        return frame_data;
    }
 
    public int getFrame_length() {
 
        return frame_length;
    }
 
    public int getCommandNotification() {
        return COMMAND_NOTIFICATION;
    }
 
    public void pack() {
        
 
        // payload Data를 담는다.

        String payload = get_jsonData();

        try {
            
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            
            
            baos.write(ITEM_ID_DEVICE_TOKEN);
            baos.write(intTo2ByteArray(32));
            baos.write(DatatypeConverter.parseHexBinary(device_token));
            
        
            
 
            baos.write(ITEM_ID_PAYLOAD);
            baos.write(intTo2ByteArray(payload.getBytes().length));
            baos.write(payload.getBytes("UTF-8"));
    
            
            baos.write(ITEM_ID_NOTIFICATION_ID);
            baos.write(intTo2ByteArray(4));
            baos.write(intTo4ByteArray(5));
 
            
            baos.write(ITEM_ID_EXPIRATION_DATE);
            baos.write(intTo2ByteArray(4));
            baos.write(intTo4ByteArray(5));
            
            baos.write(ITEM_ID_PRIORITY);
            baos.write(intTo2ByteArray(1));
            baos.write((byte)10);
            
            
            
            frame_length = baos.size();
            frame_data = new byte[baos.size()];
            frame_data = baos.toByteArray();
 
baos.close();
    
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 
    }
 
    // json 데이터를 가져오는 과정
    public String get_jsonData() {
 
        JSONParser parser = new JSONParser();
        JSONObject jObj = new JSONObject();
        Object obj = null;
 
        try {
            String path =  Main.class.getResource("").getPath();
            obj = parser.parse(new FileReader(path));
            jObj = (JSONObject) obj;
 
            System.out.println("payLoad:" + jObj.toJSONString());
 
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (ParseException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 
        return jObj.toString();
    }
 
    private static final byte[] intTo4ByteArray(int value) {
        return ByteBuffer.allocate(4).putInt(value).array();
    }
 
    private static final byte[] intTo2ByteArray(int value) {
        int s1 = (value & 0xFF00>> 8;
        int s2 = value & 0xFF;
        return new byte[] { (byte) s1, (byte) s2 };
    }
 
}
cs



-> 전체 Frame Data에 5개의 ITEM 추가 하였다.

 






Binary Provider API


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
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
 
import javax.net.ssl.SSLSocket;
 
 
public class Binary_Provider_API {
 
    private SSLSocket sSock;
    private String device_token;
 
    public Binary_Provider_API(SSLSocket sSock, String device_token) {
        this.sSock = sSock;
        this.device_token = device_token;
 
    }
 
    
    public void send() {
 
        try {
            Frame frame = new Frame(device_token);
            frame.pack();
            OutputStream outputStream = sSock.getOutputStream();
            outputStream.write(frame.getCommandNotification());
            System.out.println("api_send_command:" + frame.getCommandNotification());
 
            outputStream.write(intTo4ByteArray(frame.getFrame_length()));
            System.out.println("api_send_length:" + frame.getFrame_length());
 
            outputStream.write(frame.getFrame_data());
            System.out.println("Item Packet");
            System.out.println(Arrays.toString(frame.getFrame_data()));
 
            outputStream.flush();
outputStream.close();
 
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 
    }
    private static final byte[] intTo4ByteArray(int value) {
        return ByteBuffer.allocate(4).putInt(value).array();
    }
cs



frame data를 가지고 온 이후, Binary Provider API Format에 맞게 보내면 끝.




command / frame_length / frameData






Apns Provider API



1. HTTP/2 프로토콜을 통해 APNS와 연결 가능하며, 인증서와 JWT Token을 이용해 푸쉬 알림을 보낼 수 있다.


2. HTTP/2 프로토콜을 이용한 APNS는 다음과 같은 특징을 갖고 있다.


http/2 ping Frame을 통해 연결 유지 가능하다.

http/2 goaway Frame을 통해 연결 종료 가능하다.








Request Format 





<HTTP/2 Header> 



Method

POST

PATH

/3/device/<장치토큰>




위의 두가지는 필수적으로 추가해야한다.

추가적으로 포함시켜야 하는 키와 데이터는 다음과 같은 항목이 있다,.



- authorization : 인증서 방식이 아닌 토큰 방식을 사용할 때 사용되는 키값이다.

 (토큰은 base64url인코딩된(Signature) jwt format 이어야 한다구체적으로 Bearer + <jwt token> 형태이다.)


- apns-topic : 인증서 방식이 아닌 토큰 방식을 사용되는 키값이다. 토큰 방식은 모든 앱에 전송할 수 있다.

 그렇기 때문에 apns-topic 키 값의 앱의 bundle_id에 해당하는 데이터를 입력한다.


- apns-idUUID , 형태는 32글자의 소문자를 포함한 16진수 Ex) 123e4567-e89b-12d3-a456-42665544000)


apns-expirationutc 시간, 0이면 저장하지 않고 바로 보낸다.


apns-priority : 10이면 즉시보내고, 5이면 클라이언트의 전원장치를 고려하여 전송한다. 기본값은 10이다.


apns-collapse-id64바이트를 초과하지 않아야 하고, 동일한 식별자 값의 알림을 단일 알림으로 표시한다.






<HTTP/2 Body>


- 보내고자 하는 Payload 데이터를 담아 보낸다.












Response Format




<HTTP/2 Header> 


Apns-id

Request apns-id의 값. 요청에서 포함하지 않았다면 apns 서버 자체적으로 새uuid를 만들고 헤더에 반환한다.

Status


응답 코드





Status 









<HTTP/2 body> 


- 성공적으로 request 요청이 수행되면 body 데이터는 비어있다.


- 실패하면 다음과 같은 항목이 포함된다.




Reason

실패에 대한 이유

timestamp


410 에러 상태일 때만 나오는 키 값 , 디바이스 토큰이 마지막으로 활성화 되었을 때의 시간






Status 및 Reason 









Netty를 통해 샘플코드를 작성해볼 수있다.





참고

https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/BinaryProviderAPI.html#//apple_ref/doc/uid/TP40008194-CH13-SW1









+ Recent posts