らっちゃいブログ

日々の学びと気づきを発信するブログ

いきなりステーキで極厚ステーキ300gを食べてきた

食べてきたシリーズも三回目となりました。

racchai.hatenablog.com

racchai.hatenablog.com

今回はステーキを食します。

お店に到着

『ステーキは厚切りレアーで召し上がれ』

f:id:racchai:20160521115927j:plain

テンションあがりますね!

列ができているので、さっそく並びます。お昼時にしては、行列が短くて助かりました。

f:id:racchai:20160521120121j:plain

注文

立ち食いなのもあり、なかなかの回転率です。ほとんど並ばずに入店できました。

それではさっそく注文です。ランチメニューは以下の二択。どちらもライス・サラダ・スープ付きです。

  • ワイルドステーキ300g 1,350円
  • ワイルドハンバーグ300g 1,000円

どっちを選んでも300gとは恐れ入ります。せっかくここまできたんですから、値段にひよってられません。

ワイルドステーキをくれ!ごはんは大盛で。

食す

注文後、ものの数秒でサラダとスープが運ばれてきました。事前にストックしてあるものを持ってくる形式のあれです。

一応写真をパシャり。

f:id:racchai:20160521120204j:plain

と、写真を撮っていたらもうステーキが来ました。はやー!

f:id:racchai:20160521120237j:plain

(写真からは伝わらないけど)分厚い!うまそう。

厨房で塩コショウを振っているのが見えたので、もう味はついているんだろうと推測。そのままナイフを入れて口に運びました。

うまー!やわらか!

すばらしいクオリティです。ごはんが進みます。

2口食べたあたりで、もっと味を濃くしたくなったため、備え付けられてるタレを流し入れました。すると鉄板の熱さでタレが飛び散る飛び散る。紙エプロンしておいて正解でした。

タレをかけると余計にごはんが進む味に。大盛にしておいてよかった。

ちなみにステーキあるあるですが、端っこにある脂身って嫌ですよね。筋っぽくて固いしおいしくない。いつも残してしまいます。

いきなりステーキでもやはり脂身は存在します。でも、今回はほぼ食べきってしまいました。というのも、脂身が甘いんです。一口食べて『これはイケるやつだ』と判断しました。決して柔らかいとは言えませんが、いつもの固くて食べれない脂身とは全然違います。

そんなこんなで10分ほどで完食。いやーおいしかった。

お会計

いやーこんなうまい肉が 1,350円とは!

店員さん『1,458円になります』

わたし『えっ』

メニューの価格は税抜きだったようです。最後の最後にひよりました。。

ひとこと

また明日から節約生活やで。

django-rest-framework-jwtの認証をカスタマイズする方法

django-rest-framework-jwt については以前こちらの記事でご紹介しました。

racchai.hatenablog.com

django-rest-framework-jwt では認証APIを標準で用意してくれているので、通常はそれを使うことになります。

ですが、論理削除されたユーザーの場合は認証を失敗させたりだとか、認証処理をカスタマイズしたい場合は自前で認証してアクセストークンを返す処理を用意する必要があります。

本記事では、企業コード/メールアドレス/パスワードにて認証するケースを想定したカスタマイズ方法についてご紹介します。

Django および Django REST framework が初めてという方はこちらをどうぞ!

racchai.hatenablog.com

racchai.hatenablog.com

では、さっそく始めます。

プロジェクトを作成する

さくっと作成していきます。

$ django-admin startproject racchai
$ cd racchai

settings.py にて INSTALLED_APPSDATABASES を編集。

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
+   'rest_framework',
+   'rest_framework_jwt',
+   'racchai',
)

DATABASES = { 
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'racchai',
        'USER': 'root',
        'PASSWORD': 'root',
        'HOST': 'localhost',
    }   
}

ついでにMySQLにデータベースを作成しておきましょう。

$ mysql -uroot -p -e "CREATE DATABASE racchai";

はい、以上です。

認証モデルを定義する

Django のユーザーテーブルには企業コードは存在しませんので、追加しておく必要があります。

ユーザーテーブルのカス高い図については、ここでは詳しい解説は割愛します。よくわからないという方はこちらをご参照ください。

racchai.hatenablog.com

今回は models.py を以下のように作成するだけでひとまずはOKです。

from django.db import models    
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager

class RacchaiUserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError('Users must have a email address')

        email = RacchaiUserManager.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password):
        return self.create_user(email, password)

