utamaro’s blog

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

SpringSecurityを使ったシンプルな認証の作り方

spring securityの使い方

spring securityを使用して認証機能を実装するまでです。
※ 少しだけ認可するためのコードを書いています。

この記事ではDBへの接続は行わずにコードを書いています。

Spring Securityの実装方法を調べると、インメモリDBを使う方法や、jdbcを使う方法、mybatisを使う方法など様々な方法があります。

ただ、これらが関わると分かりづらい(理解しづらい)ので、省いて作ります。

準備

spring initializerを使ってプロジェクトを用意したあと、以下の依存関係を追加します。

※ spring-boot-starter-securityの中にspring-securityを使うための依存関係が含まれています。

※ 個別に管理したい場合はリンク先を開いて、追加してください。

// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security
'org.springframework.boot:spring-boot-starter-security:2.0.3.RELEASE',

// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf
'org.springframework.boot:spring-boot-starter-thymeleaf:2.0.3.RELEASE',

thymeleafの用意

application.ymlか、application.propertiesに以下の内容を追記します。

spring:
    thymeleaf:
        cache: false
        check-template: true
        check-template-location: true
        content-type: text/html
        enabled: true
        encoding: UTF-8
        mode: HTML
        prefix: classpath:/templates/
        suffix: .html

この設定は、https://memorynotfound.com/spring-boot-thymeleaf-configuration-example/ こちらのサイトを参考にしました。

templateファイルを用意する

resourceフォルダ以下にtemplatesフォルダを用意します。

templatesフォルダの中に、login.htmlとhome.htmlを用意します。 - login.html: ログイン用のページ - home.html: ログイン処理後のホーム画面(ログインしないと見れないページ)

それぞれのファイルは以下の内容を書きます。

login.html

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form th:action="@{/login}" method="post">
        <fieldset>
            <h1>Please Sign In</h1>

            <div th:if="${param.error}">
                <div>
                    Invalid username and password.
                </div>
            </div>
            <div th:if="${param.logout}">
                <div>
                    You have been logged out.
                </div>
            </div>

            <div>
                <input type="text" name="username" id="username" placeholder="UserName" required="true" autofocus="true"/>
            </div>
            <div>
                <input type="password" name="password" id="password" placeholder="Password" required="true"/>
            </div>

            <div>
                <div>
                    <input type="submit" value="Sign In"/>
                </div>
            </div>
        </fieldset>
    </form>
</body>
</html>

home.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    home view
</body>
</html>

これでhtmlファイルの準備は完了です。

spring securityの設定について

以下の5ファイルを作ります。

  • UserController
  • SecurityConfig.java
  • CustomUserDetailsModel
  • CustomUserDetialsService
  • GrantedAuthorityCode

それぞれのファイルのソースコードを書きます。

UserController.java

// package ry...;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

// @Controllerを使って画面を表示するためのpathを作ります
@Controller
public class UserController {

    @RequestMapping(path = "/login") // methodの指定が無いとGET methodが対象になります。
    public String loginView() {
        // resources/templates/login.htmlが開かれます。
        return "login";
    }

    @RequestMapping(path = "/home")
    public String homeView() {
        // 本来はHomeControllerのようなファイルを作ること
        // 今回はファイル数を減らして、わかりやすくするために同じControllerに作ってます。
        return "home";
    }
}

SecurityConfig.java

// package ry...;

