Tech Hotoke Blog

IT観音とは私のことです。

【Java】Java17 まとめ~Java8との変更点~

f:id:TechHotoke:20211216134400p:plain

Java17

  • 2021年9月リリース
  • Java11以来のLTS版という事で注目度高い
  • Java10以降半年一回のリリース、3年ごとのLTSが発表されるというサイクルに固定化された
  • LTS版は3年LTS版でないJavaはサポート期間が半年( Oracle

主要ディスとリビュータごとのサポート期間

Java8の方がJava11よりも最大4年サポート期間が長い。

Java8→Java17への乗り換えの方がリスクが低いのでは...?

2018年にリリースされた最初のLTS(long-term support)であるJDK 11以降の14のJEPが含まれている。


Java8からの変更点

Recordクラス

Recordクラスとは?

  • ファイルやソケットで入出力するデータを扱うためのクラスや、データベースに出し入れするためのデータを扱うクラスのようなデータの入れ物としてのクラス
  • 従来の冗長な記述(setter/getter など)を排除することが可能
  • immutableな性質を持つ

Recordの書き方

定義

record Bike( String model, int disp, boolean big){
}

呼び出し

var cbr = new Bike(" 600 F 4 i", 599, true);

System.println(cbr.model())

レコード定義時のパラメータ名がそのままgetterとなる。

String model, int disp, boolean big){
     public Bike( String model, int disp, boolean big){ 
            if( disp > 400 && !big){ 
                throw new IllegalArgumentException( model + " should be big");
            } else if( disp <= 400 && big){
                 throw new IllegalArgumentException( model + " shouldn' t be big"); 
            } 
                this. model = model; 
                this. disp = disp; 
                this. big = big; 
            }
     }

     public float dispAsLiter(){ 
         return disp / 1000. 0 f;
     }
}

上記のようにコンストラクタを作成し、その中でバリデーションを行うことも可能

メソッドを別途定義することも可能。

基本的な使い方はclassと変わらない。

注意

immutableな性質を持つ=setterは存在しない

あとからフィールド値を上書きすることはできない。

たとえば データベースから検索してきたデータを加工して設定し、それをファイル へ 出力する ようなことはできない。

実態

実態としてはjava.lang.Recordクラスを継承したclassとしてコンパイルされる。

また、各フィールドはfinalとして定義されている。(immutableの正体)

getterや共通コードなどはコンストラクタが定義されている。

Compiled from "Bike. java" 
final class Bike extends java. lang. Record { 
    Bike( java. lang. String, int, boolean); 
    public final java.lang. String toString(); 
    public final int hashCode();
    public final boolean equals( java. lang. Object); 
    public java. lang. String model(); 
    public int disp(); 
    public boolean big(); 
}

Sealed

Java の クラス や インタフェース( 以降、 まとめ て「 タイプ)は 誰でも継承できるか、final 宣言することで誰も 継承でき ないかの 0 か100 かの二択だった。

これに対して、明示的に指定 さ れ たタイプにだけ 継承 を 許可 することが可能になる仕組み。

スーパータイプの作者が明示的に継承先を指定できるようになったため、システム的にAPIの整合性を守れるようになったことが大きなメリットとして挙げられる。

使い方

public sealed interface Motor permits RocketMotor, MotorBike, Car {}

クラス・インターフェイス宣言の直前にsealed宣言する

タイプの直後にpermits句を宣言(permitsされるクラスが同一のソースだけにあるのであれば、permitsは省略できる)

permits句にカンマ区切りで継承を許可するtypeを明示する

継承制限の継承

permits 句 で指定したサブタイプの継承制限は継承されない。

この ため、明示 的にSealedである かどうかを 示す必要 がある。

  • サブ タイプ も Sealed に し て 継承 先 を 制限 し たい 場合 は sealed 宣言
  • Sealed に し たく ない 場合 は non-sealed 宣言
  • それ 以上 継承 をさ せ たく ない 場合 は final 宣言

を行う。

制約

permits句で指定したtypeは同一モジュール内にある必要がある。

より詳しく