class RacchaiUser(AbstractBaseUser):
    email = models.EmailField(max_length=128, unique=True)
    company = models.CharField(max_length=128, null=True)

    USERNAME_FIELD = 'email'

    objects = RacchaiUserManager()

    class Meta:
        db_table = 'racchai_user'
        swappable = 'AUTH_USER_MODEL'

本当は ./manage.py createsuperuser で企業コードも設定できるようにしたいところですが、今回はそこまではやりません。

RacchaiUserを作成したら、settings.py に以下を追記して認証モデルを差し替えておきます。

AUTH_USER_MODEL = 'racchai.RacchaiUser'

最後にマイグレーションを実行したら完了です。

$ ./manage.py migrate

念のためテーブル定義を確認。問題なさそうですね。

mysql> desc racchai_user;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| password   | varchar(128) | NO   |     | NULL    |                |
| last_login | datetime(6)  | YES  |     | NULL    |                |
| email      | varchar(128) | NO   | UNI | NULL    |                |
| company    | varchar(128) | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+

Serializerを定義する

次はAPIの入力情報を扱うSerializerを定義します。今回は入力値が3つあるので、以下のようになります。

jwt_auth.py

from rest_framework import serializers

class AuthInputSerializer(serializers.Serializer):
    company = serializers.CharField()
    email = serializers.EmailField()
    password = serializers.CharField()

View を定義する

最後に API 本体の実装です。さきほど Serializer を定義した jwt_auth.py に以下を追記しましょう。

from django.contrib.auth import authenticate
from rest_framework_jwt.settings import api_settings
from rest_framework.generics import GenericAPIView
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.response import Response

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

class AuthView(GenericAPIView):
    serializer_class = AuthInputSerializer
    def post(self, request):
        serializer = AuthInputSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = authenticate(**serializer.data)
        if not user:
            raise AuthenticationFailed()
        # 以下2行が今回のカスタマイズ部分
        if user.company != serializer.data['company']:
            raise AuthenticationFailed()
        payload = jwt_payload_handler(user)
        return Response({
            'token': jwt_encode_handler(payload),
        })

最後に urls.py に以下を追記して /jwt_auth でアクセスできるようにします。

from racchai.jwt_auth import AuthView

urlpatterns = [
    url(r'^jwt_auth', AuthView.as_view()),
]

これで認証ができるようになったはずです。

試してみる

API を試す前に、まだユーザーを登録していなかったので先にユーザーを作成しておきます。

$ ./manage.py createsuperuser
Email: jwt@racchai.com
Password: jwt
Password (again): jwt
Superuser created successfully.

前述の通り、この時点ではユーザーレコードに会社コードが設定されていませんので、SQL で直接会社コードを設定してしまいます。会社コードは仮に0001としておきます。

$  mysql -uroot -p racchai -e "update racchai_user set company = '0001'"

準備が整ったところで、開発サーバーを起動して認証APIを実行してみます。

$ ./manage.py runserver
....

$ curl http://localhost:8000/jwt_auth -d "email=jwt@racchai.com&password=jwt&company=0001"
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imp3dEByYWNjaGFpLmNvbSIsInVzZXJfaWQiOjIsImVtYWlsIjoiand0QHJhY2NoYWkuY29tIiwiZXhwIjoxNDYzNjI1NDQ5fQ.YX6a_9680xDDYOiqwXX1kAZA1dMH3rLIXq7oGTaOawc"}

無事JWTアクセストークンを取得できましたね!

念のため、会社コードを間違えたらどうなるのか見ておくことにします。

$ curl http://localhost:8888/jwt_auth -d "email=jwt@racchai.com&password=jwt&company=0002"
{"detail":"Incorrect authentication credentials."}

ちゃんと認証に失敗することも確認できましたね。

まとめ

だいぶ早足でしたが、カスタマイズ自体は難しいことではないってことだけ伝われば幸いです!

iOS ChromeではFacebook JavaScript SDKによる認証が動作しない

連日の iOS ネタです。

現象

開発中のサービスにて、iOS Chrome でのみFacebookアカウントでの会員登録をしようとすると以下のエラーとなりました。Safari を使えと。

f:id:racchai:20160518105140j:plain:w250

Facebook JavaScript SDKを使ってクライアントサイドで認証しているのですが、FB.login を実行したタイミングで発生するため、使い方がおかしいわけではなさそうです。別な環境では動きますしね。

