utamaro’s blog

誰かの役に立つ情報を発信するブログ

jwtを検証するアノテーションを作って認証済みか判定する

jwtを検証するアノテーションを作って認証済みか判定する

jwtをcookieに追加してAPIの実行を管理したいと思いました。 そのときにやった内容です。

ざっと説明すると、ログインしていたらjwtがcookieに入っていて、そのcookieを検証することで認証を管理しています。 少々めんどくさい実装になってしまったので改善が必要ですが、アノテーションの自作や、interceptor、cookieの取り扱いなど勉強になることが多々ありました。

@VerifyJwtというアノテーションを付けることで検証が可能になります。

WebMvcConfigurerの設定を行う

WebMvcConfigurerの設定を行います。 interceptorの適用範囲を定義しています。 この定義によってアノテーションを適用する範囲を決めています。

import com.xxx.xxx.settings.interceptor.LoginRequiredInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private LoginRequiredInterceptor loginRequiredInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginRequiredInterceptor)
                .addPathPatterns("/admin/**") // 適用対象のパス(パターン)を指定する
                .excludePathPatterns("/error")
                .excludePathPatterns("/static/**"); // 除外するパス(パターン)を指定する
    }
}

この設定によって/admin/**に対してアノテーションを有効にしています。 また、エラー時、静的ファイル要求時にinterceptorが無効になるように設定しています。 静的ファイルで無効にする方法として、interceptor内での実装方法もあるがこちらのほうが楽に設定できます。 また、有効時、無効時のパスがまとまるので見通しが良くなります。

LoginRequiredInterceptorの実装を行う

先程作成したWebMvcConfigで使用したloginRequiredInterceptorを作成します。 preHandleを実装してアノテーションのついている場合の処理を作成します。

このクラス内では以下のクラスの実装が必要です。

  • com.xxx.xxx.apps.admin.user.model.UserModel
  • com.xxx.xxx.apps.admin.user.service.UserService
  • com.xxx.xxx.jwt.model.JwtUserClaimModel
  • com.xxx.xxx.jwt.service.JwtService
  • com.xxx.xxx.settings.annotation.VerifyJwt
import com.xxx.xxx.apps.admin.user.model.UserModel;
import com.xxx.xxx.apps.admin.user.service.UserService;
import com.xxx.xxx.jwt.model.JwtUserClaimModel;
import com.xxx.xxx.jwt.service.JwtService;
import com.xxx.xxx.settings.annotation.VerifyJwt;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Objects;

@Slf4j
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtService jwtService;

    @Autowired
    private UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Method method = ((HandlerMethod) handler).getMethod();
        VerifyJwt verifyJwtAnnotation = AnnotationUtils.findAnnotation(method, VerifyJwt.class);

        if (Objects.nonNull(verifyJwtAnnotation)) {
            log.info("do login required.");
            // ログインが必要で、jwtの検証が必要な場合の処理を行う
            String token = jwtService.takeJwt(request);
            if (Objects.nonNull(token)) {
                // tokenの検証を行う
                JwtUserClaimModel claimModel = jwtService.verify(token, JwtUserClaimModel.class);
                if (Objects.isNull(claimModel)) {
                    // トークンの検証が失敗した場合の処理。
                    if (StringUtils.isBlank(verifyJwtAnnotation.failureUrl())) {
                        return true;
                    } else {
                        response.sendRedirect(verifyJwtAnnotation.failureUrl());
                        return true;
                    }
                } else {
                    UserModel userModel = userService.fetchUser(claimModel.getUserId());
                    request.setAttribute("user", userModel);
                    if (StringUtils.isBlank(verifyJwtAnnotation.successUrl())) {
                        // 例えば、/adminが成功時に表示する画面としたときに、ログイン済みの状態で/adminにリクエストした場合、
                        // 成功時の画面に/adminを設定しているとリダイレクトし続けてしまう。
                        return true;
                    } else {
                        // 成功時のurlが設定されていればリダイレクトを行う。
                        response.sendRedirect(verifyJwtAnnotation.successUrl());
                        return true;
                    }
                }
            } else {
                if (StringUtils.isBlank(verifyJwtAnnotation.failureUrl())) {
                    return true;
                } else {
                    response.sendRedirect(verifyJwtAnnotation.failureUrl());
                    return true;
                }
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

正直ネストが深くなりすぎてる感があるので、だめだなぁと思ってます。

UserModelの実装を行う

toStringしたときにpasswordが表示されないようにすること。 これが意外と大事で、ログを出していたりすると忘れることがよくあります。

import lombok.Data;
import lombok.ToString;

import java.time.LocalDateTime;

@Data
public class UserModel {
    private int id;
    private String email;
    @ToString.Exclude
    private String password;
    private boolean isActive;
    private boolean isAdmin;
    private boolean isGeneral;
    private LocalDateTime modifiedAt;
    private LocalDateTime createdAt;
}

UserServiceの実装を行う

mapperの実装については省略します。 userIdemailを利用してデータの存在確認を行います。 今回の場合は、jwt内にuserIdを入れており、そのIDを使用してデータを取得しています。 本来はここでnullチェックが必要ですが、省略します。

import com.xxx.xxx.apps.admin.user.mapper.UserMapper;
import com.xxx.xxx.apps.admin.user.model.UserModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public UserModel fetchUser(long userId) {
        UserModel userModel = userMapper.selectUserById(userId);
        return userModel;
    }
}

JwtUserClaimModelを実装する

jwtに含めるデータを定義します。

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class JwtUserClaimModel {
    private long userId;
    private boolean isActive;
    private boolean isAdmin;
    private boolean isGeneral;
}

JwtServiceを実装する

このServiceを利用してjwtの生成、削除、検証、cookieへの追加が可能となります。 Cookieの追加についてはjwtとは別の機能なので、utilクラスを用意するのが良いと思います。

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.xxx.xxx.settings.config.CommonPropertyConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.TimeZone;

@Slf4j
@Service
public class JwtService {

    @Autowired
    private CommonPropertyConfig commonPropertyConfig;

    public String takeJwt(HttpServletRequest httpServletRequest) {
        Cookie[] cookies = httpServletRequest.getCookies();
        if (Objects.nonNull(cookies)) {
            for (Cookie cookie : cookies) {
                if (commonPropertyConfig.getJwtCookieKey().equals(cookie.getName())) {
                    String token = cookie.getValue();
                    log.debug("jwt found in cookies. token=[{}]", token);
                    return token;
                }
            }
        }
        log.debug("jwt token not found.");
        return null;
    }

    public String create(Object headerClaimModel) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(commonPropertyConfig.getJwtSecretKey());
            Map<String, Object> headerClaims = new HashMap<>();
            headerClaims.put("appName", headerClaimModel);
            Calendar calendar = Calendar.getInstance();
            calendar.add(Calendar.HOUR, 6);
            calendar.setTimeZone(TimeZone.getTimeZone("UTC"));
            Date expiredAt = calendar.getTime();
            String token = JWT.create()
                    .withIssuer(commonPropertyConfig.getJwtIssuser())
                    .withHeader(headerClaims)
                    .withExpiresAt(expiredAt)
                    .sign(algorithm);
            log.debug(token);
            return token;
        } catch (JWTCreationException exception){
            //Invalid Signing configuration / Couldn't convert Claims.
            throw exception;
        }
    }

    public <T> T verify(@NotNull String token, Class<T> claimClass) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(commonPropertyConfig.getJwtSecretKey());
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer(commonPropertyConfig.getJwtIssuser())
                    .build(); //Reusable verifier instance
            DecodedJWT jwt = verifier.verify(token);
            Claim claim = jwt.getHeaderClaim("appName");
            T claimModel = claim.as(claimClass);
            return claimModel;
        } catch (JWTDecodeException exception){
            //Invalid token
            log.warn("Invalid token. token=[{}]", token);
        } catch (JWTVerificationException exception){
            //Invalid signature/claims
            log.info("Invalid signature/claims token=[{}]", token);
        }
        return null;
    }
}

VerifyJwtアノテーションを実装する

successUrlとfailureUrlを用意したアノテーションを作成します。 ランタイムで動作し、メソッドに対して利用できます。

import javax.validation.constraints.NotBlank;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface VerifyJwt {
    @NotBlank String successUrl() default "";
    @NotBlank String failureUrl() default "";
}

controllerクラスでアノテーションを利用する

すべての準備が整いました。実際に利用します。

    @VerifyJwt(successUrl = "/admin")
    @RequestMapping(path = "/login")
    public String loginView(HttpServletRequest request, HttpServletResponse response) throws IOException {
        return "admin/login";
    }

    @RequestMapping(path = "/login", method = RequestMethod.POST)
    public String postLogin(@Validated LoginRequestParam param,
                            HttpServletRequest httpServletRequest,
                            HttpServletResponse httpServletResponse,
                            BindingResult bindingResult) {
        // bindingResultのエラーチェックを行うこと。今回は省いている。
        LoginDto dto = LoginDto.builder()
                .email(param.getEmail())
                .password(param.getPassword())
                .build();
        // アカウントの認証を行う。
        UserModel userModel = loginUserService.userExists(dto);
        if (Objects.nonNull(userModel)) {
            // ユーザーが存在するため、tokenを生成する
            JwtUserClaimModel claimModel = JwtUserClaimModel.builder()
                    .userId(userModel.getId())
                    .isActive(userModel.isActive())
                    .isAdmin(userModel.isAdmin())
                    .isGeneral(userModel.isGeneral())
                    .build();
            String token = jwtService.create(claimModel);
            Cookie cookie = new Cookie(commonPropertyConfig.getJwtCookieKey(), token);
            cookie.setMaxAge(commonPropertyConfig.getJwtCookieMaxAge());
            cookie.setPath("/");
            if ("https".equals(httpServletRequest.getScheme())) {
                cookie.setSecure(true);
            }
            cookie.setHttpOnly(true);
            httpServletResponse.addCookie(cookie);
            return "redirect:";
        } else {
            // 管理ページでのアカウント登録は行わないため、登録画面へのリダイレクトは行わない。
        }
        return "admin/login";
    }

    @VerifyJwt(failureUrl = "/admin/login")
    @RequestMapping()
    public String homeView(HttpServletRequest request) {
        // resources/templates/login.htmlが開かれます。
        return "admin/home";
    }

問題点

@VerifyJwtすごく便利!

と作ったときは思いました。

最終的にこのアノテーションは使っていません。

なぜかというと、アノテーションを付け忘れてバグとなるからです。 APIを作成するごとにつける必要があって面倒なのです。

本来はアノテーションを使用せずに同様の機能を作成するのが良かったのだと思います。

機会があれば改良して、もっと使いやすいものを作成したいと思います。

テレビニュースで情報収集しなくなった理由

僕はテレビを使って情報収集するのをやめました。

やめた理由はいくつかあるのですが、一番の決めては「情報の検証が面倒」だからです。

テレビニュースの収入源はCMの広告費です。

テレビの合間に流れる邪魔な広告が大部分の収入源となっています。

テレビ局は視聴率を稼ぎ、より広告枠の価値を上げることによって収益に反映させる構造です。

とすると、テレビが扱う情報は確実な情報である必要がなくなります。 面白さや、インパクトを重視した見た目や、お涙頂戴など人の感情に訴えかけるように編集しています。

京都アニメーション実名報道事件とか、あれって炎上商法じゃないですかね? 炎上させることでニュースを見させ、視聴率を稼ぐ。 実名報道はニュースを見させる、関心を呼ぶための餌として使用する(偏見でしょうか?)

視聴率をとれないニュースは報道しないし、テレビ局にとって不利になることは報道しないんです。 それをすると視聴率が稼げないから。 それをすると広告主が離れていくから。 それをするとお金が入ってこなくなるから。

テレビ局が発信する情報は、調べるきっかけとはなりますが確かな情報とは言いづらいです。 根底が視聴率となっているため、若干の誇張表現が混じっている可能性を否定できません。

中学生の頃に「ネットで情報を調べるときは3つ以上の一致する情報を見つけること」と習いました。 テレビも同じではないでしょうか? テレビで流れているニュースの裏付けをして、その情報が正しいかどうか確認する必要があるでしょう。

でも、調べるのってかなり面倒なんですよ…

耳から聞いた内容をすべて覚えていられるわけではないです。 かといってすべてをメモするのも難しい。

間違った情報が出た際に「〜の箇所は正しくはXXでした。お詫びして訂正致します」とサラッと言われると聞き逃すよね。 「いつのどの場面よ!」ってなります。しかも一回しか言わないし。

常日頃からそんなことをしてたら疲弊してしまいます。 倒れますよ。

ネット上で調べるのならば適宜メモしつつ、わからない箇所は再検索できます。 情報を整理しつつ、重要な単語はコピペすることも可能です。 耳で聞いてメモするという作業がなくなるため、調べ物をするのに手間がかからなくなるんです。

これがテレビを使って情報収集するのをやめた理由です。

まとめると、「メモするのが面倒だからネットで調べ物する」という理由です。

JavaのOptionalを戻り値に使用することについて消極的な理由

僕は戻り値にOptionalを使用することに消極的です。

戻り値に使用する理由に対して以下の理由をよく聞きます。

  • メソッドを利用した際にnull値を返す可能性を明示する。
  • nullを考慮したコーディングを強制させる。

これらをやる理由が無いと考えているからです。

メソッドを利用した際にnull値を返す可能性を明示する。

これ本当に明示できているのでしょうか? Optionalだから明示できているのでしょうか? Objectでもnullが入る可能性があるのだから、Objectでもnullを返す可能性は考えられるのでは無いでしょうか?

以下の条件で使い分けるのだとするなら、Optionalを使用する利点が出てくると思います。

  • Optionalを返すメソッドではnullを返す可能性がある。
  • Optionalを返さないメソッドはnullを返す可能性が無い。

ただし、戻り値から値を取り出す場合にコーディングに差異が出てきます。

idと一致するオプションが無い場合Optionalを返すメソッドを例とします。

public Optional<OptionDetail> fetch(Integer id) {
    if (id == 1) {
        OptionDetail optionDetail = new OptionDetail().setId(id).setName("SAMPLE");
        return Optional.of(optionDetail);
    }
    return Optional.ofNullable(null);
}

このとき呼び出し元は以下のように実装します。

OptionDetail optionDetailA = fetchDetailA(1)
        .orElseThrow(() -> {
            System.out.println("log message");
            throw new IllegalArgumentException("invalid id.");
        });
optionDetailA.getId();

データがない場合はログを出して、例外を出して処理を終えます。 .get()orElse(T)を利用しないのは、nullというのは、データがない状態を示しているからです。 データが無いのに、その場合の値を置き換えるとかおかしいですよね? .get()を使用した場合、値がnullだった場合にNoSuchElementExceptionが発生します。

次にOptionalを使用しない場合です。

public OptionDetail fetchDetailB(Integer id) {
    if (id == 1) {
        OptionDetail optionDetail = new OptionDetail().setId(id).setName("SAMPLE");
        return optionDetail;
    }
    return null;
}

このとき呼び出し元は以下のように実装します。

OptionDetail optionDetailB = fetchDetailB(1);
if (Objects.isNull(optionDetailB)) {
    System.out.println("log message");
    throw new IllegalArgumentException("invalid id.");
}
optionDetailB.getId();

見ればわかると思うのですが、対して違いが無いんです。

個人的には戻り値が全部Optional<T>となっているよりObjectとなっていた方が良いと思ったり、でもいちいちif文を書くの面倒だなとも思います。

そもそもを語るのなら、nullを返すメソッドはNullPointerExceptionを発生させる可能性があるため避けるべきです。 それを考慮した場合のメソッドは以下のようになるでしょう。

public OptionDetail fetchDetailC(Integer id) throws IllegalArgumentException {
    if (id == 1) {
        OptionDetail optionDetail = new OptionDetail().setId(id).setName("SAMPLE");
        return optionDetail;
    }
    throw new IllegalArgumentException("invalid id.");
}

データがない場合に例外を出します。 「存在しないデータを取得しようとした」という意味です。

利用側では以下の実装になります。

OptionDetail optionDetailC;
try {
    optionDetailC = fetchDetailC(1);
} catch (IllegalArgumentException e) {
    System.out.println("log message");
    throw e;
}
optionDetailC.getId();

Optionalの場合も同じ様な実装になります。

OptionDetail optionDetailD;
try {
    optionDetailD = fetchDetailD(1).get();
} catch (IllegalArgumentException e) {
    System.out.println("log message");
    throw e;
}
optionDetailD.getId();

どちらも同じ様な実装になるので、どっちでもいいんですよ。

ただ、Optionalの場合はorElseorElseThrowを利用できないので利用する必要性がありません。 また、fetchしてから一度.get()しないとなりません。 なにかデータを取得するメソッドを実行しているのに、なぜ何かを取得するメソッド(.get())を再度実行するのでしょうか?

IDEによってはisPresent()を利用してくださいと警告がでて混乱する可能性があるので使わないほうが良いでしょう。

これを考えると、この場合はOptionalを使わない方がマシです。

結論

  • Optionalの利用云々によってコーディングは対して違いがない。
  • nullを返さない実装の場合はOptionalは処理を煩雑にするため使用しない。
  • そもそもnullを返す実装を疑い、メソッドの利便性を考えたときにOptionalを使うメリットは小さい

nullを考慮したコーディングを強制させる。

本当にnullを考慮したコーディングを強制させるのでしょうか?

僕はそんなことは無いんじゃないかと思ってます。

.get()した場合try-catchを書かなくてもコンパイルは通ります。 そのままデプロイした場合NoSuchElementExceptionが発生する可能性があります。 それはNullPointerExceptionと何が違うのでしょうか。

Optionalを返すということはnullを返す可能性があると考えられるんだ!」

「だから.get()ではくorElse()orElseThrow()を使用しろ!」

orElseを利用するなら、なぜデフォルト値をメソッド側が返さないのでしょうか? orElseThrowを利用するなら、なぜメソッド側でIllegalArgumentExceptionなどの例外を出さないのでしょうか?

それはOptionalを利用するメリットかもしれませんが、Optionalを使わなければならない理由にはなりません。

だからOptionalについての議論が起こったりするんでしょうね。

nullを返すことに理由がある場合、なぜnullを返すのかjavadocにかかれているはずです。 そうなるとnullを返す可能性があるからと言ってjavadocすら確認しないということは無いでしょう。 そこに重要な情報が書かれていた場合のことを考えると、戻り値だけを見てコーディングするのは危険です。

(処理内容をコードから読み解く必要性はないです。必要がある場合はメソッド名が明確ではありません。)

また、nullを返すことに理由がない場合はそのメソッドの見直しが必要です。 理由のないnull返却は開発者を混乱させます。

最終的には利用するメソッドがどんな条件でnullや例外を出すのか確認して実装することになると思います。

そうなった場合、コーディング時のメソッドの使い方を確認するコストに差異はありません。 そして、それを確認したときOptionalを使用していなくても、エンジニアはそれに適した実装をします。

結論

  • nullを考慮したコーディングを強制するわけではない。
  • Optionalを利用してもしなくてもコーディング時にやることに変わりはない。

最後に

以上の理由から僕はOptionalを利用することに消極的です。

Optionalはメソッドを利用する際の処理を煩雑にしますし、実装方法によっては開発者を混乱させます。

また、javaの場合古い書き方に慣れている人のほうが絶対数が多く、Optionalの実装になれていない可能性があります。 チーム内で使い方の議論をして時間を浪費するぐらいなら使わない方が良いです。

そんなことよりも、よりよい機能について提案したり、リファクタリング作業や、業務の効率化にリソースを使ったほうが良いです。

Optionalの利用について議論をしていたり、考えている方の参考になれば幸いです。

試行錯誤しながら転職に成功したときのはなし

はじめに

転職活動をする際に自分の入りたい企業を探して、アタックをすると思います。 アタックするのは1回しかチャンスがなく、落ちた直後に再アタックしても落ちる可能性が高いです。

そんなときに、1回のチャンスをどのように成功させるのか。 僕の実体験から記事にしたいと思います。

※ いろんな事情から会社名については隠したいと思います。

僕の転職活動は少しばかり変わっていて、以下のことをしています。

  1. GreenとBizreachのみを使用する
  2. スカウトしか見ない(Greenの場合は気になるか、メールです)
  3. こちらから企業を探したりはしない

以上です。 向き不向きがあると思うので参考までに。

理由としては、やりたいことが決まっていなくて、開発だけしたいエンジニアだったからです。

こういう考えの方は結構多いのではないでしょうか?

多くの企業の中からメリット・デメリットをまとめて応募するのは探すのに多大なコストが掛かります。 そのコストを軽減しつつ、積極的に採用活動をしている企業の中から面白そうな事業をしているのを探すというやり方です。

プロフィール

僕はウェブエンジニアです。

3年の業務経験のあるサーバーサイド開発エンジニアです。

専門はjava言語で、pythonrubymysql、フロント開発ができます。

休日はプログラムを書いたりしていて外に出ることは稀です。 出るとしたら買い出しのときか、公園で筋トレする場合だけです。

僕のIQは110~120ぐらいなので、世間的に一般的なレベルだと思います。

書類選考で2社落ちた

職務経歴書が悪かったのか、履歴書が悪かったのか、もしくはどちらも論外だったのかはわかりません。

書類選考を通過できるようになるまでのステップについて、試したことをまとめます。

1社目: 職務経歴書、履歴書を自分で考えて書く。落ちた。
2社目: 職務経歴書の問題点について考えて改善したもとを提出した。落ちた
3社目: 職務経歴書は及第点と仮定して、履歴書を改善した。通過した。

3社と面談して、書類結果をもらうまでに3週間ぐらいかかってます。

とても面倒でした。

面談日の調整で3日ぐらいの候補日を提案して、返事をもらえるのが次の日です。 それを1週間続けて3社の予定を入れました。

結論としては、PDCAサイクルを回すことで効率に対策が可能ということです。

  1. plan: 書類通過するまでの計画を練る
  2. do: 計画を実行する
  3. check: 実行した結果を評価する
  4. action: 評価結果から改善事項をみつける

職務経歴書を書きます。

落ちた職務経歴書を見直しました。

すると

職務経歴書に自分の経歴書が書かれていなかったのです。 どういうことかというと、自分の強みや仕事をするモチベーションなどを書いていたのです。

例としては↓のような感じです。(もう少し具体的に書いてましたが、雰囲気はこんな感じです)

私はXを3年やってきて、趣味でもYをやってきました。
開発時には再利用性や運用を考えながら開発しています。

これを書いていたときは、書類選考で落ちました。

1社目はなんで落ちたのかわからなくて、2社目のときに同じものを面談時に出して指摘があるか聞きました。

職務経歴書です。質問ありますか?」

「職歴とか会社で何をやったのかは書いてあるけど、読まないとわからん。」

ここで「なるほどな」と。 具体的な回答はもらっていないですが、おそらく↓かと思います。

「その人がいつ、どこで、どのようなことをやってきたのかがまとまっている、見やすい資料が欲しい。」

採用担当者からしたら、自分の気持ちとか、考え方とかいらないのです。 何ができるか、何をやってきたのかが知りたいのだと思います。

ちなみに検証のために、一回修正して出しました。 その後、修正した職務経歴書で書類選考は通ったので、考え的にはあっていたのだと思います。 とすると、2社目は履歴書で落ちたことになります。

履歴書を書きます。

1社目は、「開発者がやっている取り組みに対して共感して開発に関わりたいと考えたから」という理由です。 職務経歴書がダメダメだったので、この理由があっているのかわかりませんでした。

2社目でも同じ理由で出したところ、落ちました。 2社目は職務経歴書を修正していたので、職務経歴書については及第点と仮定します。 すると履歴書が間違っていることになります。

ということで、履歴書を修正して3社目にアタックです。

3社目では、「企業の目的に共感した」という理由で応募しました。 なぜ共感したかという具体的な理由は必要です。

例えば、

Xという目標に共感して応募しました。
私は過去Yということを体験しており、Xができれば私のような経験をする人が少なくなる(以下略)

雑ですが、こんなです。

どこかで拾ってきたような文章や、無理に敬語で書く必要も無いと思います。 それで落ちませんでしたし。

※ ビジネスメールを書くレベルで良いと思います。

ようやく書類選考に通過しました。

企業のビジョンに共感していることと、共感した理由が自分の言葉で書かれていることが重要なのだと思います。

おそらく、「ゲームを作りたいので入りたいです。売上がXで潰れづらいので応募しました。」だと落ちると思います。

「小学生のころからプレイしており、思い出があります。特に〜の場面がとても熱くなりました。僕は昔から〜が好きで(以下略)」だと通過するかもしれません。

(書類選考で3社…。先は長い…)

まとめると

書類選考では以下のことを気をつけます。

  1. 職務経歴書はいつ、どこで、何をやったのか、何ができるかを見やすくまとめること。
  2. 履歴書は企業の目的に共感したという理由にすること。
  3. 履歴書に共感した理由を具体的に書くこと。

これを意識して作成したところ、書類選考に通るようになりました。

書類選考だけで2社落ちるとか、転職とは大変です。。。 何が大変って、面談だけはやっているので時間作って行かないとならないということです。

面接に慣れるために2社で練習する

どんなことにも対策というのは必要です。

面接の時にどんなことを聞かれるのか、どんな返しをすると良いかなどです。 2社はそれを調べるために使いました。

どちらも2次選考で落ちてしまいました。 というのも、練習ということで気持ちが入らなくて、別に行きたくもない会社というのがバレたのだと思います。

結果としては以下のことができればよいのだと思います。

コミュニケーション能力

不思議なことに技術的な能力を聞かれることはありませんでした。 これは実務で3年経験があるからだと思います。

ということで、実際に働いたときに社員との連携が可能かどうかをみているようでした。 「何かを作る際に必要なことは?」や「開発時に一番気をつけないといけないことは?」などを聞かれたりします。 あと「好きな言語は?」というのもありました。

これに対して論理的な回答ができるかどうかだと思います。 あと、笑顔を作るというのと、冗談を交えて笑い声を混ぜるということです。 評価するのは目の前の人なので、良い印象を与えたほうが得です。なので笑顔と笑い声です。 笑顔で話す人って話しやすいな~と思うことがあったので、実践しました。

また、面談時と面接時に必ず聞かれた質問がありました。

  1. 自己紹介をお願いします。
  2. 弊社の選考を受けた理由は?
  3. 転職する際の企業選びの基準は?

3番については1社目で聞かれました。 2社目で回答を修正して、3も絡めたところ質問はありませんでした。 これについては履歴書に書いた内容をいうだけで良いと思います。

不思議と「他の会社と比較してどう?」は聞かれませんでした。 ちなみに僕ならこのように答えると思います。

「ぶっちゃけると、他の会社とは比較していません。世の中に多くの会社があり、そのすべてを比較検討することは時間的に難しいからです。そのなかでスカウトをしてまで話を聞きたいと言っていただいたことが何よりも嬉しかったので面談しました。そのときに、〜について聞いて、自分のやりたいことに一致していたので選考に進みました。」

こう答えると「ちなみにやりたいことってのは?」と聞かれるので、その企業の理念に絡めてやりたいことを答えます。 曖昧な箇所を一箇所残して、回答すると質問を誘導できます。 これおすすめです。

まずは結論から話して、なぜその結論なのかを話すというのが大事です。 ちなみに、僕は敬語が苦手です。

一人称として「僕は」とか言いますし、「めっちゃ嬉しいです」とか「〜ができたらやばいですよねw」とか言ってました。 「やばい」っていうのは流石に「具体的にどうやばい?」って聞かれましたが(汗

ここで論理的に回答できると加点させるはずです。

いざ本番へ

本番として、スカウトの来た2社に行きたいと考えたました。

結論はどちらも内定をいただくことができました。 とてもありがたいことです。

練習時に得た経験を反映させた結果だと思います。 書類選考は難なくクリアして、面接も突破しました。

どちらの企業も僕がやりたいことをやっていたので、どちらを選んでも楽しく開発できそうでした。 最終的には年齢がまだ若いので、経験を積むということを考えて、より経験を積める会社を選択しました。

以上です。

転職時の参考になると嬉しいです。

githubの寄付提供プログラムが始まった件について

はじめに

githubはご存知でしょうか。

アカウントは持っているのですが、私は使っていません。以前からprivateリポジトリの作成が無料なbitbacketを使ってます。 ブログの内容とか、一時的にpushしたいときに利用しています。

というのはどうでもよくて、以下サイトからGitHub Sponsorsが始まったことを知りました。

codezine.jp

これまでオープンソースでの開発は収益に繋がりにくく、エンジニアとしてのメリットは少なかったです。 あるとするなら、名声を得られるので就職やヘッドハンティングに利用されることでしょうか。

こちらのヘルプページによると、最初の1年間は手数料がかからずに寄付されたお金は全額手元に入るようです。

help.github.com

ただ、いつまでも手数料無料というわけでは無いので、どこかで数%の手数料を取られることでしょう。 何%とるのかはわからないのですが、システムを維持するためにはお金がかかるので必ず取られるでしょう。

開発者はどのように動くか

世の中の開発者がどのように動くのかはわからないので、自分ならどのように動くか考えてみます。

基本は以下のように進みます。

  1. 調査
  2. 行動
  3. 振り返り

調査

まず、寄付を得られるレベルはどのレベルなのかを調べます。 PR&Mergeしたからと言って寄付を得られるわけではないでしょう。 何度も何度も課題について向き合い、それを解決し続けることによって得られるものだと思います。

1,2回参加した人にお金を払おうと考える人はいないと思います。

そこで、1つのプロジェクトでどの程度参加すれば上位に食い込めるのかを調べます。 PRやIssueを何回も出している型で、有効な意見を出している方がどのくらい貢献しているのかを基準にします。

つまり、世間的に有用な人材がどのくらい活動しているのかを調べ、それに近い働きをします。 日になんどコミットして、issueをどのくらい解決していてなどなど。

ただ、これはとても大変な道のりだと考えています。

会社で働いている場合、一つの業務をやっていれば月数十万もらえます。 寄付の場合、複数のプロジェクトに参加して、それぞれ別の知識が必要で、寄付を得るには長期間やり続ける必要があります。

根性か、情熱がなければ続けられないものです。

行動

どのくらい貢献すればよいのか調査ができたら、今度はその仮説を基に行動します。

具体的には参加人数が少ないOSSに対して参加します。

おそらくそこでは寄付を得られないでしょう。

そこでは、どのように開発を行うのかを試すために参加します。 どのようにコードを書くのが良いのか、gitのcommitやPRはどのように出すのが良いかなど、知識として得ているモノを実践します。

大きなOSSでやってしまうと、流れを止めてしまったり他のエンジニアに対して迷惑をかけてしまうので小さなところでやります。 もちろん、小さなOSSで迷惑をかけていいわけでは無いのですが、何事にも練習というのは必要だということです。 他に練習できる場所があるのなら、そこでやるべきでしょう。

振り返り

調査して、行動したら振り返りをします。

この振り返りでは、自分が立てた仮説が正しかったのか考えます。 仮説が正しかったと判断するだけの情報が得られなかった場合、行動を続けます。

判断するだけの情報を得られた場合は、仮説が正しかったかどうかを考えます。 正しければそのまま行動を続けるか、さらに仮説を簡略化するか、効率化します。

間違っていると判断すれば、不足しているだろう情報を調査して、新たに仮説を立てます。

まとめ

私ならやらないだろうなと考えます。 収入を得るまでにやらなければならないことが多く、とてもつらい道のりだからです。

そんなことより、自分が欲しいサービスを作るとかしてその成果物をもとに就職します。 それだけで月数十万の収入を得られます。

お金を得るだけならこれが一番はやいし、簡単では無いでしょうか。

お金ではなく、OSSに貢献したいという意識があるというなら参加するのが良いと思います。

株式会社トランビが信金中央金庫グループとの業務提携が決定しました。

はじめに

株式会社トランビが信金中央金庫グループとの業務提携が決まりました。

株式会社トランビは後継者不足によって廃業される事業者がM&Aを行えるプラットフォーム「トランビ」を提供しています。

実際のプラットフォームへは以下のリンクからアクセスできるので、興味がある方はみてみてください。

www.tranbi.com

信金中央金庫グループについてですが、金融機関の一つです。 それ以上の情報を持っていないのでなんとも言えないのですが、潰れることは9割方ないといえます。

トランビはどうなるのか考えてみた。

以下のサイトを見ても何を提携するのかわからないので予想になります。

koenji.keizai.biz

トランビについては「はじめに」で書いたとおりでM&Aできるプラットフォームを提供しています。

信金中央金庫グループには「信金キャピタル株式会社」が含まれています。

この会社では「M&A仲介業務と投資育成業務」を行っています。

ここらへんが関係しているのではと考えています。

銀行というのは事業者に一番近い立ち位置を持っています。

というのも銀行業務の中には「投資」「融資」「出資」があります。 それぞれ金を貸すものですが、金を貸すのは銀行です。

金の貸し方についてはそれぞれ異なるのですが、事業者は銀行からお金を借りて事業を行います。

だいたいは金を返せなくなったり、負債をかかえて事業を継続できなくなった際に廃業します。

それを察知できるのはトランビよりも銀行側の方が直接事業者と接している分だけ早いと考えます。

いち早く察知した事業者に対して、トランビを活かしてM&Aの機会を提供するのだと思います。

と、ここまで書いていてSBIのまとめたページを見つけました。

www.sbigroup.co.jp

こちらのページには業務提携の背景等書かれています。参考にしてください。

一番心配していること

私の中では、「銀行が作るシステムは堅い」という考えがあります。 堅くするために新しい技術を利用せずに安定性や・安全性を重視して開発をしていると思います。

これにトランビが引っ張られた場合、機能の追加や修正など足を引っ張られるかもなと。

あまり詳しくないので、技術視点ではこのくらいしかわかりません。

もっと詳しいサイトや、考えを載せているサイトがあれば教えていただけると助かります。

ファーウェイが独自のOSを提供する?

はじめに

アメリカが華為技術(ファーウェイ)を本気で潰しにかかっていることはご存知だと思います。

世界市場的にはスマホで第2位を誇っており、中国市場でも大きなシェアを得ています。

規制によって何が起こるのかというと、アメリカにファーウェイ製品が入らないということ。

アメリカに入ってこないことによって、アメリカに本社を置くGoogleAndroidを提供しなくなること。

これはインバウンド規制によるものです。

この規制は外国企業の通信機器の仕様をやめさせるものです。

もしGoogleがファーウェイ製品に対して製品を提供した場合、この規制に従わないことになります。

国に逆らうことはできないので、Googleの行動は理解できます。

ファーウェイ独自OS?

こちらの記事を目にしました。

japanese.engadget.com

「はじめに」で書いたように、Androidはファーウェイに搭載されなくなります。

そうなった場合、日本や各国で取り扱っているファーウェイ製品にはAndroidが入っていません。

単純にOSが入っていないので、動かない文鎮と化します。

その対応策として、ファーウェイは独自のOSを利用可能になるようです。

当然でしょう。

独自OSを搭載した端末について

まだ詳しい情報は無いようです。

Androidとは異なるOSとのことなので、Androidに対応したアプリは動かないのではないでしょうか?

そうなった場合、開発者としてはそれに対応するメリットは無いように思える(利用者が減る可能性を考えて)ので、開発者は少ないでしょう。

僕も対応したくないです。

今でさえIOSAndroidで別れてるのに。。

それから、ブラウザはどうなるのでしょうか。

GoogleChromeは使えるのでしょうか。

独自のブラウザを使うとなれば厳しいですし、既存のブラウザを利用するのではと思います。

何にしてもファーウェイとしては独自のOSを出したとしてもシェアの低下は免れないでしょう。