20211130のJavaに関する記事は4件です。

Javaアクセス制御について

この記事は、シアトルコンサルティング株式会社 Advent Calendar 2021の5日目の記事です。 こんにちは、シアトルコンサルティングの小泉です。 はじめに 今回はプログラミングを学び始めた際に苦戦したアクセス制御についてまとめました。 プログラミング初学者の方に向け作成いたしましたので、アクセス制御について知りたい方はぜひ活用してみてください。 ゴール アクセス修飾子の役割と使用場面を理解できるようになる。 フィールドとメソッドに対するアクセス制御 Javaでは、それぞれのフィールド及びメソッドに対してアクセス制御を行うことができます。 以下の4段階からアクセス制御のレベルを選ぶような仕組みになっています。 制限のレベル 名称   指定方法 アクセスを許可する範囲 制限が厳しい private private 自分自身のクラスのみ ⬆︎ package private なし(何も書かない) 自分と同じパッケージに属するクラス ⬇︎ protected protected 自分と同じパッケージに属するか、自分を継承した子クラス 制限が緩い public public 全てのクラス 注意点:クラスのアクセス制御とメソッド及びフィールドのアクセス制御のうち、制約が厳しい方を優先させます 使用例(publicとprivate) 今回はよく使われるpublicとprivateの使用例について説明します。 publicという修飾子は、それを付けた要素 (クラス、フィールド、メソッド、コンストラクタ) の可視性を決定するものです。public は「公開の」という意味を持つように、public を付けた要素はどこからでも (全てのクラスから) 参照することができます。 それに対して、private という修飾子があります。これはフィールド、メソッド、コンストラクタに付けられるものでありますが、これを付けた要素は他のクラスから参照できなくなります。 例えば、次のようなクラスを考えます。 public class Rectangle { private double width; private double height; public Rectangle(double w, double h) { this.width = w; this.height = h; } public double area() { return this.width * this.height; } } 上記のようなクラスを作った際には、public のついているコンストラクタ、area メソッドには他のクラスからでもアクセスできます。 // 他のクラス public class RectangleAction1 { public static void main(String[] args) { Rectangle r = new Rectangle(1.0, 2.0); // --OK System.out.println("area = " + r.area()); // --OK } } ただし、private のついているフィールド double、height には他のクラスからはアクセスできません。 // 他のクラス public class RectangleAction2 { public static void main(String[] args) { Rectangle r = new Rectangle(1.0, 2.0); // --OK // private なのでどちらもコンパイルエラー r.width = 3.0; r.height = 4.0 System.out.println("area = " + r.area()); // --OK } } このように private を使用すると、「書き換えてほしくない情報を書き換えられないで済む」という利点があります。また、間違えて書き換えようとしてもコンパイルエラーとなるので、わざわざ実行しなくてもプログラムミスであることが分かります。 privateは同じクラスからならば読み出すことも書き換えることができます。 public class Rectangle { private double width; private double height; public Rectangle(double w, double h) { // 自分のクラスからは書き込めます this.width = w; this.height = h; } public double area() { // 自分のクラスからは読み出せます return this.width * this.height; } } さいごに アクセス制御について理解することで悪意や間違いによる利用を防止することができます。 開発を行う上では必要な知識になると思うので、この記事が皆様の手助けになれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SpringSecurityログインページをカスタマイズする