専用のエラーメッセージがあるくらいなので何か事情があるのでしょう。iOS でブラウザを作るのは制約多そうですしね。グーグルさんホントお疲れ様っす。

回避方法

調べてみたところ、みなさん同じような現象に遭遇していました。

javascript - Facebook OAuth "Unsupported" in Chrome on iOS - Stack Overflow

回避するには、iOS Chrome の場合は自前で window.open して認証画面を開くようにすればよいそうです。ただ、個人的にブラウザ検出って好きじゃないというか、もはややるべきでないと思っているのでそこは不満ではありますが、こうなっては仕方がありません。

ブラウザ検出するために iOS Chrome の User-Agent を以下のサイトで調べてみました。

User Agent Strings - Google Chrome

User-Agent が判明。

Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_1_1 like Mac OS X; en) AppleWebKit/534.46.0 (KHTML, like Gecko) CriOS/19.0.1084.60 Mobile/9B206 Safari/7534.48.3

stackoverflow でも言っていたように、CriOS が含まれているかで判定できそうです。

navigator.userAgent.match('CriOS')

次は肝心の window.open ですが、以下のように実行すればよさそうです。

var url = 'https://www.facebook.com/dialog/oauth?client_id=' + appId + '&redirect_uri='+ document.location.href +'&scope=email&response_type=token';
window.open(url, '', null);

というわけで、最終形はこちら。

if (navigator.userAgent.match('CriOS')) {
    var url = 'https://www.facebook.com/dialog/oauth?client_id=' + appId + '&redirect_uri='+ document.location.href +'&scope=email&response_type=token';
    window.open(url, '', null);
} else {
    FB.login(function(response) {
        ....
    });
}

redirect_uri を設定しておくことで、認証が成功したらリダイレクト先でアクセストークンを取得できるようになります。アクセストークンはURL内にハッシュパラメータとして付与されていますので、以下のようにして抽出すればOKです。

window.location.hash.match('access_token=([^&]+)')[1]

アクセストークンさえ取得できてしまえば、あとは通常通りAPIを叩くだけですね。

まとめ

ブラウザ検出はしてしまっていますが、まともな回避方法があってよかったです。

それにしても iOS むずい。

iOS Safari限定でAjaxリクエストが失敗してハマった話

以前こういった記事を投稿しましたが、今回ハマったのも JWT トークンを使ったAjaxリクエストでした。

racchai.hatenablog.com

状況

簡潔に状況を説明すると、事前に取得しておいた JWT トークンを Authrorization ヘッダーに乗せてリクエストしたものの、なぜかデータが取得できないという現象に遭遇しました。それも iPhoneSafari でしか再現しないというめんどくさい状況でした。

CORS 問題

サーバーのアクセスログを確認すると、どうやらそもそもリクエストがサーバーに届いていないことが判明。そういえばサーバー側で CORS 関連のヘッダーを設定するのを忘れていたことに気付きました。

そういうわけで nginx に add_header 群を追記して再トライ。

add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "POST, GET, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Origin,Authorization,Accept,X-Requested-With";

結果:変わらず

プリフライトリクエスト問題

Webインスペクトを使って、iPhone側で実際に何が起きているのかちゃんと確認してみたところ、どうやらプリフライトリクエストに失敗していることがわかりました。X-Requested-With ヘッダーを付与しているために、事前に OPTIONS を送信するようになっているのです。

OPTIONS も許可はしていたものの、この設定のままでは OPTIONS リクエストに対して 405 で返してしまう模様。Nginx はハマりどこりが多い奥が深いです。

こちらの記事参考に、OPTIONS リクエストには 204 で返すように設定してみました。

Handling CORS with Nginx — osteel's blog

以上の結果がこちら。今度こそ大丈夫そう。

add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "POST, GET, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Origin,Authorization,Accept,X-Requested-With";

location / {
    if ($request_method = OPTIONS) {
        add_header Content-Length 0;
        return 204;
    }
}

結果:プリフライトリクエストは無事通過したものの、401 エラー発生

設定見直し

401 エラーということで、ようやくリクエストがサーバーに届くようになりました。でも Authorization ヘッダーは届いていないんでしょうか。

その後いろいろ調べていたら、こちらの記事にたどり着きました。

qiita.com

クレデンシャルを必要とするリクエストの場合、Access-Control-Allow-Originにワイルドカードは使えまえん。キチンと許可オリジンを指定しましょう。