https://www.infoq.com/jp/articles/java-sealed-classes/

HTTP/2のサポート

  • 同期/非同期

    HttpClientでは同期的な通信だけでなく、非同期通信もサポートします。

    レスポンスはCompletableFutureで受け取れるため、レスポンス受信後の処理を非同期前提に記述できる。

  • ReactiveStream

    HttpClientをFlowAPIを用いたReactive Streamで扱うことができ る。

    ただし利用にはFlowの実装、あるいはAkka Streamsのような実装クラスとともに 用いる必要がある。

参考

【Spring】General error during conversion: Unsupported class file major version 61の対応

f:id:TechHotoke:20211216134543p:plain

環境

  • Mac BigSir11.4
  • JDK11
  • SpringBoot2.5.6

ソース

build.gradle

plugins {
    id 'org.springframework.boot' version '2.5.6'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
    runtimeOnly 'mysql:mysql-connector-java'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

test {
    useJUnitPlatform()
}

task npmRunBuild() {
    doFirst{
        if (!file("${rootDir}/src/frontend/node_modules").exists()) {
            "npm --prefix ${rootDir}/src/frontend install ${rootDir}/src/frontend"
                    .execute()
                    .waitForProcessOutput(System.out, System.err)
        } else {
            "npm --prefix ${rootDir}/src/frontend run build"
                .execute()
                .waitForProcessOutput(System.out, System.err)
        }
    }
}

processResources {
    dependsOn npmRunBuild
}

上記のgraldeファイルをgradle buildコマンドでビルドを行ったところ下記エラーが出てきたので対処

FAILURE: Build failed with an exception.

* Where:
Settings file '/Users/yudainoda/Documents/workspace-STS/TempleWorkSystemProto/settings.gradle'

* What went wrong:
Could not compile settings file '/Users/yudainoda/Documents/workspace-STS/TempleWorkSystemProto/settings.gradle'.
> startup failed:
  General error during conversion: Unsupported class file major version 61

  java.lang.IllegalArgumentException: Unsupported class file major version 61

調査

上記エラーに該当する事例は見つけられなかった。

似ていたのはこのあたり。

https://yusuke.blog/2021/03/04/2942

内容としてはGradle6系だとJava16以降のバージョンをサポートしていないということでした。

ただ、自分の使用しているJavaのバージョンは11かつGradleは7系だったので、該当せず。

gradle-wrapper.properties

distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip

なので、一旦バージョンに起因するエラーの説を疑い、下記のように書き換えプロジェクトの再び読み込みを行った。

distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip

結果

上記対応で問題なく起動できた。

Gradleのバージョンは新しいものに似たようなバグの報告などもあったので、今後は安定した二、三個古いバージョンのものを使うべしと思った。

参考

Vuex入門

f:id:TechHotoke:20211216134611p:plain

目的

Vuexについての備忘録として。

前提

  • Vueの基本的な知識があること。

データ管理の考え方

結論

アプリケーションの規模が大きくなるにつれて、状態を複数のcomponentから参照する場合に管理が複雑になるため、

タスク管理ツールなどでタスクの状態に応じて絞り込みを行うなどの機能を実装した場合に各componentごとにロジックが分散しないように一箇所でそれらのロジックを管理する。

データフロー設計のデザインパターン

  • 信頼できる唯一の情報源(SingleSourceofTruth)

管理する対象のデータを一箇所に集約することで管理を容易にすることを目的としたもの。

状態管理のコストを下げることを目的としたもの。componentが参照するデータのロジックを隠蔽することで実現する。

  • 単方向データフロー

状態の取得、更新を簡潔にすることを目的としたもの。

双方向にデータのやり取りができる場合、各componentに更新のロジックが生成される形となり使い回しが効かなくなる。

また、類似したロジックがアプリケーション内に散在することになる。

結果的に管理のコストが増大する。

f:id:TechHotoke:20211127075138p:plainf:id:TechHotoke:20211127075145p:plain

Vuex

Flux、Elmアーキテクチャ、Reduxを参考にしたもの。

Fluxアーキテクチャ

Fluxを構成する3つの要素

  • Store(ストア):アプリケーションの状態データを保持するオブジェクト、状態の更新を実施する処理

  • Action(アクション):状態を更新するための指示内容を表すメッセージ

  • Dispatcher(ディスパッチャー):Storeに対してActionによる更新指示を行う関数

データフロー図 f:id:TechHotoke:20211127080308p:plain

状態の更新指示内容である「Action」を関数である「Dispatcher」で「Store」に伝えて状態を更新し、その結果を「View(React)」に伝える、といった流れになっている。

 実際にアプリケーションに組み込んだ場合には、ユーザーが画面を操作したイベントに応じて新たなActionを発行(dispatch)して、状態と画面を更新していく形になるので、次の通りになります。 f:id:TechHotoke:20211127080411p:plain

2020年時点でのデファクトスタンダード的なライブラリはReduxのようです。

f:id:TechHotoke:20211127081435p:plain

Vuex

  • ストア 状態の保持を一元的に行う。

  • Vuexのデータフロー f:id:TechHotoke:20211127082204p:plain

  • Actions

Mutationsを介して、Stateを更新するメソッドです 非同期処理でなければなりません

  • Mutations

Stateを更新するメソッドです 同期処理でなければなりません

  • Getters

Stateの内容から算出される値です

Componentにデータを加工して提供します(Viewに表示させる)


こちらの記事がわかりやすかったです。↓

qiita.com

参考

アプリの状態管理を安全に行うためのFluxとRedux (2/3):CodeZine(コードジン)

Vuex とは何か? | Vuex

https://2020.stateofjs.com/en-US/technologies/datalayer/

Vue×SpringでSPA作成2【ログイン機能のカスタマイズ~バックエンド編~】

f:id:TechHotoke:20211216134128p:plain

まえがき

こちらの記事の続編です。 techhotoke.hatenablog.com

目的

VueとSpringで作成したプロジェクトの構築手順の備忘録。

備忘録のため、詳細な説明を省略している部分があります。

前提

基本的なJavaの知識やSpring、Vueの知識があること。

環境

  • Java 11
  • Spring Boot2.5.6
  • Gradle 7.1.1 * Vue3
  • Vue 2.6(Vuetifyが対応していなかったのでバージョン下げました。確認不足・・・)
  • IDESTS

やること

  • 前回はSpring Securityのデフォルト機能しか使っていなかったので、独自認証処理の実装。
  • バックエンド側の処理

実装

まずは独自認証用のUserFormクラスを作成します。

UserForm

public class UserForm {
        private String emailAddress;
        private String password;
     
        public void encrypt(PasswordEncoder encoder){
            this.password = encoder.encode(password);
        }
     
        @Override
        public String toString() {
            return "UserForm{" +
                    "emailAddress='" + emailAddress + '\'' +
                    ", password='" + password + '\'' +
                    '}';
        }
}

今回はemailAddressをユーザーIDとして扱います。

次に、独自認証用の設定を行なっていきます。

SpaAuthenticationFilter

@SuppressWarnings("unused")
public class SpaAuthenticationFilter  extends UsernamePasswordAuthenticationFilter{

    private static final Logger LOGGER = LoggerFactory.getLogger(SpaAuthenticationFilter.class);
    
    private AuthenticationManager authenticationManager;
    private BCryptPasswordEncoder bCryptPasswordEncoder;
 
    @Autowired
    private DataSource dataSource;
 
    public SpaAuthenticationFilter(AuthenticationManager authenticationManager, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.authenticationManager = authenticationManager;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
 
        // ログイン用のpathを変更する
        setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/api/login", "POST"));
 
        // ログイン用のID/PWのパラメータ名を変更する
        setUsernameParameter("emailAddress");
        setPasswordParameter("password");
 
        // ログイン後のリダイレクトを抑制
        this.setAuthenticationSuccessHandler((req, res, auth) -> res.setStatus(HttpServletResponse.SC_OK));
        // ログイン失敗時のリダイレクト抑制
        this.setAuthenticationFailureHandler((req, res, ex) -> res.setStatus(HttpServletResponse.SC_UNAUTHORIZED));
    }
 
    // 認証の処理
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req,
                                                HttpServletResponse res) throws AuthenticationException {
        try {
            // requestパラメータからユーザ情報を読み取る
            UserForm userForm = new ObjectMapper().readValue(req.getInputStream(), UserForm.class);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            userForm.getEmailAddress(),
                            userForm.getPassword(),
                            new ArrayList<>())
            );
        } catch (IOException e) {
            LOGGER.error(e.getMessage());
            throw new RuntimeException(e);
        }
    }
}