import com.example.starter.app.user.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private CustomUserDetailsService customUserDetailsService;
    
    @Override
    protected UserDetailsService userDetailsService() {
        // 独自に作ったUserDetailsServiceを実行してもらうようにします
        return customUserDetailsService;
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                    .anyRequest()
                    .authenticated()
                .and()
                    .formLogin()
                    .loginPage("/login")
                    .usernameParameter("email")
                    .permitAll()
                    .defaultSuccessUrl("/home", true)
                .and()
                    .logout()
                    .permitAll()
                    .logoutUrl("/logout")
                    .deleteCookies("JSESSIONID")
        ;
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(customUserDetailsService)
                .passwordEncoder(passwordEncoder())
        ;
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

CustomUserDetailsModel.java

// package ry...;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

public class CustomUserDetailsModel extends User {

    public CustomUserDetailsModel(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        // Userクラスを継承しているので、とてもシンプルな実装になっています。
        // すべてを実装したい場合は、UserDetailsクラスをimplementsすると良いです。
        super(username, password, authorities);
    }
}

CustomUserDetialsService.java

// package ry...;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

@Service
public class CustomUserDetialsService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;
    // passwordEncoderはSecurityConfigクラスでBeanを定義しているのでAutowiredできます。
    // AutowiredするにはSecurityConfigクラスに@Configurationが必要です。

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        if (Objects.isNull(email)) {
            throw new UsernameNotFoundException("error");
        }
        if (email.equals("test@test.com")) {
            // DBがかかわらないようにしたかったので、email, passwordともにべた書きです。
            // DBからselectしたデータを使っても良いです。
            String password = passwordEncoder.encode("password");
            // ログインしたユーザの権限を設定しています。
            List<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add(GrantedAuthorityCode.ADMIN);
            // ↓のコードが実行されると、spring側がpasswordの検証までやってくれます。
            // 検証が成功すると、ログイン成功時のページにリダイレクトされます。
            return new CustomUserDetailsModel(email, password, authorities);
        }
        throw new UsernameNotFoundException("user not found.");
    }
}

GrantedAuthorityCode.java

// package ry...;

import org.springframework.security.core.GrantedAuthority;

public enum GrantedAuthorityCode implements GrantedAuthority {
    // 権限ごとにクラスを作ってGrantedAuthorityを実装するよりも、
    // enumでまとめて管理した方が簡単です。

    ADMIN("ADMIN"),
    ;

    private String authority;

    GrantedAuthorityCode(String authority) {
        this.authority = authority;
    }

    @Override
    public String getAuthority() {
        return authority;
    }
}

次にやるべきこと

DBにハッシュ化したパスワードを保存して、認証できるようにすること。
これは結構簡単にできます。

※ ヒントです

BCryptPasswordEncoderを使ってエンコードしているので、DBにパスワードを保存する際はハッシュ化して保存します。

String hashPassword = passwordEncoder.encode("test");

もしくは

String salt = BCrypt.gensalt();
String hashPassword = BCrypt.hashpw("test", salt);

でハッシュ化できます。

spring securityを使ったパスワードの検証では、passwordEncoder.match(A, hashA)で検証されているので、 DBにsaltキーを保存する必要はありません。

感想

spring securityを使った認証は苦労しましたが、振り返ると結構簡単に実装できると思いました。

ですが、実装するまで調べたりすることが多かったので、生産性はDjango,railsより劣ると思いました。
(慣れの問題かもしれませんが)

javaのコーディングは補完が強力なこと、事前にコンパイルされるので、ヌルポ以外でエラーが起きづらいといったことが助かりました。

...

認証後に生成されたJSESSIONIDのExpires / Max-Age1969-12-31T23:59:59.000Zになっているのが謎です。

どうみても過去です。

vuejsもreactjsも使わないchrome拡張機能の作り方

chrome拡張機能の作り方

この記事では、一人でchrome拡張機能を開発して、ストアに上げるためのデータを作るまでをご紹介します。

また、有料のサンプルコードをBaseにて提供しています。
googleで検索したページのタイトルをジョジョの名言に置き換える拡張機能です。)
(購入はこちらから)

utamaro.base.shop

サンプル画像(左の検索結果が、右の画像のようになるサンプルです)

f:id:miyaji-y26:20180716182713p:plainf:id:miyaji-y26:20180716182717p:plain

※ html, css, javascript, npmで依存関係の管理ができることを前提条件としています。