オリジン情報乗せるのめんどくさくてワイルドカード指定してしまってました。

Access-Control-Allow-Credentials: true も付けましょう クレデンシャルを必要とする場合はこのヘッダがないとブラウザはレスポンスを捨ててしまうのでセットで付けておきましょう。

へー!いままで捨てられてた気はしないですが、そういう事情もあるらしいのでこれもつけておくことにしました。

add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Methods "POST, GET, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Origin,Authorization,Accept,X-Requested-With";
add_header Access-Control-Allow-Credentials: true

location / {
    if ($request_method = OPTIONS) {
        add_header Content-Length 0;
        return 204;
    }
}

結果:変わらず 401 エラー

Authorization ヘッダー問題

設定見直しは意味がなかった(?)ご様子。そうなれば、実はもう Authorization ヘッダーは届いてるだろうと思い、アクセスログに Authorization ヘッダー値を出力してみました。

すると・・「Basic ********」なる文字列が記録されているではないですか。

あれれ、ベーシック認証??

そうなんです。

ここまで伏せてきましたが、実はエラーが起きていたのはステージング環境で、第三者からのアクセスを避けるためにベーシック認証を設定していました。JWT リクエストとベーシック認証のヘッダーはどちらも Authorization を使うため、どうやらベーシック認証で JWT トークンを上書きされてしまっていたようです。あちゃーです。

でもXHRだとブラウザがベーシック認証を自動で設定するなんてこと・・あるんです。そう、iOS Safari ならね。

完全に余計なことをしやがって状態ですが、仕方がないのでベーシック認証をはずしてみました。

結果:直った!

まとめ

ベーシック認証と JWT 認証(django-rest-framework-jwt)は相性が悪い!

いやーはまった。半日つぶれました。

Chromeで送信したリクエストをターミナル上でそのまま実行可能なcURL形式で取得する方法

おまえは何を言っているんだ状態です。興奮しすぎてタイトルに情報を詰め込み過ぎてしまいました。反省します。

何がしたかったかというと、数分に一度しかリクエストされないXHRリクエストを curl で何度も叩きたかったのです。正直、対象のリクエストを curl でエミュレートするのはいろいろヘッダー付けたりで面倒ですし、デバッグ目的だったりするのでなるべく同じ条件で実行したいという願いもあります。

そこで、Chrome の DevTools の出番です!百聞は一見に如かず。こちらを見てください。

f:id:racchai:20160513160218p:plain

画像は本ブログのabout画面です。この画面を取得するリクエストをChromeから取得してみます。

DevTools を開き、[ネットワーク] タブを選択します。一番上に about 画面へのリクエストが表示されているので、右クリックしてみると・・・

f:id:racchai:20160513161125p:plain

いろいろコピーできそうなメニューが表示されました。ここで「Copy as cURL」を選択します。

Chrome での操作は以上です。そのままターミナルに移動してペーストしてみましょう。

# Cookie ヘッダー値だけ手動で潰しました
$ curl 'http://racchai.hatenablog.com/about' -H 'Accept-Encoding: gzip, deflate, sdch' -H 'Accept-Language: ja,en-US;q=0.8,en;q=0.6' -H 'Upgrade-Insecure-Requests: 1' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' -H 'Cache-Control: max-age=0' -H 'Cookie: *****' -H 'Connection: keep-alive' --compressed

リクエスト情報が全部コピーされている!やったー。Cookie ヘッダーもコピーされるので、認証が必要な画面やAPIでも再現することが可能です。

全く同じリクエストを何度も送信したい方は幸せになれると思いますので、ぜひお試しください。

いちいち調べてられない!よく使うopensslコマンドまとめた

よく openssl コマンドを使うのですが、なかなか覚えられないのが悩みです。必要になったら都度調べているのですが、効率が悪いのでそろそろ使う頻度が高いコマンドくらいは覚えてやろうと思い、まとめてみることにしました。

サブコマンドの種類を確認

そもそも、どれくらいのサブコマンドがあるのか確認してみます。openssl helpと打ってみたら、そんなコマンドないぞって怒られながらもサブコマンドの一覧を得ることに成功しました。

$ openssl help
openssl:Error: 'help' is an invalid command.