WebSecurityConfig

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
 
    @Autowired
    private DataSource dataSource;
 
    //ユーザIDとパスワードを取得するSQL文
    // 使用可否は全てTRUEで設定
    private static final String USER_SQL = "SELECT "
            + "tmpl_mail AS username, "
            + "tmpl_password AS password, "
            + "true "
            + "FROM tbl_temple "
            + "WHERE tmpl_mail = ?";
 
    private static final String ROLE_SQL = "SELECT "
            + "tmpl_mail AS username, "
            + "role "
            + "FROM tbl_temple "
            + "WHERE tmpl_mail = ?";
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery(USER_SQL)
                .authoritiesByUsernameQuery(ROLE_SQL)
                .passwordEncoder(bCryptPasswordEncoder());
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.logout().logoutUrl("/api/logout")
                .deleteCookies("JSESSIONID")
                .invalidateHttpSession(true)
                .logoutSuccessHandler((req, res, auth) -> res.setStatus(HttpServletResponse.SC_OK))
        ;
 
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/api/login").permitAll()
                .antMatchers("/frontend/**").permitAll()
                .antMatchers("/api/**").authenticated()
        ;
 
        // Spring Securityデフォルトでは、アクセス権限(ROLE)設定したページに未認証状態でアクセスすると403を返すので、
        // 401を返すように変更
        http.exceptionHandling().authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
 
        http.addFilter(new SpaAuthenticationFilter(authenticationManager(), bCryptPasswordEncoder()));
        http.csrf().ignoringAntMatchers("/api/login").csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}
  • ログインは/api/loginで実施する。

  • vueは/frontend/配下に配置して認証を必要としない。

  • APIは/api/配下のURLとして認証を必要とする。

  • 認証はCookieを利用する。

  • CSRF対策用にX-CSRF-TOKENをCookieに含める。

