APIのアクセストークンをどうするか検討する機会があったので、SecureRandomクラスを使う方法とJWTを生成する方法の2種類を試してみました。

環境

  • Java 1.8

アクセストークンの表現方法

OAuth 2.0 + OpenID Connect のフルスクラッチ実装者が知見を語るを参考にしました。

アクセストークンの表現方法としては大まかに分けて以下の2パターンなので、それぞれの方法でトークンを生成してみました。

  1. 無意味な文字列
  2. ユーザーIDや有効期限などの情報をエンコード

SecureRandomクラスで乱数を生成

「無意味な文字列」を生成する方法としてSecureRandomクラスを使いました。SecureRandomクラスは、暗号用に強化された乱数ジェネレータです。

参考ページ

CSRF の安全なトークンの作成方法(Java編)

ソースコード

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import javax.xml.bind.DatatypeConverter;

public class RandomToken {
    
    private static int TOKEN_LENGTH = 16;  //16*2=32バイト
    
    public static void main(String[] args) {
        byte token[] = new byte[TOKEN_LENGTH];
        StringBuffer buf = new StringBuffer();
        SecureRandom random = null;
        String tokenString = null;
        
        try {
            random = SecureRandom.getInstance("SHA1PRNG");
            random.nextBytes(token);
            
            for(int i = 0; i < token.length; i++) {
                buf.append(String.format("%02x", token[i]));
            }
            tokenString = buf.toString();
            
            System.out.println("String.format: " + tokenString);
            System.out.println("DatatypeConverter: " + DatatypeConverter.printHexBinary(token));
            
        } catch(NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }
}

コンソール

String.format: c43290127da70d78f3a22281bade47dc
DatatypeConverter: C43290127DA70D78F3A22281BADE47DC

※DatatypeConverterを使うと英字が大文字になります。

Json Web Token(JWT)を生成

2パターン目のユーザーIDや有効期限など認証(認可)に必要な情報をエンコードする方法として、JWTを生成してみました。

利用したライブラリ

JJWT

事前準備

pom.xmlに依存関係を追加

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>

ソースコード

import java.security.Key;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Base64;
import java.util.Date;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.impl.crypto.MacProvider;

public class JsonWebToken {

    public static void main(String[] args) {
        
        Key key = MacProvider.generateKey();
        
        LocalDateTime now = LocalDateTime.now();
        System.out.println("Current Timestamp: " + now);
        
        //7日後の日時
        ZonedDateTime expirationDate = now.plusDays(7).atZone(ZoneId.systemDefault());
        
        //JWTを生成
        System.out.println("*** JWT Create ***");
        String compactJws = Jwts.builder()
                .setSubject("hoge")
                .setExpiration(Date.from(expirationDate.toInstant()))
                .signWith(SignatureAlgorithm.HS512, key)
                .compact();
        System.out.println("JWT: " + compactJws);
        
        //Base64でデコードしてみる
        System.out.println("*** Base64 decode ***");
        String[] jwtSections = compactJws.split("\\.");
        String header = new String(Base64.getDecoder().decode(jwtSections[0]));
        String claim = new String(Base64.getDecoder().decode(jwtSections[1]));
        System.out.println("JWT Header: " + header);
        System.out.println("JWT Claim: " + claim);
        
        //クレームの確認
        System.out.println("*** Claim ***");
        Date exp = Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws).getBody().getExpiration();
        String sub = Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws).getBody().getSubject();
        System.out.println("exp(Expiration Time): " + LocalDateTime.ofInstant(exp.toInstant(), ZoneId.systemDefault()));
        System.out.println("sub(Subject): " + sub);
        
        //署名の検証
        System.out.println("*** Signature Validation ***");
        try {
            Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws);
            System.out.println("Use Correct Key: Validation Success");
        } catch(SignatureException e) {
            System.out.println("Use Correct Key: Validation Fail - " + e.getMessage());
        }
        
        Key wrongKey = MacProvider.generateKey();
        try {
            //生成時とは別のキーを使用
            Jwts.parser().setSigningKey(wrongKey).parseClaimsJws(compactJws);
            System.out.println("Use Wrong Key: Validation Success");
        } catch(SignatureException e) {
            System.out.println("Use Wrong Key: Validation Fail - " + e.getMessage());
        }
    }
}

コンソール

Current Timestamp: 2017-05-11T23:33:15.318
*** JWT Create ***
JWT: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJob2dlIiwiZXhwIjoxNDk1MTE3OTk1fQ.pWxuaNKkJeLV_lO2qyhn7F7ytUfJZeUUax16zFL2Q4_OivjoIEPg4ckfnU4JdIotNH8TKE6zsxxhefpPIhdLIw
*** Base64 decode ***
JWT Header: {"alg":"HS512"}
JWT Claim: {"sub":"hoge","exp":1495117995}
*** Claim ***
exp(Expiration Time): 2017-05-18T23:33:15
sub(Subject): hoge
*** Signature Validation ***
Use Correct Key: Validation Success
Use Wrong Key: Validation Fail - JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.

誤ったキーを使うとパースに失敗しているので、改ざんされていないことをチェックできています。