まずは、拡張機能を作るにあたっての要件を決めます。

  • 要件
    • 規模が大きくなっても、効率的に開発できること
    • console.logデバッグをビルド段階で削除すること(ビルド後のソースに含まれない)
    • html内のdomを作成する際はテンプレートとして用意したものを使う
    • scssで開発する
    • font-awesomeを使う(アイコンを表示するための神ライブラリ)
    • ファイルを保存して、画面を更新すると変更が適用されていること(毎回手動でのビルドは不要)
    • 開発段階と、リリース段階の設定ファイルを分けること
    • htmlからのjs, cssの読み込みは最低限(多くてもそれぞれ2つまで)とする
    • ビルドで作成されたソースコードはminifyされていること
    • 追加で勉強することが少ない(少しググるだけ)

これらの要件に当てはまるように使用するライブラリを選定します。

chrome拡張機能を作るための構成

有料で用意したサンプルの構成を基に解説します。

サンプルでは「googleの検索結果のタイトルをジョジョの名言に置き換える拡張機能」を作りました。

こちらで解説するのは以下の項目についてです。 - ファイル構成 - package.jsonの設定 - コマンド使用例 - templateについて

その他についてはソースコード内にコメントで書いています。

ファイル構成

サンプルコードのファイル構成は以下のようになっています。

.
├── extension  ⇠ このファイルをzip化するとそのままストアに上げることができます。
│   ├── assets
│   │   ├── css  ⇠ scssのファイルがコンパイルされると更新されます。
│   │   └── js  ⇠ jsファイルがコンパイルされると更新されます。
│   ├── icons  ⇠ 拡張機能に必要なアイコンを入れます。
│   │   ├── icon128.png
│   │   ├── icon16.png
│   │   ├── icon256.png
│   │   └── icon48.png
│   ├── manifest.json
│   └── popup.html
├── js  ⇠ 開発用のjsファイルです。コンパイルが必須です。
│   ├── content.js
│   └── script.js
├── node_modules
│   └── ...
├── package-lock.json
├── package.json
├── scss  ⇠ 開発用のscssファイルです。コンパイルが必須です。
│   ├── contentStyle.scss
│   ├── popstyle.scss
│   └── reset.scss
├── templates
│   └── helloMessage.handlebars
├── web-fonts-with-css  ⇠ font-awesomeで使用します。
│   └── scss
├── webpack.base.config.js  ⇠ dev,productionのベースとなるwebpackコンフィグです。
├── webpack.dev.config.js  ⇠ 開発用のwebpackコンフィグです。
└── webpack.production.config.js  ⇠ リリース用のwebpackコンフィグです。

package.jsonの設定

{
  "name": "starter01",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "scss-dev-w": "node-sass -r ./scss -o ./extension/assets/css/ -w --output-style expanded",
    "scss-dev": "node-sass -r ./scss -o ./extension/assets/css/ --output-style expanded --indent-type tab --indent-width 1",
    "scss-product": "node-sass -r ./scss -o ./extension/assets/css/ --output-style compressed",
    "webpack-dev": "webpack --config webpack.dev.config.js --mode development --progress --display-error-details",
    "webpack-dev-w": "webpack --config webpack.dev.config.js --mode development --progress --display-error-details --watch",
    "webpack-production": "webpack --config webpack.production.config.js --mode production --progress --display-error-details"
  },
  "author": "y-miyajima",
  "license": "ISC",
  "dependencies": {
    ...
  },
  "devDependencies": {},
  "directories": {
    "lib": "lib"
  },
  "description": ""
}

package.jsonのscriptsによく使うコマンドを用意しています。

例えば、scss-devですが、これはnode-sassというnpmモジュールを使ってscssをcssに変換するコマンドです。

また、-wがついているものは、ファイルが更新されると自動的に変換されます。

ただし、更新を監視するフォルダを指定しているので、環境が変わった際は修正が必要です。