次に今回はHTML5 History APIを利用したルーティング機能を使用していきたいので、それに関する設定を行なっていきます。 Springのデフォルト設定での静的リソースへのアクセスはトップページ以外はファイル名を拡張子まで指定しなければアクセスできません。 そのため、トップページ以外へ直接アクセスした場合や、画面リフレッシュ時に正しくページが表示できなくなるため、別途設定が必要です。

Html5HistoryModeResourceConfig

/**
 * 対応しないURLの場合、固定ページを返す。
 */
@Configuration
public class Html5HistoryModeResourceConfig implements WebMvcConfigurer {
 
    private final Resources resourceProperties = new Resources();
 
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**") // 全パスをこのリソースハンドラーの処理対象にする
                .addResourceLocations(resourceProperties.getStaticLocations()) // 静的リソース配置先のパスを指定する
                .resourceChain(resourceProperties.getChain().isCache()) // 開発時はfalse、本番はtrueが望ましい。trueにしておくとメモリ上にキャッシュされるためI/Oが軽減される
                .addResolver(new SpaPageResourceResolver()); // 拡張したPathResourceResolverを読み込ませる
    }
 
    public static class SpaPageResourceResolver extends PathResourceResolver {
        @Override
        protected Resource getResource(String resourcePath, Resource location) throws IOException {
            Resource resource = super.getResource(resourcePath, location); // まずはPathResourceResolverで静的リソースを取得する
            return resource != null ? resource : super.getResource("/frontend/index.html", location); // 取得できなかった場合は、index.htmlを返す
        }
    }
}

最後に、ログインフォームからログインが成功した時に返却されるAPIを実装して置きます。 Controllerパッケージ配下にHomeController.javaを作成し、下記のように記述します。