Standard commands
asn1parse         ca                ciphers           cms               
crl               crl2pkcs7         dgst              dh                
dhparam           dsa               dsaparam          ec                
ecparam           enc               engine            errstr            
gendh             gendsa            genpkey           genrsa            
nseq              ocsp              passwd            pkcs12            
pkcs7             pkcs8             pkey              pkeyparam         
pkeyutl           prime             rand              req               
rsa               rsautl            s_client          s_server          
s_time            sess_id           smime             speed             
spkac             srp               ts                verify            
version           x509              

Message Digest commands (see the `dgst' command for more details)
md4               md5               rmd160            sha               
sha1              

Cipher commands (see the `enc' command for more details)
aes-128-cbc       aes-128-ecb       aes-192-cbc       aes-192-ecb       
aes-256-cbc       aes-256-ecb       base64            bf                
bf-cbc            bf-cfb            bf-ecb            bf-ofb            
camellia-128-cbc  camellia-128-ecb  camellia-192-cbc  camellia-192-ecb  
camellia-256-cbc  camellia-256-ecb  cast              cast-cbc          
cast5-cbc         cast5-cfb         cast5-ecb         cast5-ofb         
des               des-cbc           des-cfb           des-ecb           
des-ede           des-ede-cbc       des-ede-cfb       des-ede-ofb       
des-ede3          des-ede3-cbc      des-ede3-cfb      des-ede3-ofb      
des-ofb           des3              desx              rc2               
rc2-40-cbc        rc2-64-cbc        rc2-cbc           rc2-cfb           
rc2-ecb           rc2-ofb           rc4               rc4-40            
seed              seed-cbc          seed-cfb          seed-ecb          
seed-ofb          

うわあ、こんなにあるのか。。ちょっと引いた。

でも今回まとめるのは自分がよく使うコマンドに限定するので、まあいいか。

使い方

では、得られた一覧の上から順に使いそうなサブコマンドをピックアップしてまとめていきます。

ca

CRL を作成する

$ openssl ca -gencrl -out CA.crl

証明書を失効させる

$ openssl ca -revoke newcerts/cert.pem

ciphers

サイファースイート一覧を取得

$ openssl ciphers

crl

CRL の内容を確認

$ openssl crl -in CA.crl -text

genrsa

秘密鍵を作成(平文)

$ openssl genrsa 2024 > key.pem

秘密鍵を作成(パスフレーズ付き)

$ openssl genrsa -aes256 2024 > key.pem

公開鍵を作成

$ openssl rsa -in key.pem -pubout -out pub.pem

pkcs12

pkcs12(pfx)形式にする

$ openssl pkcs12 -export -inkey key.pem -in cert.pem -certfile cacert.pem -out racchai.p12 -caname "Private_CA"

pkcs12形式から秘密鍵を抜き取る

$ openssl pkcs12 -in racchai.pfx -nocerts -nodes -out key.pem

pkcs12形式から証明書を抜き取る

$ openssl pkcs12 -in racchai.pfx -clcerts -nokeys -out cert.pem

pkcs12形式からルート証明書を抜き取る

$ openssl pkcs12 -in racchai.pfx -cacerts -nokeys -out ca.pem

req

CSR を作成

$ openssl req -new -sha256 -key key.pem > csr.pem

CSR の内容を確認

$ openssl req -in csr.pem -text

rsa

秘密鍵パスフレーズ解除

$ openssl rsa -in pass.pem -out plain.pem

秘密鍵の内容を確認

$ openssl rsa -in key.pem -text

s_client

SSL/TLS での動作確認

$ openssl s_client -connect www.google.com:443

証明書チェインを確認

$ openssl s_client -connect www.google.com:443 -showcerts

verify

証明書の検証

$ openssl verify -CAfile cacert.pem cert.pem

verion

バージョン確認

$ openssl version

x509

CSR に署名する

$ openssl x509 -in csr.pem -sha256 -days 365 -req -signkey ca_key.pem > cert.pem

証明書の中身を確認

$ openssl x509 -in cert.pem -text

証明書から公開鍵を取り出す

$ openssl x509 -in cert.pem -pubkey -noout

おまけ

サーバー証明書を一から作る

$ openssl genrsa 2024 > key.pem
$ openssl req -new -sha256 -key key.pem > csr.pem
$ openssl x509 -in csr.pem -sha256 -days 365 -req -signkey ca_key.pem > cert.pem

sshアクセス用の秘密鍵/公開鍵の作成を一発で(opensslじゃないけど)

github にアクセスするつもりで出力。

$ ssh-keygen -t rsa -f ~/.ssh/github

蛇足。

$ cat >> ~/.ssh/config <<EOS
Host github
HostName github.com
User git
IdentityFile ~/.ssh/github

まとめ

結局この記事見直せば覚えなくていいってことに気づいた。

暗号技術入門 第3版 秘密の国のアリス

暗号技術入門 第3版 秘密の国のアリス

フリーランスやってるけど報酬額はこうやって決めた

受託開発の依頼内容を一通り説明された後、こう聞かれる。

『こういう内容だけど、おいくらくらいですか?』

最初の頃はよくこのように聞き返していた。

『逆にいくらくらいが妥当ですかね?w』

知り合いとはいえ、客に価格を決めさせていた。開発期間も感覚で見積もった。それでもきっとうまくいくと思っていた。社会を舐めきっていたのだ。

結果、こういうことになった。お恥ずかしい限りだ。

racchai.hatenablog.com

このままではいけない。そう思い、報酬の決定方法を確立することにした。このとき、開業からすでに4ヶ月が経っていた。

報酬額の計算式

これは単純に [人月] × [月単価] とすることにした。考えうる最もシンプルな方式だろう。シンプルであればあるほど、説明がしやすいし納得感がある。

それらしい理由を挙げることができたので、これ以外の方法が全く思いつかなかったというのはこの際伏せておこうと思う。

見積もる

受けた仕事を納品するまでに、どのくらいの日数がかかるのかを見積もる必要がある。自慢ではないが、これまで受託開発の経験はほとんどない。なので、見積もり手法など身に付いているはずもなかった。

ひとまず、この4ヶ月で経験したプロジェクトを思い返すことにした。結果、変な汗をかいただけだった。辛い思い出だった。

そしてふと思った。そもそも、見積もりは正確である必要があるのか?

開き直りだった。

最初に正確な見積もりを出したところで、必ず意図しない仕様変更や追加機能は発生するのだ。必ずだ。だったら、すべてを多く見積もっておこう。時間が余ったらその分品質を上げることだってできるし、後から出てきた要望も快く受け入れられる。

開き直ったら強い。正確に見積もることをやめて、日数が足りなくならないことだけを考えて見積もる方針に舵を切った。

ここで、変な汗をかくことも覚悟の上、改めて過去のプロジェクトを思い返してみた。結果、当時感覚で見積もった人月に 1.5 をかけていれば、多少余裕のあるプロジェクトになっていたことがわかった。これで報酬額の計算式と同様、複雑な計算をする必要のない、シンプルな手法に決めることができた。

ちなみにこの 1.5 をかけるというのが本当に絶妙で、ちょっとした考慮漏れや仕様変更なら軽く吸収してくれる上、週1,2回の定休日を設定することさえ可能となった。

それまで定休日という発想自体がなかったため、毎週休みが来るというのが非常に斬新だった。フリーランスだって休んでもよいのだ。

定休日を設定したことで、決して余裕のあるスケジュールとは言えなくなってしまった。そのかわり、深夜まで作業することもほとんどなくなった。

これで心の健康は守られた。

月単価

次は月単価の決定だ。まず、世間ではどのくらいが相場なのかを調べることにした。敵を知り、己を知れば、百戦危うからずだ。使いどころはあっているだろうか。

いろいろサイトやブログを読み漁ったところ、どうやら60 - 90万円が相場であることがわかった。

同じような価格でやっていては仕事がもらえないかもしれない。固定客が付くまでは相場よりも安く設定しておこう。

弱気な考えで月単価は 50 万円とした。ちなみに固定客がついたいまでもこの価格は変えていない。いまではこれがお得意様価格でもあるのだ。

そのかわり、絶対にこれ以上価格を落とすことはしないことに決めた。すでに相場より安いのだから、これ以上下げれないの一点張りで押し通すことにした。

まとめ

ここまでの検討で、以下のような計算式が出来上がった。

報酬額=(感覚で見積もった人月 * 1.5)  * 50

すばらしくシンプルだ。雑とも言える。雑と思われる方が大多数かもしれない。でもいいのだ。

この計算式が出来て以降、いつもの質問がきても焦らなくて済むようになったのだから。

『こういう内容だけど、おいくらくらいですか?』

(大体2ヶ月くらい掛かりそうだからそれに1.5をかけて)3ヶ月くらいかかるので、(3ヶ月 * 50 で) 150万円ほどになります!(キリッ)