それぞれのコマンドについての説明は以下のとおりです。

  • scss-dev, scss-dev-w
    • node-sassを使ってscssをcssに変換する
    • -r オプションによって./scssに配置した.scssファイルを再帰的に監視する
    • -o オプションによって./extension/assets/css/に変換後のファイルを保存する
    • --output-style の次がexpandedなら通常表示、compressedならminifyされます。 直感的にわかるサイトを見つけたので、参考にしてください。 https://web-design-weekly.com/2014/06/15/different-sass-output-styles/
  • scss-product
    • scss-devとほとんど同じです
    • compressedをつけているので、cssがminifyされます。
  • webpack-dev, webpack-dev-w
    • webpackを使ってjsをコンパイルする
    • --config オプションを使用すると環境ごとに分けた設定ファイルを指定できる
    • --mode オプションを指定するとwebpack側で組み込みの設定を最適化してくれます。あまり恩恵は感じていませんが、追加しています。https://webpack.js.org/concepts/mode/
    • --progress コンパイル時のログを表示します。
    • --display-error-details 読んで時のごとく、エラーの詳細を出します。個人的には、このオプションを付けないと何でエラーになったのかわかりませんでした。
    • --watch ファイルの更新を監視します。更新されたときにコンパイルが実行されます。
  • webpack-production
    • リリース時に使用するコマンドです。
    • ほとんどwebpack-devと同じです。

以上のコマンドは以下のように使用できます。

npm run css-dev-w
npm run webpack-dev-w
npm run webpack-dev-w

リリース時は以下のコマンドを実行します。

npm run webpack-production

templateについて

templateについてですが、listのアイテムを複製する際に繰り返し使用するdomを指しています。

例えば、サンプル内ではhelloの文字列をtemplateとしています。

使い方によっては以下のようなulタグ内のliタグを追加したりできます。

<ul>
    <li>item</li>  ⇠ これをtemplate化して、複数itemをulタグ内に追加する
</ul>

サンプルではhandlebarsというライブラリを使ってtemplateを解決しています。 他の使い方については https://handlebarsjs.com/ こちらを参考にしてください。

tempalteと聞いてvuejsを使わないのはなぜかと疑問が浮かぶかもしれません。

私もvuejsを使った開発経験があるのですが、久しぶりに自分が書いたコードを見ると理解するまでに時間がかかりました。

久しぶりに読むコードが読めない場合、知識が足りないか、理解が足りないかのどちらかだと考えています。

理解するまでに時間がかかるということは、1ヶ月後にバグが見つかり修正することになったときに、すぐに取り掛かれないのでは?と考えました。

そのため、知識が必要なvuejsではなく、理解しやすいhandlebarsを使用することに決めました。

また、使い慣れたjspやjinja2(pythonのテンプレートエンジン)に似ていることも理由です。

font-awesomeを使う際の注意点

scssを使うのが良いと思います。

cssではhtmlで読み込む必要があり、linkタグが多くなってしまうためです。

scssで使うときは、以下のように設定します。

まずはfont-awesomeをダウンロードし、必要なファイルをコピーして配置します。 https://use.fontawesome.com/releases/v5.1.0/fontawesome-free-5.1.0-web.zip

必要なファイルは以下の2つです。 - fontawesome-free-5.0.13/web-fonts-with-css/scss - fontawesome-free-5.0.13/web-fonts-with-css/webfonts

scss/_variables.scssの中のwebfontsへのパスを修正します。

// scssをcssにすると以下のようになります。
// src: url("./webfonts/fa-regular-400.eot");
// 実際に使用するcssからのパスを設定すること
$fa-font-path:                "./webfonts" !default;
// ...

あとは、font-awesomeをscssファイルからimportするだけです。

// ↓ fontawesomeを使うためのimport
@import "../web-fonts-with-css/scss/fontawesome.scss";
@import "../web-fonts-with-css/scss/fa-regular.scss";
// ↑ solid, brandsがあるので使いたいものをimportすること