@RestController
@RequestMapping("/api")
public class HomeController {
    
    @GetMapping("/home")
    @ResponseBody
    public String getHomeInfomation(){
        
        String text = "Home画面です";
        
        return text;
    }
}

一旦ここまでの設定を行い、http://localhost:8081/frontendにアクセスしてみるとvueのソースへのアクセスなので、設定を変更したため、今は直接アクセスすることが可能になってるはずです。

一旦今回はここまでで、次回はフロントエンド側の実装を行なっていきます。

ここまで読んで頂きありがとうございます。

【Java】Javaでログイン機能を実装してみた。

f:id:TechHotoke:20211216134635p:plain

目的

Springなどのフレームワークを使用してログイン機能を実装する機会が多く、中身がよく分かっていなかったので学習の一環として自力で実装すること

前提

  • JDKのインストールが完了していること
  • Tomcatのインストールが完了していること
  • Eclipseのインストール・セットアップが完了していること
  • JavaCSS、HTML、JSP/Servletの基本的な知識があること

環境

実装

プロジェクト作成~ログイン遷移

まずプロジェクトを作成し、それっぽいログイン画面を作成します。

f:id:TechHotoke:20211121222309p:plain

  • ログイン画面

f:id:TechHotoke:20211121222011p:plain

動的webプロジェクトを作成し、index.jspファイルを作成します。

index.jsp

<%@ page language="java" contentType="text/html;
charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>ログイン画面</title>
<link rel="stylesheet" type="text/css" href="css/login.css">
<link
    href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css"
    rel="stylesheet"
    integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1"
    crossorigin="anonymous">
</head>
<body>
    <div class="container">
        <div class="card card-container">
            <img id="profile-img" class="profile-img-card"
                src="//ssl.gstatic.com/accounts/ui/avatar_2x.png" />
            <p id="profile-name" class="profile-name-card"></p>
            <form class="form-signin" method="post" action="LoginServlet">
                <span id="reauth-email" class="reauth-email"></span> 
                <input
                    type="email" id="inputEmail" class="form-control"
                    placeholder="Email address" name="user-id" required autofocus> 
                <input
                    type="password" id="inputPassword" class="form-control"
                    placeholder="Password" name="password" required>
                <button class="btn btn-lg btn-primary btn-block btn-signin"
                    type="submit" value="signin">ログイン</button>
            </form>
            <!-- /form -->
            <a href="#" class="forgot-password"> パスワードを忘れた方はこちら </a>
        </div>
        <!-- /card-container -->
    </div>
    <!-- /container -->
    <script
        src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW"
        crossorigin="anonymous"></script>
</body>
</html>

続いて、Login処理を行うLoginServletクラスを作成します。

MVC構成で作成するので、controllerパッケージを作成し、その直下に作成します。

LoginServlet

package controller;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class LoginServlet
 */
@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
       
    /**
     * @see HttpServlet#HttpServlet()
     */
    public LoginServlet() {
        super();
    }

    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //JSPから、ユーザーIDとパスワードの取得
        String user_id = request.getParameter("user-id");
        String password = request.getParameter("password");

        //ログ
        System.out.println("ユーザーID:" + user_id);
        System.out.println("パスワード:" + password);
        
        //ログイン完了ページへ転送
        request.getRequestDispatcher("/WEB-INF/top.jsp")
        .forward(request, response);

        return;
    }

    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

}
  • doGetメソッドに記載。

  • ユーザーID、パスワードのinputタグ内のname属性値が取得されるのでgetParameterの引数にname属性の値を記述。

  • ログイン後遷移するtop.jspにforwardする。

次にtop.jspを作成します。

top.jsp

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>ログインに成功しました。</h1>
</body>
</html>
  • top.jspはWEB-INF配下に作成します。(WEB-INF配下には直接アクセスすることが出来ず、ログイン処理が必要なファイルはこちらに配置して隠蔽するのが良いので)

遷移が正常に行われるか確認します。

f:id:TechHotoke:20211122063241p:plain

コンソール