はじめに SpringSecurity その2です。今回はログインページをカスタマイズしていきます。 完全に理解することができていない為、不備等あるかもしれませんがご了承ください。 SpringSecurityシリーズ 項目表 NO タイトル その1 SpringSecurity導入から基本的な認証フロー その2 SpringSecurityログインページをカスタマイズする その3 DBアクセス処理実装 (現在執筆中) 前回の続きからとなる為、その1を実装後にご覧ください。 実装 SpringSecurity設定 ログインページをカスタマイズするための設定を追加します。 SecurityConfig.java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { <!-- 中略 --> @Override protected void configure(HttpSecurity http) throws Exception { // アクセス権限の設定 http.authorizeRequests() // 制限なし .antMatchers("/", "/login*", "/logout").permitAll() // '/admin'は、'ADMIN'ロールのみアクセス可 .antMatchers("/admin").hasRole("ADMIN") // 他は制限あり .anyRequest().authenticated(); // ログイン処理の設定 http.formLogin() // ログイン処理のURL .loginPage("/login") // usernameのパラメータ名 .usernameParameter("user") // passwordのパラメータ名 .passwordParameter("password") // ログイン失敗時の遷移先URL .failureForwardUrl("/login-error"); // ログアウト処理の設定 http.logout() // ログアウト処理のURL .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // ログアウト成功時の遷移先URL .logoutSuccessUrl("/login") // ログアウト時に削除するクッキー名 .deleteCookies("JSESSIONID") // ログアウト時のセッション破棄を有効化 .invalidateHttpSession(true); } <!-- 中略 --> } formLogin().loginPage("/login")にて、カスタマイズログインページの設定を行います。 loginPage(...)でカスタマイズのログインパスを指定します。この例だとデフォルトログインページも同じパスですが、明示的に指定しないとカスタマイズページに届きません。 usernameParameter(...)とpasswordParameter(...)で認証時に使用するパラメータの設定を行います。html内のname属性に合わせて設定します。 メッセージ追加 カスタマイズするエラーメッセージを設定します。 src/main/resources/messages.properties AbstractUserDetailsAuthenticationProvider.badCredentials=ユーザーIDまたはパスワードが違っています。 画面遷移用ページの用意 ページ遷移の確認用に、必要最小限のControllerとViewファイルを用意しております。 Controller SecurityController.java @Controller public class SecurityController { <!-- 中略 --> @GetMapping("/login") public String login() { // login.htmlを表示 return "login"; } @PostMapping("/login*") public String login(@ModelAttribute("user") String user, @RequestAttribute(name = WebAttributes.AUTHENTICATION_EXCEPTION, required = false) Exception exception, Model model) {         // 入力されたユーザ名をセットする model.addAttribute("message", user); // exceptionがnullではない(エラー)場合、エラーメッセージをセットする if (exception != null) { model.addAttribute("message", exception.getMessage()); } // login.htmlを表示 return "login"; } } View login.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>loginページ</title> </head> <body> <h1>login</h1> <div th:if="${message} != null"> <span th:text="${message}"></span> </div> <form th:action="@{/login}" method="post"> <p>ユーザー: <input type="text" name="user" th:value="${user}" /></p> <p>パスワード: <input type="password" name="password" /></p> <p><input type="submit" value="ログイン" /></p> </form> </body> </html> 動作確認 loginページ /loginでloginページを表示する。 カスタマイズのログインページを閲覧することができました。HTMLやCSSを編集することでオシャレなページにする事も可能です。 ユーザ名、パスワード入力誤り ユーザ名、パスワードを誤るとログインページに戻ってきてエラー表示されます。 ログイン成功 正しいユーザ名、パスワードを入力します。 ログインに成功しトップページに遷移します。 おわりに これでSpring Securityのログインページをカスタマイズすることができました。 間違っている箇所ありましたら、ご指摘いただければ幸いです。 次はDBアクセス処理実装を行なっていきます。 その3 DBアクセス処理実装 (現在執筆中です。)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

楕円曲線暗号の鍵をJavaで読み込む

OpenSSLで生成した楕円曲線暗号の鍵をJavaで読み込む方法。RSAはあるけど楕円曲線が無かったので。 手順 OpenSSLで鍵生成 $ openssl ecparam -genkey -name secp256r1 -out keypair.pem $ openssl ec -in keypair.pem -outform PEM -pubout -out public.pem $ openssl ec -in keypair.pem -outform PEM -out private2.pem $ openssl pkcs8 -topk8 -nocrypt -in private2.pem -out private.pem できたもの public.pem:公開鍵 private.pem:秘密鍵(PKCS#8形式) いらないもの keypair.pem private2.pem ポイント1:-name で指定する楕円曲線の一覧は openssl ecparam -list_curves で出力できる。 ポイント2:秘密鍵はPKCS#8形式に変換する(重要!)。 Javaで読み込み 秘密鍵 import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.security.KeyFactory; import java.security.interfaces.ECPrivateKey; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; public class Main { public static void main(String[] args) throws Exception { var kf = KeyFactory.getInstance("EC"); var privatePem = readPem("private.pem"); // ① var privateDer = Base64.getDecoder().decode(privatePem); // ② var privateKeySpec = new PKCS8EncodedKeySpec(privateDer); var privatekey = (ECPrivateKey) kf.generatePrivate(privateKeySpec); // ➂ System.out.println(privatekey.getParams()); } private static String readPem(String filename) throws IOException { return Files.readString(Paths.get(filename)) .replaceAll("-----.+?-----", "") .replaceAll("\\r?\\n", "") .trim(); } } ポイント①:いつものように、PEMのヘッダー、フッター、改行を取る。 ポイント②:Base64デコードして、DER(バイナリ)形式に変換する。 ポイント➂:秘密鍵の読み込み。PKCS#8に変換しないと、次のようなエラーがでる。 Exception in thread "main" java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence at jdk.crypto.ec/sun.security.ec.ECKeyFactory.engineGeneratePrivate(ECKeyFactory.java:170) at java.base/java.security.KeyFactory.generatePrivate(KeyFactory.java:389) at test5.Main.main(Main.java:22) Caused by: java.security.InvalidKeyException: IOException : algid parse error, not a sequence at java.base/sun.security.pkcs.PKCS8Key.decode(PKCS8Key.java:135) at java.base/sun.security.pkcs.PKCS8Key.<init>(PKCS8Key.java:95) at jdk.crypto.ec/sun.security.ec.ECPrivateKeyImpl.<init>(ECPrivateKeyImpl.java:75) at jdk.crypto.ec/sun.security.ec.ECKeyFactory.implGeneratePrivate(ECKeyFactory.java:245) at jdk.crypto.ec/sun.security.ec.ECKeyFactory.engineGeneratePrivate(ECKeyFactory.java:166) ... 2 more 公開鍵 import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.security.KeyFactory; import java.security.interfaces.ECPublicKey; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; public class Main { public static void main(String[] args) throws Exception { var kf = KeyFactory.getInstance("EC"); var publicPem = readPem("public.pem"); // ① var publicDer = Base64.getDecoder().decode(publicPem); // ② var publicKeySpec = new X509EncodedKeySpec(publicDer); var publicKey = (ECPublicKey) kf.generatePublic(publicKeySpec); System.out.println(publicKey); } private static String readPem(String filename) throws IOException { return Files.readString(Paths.get(filename)) .replaceAll("-----.+?-----", "") .replaceAll("\\r?\\n", "") .trim(); } } ポイント①:いつものように、PEMのヘッダー、フッター、改行を取る。 ポイント②:Base64デコードして、DER(バイナリ)形式に変換する。 おまけ - JWTの署名と検証 Nimbus使うので、事前にいれておいてください。 JWT生成(署名) import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.security.KeyFactory; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSObject; import com.nimbusds.jose.Payload; import com.nimbusds.jose.crypto.ECDSASigner; import com.nimbusds.jose.crypto.ECDSAVerifier; import com.nimbusds.jose.jwk.Curve; import com.nimbusds.jose.jwk.ECKey; import com.nimbusds.jose.jwk.KeyUse; public class Main { public static void main(String[] args) throws Exception { var kf = KeyFactory.getInstance("EC"); var publicPem = readPem("public.pem"); var publicDer = Base64.getDecoder().decode(publicPem); var publicKeySpec = new X509EncodedKeySpec(publicDer); var publicKey = (ECPublicKey) kf.generatePublic(publicKeySpec); System.out.println(publicKey); var jwk = new ECKey(Curve.P_256, publicKey, privatekey, KeyUse.SIGNATURE, null, null, null, null, null, null, null, null); // ① var signer = new ECDSASigner(jwk); var jwt = new JWSObject( new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(jwk.getKeyID()).build(), new Payload("{...}")); jwt.sign(signer); var str = jwt.serialize(); System.out.println(str); } private static String readPem(String filename) throws IOException { return Files.readString(Paths.get(filename)) .replaceAll("-----.+?-----", "") .replaceAll("\\r?\\n", "") .trim(); } } JWTパーズ(検証) import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.security.KeyFactory; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSObject; import com.nimbusds.jose.Payload; import com.nimbusds.jose.crypto.ECDSASigner; import com.nimbusds.jose.crypto.ECDSAVerifier; import com.nimbusds.jose.jwk.Curve; import com.nimbusds.jose.jwk.ECKey; import com.nimbusds.jose.jwk.KeyUse; public class Main { public static void main(String[] args) throws Exception { var kf = KeyFactory.getInstance("EC"); var privatePem = readPem("private2.pem"); var privateDer = Base64.getDecoder().decode(privatePem); var privateKeySpec = new PKCS8EncodedKeySpec(privateDer); var privatekey = (ECPrivateKey) kf.generatePrivate(privateKeySpec); System.out.println(privatekey.getParams()); var jwk = new ECKey(Curve.P_256, publicKey, privatekey, KeyUse.SIGNATURE, null, null, null, null, null, null, null, null); // ① var str = "...(JWTの文字列)..."; var jwt = JWSObject.parse(str); var verifier = new ECDSAVerifier(jwk); jwt.verify(verifier); System.out.println(jwt); } private static String readPem(String filename) throws IOException { return Files.readString(Paths.get(filename)) .replaceAll("-----.+?-----", "") .replaceAll("\\r?\\n", "") .trim(); } } ポイント①-1:楕円曲線の種類は、OpenSSLで生成した曲線名に合わせる。この記事では secp256r1 (P-256) なので、P_256 を指定。 ポイント①-2:署名だけでも、検証だけでも、公開鍵と秘密鍵の両方が必要になる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NullPointerExceptionのメッセージの改善について

エンジニアとしてシステムの開発や保守を経験してきました。その中で障害が発生した場合は迅速な対応が求められるのはエンジニアの方々にとっては当たり前のことだと思います。 私自身も多くの障害対応を経験してきましたが、対応は容易でも調査に時間がかかるのはやはりNullPointerExceptionではないかと思っています。 NullPointerExcepitonとは? 恐らくこのエラーに遭遇した人はいないと思われるほど有名なエラーですが、以下のリンクを参照してください。 簡単なソースですがこんな感じですね。 sample.java public static void main(String[] args) { String data = getSubstring(null); System.out.println(data); } static String getSubstring(String str) { String data = str.substring(3); return data; } 問題なのは言うまでもなく、  String data = str.substring(3); strがnullなのに、文字列抽出しようものならばエラーになりますよね。 JDK11の環境で実施すると以下のエラーが出力されます。 Exception in thread "main" java.lang.NullPointerException at nullSample.sample.getSubstring(sample.java:14) at nullSample.sample.main(sample.java:6) このような簡単なロジックであれば行数を見て原因を調査することも容易ですが、それなりの規模のシステムであったり途中参画したシステムでこの事象が発生すると、ログの調査から原因特定、事象再現など大変です。 特に大変と感じているのが原因特定で、「どこで」「なにが」「どういう条件で」を調べて対応するには時間も労力も使います。 そのために、各処理の前にnullチェックを行うというのは必要な処理であり、プログラム教育やOJTで最初に習うことの一つだと思います。 対策を講じるのは簡単でも、実際にエラーが発生すると調査が大変なのがこのエラーの厄介なところでしょう。 NullPointerExceptionのメッセージ改善 ここからがようやく本題ですが、JDK14のバージョンからNullPointerExceptionが改善されていました。(恥ずかしながら最近知りました。) 簡単に言うと、原因特定に必要な「どこで」「なにが」がわかりやすくなったというものです。 早速JDK14以降のバージョン(ここではJDK17を使用)で上記のロジックを実行してみました。 Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.substring(int)" because "str" is null at nullSample.sample.getSubstring(sample.java:14) at nullSample.sample.main(sample.java:6) エラーが発生した場所と理由がメッセージとして出力されていますね。 まとめ 長期間の開発や保守を行っていると、対象の開発や保守の環境の知識は深くなりますが日々改善されているJavaやその他のプログラミング言語に対して、エンジニアとしてもっとアンテナを広げていかなければなと思う今日この頃でした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む