f:id:TechHotoke:20211122063339p:plain

データ連携〜ログインロジックの作成

それでは続いてデータの連携を行なっていきます。

今回、DB接続は行わずデータのやり取りはSessionを利用します。

まずはデータを格納するBeanクラスを作成します。

Javaソースの直下にdomainパッケージを作成し、UserBeanクラスを作成します。

UserBean

package domain;

import java.io.Serializable;

public class UserBean implements Serializable {

    private static final long serialVersionUID = 1L;
    
    private String user_id;
    private String user_name;
    private String password;
    private String phonenumber;
    private String address;
    private String role; // 権限
    private int status;// OK:0,未登録:1,パスワード不一致:2

        // setter/getterは省略

}

次に、Daoを作成します。 Javaソース直下にDaoパッケージを作成し、UserDaoクラスを作成します。

それらしいデータをBeanクラスにセットします。

※住所などの個人情報はジェネレータを使用しています。

package dao;

import domain.UserBean;

public class UserDao {
    /**
     * ユーザーIDからUserBeanを取得
     * 
     * @param user_id ユーザーID
     * @return UserBean ユーザーBean
     */
    public UserBean getUser(String user_id) {
        UserBean userBean = new UserBean();
        
        switch (user_id) {
        case "001":// user_id=001の場合
            userBean.setUser_id(user_id);
            userBean.setUser_name("前田");
            userBean.setPassword("pass");
            userBean.setPhonenumber("090-0000-1111");
            userBean.setAddress("佐賀県杵島郡大町町福母3-2-2 パークハイツ福母 9F");
            userBean.setRole("member");
            break;
        case "002":// user_id=002の場合
            userBean.setUser_id(user_id);
            userBean.setUser_name("鈴木");
            userBean.setPassword("pass2");
            userBean.setPhonenumber("090-5561-1174 ");
            userBean.setAddress("三重県四日市市前田町9-1-9");
            userBean.setRole("member");
            break;
        case "100":// user_id=100の場合
            userBean.setUser_id(user_id);
            userBean.setUser_name("佐藤");
            userBean.setPassword("pass3");
            userBean.setPhonenumber("070-2968-0125 ");
            userBean.setAddress("香川県綾歌郡綾川町萱原8-3-3");
            userBean.setRole("admin");
            break;
        default:// ユーザーIDが存在しない場合
            userBean.setUser_id(user_id);
            userBean.setStatus(1);
        }

        return userBean;
    }

}

続いて、ログインロジックを実装します。

package model;

import dao.UserDao;
import domain.UserBean;

public class LoginService {
    /**
     * @param user_id  ユーザーID
     * @param password パスワード
     * @return UserBean
     */
    public UserBean checkLogin(String user_id, String password) {
        UserDao userDao = new UserDao();
        UserBean userBean = new UserBean();

        // UserBeanの取得
        userBean = userDao.getUser(user_id);

        // ID存在チェック
        // status: 0:OK, 1:不在, 2:NG
        if (userBean.getStatus() == 1) {
            return userBean;
        } else {
            // パスワードのチェック
            if (userBean.getPassword().equals(password)) {
                userBean.setStatus(0);
            } else {
                userBean.setStatus(2); 
            }
            return userBean;
        }
    }
}

そして、LoginServletに下記のようにチェックロジックを追加します。

     LoginService loginService = new LoginService();

        // UserBeanの取得
        UserBean userBean = loginService.checkLogin(user_id, password);

        // ログイン失敗の場合は、失敗ページへ
        if (userBean.getStatus() != 0) {
            // 失敗ページの転送
            request.getRequestDispatcher("/WEB-INF/login-failed.jsp").forward(request, response);

            return;
        }

さらに、ログイン失敗後の遷移画面も作っていきます。

LoginServletのパスにlogin-failed.jspという名称を入れているのでそこに合わせたファイル名のJSPを作成します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1 style="color: red;">Sign in failed</h1>
</body>
</html>

設定したパスワード・ユーザーIDでログインしたときは成功画面へ、それ以外のパスワード・ユーザーIDでログインしたときは失敗画面へ遷移すればOKです。

SessionにUserBeanのデータを格納し画面に表示する

続いて、Userの情報を画面に表示させていきます。

まずは、LoginServletに以下を追記します。

    //UserBeanをセッションにセット
        HttpSession session = request.getSession(true);
        session.setAttribute("userBean", userBean);

これで、userBeanオブジェクトとuserBeanパラメータが紐づきました。

続いて画面上にデータを表示させていきます。 今回はJSTLを使用します。

※EL式を利用する場合はXSSクロスサイトスクリプティング)の脆弱性に注意してください。

こちらのサイトからzipファイルをダウンロードし、WEB-INF配下のlibフォルダ直下に配置します。

f:id:TechHotoke:20211123001216p:plain

タグリブを使用する場合は、使用するJSPファイルに宣言を追加してください。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

トップ画面に以下の記述を追加し、ログインした際にユーザー情報が反映されていることを確認します。

          <div>
        ユーザーID:
        <c:out value="${userBean.user_id}" />
    </div>
        <div>
        ユーザー名:
        <c:out value="${userBean.user_name}" />
    </div>

f:id:TechHotoke:20211123002656p:plain

login-failed.jspにも似たような形でデータを表示させます。

こちらはステータスに応じてメッセージを分岐させています。

<body>
    <h1 style="color: red;">Sign in failed</h1>
    <!-- 判定フラグ -->
    <c:if test="${userBean.status == 1}" var="flg" />

    <%--IDが存在しない時(ステータス1の場合) --%>
    <c:if test="${flg}">
        <p>
            ユーザーID: <strong><c:out value="${userBean.user_id}" /></strong>
            は存在しません。<br />
        </p>
    </c:if>

    <%-- パスワード間違いの時(ステータス2の場合) --%>
    <c:if test="${!flg}">
        <p>
            <strong>パスワードが間違っています。</strong><br /> 
        </p>
    </c:if>

    <a href="javascript:history.back();">戻る</a>

</body>

これでSessionのデータを画面に表示させる実装が完了しました。


[余談] JSTLを使用することで悪意のあるスクリプトが埋め込まれた時にタグを反映させないc:outなどを使用することで対策が可能です。 EL式ではタグがそのまま反映されてしまうため、XSS攻撃に対して脆弱性があると言われています。


フィルターの実装

フィルターは共通処理を行う仕組み。 JSPサーブレットを呼び出す前後で⾃動的に呼び出されるので認証・認可の実装などに用いられます。

それではこれまでの流れに沿ってtop.jspから会員のみ閲覧可能なalbum.jspを作成していきます。 (jspファイルなどの実装工程は冗長なので省略。GitHubのソースをご参照ください)

変更点としては三つで

  • top.jspからalbum.jspに遷移するリンクを作成したこと

  • album.jspを作成したこと

  • AlbumServletを作成したこと

です。

それではフィルターの実装を行なっていきます。

f:id:TechHotoke:20211123011128p:plain

画像の項目を選択し、LoginFilterクラスを作成。

以下のようにFilterの設定を行います。urlPatternsのパスにフィルターの対象とするServletを記述します。

/* フィルターの設定 */
@WebFilter(filterName = "LoginFilter", urlPatterns = { "/AlbumServlet" })
public class LoginFilter implements Filter {
//省略
}

次にdoFilterメソッドに処理を記述します。

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // HttpServletに変換
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        // セッションから情報取得
        HttpSession session = req.getSession();
        UserBean userBean = (UserBean) session.getAttribute("userBean");

        // 未ログインの場合、ログイン画面にリダイレクト
        if (session == null || userBean == null) {
            System.out.println("未ログイン");
            res.sendRedirect("index.jsp");
            return;
        }

        // pass the request along the filter chain
        chain.doFilter(request, response);
    }

ここまでで、AlbumSevletに直接アクセスしようとするとログイン画面にリダイレクトされて、コンソールに「未ログイン」の文字が表示されればOKです。

ログアウト処理の実装

Sessionが残っている場合、AlbumServletにログイン処理を行わなくともアクセス出来てしまう状態なのでSessionを破棄するように実装していきます。

LogoutServletを作成し、以下のように処理を記述します。

 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
          //セッション取得
        HttpSession session = request.getSession();

        //セッション破棄
        session.invalidate();

        //ページ転送
        request.getRequestDispatcher("index.jsp")
        .forward(request, response);
    }

top.jspにログアウト用のリンクを作成し、

  <p class="logout">
         <a href="LogoutServlet">Logout</a>
     </p>

ログイン→ログアウトリンクを押下→AlbumServletに直接アクセス

の手順を踏んだ時にログイン画面に遷移されればOKです。


以上長くなりましたがこれで完了です。気が向いた頃に認可処理も実装してみようと思います。 ありがとうございました。

参考

github.com

【Git】ひとつのGitリポジトリからサブリポジトリに分割して管理する方法

f:id:TechHotoke:20211216134726p:plain

やりたいこと

./src
├── frontend
│   ├── node_modules
│   ├── public
│   └── src
--------------------------------
↑リポジトリAで管理 ↓リポジトリBで管理
--------------------------------
├── main
│   ├── java
│   └── resources
└── test
    └── java

ひとつのリポジトリからファイルを切り出して別のリポジトリで管理できるように分割する

方法

  1. メインとなるローカルリポジトリとリモートリポジトリを作成し、そちらにcommit /pushして、コードを格納する。 (手順は省略します)

  2. サブとなるリモートリポジトリを作成

  3. サブディレクトリを作成したい階層に移動して2で作成したリポジトリをクローン(先にローカル環境でディレクトリを作成している場合はこの手順は不要です)

  4. プロジェクトのルートディレクトリにて、下記コマンドを実行します。

git filter-branch --subdirectory-filter [任意のpath] HEAD

注意 pathを指定する際に/をpathの末尾に付けないと下記のようなエラーが発生しました。

failed to unpack tree object 
Could not initialize the index

上記コマンドを実行すると、下記のように履歴が書き換えられた旨が表示されます。

Ref 'refs/heads/main' was rewritten

試しにルートディレクトリでlsコマンドを実行してみると、frontend配下のソースのみ表示されるはずです。

  • サブディレクトリ配下のソースを格納するリモートリポジトリにpushする まずリモートリポジトリを登録します。(これをやらずにpushするとメインのリポジトリが書き換えられてしまうので慎重に・・・)
 git remote set-url origin [作成したリモートリポジトリのURL]

その後にサブディレクトリのソースたちをinitしてaddしてcommitしてから、pushします。 リモートリポジトリにソースが格納されて入れば完了です。 f:id:TechHotoke:20211118063658p:plain

Tomcatのディレクトリ構造おさらい

f:id:TechHotoke:20211216134816p:plain

目的

Tomcatディレクトリ構成のおさらい兼見返せる用のメモ書き

前提

  • Tomcatのインストールが完了していること

ディレクトリ構成

f:id:TechHotoke:20211121154539p:plain

  • bin:実行ファイル

  tomcatの実行に関するファイル群が格納される場所

  • conf:設定ファイル

  tomcatの設定に関するファイル群が格納される場所

  • lib:APIファイル

  • logs:ログファイル

  tomcatのログファイル群が格納される場所

  • temp:一時ファイル

  一時ファイル群が格納される場所

  • webapps:プログラムファイル

  webアプリケーションを構成するファイル群が格納される場所

  • work:作業ファイル

webapps配下にアプリケーションのソース達を配置するというのがポイント。

webapps/examples配下のディレクトリ構成

webappsフォルダを展開するとその直下にexamplesというフォルダが格納されておりこちらを参考に実際のディレクトリ構成を確認することができる。 f:id:TechHotoke:20211121160740p:plain

この中で重要なのがWEB-INFディレクトリ。 こちらを展開すると、 f:id:TechHotoke:20211121160933p:plain

このような構成になっており、この中で重要なものが以下の三つ。

  • classes

  プログラムが配置される場所

  • libs

  プロジェクトで使用されるAPIが配置される場所

  プロジェクト全体の設定を行うファイル