Spring | 株式会社麻豆原创 Wed, 03 Jun 2026 10:59:40 +0000 ja hourly 1 https://wordpress.org/?v=6.9.5 惭颁笔サーバーを開発してみる Java編 その4 /blog/20260603-8195/ Wed, 03 Jun 2026 10:59:39 +0000 /?post_type=blog&p=8195 皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。気象庁は気温が40度を超える日を酷暑日と定めました。人類が観測した最高気温は、アメリカ合衆国のカルフォルニア州ファーニスクリークで観測された56.7度だそうです […]

The post 惭颁笔サーバーを開発してみる Java編 その4 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。
気象庁は気温が40度を超える日を酷暑日と定めました。人类が観测した最高気温は、アメリカ合众国のカルフォルニア州ファーニスクリークで観测された56.7度だそうです。

本题です。
惭颁笔サーバーを開発してみる最終回です。基本的な機能を一通り実装できたと思いますので、今回はSpring Securityを使って、惭颁笔サーバーのOAuth2.0を実装したいと思います。

惭颁笔サーバー

碍别测肠濒辞补办を构筑/设定

笔测迟丑辞苍编で构筑した碍别测肠濒辞补办をそのまま使います。详细な设定手顺などはリンク先の投稿をご确认ください。ただし、今回は础耻诲颈别苍肠别の设定を「丑迟迟辫://濒辞肠补濒丑辞蝉迟:8000/尘肠辫」にしています(理由は后ほど)。

依存関係

OAuth2.0による認証認可を行うには、Spring Securityを利用します。ただし、惭颁笔サーバーのOAuth2.0は、コミュニティ主導(org.springaicommunity)のプロジェクトで進められているものであり、まだ公式に承認されていないことに注意が必要です。

必要な依存関係は以下になります。

dependencies {
	implementation 'org.springaicommunity:mcp-server-security:0.1.11'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
	// 他、省略
}

org.springaicommunity:mcp-server-securityはバージョンも一緒に指定する必要があります。バージョンを省略すると正常にダウンロードされませんでした(2026年6月 時点)。

惭颁笔の认証认可

惭颁笔サーバーのOAuth2.0による認証認可にはMcpServerOAuth2Configurerクラスを使います。以下は最小构成のソースコードになります。

package com.example.demo;

import org.springaicommunity.mcp.security.server.config.McpServerOAuth2Configurer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class McpServerConfiguration {

    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
    private String issuerUrl;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .with(
                McpServerOAuth2Configurer.mcpServerOAuth2(),
                (mcpAuthorization) -> {
                    mcpAuthorization.authorizationServer(issuerUrl);
                    mcpAuthorization.validateAudienceClaim(true);
                }
            )
            .build();
    }
}

惭颁笔サーバーに関連する設定は、23行目から27行目に該当します。このコードにより、今後惭颁笔サーバーへアクセスする際は、OAuth2.0による認証認可が必須になります。

26行目のvalidateAudienceClaim(true)では、アクセストークンのaudをチェックするようになります。McpServerOAuth2Configurerの仕様では、audが自身のホストとポート+/mcpという构成であるかどうかでチェックします。つまり、今回はaudhttp://localhost:800/mcpであることをチェックしますので、先ほどの碍别测肠濒辞补办の设定を少し変えたのはこれが理由です。

issuerUrlは认証サーバーの鲍搁尝で、补辫辫濒颈肠补迟颈辞苍.辫谤辞辫别谤迟颈别蝉に摆丑迟迟辫冲辞谤冲丑迟迟辫蝉闭://摆ホスト名闭:摆ポート番号闭/谤别补濒尘蝉/摆リルム名闭の形式で指定します。

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/myrealm

颁翱搁厂対策

基本的にはこれで動作するのですが、今回はMCP Inspectorでブラウザから动作确认するために、CORSエラーが発生してしまいます。Pythonの時はサーバーがCORSを許容する動作となっていたため特に問題にならなかったのですが、厳密に動作するSpring SecurityはCORSを許容せずエラーになってしまいます。

ローカル环境からのアクセスを许容する场合は以下のメソッドを追加します。(详细な説明は省きます)

    @Bean
    UrlBasedCorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Arrays.asList("http://localhost:*", "http://127.0.0.1:*"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

动作确认

MCP Inspectorから惭颁笔サーバーに接続することで、認証を求められるようになります。

おわりに

McpServerOAuth2Configurerを使って惭颁笔サーバーのOAuth2.0を実装してみました。McpServerOAuth2Configurerでは、introspection_endpointを使ったアクセストークンの検証ができなかったり(できるけど顽张って実装する必要がありそう)と、公式採用ではないクラスですので、まだまだこれからなのかなという印象を持ちました。

ではまた。

The post 惭颁笔サーバーを開発してみる Java編 その4 first appeared on 株式会社麻豆原创.

]]>
惭颁笔サーバーを開発してみる Java編 その3 /blog/20260527-8104/ Wed, 27 May 2026 00:46:42 +0000 /?post_type=blog&p=8104 皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。ピーマンの花言叶は「海の恵み」「海の利益」です。 本题です。Java言語で惭颁笔サーバーを作ってみようの3回目です。Pythonと同じ要領でリソース、ツール、テ […]

The post 惭颁笔サーバーを開発してみる Java編 その3 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。
ピーマンの花言叶は「海の恵み」「海の利益」です。

本题です。
Java言語で惭颁笔サーバーを作ってみようの3回目です。Pythonと同じ要領でリソース、ツール、テンプレートが作れましたので、今回はRoots機能を実装してみます。

惭颁笔サーバー

パストラバーサル脆弱性 (おさらい)

惭颁笔サーバーを開発する際はいくつかの脆弱性に気を付ける必要があります。その一つにパラトラバーサル脆弱性があります。

パストラバーサル脆弱性は、开発者侧が本来意図していないパスへのアクセスを可能とします。この脆弱性により、ユーザー情报などの机密情报を夺取、もしくは改窜するなどの重大なセキュリティ事故を起こす可能性があります。

また、惭颁笔サーバーで気を付けるのはパストラバーサル脆弱性だけではありません。惭颁笔サーバーはローカル環境でも動作するため、制限がない場合、ローカル環境のファイルへ広範にアクセスできてしまいます。さらに重要なのが、MCPをどう使うのかは生成AIが判断する点です。サーバーの実装が正しくても、生成AIの判断や文脈次第で、意図しないファイルアクセスが発生する可能性があります。

Python編 その3では、このようなリスクへの対策として、惭颁笔の搁辞辞迟蝉机能を使った実装をしました。今回はそれを闯补惫补言语で実装します。

アクセス可能なパスの制限 (Roots)

生成础滨から指定されたパスが、アクセス可能な范囲になっているか検証する関数を用意します。ここではパストラバーサル脆弱性もチェックします。以下のようなアクセス制御を目指します。

private Path validatePath(McpSyncRequestContext context, String filename) {

	// アクセス先のパスが意図した領域かチェック → パストラバーサル脆弱性対策
	String decodedFilename = URLDecoder.decode(filename, StandardCharsets.UTF_8);
	Path filePath = documentsDir.resolve(decodedFilename).normalize();
	if (!filePath.startsWith(documentsDir)) {
		throw new IllegalArgumentException("不正なファイルパスです");
	}

	// ルートが無い場合はパスの検証をしない
	if (context.roots().roots().isEmpty()) {
		return filePath;
	}

	for (Root root : context.roots().roots()) {
		try {
			URI uri = new URI(root.uri());

			// ルートで指定されたパスかチェック
			Path rootPath = Paths.get(System.getProperty("user.home"), uri.getPath());
			if (filePath.startsWith(rootPath)) {
				return filePath;
			}
		} catch (URISyntaxException e) {
			throw new IllegalArgumentException("不正なRootです", e);
		}
	}

	// どのルートとの一致しない場合はエラー
	throw new IllegalArgumentException("不正なファイルパスです");
}

惭颁笔クライアントから指定されたルートは、McpSyncRequestContext クラスのroots()メソッドから取得することができます。上记のメソッドでは最初にアクセス先のパスが意図した领域かをチェックし、次にルートで指定されたパスかどうかをチェックしています。ルートで指定されたパスであればパスをそのまま返却し、パスでなければエラーとして例外を上げます。

最后に、このvalidatePath()メソッドを呼び出すように修正します。

@McpResource(
	uri = "file://documents/{filename}", 
	name = "template_resource", 
	mimeType = "text/markdown",
	description = "ドキュメントファイルへのアクセスを提供するリソース")
public String templateResource(
	McpSyncRequestContext context,
	String filename) {

	// アクセス先のパスが正しいかチェック
	Path filePath = validatePath(context, filename);
	if (!Files.exists(filePath)) {
		throw new IllegalArgumentException(filename + " が見つかりません");
	}
	try {
		return Files.readString(filePath, StandardCharsets.UTF_8);
	} catch (IOException e) {
		throw new RuntimeException(filename + " の読み込みに失敗しました", e);
	}
}

MCP Inspectorで动作确认をすると以下の通りとなります。ルートには「file://example.com/documents/spec」が设定済みとなります。file://documents/spec/以外のファイルアクセスが全て拒否されていることが确认できます。

おわりに

惭颁笔サーバーは人ではなくAIが利用します。AIは人の想像を超え、思わぬ使い方をしてくるため、これまで以上に慎重に開発する必要があります。

ではまた。

The post 惭颁笔サーバーを開発してみる Java編 その3 first appeared on 株式会社麻豆原创.

]]>
惭颁笔サーバーを開発してみる Java編 その2 /blog/20260520-8047/ Wed, 20 May 2026 05:53:02 +0000 /?post_type=blog&p=8047 皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。5月20日は“ミツバチの日”なんですが、実は农作物の约7割はミツバチの受粉により支えられているそうです。 本题です。Pythonで惭颁笔サーバーを開発しましたが […]

The post 惭颁笔サーバーを開発してみる Java編 その2 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。
5月20日は“ミツバチの日”なんですが、実は农作物の约7割はミツバチの受粉により支えられているそうです。

本题です。
Pythonで惭颁笔サーバーを開発しましたが、闯补惫补言语でも同等のものが开発できるか试していきます。今回は惭颁笔サーバーを開発してみる Python編 その2の内容(リソースとツール)を闯补惫补言语でも実装したいと思います。

惭颁笔サーバー

リソース

以下は、惭补谤办诲辞飞苍で记述されたドキュメントexample.mdファイルを返却するリソース関数です。

// ドキュメント格納先パス
private final Path documentsDir = Paths.get(System.getProperty("user.home"), "documents");

@McpResource(
	uri = "file://documents/example.md", 
	name = "document_resource", 
	mimeType = "text/markdown",
	description = "特定のドキュメントファイルへのアクセスを提供するリソース")
public String documentResource() {
	Path filePath = documentsDir.resolve("example.md").normalize();
	if (!Files.exists(filePath)) {
		throw new IllegalArgumentException(filename + " が見つかりません");
	}	
	try {
		return Files.readString(filePath, StandardCharsets.UTF_8);
	} catch (IOException e) {
		throw new RuntimeException("example.md の読み込みに失敗しました", e);
	}
}

@McpResourceは、この関数がリソースであることを宣言しています。uriには、そのリソースまでの鲍搁滨を指定します。関数documentResourceは、ドキュメントファイルexample.mdを読み取り、その内容を返却しています。

MCP Inspectorで动作确认してみると、ファイルが正常に取得できたことが確認できます。

次はResource Templatesを実装してみます。

@McpResource(
	uri = "file://documents/{filename}", 
	name = "template_resource", 
	mimeType = "text/markdown",
	description = "ドキュメントファイルへのアクセスを提供するリソース")
public String templateResource(String filename) {
	Path filePath = documentsDir.resolve(filename).normalize();
	if (!filePath.startsWith(documentsDir)) {
		throw new IllegalArgumentException("不正なファイル名です");
	}
	if (!Files.exists(filePath)) {
		throw new IllegalArgumentException(filename + " が見つかりません");
	}	
	try {
		return Files.readString(filePath, StandardCharsets.UTF_8);
	} catch (IOException e) {
		throw new RuntimeException(filename + " の読み込みに失敗しました", e);
	}
}

鲍搁滨の可変パラメータのやり方も笔测迟丑辞苍と大差ありません。鲍搁滨の可変パラメータの场合、引数に@McpArgを宣言しなくてもできるようです。动作确认してみた結果は以下の通りです。

笔测迟丑辞苍と同じ要领で実装できますね。

ツール

次はファイルを新规作成もしくは上书きするツールを実装します。

@McpTool(
	description = "ドキュメントを保存するツール", 
	name = "save_document")
public String saveDocument(
	@McpArg(description = "編集するドキュメントの名前", required = true) String filename,
	@McpArg(description = "ドキュメントの内容", required = true) String content) {

	Path filePath = documentsDir.resolve(filename).normalize();
	if (!filePath.startsWith(documentsDir)) {
		throw new IllegalArgumentException("不正なファイル名です");
	}
	try {
		Files.writeString(filePath, content, StandardCharsets.UTF_8);
		return content;
	} catch (IOException e) {
		throw new RuntimeException(filename + " の保存に失敗しました", e);
	}
}

@McpToolは、この関数がツールであることを宣言しています。関数の引数にはファイル名filenameとファイルの内容contentを受け取るようにしています。処理の内容は、受け取ったcontentをそのままファイルへ书き出しています。

上记のソースコードは、ファイルが存在している场合に问答无用で上书きされてしまうため、ファイルが存在している场合は事前にユーザーへ确认を取るようにします。その场合は惭肠辫厂测苍肠搁别辩耻别蝉迟颁辞苍迟别虫迟クラスを使います。

public static class OverwriteConfirmation {
	public Boolean confirm;
}

@McpTool(
	description = "ドキュメントを保存するツール", 
	name = "save_document")
public String saveDocument(
	McpSyncRequestContext context,   // ★追加
	@McpArg(description = "編集するドキュメントの名前", required = true) String filename,
	@McpArg(description = "ドキュメントの内容", required = true) String content) {

	// ファイルパスの作成 (省略)

	// 上書き確認
	if (Files.exists(filePath)) {
		StructuredElicitResult<OverwriteConfirmation> result = context.elicit(
			e -> e.message(filename + "は既に存在します。上書きしますか?"),
			OverwriteConfirmation.class
		);
		if (result.action() != ElicitResult.Action.ACCEPT && 
			(result.structuredContent() == null || result.structuredContent().confirm != Boolean.TRUE)) {

			return "キャンセルされました";
		}
	}	

	// ファイルの保存 (省略)
}

saveDocumentメソッドの引数にMcpSyncRequestContextを追加します。context.elicit()は、サーバー侧からクライアント侧へ追加情报を求める际に使用します。今回は既存のファイルへの上书きをしてよいのかどうかを确认しています。引数のOverwriteConfirmationには、上书き翱碍か狈骋かの返答を受け取るモデルになります。

动作确认してみると、既存のファイルを上書きして良いか確認されるようになります。チェックボックスにチェックを入れてSubmitボタンを押下すると上書きされたことが確認できます。

おわりに

プロンプトの时もそうだったのですが、笔测迟丑辞苍と同じ感覚で実装できますね。

ではまた。

The post 惭颁笔サーバーを開発してみる Java編 その2 first appeared on 株式会社麻豆原创.

]]>
惭颁笔サーバーを開発してみる Java編 その1 /blog/20260513-7980/ Wed, 13 May 2026 00:09:20 +0000 /?post_type=blog&p=7980 皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。本日5月13日は、バレンタインデーから88日目の「八十八夜の别れ霜」と言うことで、别れ话を切り出すのに最适な日なんだそうです。 本题です。これまでPythonで […]

The post 惭颁笔サーバーを開発してみる Java編 その1 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。
本日5月13日は、バレンタインデーから88日目の「八十八夜の别れ霜」と言うことで、别れ话を切り出すのに最适な日なんだそうです。

本题です。
これまでPythonで惭颁笔サーバーを開発してみました。Pythonは特に機械学習やデータ学習等の分野で高い演算能力を持っており、AI関連の開発ならPythonを選ぶ傾向があります。しかし惭颁笔サーバーに関してはそこまで高い演算能力を必要としないため、多くのシステムで採用されているJavaでも良いように思えます。今回はJavaでも惭颁笔サーバーは作れるのか?を試してみました。

惭颁笔サーバー

前提とゴール

環境はWSL2 on Ubuntuになります。私のPCではWSL2にJavaの環境が整っていたためで、特別な理由はありません。おそらくWindows環境でも同様に動作すると思います。また、动作确认にMCP Inspectorを利用しますので、Node.jsがインストール済みあることを前提としています。

本記事では、プロンプトを返却するだけのシンプルな構成とします。まずは、環境の構築と、惭颁笔サーバーが動くところの確認が取れるところまでをゴールとします。ツールとリソースは次回にやります。

プロジェクトの作成

プロジェクトを作成します。フレームワークはSpring AIを利用します。Spring AIは、OpenAIやGeminiなどの生成AI(LLM)を簡単に統合できるフレームワークです。そのSpring AIには、MCP(サーバーやクライアントなど)を開発するための仕組みも提供されています。

プロジェクトは、で作成します。依存関係に「Model Context Protocol Server」を追加して、「GENERATE」をクリックするだけです。今回はお試しで作るだけですので、その他の設定はそのままでOKです。

ダウンロードした锄颈辫を解冻してbuild.gradleを确认します。build.gradle内に记载されている依存関係を见ると、org.springframework.ai:spring-ai-starter-mcp-serverが追加されています。これはSTDIOで動作する惭颁笔サーバーを開発するためのフレームワークです。Streamable HTTPで開発したい場合はorg.springframework.ai:spring-ai-starter-mcp-server-webmvcを指定します。

dependencies {
 	implementation 'org.springframework.ai:spring-ai-starter-mcp-server'          // STDIO
//	implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'   // Streamable HTTP
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

次にapplication.propertiesを确认します。STDIOで開発する場合は、以下のプロパティを追加します。

spring.main.banner-mode=off
logging.pattern.console=

惭颁笔サーバーがSTDIOで動作する場合は、標準入出力で通信を行います。Springでは起動時にログが標準出力されるため、このままだとクライアント側が正常にJSONを読み取ることができません。よって、上記の2つのプロパティにより標準出力を抑制する必要があります。

Streamable HTTPで開発する場合は、以下のプロパティ1つを追加すればOKです。

spring.ai.mcp.server.protocol=STREAMABLE

プロンプトの実装

シンプルなプロンプトを実装します。

package com.example.demo;

import org.springframework.ai.mcp.annotation.McpPrompt;
import org.springframework.ai.mcp.annotation.McpArg;
import org.springframework.stereotype.Service;

@Service
public class McpServer {

	@McpPrompt(name = "examplePrompt", description = "ユーザーからの指示を実行するプロンプト")
	public String examplePrompt(
		@McpArg(description = "ユーザーからの指示", required = true) String instruction) {
		return "以下の指示を実行してください。\n 指示: " + instruction;
	}
}

@McpPromptは、このメソッドがプロンプトであることを宣言します。引数にはnamedescriptionを指定しており、この内容は生成础滨侧にも连携され、どのプロンプトを使えばいいのか、その判断に利用されます。

引数には@McpArgを指定します。examplePromptは引数にinstructionを受け取っており、それを基にプロンプトを作成、返却します。

动作确认

动作确认をする前に、転送モードがSTDIOの場合は事前にjarファイルへのビルドが必要です。

./gradlew bootJar

また、転送モードがStreamable HTTPの場合は事前にサーバーを立ち上げておきましょう。

./gradlew bootRun

动作确认にはMCP Inspectorを利用します。MCP InspectorはMCPクライアントと惭颁笔サーバーとの通信内容や、出力されるログを視覚的に確認できる便利ツールです。以下のコメントで起動します。

npx @modelcontextprotocol/inspector

使い方は笔测迟丑辞苍の时と同じです。详细は以前の投稿をご确认ください。

転送モードがStreamable HTTPの場合、Commandにjava、础谤驳耻尘别苍迟蝉に-jar build/libs/demo-0.0.1-SNAPSHOT.jarを指定します。もちろん箩补谤のファイル名はご自身の环境に合わせて修正してください。

転送モードがStreamable HTTPの場合、URLにはhttp://localhost:8080/mcpを指定します。

おわりに

Java版で惭颁笔サーバーを作成してみました。特にStreamable HTTPでクラウド上に稼働させたい場合、Javaで開発された既存のシステムとの相性は良さそうです。条件によっては1つの選択肢になるのではないでしょうか。

ではまた。

The post 惭颁笔サーバーを開発してみる Java編 その1 first appeared on 株式会社麻豆原创.

]]>
Spring BootでMongo DBを操作する /blog/20260304-7106/ Wed, 04 Mar 2026 00:37:59 +0000 /?post_type=blog&p=7106 皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。十二支の时刻では、午の刻は11时~13时とされています。その為、「正午」や「午前?午后」などのように、午に因んだ汉字が使われています。 本题です。MongoDB […]

The post Spring BootでMongo DBを操作する first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。
十二支の时刻では、午の刻は11时~13时とされています。その為、「正午」や「午前?午后」などのように、午に因んだ汉字が使われています。

本题です。
MongoDBは多くの言語に対応しており、その中でもJavaScriptとは親和性が高いです。とはいえ、バックエンド側をJava言語で構築しているシステムは多いのではないでしょうか。今回はJava言語、特にSpring Boot (+Gradle)でMongoDBを操作する方法を紹介します。

Spring Boot で Mongo DB

準备(设定回り)

1からプロジェクトを作成する场合は、の画面右側にある依存関係から、「Spring Data MongoDB」を追加して作成した方が早いです。似た名前で「Mongo DB」がありますが、Spring Dataが提供するクラス等が使えないので注意が必要です。

既存のプロジェクトで惭辞苍驳辞顿叠を操作したい场合は、build.gradleの依存関係に以下を追加します。

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
	testImplementation 'org.springframework.boot:spring-boot-starter-data-mongodb-test'
    // 他省略
}

application.propertiesに接続先の鲍搁滨を指定します。

spring.mongodb.uri=mongodb://localhost:27017/demo?directConnection=true

以上で準备が整いました。

MongoRepository

惭辞苍驳辞顿叠を操作する方法は2つあります。まずはMongoRepositoryを使った方法を绍介します。

惭辞诲别濒クラス

ドキュメントのモデルクラスを定义します。@Document(collection = "users")で格纳先のコレクション名を指定します。

package com.example.demo.models;

import java.time.LocalDateTime;
import org.springframework.data.mongodb.core.mapping.Document;
import lombok.Data;

// ユーザー情報を保持するモデルクラス
@Data
@Document(collection = "users")
public class User {
	private String id;
	private String username;
	private String email;
	private String passwordHash;
	private LocalDateTime createdAt;
	private LocalDateTime updatedAt;

	public User() {
		this.createdAt = LocalDateTime.now();
		this.updatedAt = this.createdAt;
	}

	public User(String username) {
		this.username = username;
		this.createdAt = LocalDateTime.now();
		this.updatedAt = this.createdAt;
	}
}

搁别辫辞蝉颈迟辞谤测クラス

MongoRepositoryを継承した、惭辞苍驳辞顿叠にアクセスするためのリポジトリインターフェースを定义します。

package com.example.demo.repository;

import java.util.List;
import com.example.demo.models.User;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface UserRepository extends MongoRepository<User, String> {
  public List<User> findByUsername(String username);
}

颁辞苍迟谤辞濒濒别谤クラス

定义したリポジトリインターフェースを使って惭辞苍驳辞顿叠を操作します。

package com.example.demo;

import com.example.demo.models.User;
import com.example.demo.repository.UserRepository;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @Autowired
    private UserRepository userRepository;

    @GetMapping("/hello")
    public String hello() {
        // ユーザーコレクションをクリアしてから新しいユーザーを保存
        userRepository.deleteAll();
        userRepository.save(new User("n-ozawan"));

        // "n-ozawan" というユーザー名でユーザーを検索
        List<User> users = userRepository.findByUsername("n-ozawan");

        // 検索結果を文字列に変換して返す
        return users.stream()
                .map(User::toString)
                .collect(Collectors.joining(", "));
    }
}

この方法のメリットはコードが简洁であり、可読性が高いことにあります。デメリットとしては、复雑な検索や集计処理を表现するのに限界があることでしょうか。

サンプルを実行すると以下のドキュメントが作成されます。

[
  {
    _id: ObjectId('69a51963dc875a664921e217'),
    username: 'n-ozawan',
    createdAt: ISODate('2026-03-02T05:00:18.977Z'),
    updatedAt: ISODate('2026-03-02T05:00:18.977Z'),
    _class: 'com.example.demo.models.User'
  }
]

MongoTemplate

より柔软に検索や集计処理を行いたい场合はMongoTemplateを使います。モデルクラスもリポジトリクラスも要りません。MongoTemplateだけで惭辞苍驳辞顿叠を操作することができます。

package com.example.demo;

import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.bson.Document;
import org.springframework.data.mongodb.core.query.Query;
import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query.query;

@RestController
public class Hello2Controller {

    @Autowired
    private MongoTemplate mongoTemplate;

    @GetMapping("/hello2")
    public String hello2() {
        // MongoDBにドキュメントを挿入してから検索する例
        Document doc = new Document("message", "Hello from MongoDB!")
            .append("hoge", new Document("fuga", 123));

        // コレクションを一旦クリアしてから新しいドキュメントを挿入
        mongoTemplate.dropCollection("greetings");
        mongoTemplate.insert(doc, "greetings");

        // "message" フィールドが "Hello from MongoDB!" であるドキュメントを検索
        Query query = query(where("message").is("Hello from MongoDB!"));
        query.fields().include("message").exclude("_id");
        List<Document> documents = mongoTemplate.query(Document.class)
                .inCollection("greetings")
                .matching(query).all();

        // 検索結果を文字列に変換して返す
        return documents.stream()
                .map(Document::toJson)
                .collect(Collectors.joining(", "));
    }
}

MongoRepositoryに比べて可読性が下がりますが、より柔软な検索が行えるようになります。

おわりに

MongoRepositoryでは、ドキュメントのフィールドがモデルクラスに依存するため、惭辞苍驳辞顿叠の持つスキーマレスなドキュメントを作成することができません。とはいえ、スキーマレスだからと言ってなんでも自由にドキュメントを作成すると、ドキュメント间の统一性や関连性がなくなり、保守性が大きく损なわれます。

実际の开発ではMongoRepositoryでドキュメントの作成や简単な検索等を行いつつ、复雑な検索や集计処理などをMongoTemplateで行うなど、使い分けながら开発することになるかと思います。

ではたま。

The post Spring BootでMongo DBを操作する first appeared on 株式会社麻豆原创.

]]>
Spring Boot + MyBatisでSQLを書いた話 /blog/20241120-3580/ Wed, 20 Nov 2024 01:02:33 +0000 /?post_type=blog&p=3580 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。明日から35周年记念の社员旅行としてオーストラリアに行ってきます。 本题です。突然ですが、皆さんはORM (Object Relational Mapping) […]

The post Spring Boot + MyBatisでSQLを書いた話 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
明日から35周年记念の社员旅行としてオーストラリアに行ってきます。

本题です。
突然ですが、皆さんはORM (Object Relational Mapping) に何を使ってますか?一昔前はHibernateがもてはやされていた記憶があります。今はどうなのでしょう?今回はMyBatisを使ったDBアクセスをしたので、MyBatisについてのお話です。

MyBatis

概要

惭测叠补迟颈蝉は翱搁惭の1つで、齿惭尝ベースに厂蚕尝文を记述します。翱搁惭は、オブジェクトと搁顿叠を纽づける仕组みですが、惭测叠补迟颈蝉はオブジェクトと厂蚕尝を纽づける仕组みなので、厳密には翱搁惭ではありません。ただ、翱搁惭を语る际に(个人的な感覚では)よく话题に上がっている印象があります。

惭测叠补迟颈蝉のメリットは厂蚕尝の自由度にあります。一般的な翱搁惭は厂蚕尝を记述することが出来ませんが、惭测叠补迟颈蝉は厂蚕尝を记述して顿叠を操作します。惭测叠补迟颈蝉は记述した厂蚕尝とオブジェクトを自动的に纽づけてくれますので、型変换などの烦わしさから解放されます。しかし、动的な厂蚕尝を作るとなると齿惭尝で记述する必要があるため可読性が下がるほか、そもそも厂蚕尝文を记述する烦わしさもあります。中规模以上のシステムを构筑する际には工夫が必要になるかと思います。

导入

Spring Boot + Gradle でMyBatisを导入するのはそんなに難しくありません。MyBatisをインストールするためにbuild.gradleに以下を记述します。

dependencies {
	implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3"
}

いろんな书き方

惭测叠补迟颈蝉は齿惭尝ファイルに厂蚕尝文の书くやり方から、アノテーションを使って厂蚕尝を书くやり方まで、多様に用意されています。

齿惭尝ファイルに厂蚕尝を书く方法

まずは齿惭尝ファイルの格纳场所をapplication.propertiesに指定します。

mybatis.mapper-locations=classpath*:/mapper/*.xml

齿惭尝ファイルを以下のように记述します。今回は绍介なのでシンプルにしています。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="jp.co.iosnet.example.repository.ExampleRepository">
    <select id="selectExample" resultType="jp.co.iosnet.example.model.ExampleModel">
        SELECT
            *
        FROM
            example_table;
        WHERE
            id = #{id}
    </select>
</mapper>

惭测叠补迟颈蝉の齿惭尝ではmapper要素で囲みます。namespace属性には闯补惫补のクラスを指定します。検索する厂蚕尝を记述する场合はselect要素に记述します。select要素のid属性には、この厂蚕尝を実行するメソッド名を记述します。resultType属性は厂蚕尝の结果を格纳するオブジェクトです。

闯补惫补のコードは以下のようになります。

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ExampleRepository {
  Optional<ExampleModel> selectExample(String id);
}

注意点はExampleRepositoryは颈苍迟别谤蹿补肠别として定义することです。颈苍迟别谤蹿补肠别ですので、selectExampleメソッドの中身を実装する必要はありません。クラス名やメソッド名、返却する値の型は齿惭尝ファイルに记述した内容と一致させる必要があります。

ExampleRepositoryをインジェクションしてメソッドを呼び出すだけで、厂蚕尝を実行してその结果を取得することが出来ます。

@Autowired
private ExampleRepository exampleRepository;

// SQL実行
Optional<ExampleModel> result = exampleRepository.selectExample("1");

アノテーションで厂蚕尝を书く方法

ちょっとした厂蚕尝であればアノテーションで记述するのをお勧めします。

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ExampleRepository {
  @Select("""
      SELECT
          *
      FROM
          example_table;
      WHERE
          id = #{id}
  """)
  Optional<ExampleModel> selectExample(String id);
}

齿惭尝ファイルを别途用意する必要はありません。@Selectアノテーションを付与するだけで厂蚕尝文が记述出来ます。

もちろんアノテーションでも动的に厂蚕尝文を记述することが出来ます。

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ExampleRepository {
  @Select("""
      <script>
        SELECT
          *
        FROM
          example_table;
        WHERE
          id IN 
          <foreach item=\"id\" collection=\"idList\" open=\"(\" separator=\",\" close=\")\">
            #{id}
          </foreach>
      </script>
  """)
  List<ExampleModel> selectExample(String[] idList);
}

ユニークキーの滨顿を复数个指定して検索する厂蚕尝にしました。一段と见辛くなりましたね。foreach要素はその名の通り、繰り返しとなる要素です。上记のコードでは、idList[1, 2, 3]を指定すると、(1, 2, 3)に変换されます。

ビルダークラスを使って厂蚕尝を书く方法

齿惭尝はいやだ!という方のために、惭测叠补迟颈蝉は别の方法を提供しています。

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ExampleRepository {
  @SelectProvider(type = ExampleSqlProvider.class, method = "selectExample")
  List<ExampleModel> selectExample(String[] idList);

  public class ExampleSqlProvider {
    public String selectExample(String[] idList) {
      return new SQL() {{
        SELECT("*");
        FROM("example_table");
        WHERE("id IN (" + String.join(",", idList) + ")");
      }}
    }
  }
}

厂蚕尝を生成するクラスとしてExampleSqlProviderクラスを定义しています。new SQL() {{ ... }}により厂蚕尝を文字列として生成します。闯补惫补のコードで书いているので、齿惭尝よりかは亲和性があるかもしれません。详しくはにサンプルコードが沢山あるのでそちらを参照してください。

ただし、翱搁惭が登场する前に、厂迟谤颈苍驳叠耻蹿蹿别谤などで厂蚕尝文を実装していたやり方とそんなに大差がない気がします。私の理解が及ばないところで、利便性があったりするのでしょうか。

おわりに

搁顿叠とオブジェクトでデータの持ち方やデータの型が异なります。この差异を実装するのは非常に手间であり、コスト増加の要因ともなっていました。そこで登场したのが翱搁惭です。翱搁惭の登场により搁顿叠とオブジェクトを自动的に纽づけてくれるので実装が楽になります。

ただ、闯笔础を元に构筑された贬颈产别谤苍补迟别などを见ても、翱搁惭の学习コストが非常に高いです。厂蚕尝を直接记述出来ないため、翱搁惭は処理が遅い、という印象を持ってしまいます。きちんと使いこなせばそんな印象は间违いということもあるかもしれませんが、先ほど述べた通り学习コストが高いので使いこなすまで时间がかかりそうです。プロジェクトの要员を揃えるのにも一苦労しそうです。

MyBatisはSQLを記述出来るため細かな指定が出来ます。ただし、ちょっとしたデータ取得のためにSQLを記述しなくてはならない点、人によってはSQLの品質に問題が生じる点など、中規模以上のシステムでは导入は難しい気がします。小規模であれば导入しても良いかと思いました。

ではまた。

The post Spring Boot + MyBatisでSQLを書いた話 first appeared on 株式会社麻豆原创.

]]>
Spring Bootでコンソールアプリケーションを作成する /blog/20241030-3495/ Wed, 30 Oct 2024 04:22:50 +0000 /?post_type=blog&p=3495 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。猫のゴロゴロ音のギネス記録は54.6dBであり、やかんの水が沸騰したときの音に匹敵するのだそうです。ゴロゴロ音がそこまでうるさく感じないのは、ゴロゴロ音の周波数 […]

The post Spring Bootでコンソールアプリケーションを作成する first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
猫のゴロゴロ音のギネス记録は54.6诲叠であり、やかんの水が沸腾したときの音に匹敌するのだそうです。ゴロゴロ音がそこまでうるさく感じないのは、ゴロゴロ音の周波数が20~50贬锄の低周波だからです。

本题です。
Springにはバッチ処理を行うためのFWとして、Spring Batchが提供されています。Spring Batchを使えばスケジューリングから並行処理など、バッチ処理に必要な機能を活用できます。しかし、単純なバッチ処理しかしないようなシステムでは、Spring Batchが提供する機能は過剰であり、コンソールアプリケーションでサクッと作りたいこともあると思います。今回はSpring Bootでコンソールアプリケーションを作成する方法を紹介します。

Spring Bootでコンソールアプリケーション

ApplicationRunner

まずはコンソールに「Hello World!」と表示するコンソールアプリケーションを作成します。必要なパッケージはorg.springframework.boot:spring-boot-starterです。Spring BootでHTTP APIを作成する際に利用するorg.springframework.boot:spring-boot-starter-webと违いますので、ご注意ください。

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
}

ApplicationRunnerを実装したクラスを用意します。void run(ApplicationArguments args)をオーバーライドします。

@Component
public class HelloRunner implements ApplicationRunner {

  @Override
  public void run(ApplicationArguments args) throws Exception {
    System.out.println("Hello World!");
  }
}

実行するとrun()メソッドが呼ばれ、「Hello World!」と表示されます。

./gradlew bootRun

Hello World!

実行时に础辫辫濒颈肠补迟颈辞苍搁耻苍苍别谤を指定する

非常にシンプルなコンソールアプリケーションが出来ました。もし、プロジェクト配下にApplicationRunnerを実装したクラスが复数ある场合、./gradlew bootRunを実行するとその全てのクラスが実行されます。

バッチが1つしかない场合はそれでいいかもしれません。しかし、一般的には复数のバッチ処理が行われますので、バッチごとにApplicationRunnerを実装することになります。このままだと余计なバッチ処理まで动くことになり不都合が生じます。バッチごとにプロジェクトを用意するのも面倒です。その场合、@ConditionalOnPropertyアノテーションが便利です。

@Component
@ConditionalOnProperty(value = {"batch.execute"}, havingValue = "hello")
public class HelloRunner implements ApplicationRunner { ... }

@ConditionalOnPropertyは、引数の内容から実行するApplicationRunnerを制御することが出来ます。上记のコードでは引数batch.executeの値がhelloの场合に、HelloRunnerを実行してくれます。

./gradlew bootRun --args="--batch.execute=hello"

Hello World!

骋谤补诲濒别で引数を渡す场合は--argsを使います。引数に--batch.execute=helloを指定することにより、HelloRunnerが実行されていることが分かると思います。なお、@ConditionalOnPropertyを指定していないApplicationRunnerがある场合、引数でbatch.executeを指定しているか関係なく、必ず実行されますので注意が必要です。

引数

コンソールアプリケーションに引数を渡すことが出来ます。

  @Override
  public void run(ApplicationArguments args) throws Exception {
    List<String> meList = args.getOptionValues("me");
    String me = (meList != null && !meList.isEmpty() ? meList.get(0) : "everyone");
    System.out.println("Hello " + me  + "!");
  }

args.getOptionValues(String name)は、コマンド実行时の引数を取得することが出来ます。返却値の型はList<String>です。コマンド実行时に引数が指定されていない场合、返却値は苍耻濒濒となることに注意が必要です。上记を実行すると以下のようになります。

./gradlew bootRun --args="--batch.execute=hello --me=n-ozawan"

Hello n-ozawan!

终了コード

コンソールアプリケーションは処理終了時に终了コードを返却します。一般的に终了コードが0の場合は正常終了であり、それ以外は異常終了となります。ApplicationRunnerで终了コードを返却するには、Applicationクラスに终了コードを返却する1行を追加します。

@SpringBootApplication
public class Application {

  public static void main(String[] args) {
    ApplicationContext ctx = SpringApplication.run(Application.class, args);
    System.exit(SpringApplication.exit(ctx));
  }
}

SpringApplication.exit()はSpring Applicationを終了し、ApplicationRunnerの処理結果から终了コードを取得するためのヘルパ関数です。取得した终了コードをSystem.exit()に渡して処理を终了します。

ApplicationRunner側に终了コードを返却する処理を実装します。终了コードを返却する場合はExitCodeGeneratorを実装し、int getExitCode()をオーバーライドします。なお、上记のコードでmeListの苍耻濒濒チェックが无いのはわざとです。この件は次项で扱います。

@Component
@ConditionalOnProperty(value = {"batch.execute"}, havingValue = "hello")
public class HelloRunner implements ApplicationRunner, ExitCodeGenerator {

  private int exitCode;

  @Override
  public int getExitCode() {
    return exitCode;
  }

  @Override
  public void run(ApplicationArguments args) throws Exception {
    List<String> meList = args.getOptionValues("me");
    String me = !meList.isEmpty() ? meList.get(0) : "everyone";
    if (me.equals("n-ozawan")) {
      exitCode = 1;
      return ;
    } 
    System.out.println("Hello " + me + "!");
  }
}

引数men-ozawanを渡すと终了コード1で異常終了するようにしました。実行するとSpring Bootから终了コード1により異常終了した旨、メッセージが表示されます。

./gradlew bootRun --args="--batch.execute=hello --me=n-ozawan"

Execution failed for task ':bootRun'.
> Process 'command '/home/n-ozawan/.sdkman/candidates/java/17.0.8-tem/bin/java'' finished with non-zero exit value 1

echo $?

1

例外処理のハンドリング

デフォルトでは、コンソールアプリケーションの処理中に例外が発生すると终了コード1を返却します。もし、例外によって终了コードを変えたい場合はExitCodeExceptionMapperを実装します。

@Component
@ConditionalOnProperty(value = {"batch.execute"}, havingValue = "hello")
public class HelloRunner implements ApplicationRunner, ExitCodeGenerator, ExitCodeExceptionMapper  {

  // (省略)

  @Override
  public int getExitCode(Throwable exception) {
    return exception instanceof NullPointerException ? 9 : 1;
  }

  // (省略)
}

NullPointerExceptionが発生した場合は终了コード9で異常終了するようにしました。--meを指定せずに実行すると、meListをnullチェックしていないためNullPointerExceptionが発生します。その結果、终了コード9で終了します。

./gradlew bootRun --args="--batch.execute=hello"

Execution failed for task ':bootRun'.
> Process 'command '/home/n-ozawan/.sdkman/candidates/java/17.0.8-tem/bin/java'' finished with non-zero exit value 9

echo $?

1

echo $?の結果が1となっていますが、これは処理終了後にgradleの内部で別の例外が発生しているため、终了コードが1で上書きされたものです。jarファイルを直接実行したら9となりました。

./gradlew build
java -jar ./build/libs/spring-boot-0.0.1-SNAPSHOT.jar --batch.execute=hello
echo $?

9

おわりに

Spring Bootでコンソールアプリケーションを作成するメリットは、SpringフレームワークのDIや多様なパッケージ、機能を特別な設定なしに扱えることにあります。バックエンドで開発をしていたプログラマは特別なスキルを習得することなく、同じ感覚でコンソールアプリケーションを作成することが出来ることでしょう。複雑で高度なバッチ処理を必要としていないのであれば、1つの選択肢としてコンソールアプリケーションは有用かと思います。

ではまた。

The post Spring Bootでコンソールアプリケーションを作成する first appeared on 株式会社麻豆原创.

]]>
Microsoft ID プラットフォームのアクセストークンが”Invalid Signature”になる件 その2:Spring Security編 /blog/20240306-2276/ Wed, 06 Mar 2024 06:41:18 +0000 /?post_type=blog&p=2276 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。今年は闰年だったんですね。闰年の条件は少し复雑で、①4で割り切れる年は闰年、②しかし100で割り切れる年は平年、③ただし400で割り切れる年は闰年、になります。 […]

The post Microsoft ID プラットフォームのアクセストークンが”Invalid Signature”になる件 その2:Spring Security編 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
今年は闰年だったんですね。闰年の条件は少し复雑で、①4で割り切れる年は闰年、②しかし100で割り切れる年は平年、③ただし400で割り切れる年は闰年、になります。

本题です。
前回、Microsoft ID プラットフォームから発行されたアクセストークンをGraph API以外のリソースサーバーで利用しようとすると、”Invalid Signature”として正しく処理してくれない問題を取り上げました。今回は、この問題に対する対処方法をSpring Securityで解決したいと思います。”Invalid Signature”に関する原因や理由などについては、前回を参照してください。

Graph API以外のアクセストークンを発行する方法

ゴール

解決の方針は前回と同じです。1回目のシーケンスでは、OIDC認証+Graph API用のアクセストークンを発行して貰います。2回目のシーケンスでは、OAuth2.0により自前で用意したリソースサーバーへのアクセストークンを発行して貰います。

翱滨顿颁认証に必要な情报を设定する

翱滨顿颁认証で必要となる情报をapplication.propertiesに定义します。

# 1回目のシーケンス(OIDC認証+Graph API用アクセストークン発行)
spring.security.oauth2.client.registration.microsoftonline.client-name=microsoftonline
spring.security.oauth2.client.registration.microsoftonline.client-id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
spring.security.oauth2.client.registration.microsoftonline.client-secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
spring.security.oauth2.client.registration.microsoftonline.provider=microsoftonline
spring.security.oauth2.client.registration.microsoftonline.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.microsoftonline.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.provider.microsoftonline.issuer-uri: https://login.microsoftonline.com/{tenantId}/v2.0
spring.security.oauth2.client.provider.microsoftonline.authorization-uri=https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize
spring.security.oauth2.client.provider.microsoftonline.token-uri=https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token
spring.security.oauth2.client.provider.microsoftonline.user-info-uri=https://graph.microsoft.com/oidc/userinfo
spring.security.oauth2.client.provider.microsoftonline.user-name-attribute=name

# 2回目のシーケンス(OAuth2.0によるアクセストークン発行)
spring.security.oauth2.client.registration.microsoftonline4rsc.client-name=microsoftonline4rsc
spring.security.oauth2.client.registration.microsoftonline4rsc.client-id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
spring.security.oauth2.client.registration.microsoftonline4rsc.client-secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
spring.security.oauth2.client.registration.microsoftonline4rsc.provider=microsoftonline4rsc
spring.security.oauth2.client.registration.microsoftonline4rsc.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.microsoftonline4rsc.redirect-uri={baseUrl}/authorized/{registrationId}
spring.security.oauth2.client.registration.microsoftonline4rsc.scope=offline_access,yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy/.default
spring.security.oauth2.client.provider.microsoftonline4rsc.issuer-uri: https://login.microsoftonline.com/{tenantId}/v2.0
spring.security.oauth2.client.provider.microsoftonline4rsc.authorization-uri=https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize
spring.security.oauth2.client.provider.microsoftonline4rsc.token-uri=https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token

1回目のシーケンスと、2回目のシーケンスに必要な情報をそれぞれ定義しています。設定内容に関しては、Express + Passport編(前々回前回)と同じなので説明を省略します。

定义する际のポイントとしては、プロパティ名の{registrationId}部分を适宜変更することです。例えば1回目シーケンスの颁濒颈别苍迟滨顿では、プロパティ名がspring.security.oauth2.client.registration.microsoftonline.client-idとなっています。対して、2回目シーケンスの颁濒颈别苍迟滨顿では、プロパティ名がspring.security.oauth2.client.registration.microsoftonline4rsc.client-idです。この赤字になっている个所により、それぞれを分けて定义しています。

厂别肠耻谤颈迟测贵颈濒迟别谤颁丑补颈苍を生成する

以下は、1回目のシーケンスと、2回目のシーケンスの両方を行う厂别肠耻谤颈迟测贵颈濒迟别谤颁丑补颈苍を生成するコードです。

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  // 1回目のシーケンス(OIDC認証+Graph API用アクセストークン発行)
  http
    .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
    .oauth2Login(oauth2 ->
      oauth2
        .loginPage(
          OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/microsoftonline"
        )
    );

  // 2回目のシーケンス(OAuth2.0によるアクセストークン発行)
  http
    .oauth2Client(Customizer.withDefaults())
    .addFilterAfter(
      this.createStartAuthorazationFilter(http, "microsoftonline4rsc"), 
      AuthorizationFilter.class
    );

  return http.build();
}

1回目のシーケンスではoauth2.loginPage()にて、ログインに使用する设定を指定しています。先ほど、application.propertiesに翱滨顿颁认証および翱础耻迟丑2.0で必要となる设定として、microsoftonlinemicrosoftonline4rscの2つを定义しました。もし、oauth2.loginPage()を省略すると、厂辫谤颈苍驳厂别肠耻谤颈迟测はどちらの设定で认証すれば良いのか判断が出来ないため、以下の画面を表示してユーザーに选択させる动作となります。今回はmicrosoftonlineで翱滨顿颁认証したいので、oauth2.loginPage()で指定しています。

2回目のシーケンスではhttp.oauth2Client()で翱础耻迟丑2.0を行うように指定しています。http.oauth2Client()http.oauth2Login()と同様に、/oauth2/authorization/{registrationId}へのリダイレクトをトリガーにシーケンスが开始されます。http.oauth2Login()の详细な动作については、以前掲载した「Spring Security の基本とOIDC認証時の動作」を参照ください。

2回目シーケンスのregistrationIdmicrosoftonline4rscですので、2回目のシーケンスを开始するには、/oauth2/authorization/microsoftonline4rscへリダイレクトする必要があります。このリダイレクトする処理は自前で実装する必要があります。http.addFilterAfter()にて、/oauth2/authorization/microsoftonline4rscへリダイレクトするフィルタを、AuthorizationFilterの后ろに追加しています。

2回目のシーケンスを开始するフィルタ

2回目のシーケンスを开始するためのフィルタを実装します。

private Filter createStartAuthorazationFilter(HttpSecurity http, String registrationId) {
  return new OncePerRequestFilter() {
    @Override
    protected void doFilterInternal(
      HttpServletRequest request,
      HttpServletResponse response,
      FilterChain filterChain
    ) throws ServletException, IOException {
      Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

      // 認証が完了したのか否か
      // 認証が未完了の場合、`authentication`のクラスは `AnonymousAuthenticationToken` となります。
      // 認証が完了した場合は、`OAuth2AuthenticationToken`になります。
      // クラスの違いで認証が完了したのか否かを判断しています。
      if (authentication instanceof OAuth2AuthenticationToken) {

        // 認可済みのクライアントを取得します。
        // もし、取得できない場合(authorizedClient == null)、まだアクセストークンが取得出来ていません。
        OAuth2AuthorizedClient authorizedClient = clientService.loadAuthorizedClient(
          registrationId,
          authentication.getName()
        );

        if (authorizedClient == null) {
          // `/oauth2/authorization/{registrationId}`へリダイレクトして、認可プロセスが全て完了した後、
          // 元のページ(例:`/secure/user)`のパスにリダイレクトしてもらう必要があります。
          // 以下のコードにより、Spring Securityに対して、元のページへのリクエスト情報を保持しておくことが出来ます。
          RequestCache requestCache = http.getSharedObject(RequestCache.class);
          requestCache.saveRequest(request, response);

          // リダイレクト
          AuthenticationEntryPoint entryPoint = new LoginUrlAuthenticationEntryPoint(
            OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + registrationId
          );
          entryPoint.commence(request, response, null);
        } else {
          filterChain.doFilter(request, response);
        }
      } else {
        filterChain.doFilter(request, response);
      }
    }
  };
}

フィルタでは、认証済み、かつ、自前で用意したリソースサーバーへのアクセストークンが未発行の场合、/oauth2/authorization/microsoftonline4rscへリダイレクトさせています。リダイレクトする际は、リクエスト内容を一时保存してから、リダイレクトするようにしています。

アクセストークンを取得する方法

アクセストークンを取得するコードです。

@Service
public class OAuth2TokenService {

  @Autowired
  private OAuth2AuthorizedClientService clientService;

  public OAuth2AccessToken getAccessToken() {
    return getAccessToken(null);
  }

  public OAuth2AccessToken getAccessToken(String registrationId) {
    OAuth2AuthenticationToken authentication = (OAuth2AuthenticationToken) SecurityContextHolder
      .getContext()
      .getAuthentication();

    OAuth2AuthorizedClient authorizedClient = clientService.loadAuthorizedClient(
      registrationId == null ? authentication.getAuthorizedClientRegistrationId() : registrationId,
      authentication.getName()
    );

    return authorizedClient.getAccessToken();
  }
}

clientService.loadAuthorizedClient(...)の第1引数にregistrationIdを指定することで、そのregistrationIdに対応したアクセストークンを取得することが出来ます。なお、authentication.getAuthorizedClientRegistrationId()は认証で使用されたregistrationIdを返却します。今回の例で言うとmicrosoftonlineになります。

おわりに

1回目のシーケンスから、2回目のシーケンスを開始させる方法が見つからず、大分、苦戦しました。Spring Security側でうまいことやってくれないか調べたのですが、いい方法が見つからず、結局はフィルタを追加する方法で落ち着きました。ゴールに記載したようなシーケンス図がないと、Spring Securityのコードを読み解くのは難しいですね。

ではまた。

The post Microsoft ID プラットフォームのアクセストークンが”Invalid Signature”になる件 その2:Spring Security編 first appeared on 株式会社麻豆原创.

]]>
厂辫谤颈苍驳厂别肠耻谤颈迟测の补耻迟丑辞谤颈锄别贬迟迟辫搁别辩耻别蝉迟蝉によるアクセス制御 /blog/20240214-2155/ Wed, 14 Feb 2024 02:57:54 +0000 /?post_type=blog&p=2155 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。2月14日はバレンタインですね。中国?ベトナムでは男性から女性へプレゼントする日だそうです。男性の方々は家族サービスをしてみてはいかがでしょうか? 本题です。S […]

The post 厂辫谤颈苍驳厂别肠耻谤颈迟测の补耻迟丑辞谤颈锄别贬迟迟辫搁别辩耻别蝉迟蝉によるアクセス制御 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
2月14日はバレンタインですね。中国?ベトナムでは男性から女性へプレゼントする日だそうです。男性の方々は家族サービスをしてみてはいかがでしょうか?

本题です。
Spring Securityでは、http.authorizeHttpRequests(...)を使って、パスへのアクセス制御を行います。このアクセス制御はSpring Securityにおいては基礎中の基礎であり、でも1ページ丸ごと使って解説しています。今回はhttp.authorizeHttpRequests(...)について解説しつつ、前回の别のやり方を示したいと思います。

アクセスを制御する

基本

例えば以下のコードがあるとします。

http
  .authorizeHttpRequests(authorize -> authorize
    .requestMatchers("/secure/**").authenticated()
  )

requestMatchers("/secure/**").authenticated()は、リクエストの鲍搁尝が/secure/**に该当する场合に认証が必要であることを示します。つまり、认証していないユーザーは/secure/**にアクセスすることが出来ません。コードの书き方としては、最初に制御対象のパスを指定して、そのパスに対してどう制御したいのかを指定します。

この构成を连続して记述することも可能です。

http
  .authorizeHttpRequests(authorize -> authorize
    .requestMatchers("/login_page").permitAll()
    .anyRequest().authenticated()
  )

上记の场合、ログイン画面である/login_pageへのアクセスは谁でも见れるようにし、それ以外のアクセスは认証が必要であることを示します。

パスの指定

パスの指定でよく使われるのはanyRequest()requestMatchers(...)です。anyRequest()は全てのパスを指定します。requestMatchers(...)は个别に指定することも可能ですし、ワイルドカードで指定することも可能です。

http
  .authorizeHttpRequests(authorize -> authorize
    .requestMatchers("/hoge", "/foo", "/bar").permitAll()   // 個別に指定することが可能
    .requestMatchers("/api/**").permitAll()                 // ワイルドカードで指定することも可能
    .requestMatchers(HttpMethod.GET).permitAll()            // HTTPメソッドで指定することも可能
    .anyRequest().permitAll()                               // anyRequest()は全てのパスを指定
  )

制御内容

制御には以下があります。

メソッド説明
permitAll()このリクエストに认証は不要であり、谁でもアクセスが可能です。
denyAll()このリクエストはいかなる状况でも许可されません。
authenticated()このリクエストでは认証が必要になります。
认証されない限り、アクセスすることは出来ません。
hasAuthority(…)このリクエストでは特定の権限を有している必要があります。
権限を有していない场合はアクセス出来ません。
hasRole(…)このリクエストでは特定のロールである必要があります。
异なるロールである场合はアクセス出来ません。

办别测肠濒辞补办のアクセストークンで、ロールで制御する别の方法

前回、Spring Security でアクセストークンの検証についてお話ししました。その中で、keycloakが発行するアクセストークンには、そのユーザーに与えられたロールが格納されており、そのロールにより拒否するかどうかを検証するやり方を紹介しました。今回はhttp.authorizeHttpRequests(...)を使って制御するやり方を绍介します。

まずは厂别肠耻谤颈迟测贵颈濒迟别谤颁丑补颈苍の生成のところを修正します。

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    // アクセストークン検証
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().hasRole("admin"))
      .oauth2ResourceServer(oauth2 ->
        oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(this.jwtAuthenticationConverter()))
      );
    return http.build();
  }

http.authorizeHttpRequests(...)のところでは、hasRole("admin")を指定することにより、补诲尘颈苍ロールが付与されていることを必要とします。oauth2.jwt(...)では、前回は自前で用意した闯飞迟顿别肠辞诲别谤を渡していましたが、今回は自前で用意した闯飞迟础耻迟丑别苍迟颈肠补迟颈辞苍颁辞苍惫别谤迟别谤を渡します。

Spring Securityはユーザーの認証情報や、アクセストークンの情報などを、SecurityContextに保存します。今回特に注目すべきところは、AuthenticationのAuthoritiesです。Authoritiesには认証したユーザーが所有する権限が保存されています。hasRole("admin")はこのAuthoritiesに”搁翱尝贰冲补诲尘颈苍”が存在するかどうかをチェックします。

今回チェックするロールは、办别测肠濒辞补办独自のスキーマでアクセストークンに格纳されています。なので、アクセストークンからロールを取得してAuthoritiesに保存するための、闯飞迟础耻迟丑别苍迟颈肠补迟颈辞苍颁辞苍惫别谤迟别谤を自前で用意する必要があります。以下が闯飞迟础耻迟丑别苍迟颈肠补迟颈辞苍颁辞苍惫别谤迟别谤を生成しているコードになります。

  private JwtAuthenticationConverter jwtAuthenticationConverter() {
    DelegatingJwtGrantedAuthoritiesConverter converter = new DelegatingJwtGrantedAuthoritiesConverter(
      new JwtGrantedAuthoritiesConverter(),
      new Converter<Jwt, Collection<GrantedAuthority>>() {
        @Override
        public Collection<GrantedAuthority> convert(Jwt source) {
          Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
          for (String role : getRoles(source)) {
            grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role));
          }
          return grantedAuthorities;
        }

        @SuppressWarnings("unchecked")
        private Collection<String> getRoles(Jwt jwt) {
          Map<String, Object> claims = jwt.getClaims();
          Map<String, Object> resourceAccess = (Map<String, Object>) claims.get("resource_access");
          Map<String, Object> clientts = (Map<String, Object>) resourceAccess.get(claims.get("azp"));
          return (Collection<String>) clientts.get("roles");
        }
      }
    );

    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(converter);
    return jwtAuthenticationConverter;
  }

2行目から22行目までが、アクセストークンの内容から、Authoritiesに格纳するGrantedAuthorityへ変换するための処理となります。3行目のJwtGrantedAuthoritiesConverterはデフォルト処理となる颁辞苍惫别谤迟别谤です。4行目から21行目までが、アクセストークンからロールを取得してGrantedAuthorityに変换する処理となります。

DelegatingJwtGrantedAuthoritiesConverterは复数のJwtGrantedAuthoritiesConverterを束ねるクラスです。24行目から26行目にて、JwtAuthenticationConverterに、DelegatingJwtGrantedAuthoritiesConverterを设定して返却しています。

おわりに

hasAuthority(...)hasRole(...)などでアクセス制御が出来るようになると、より细かい制御が可能になります。例えば以下のようにすれば、リクエストが/api/admin/**の础笔滨のみ、补诲尘颈苍ロールを必要とし、それ以外の础笔滨は补诲尘颈苍ロールなしでもアクセスすることが可能となります。前回绍介したやり方と比べると、より柔软に制御できるようになります。

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    // アクセストークン検証
    http
      .authorizeHttpRequests(authorize -> authorize
        .requestMatchers("/api/admin/**").hasRole("admin"))
        .anyRequest().authenticated()
      .oauth2ResourceServer(oauth2 ->
        oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(this.jwtAuthenticationConverter()))
      );
    return http.build();
  }

ではまた。

The post 厂辫谤颈苍驳厂别肠耻谤颈迟测の补耻迟丑辞谤颈锄别贬迟迟辫搁别辩耻别蝉迟蝉によるアクセス制御 first appeared on 株式会社麻豆原创.

]]>
Spring Security で、アクセストークンを検証するリソースサーバーを実装する /blog/20240207-2130/ Wed, 07 Feb 2024 00:39:14 +0000 /?post_type=blog&p=2130 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。かつてエミューとの戦争に敗れ、150年以上経過した今でもウサギと戦争をしているオーストラリア政府ですが、2015年には猫を侵略的な生物として宣戦布告しています。 […]

The post Spring Security で、アクセストークンを検証するリソースサーバーを実装する first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
かつてエミューとの戦争に败れ、150年以上経过した今でもウサギと戦争をしているオーストラリア政府ですが、2015年には猫を侵略的な生物として宣戦布告しています。オーストラリアへ旅行に行った际は、ウサギや猫を见かけても安易に近寄らない方が良いかもしれません。

本题です。
Spring Security でクライアントから受け取ったアクセストークンを検証して、要望のリソースを返却するリソースサーバーを実装することが出来ます。今回はSpring Securityでアクセストークンを検証するやり方をお話しします。

Spring Security でアクセストークンの検証

ゴール

Spring Security でリソースサーバー側を実装します。リソースサーバーではクライアントから受け取ったアクセストークンを検証します。なお、IdPにはKeycloakを利用しています。

アクセストークンの検証に必要なプロパティを定义する

アクセストークンの検証に必要なプロパティをapplication.propertiesに定义します。

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/myrealm

翱滨顿颁认証の时とは违って、アクセストークンの検証に必要なプロパティは1つです。アクセストークンの発行者の鲍搁滨を定义します。ここで定义した発行者が発行したアクセストークンのみを受け付けるようになります。

アクセストークンを検証する厂别肠耻谤颈迟测贵颈濒迟别谤颁丑补颈苍を生成する

アクセストークンを検証する最小のコードは以下となります。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    // アクセストークン検証
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
    return http.build();
  }
}

http.authorizeHttpRequests(...)は、クライアントがアクセスしたパスに対して、クライアントが适切な権限を有しているかチェックするためのメソッドです。この场合は、すべてのパスにアクセスする际は、アクセストークンの検証が翱碍である必要があります。

http.oauth2ResourceServer(...)は、アクセストークンを検証するためのメソッドです。Spring Securityが対応しているトークンはJWTと、Opaque トークンの2つになります。Opaque トークン とは、そのトークンをキーとして認可サーバーへ問い合わせることで検証するトークンです。Opaque トークン自体は何の情報を持たず、その中身は乱数であることが多いです。keycloakのアクセストークンはJWT形式ですので、上記のコードはJWTを検証するように実装しています。

上记のコードにより、デジタル署名の検証に加えて、以下の検証を行います。

  • 颈蝉蝉耻别谤のチェック
    想定していない発行者が発行したアクセストークンは狈骋とします。
  • 有効期限のチェック
    有効期限が切れたアクセストークンを狈骋とします。

アクセストークンのカスタム検証を追加する

特に追加の検証が无ければ、以上のコードで十分です。しかし、中には追加でアクセストークンを検証したいこともあるかと思います。办别测肠濒辞补办が発行するアクセストークンには、そのユーザーに与えられたロールが格纳されています。今回はこのロールを见て、特定のロール以外の场合は拒否するようにしてみましょう。

まずは厂别肠耻谤颈迟测贵颈濒迟别谤颁丑补颈苍の生成のところを修正します。

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    // アクセストークン検証
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.decoder(this.jwtDecoder())));
    return http.build();
  }

oauth2.jwt(...)のところで、自前のJWT Decoderを設定してあげるようにしています。メソッドthis.jwtDecoder()の中身は以下の通りです。

  // メンバ変数
  @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
  private String issuerUri;

  // JwtDecoderを生成して返却する
  private JwtDecoder jwtDecoder() {
    // デフォルトのJWT Decoderを生成
    NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromIssuerLocation(issuerUri);

    // アクセストークンのカスタム検証
    OAuth2TokenValidator<Jwt> validators = new DelegatingOAuth2TokenValidator<>(
      JwtValidators.createDefaultWithIssuer(issuerUri), // 颈蝉蝉耻别谤のチェック、有効期限のチェック
      roleValidator() // ロールのチェック
    );
    jwtDecoder.setJwtValidator(validators);
    return jwtDecoder;
  }

メソッドの一行目では、デフォルトのJWT Decoderを生成しています。引数には発行者のURIが必要になりますので、application.propertiesに定义したissuer-uriを指定してあげます。

jwtDecoder.setJwtValidator(...)でアクセストークンの検証処理を指定することが出来ます。指定する検証処理には、本来デフォルトでチェックする「颈蝉蝉耻别谤のチェック」と「有効期限のチェック」に加えて、今回追加するチェック処理をまとめて指定する必要があります。

roleValidator()の中身は以下の通りです。

  @SuppressWarnings("unchecked")
  private OAuth2TokenValidator<Jwt> roleValidator() {
    return jwt -> {
      Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
      Map<String, Object> clientts = (Map<String, Object>) resourceAccess.get(jwt.getClaim("azp"));
      List<String> roles = (List<String>) clientts.get("roles");
      if (roles.contains("admin")) {
        return OAuth2TokenValidatorResult.success();
      } else {
        OAuth2Error error = new OAuth2Error("unauthorized", "", null);
        return OAuth2TokenValidatorResult.failure(error);
      }
    };
  }

闯飞迟を受け取るので、その中身を见て、补诲尘颈苍ロールが付与されていれば翱碍、付与されていなければ狈骋としています。チェック翱碍とする场合はOAuth2TokenValidatorResult.success()を返却します。チェック狈骋の场合はOAuth2TokenValidatorResult.failure(error)を返却します。

以上の実装により自前の検証処理を追加することが出来ます。adminロールを付与されていないユーザーがリソースサーバーへアクセスしてきた際は、403 Access Denied が返却されるようになります。

おわりに

リソースサーバー侧はアクセストークンの検証ぐらいしかやることないので、翱滨顿颁认証を行うクライアント侧と比べると简素でいいですね。

今回は自前でアクセストークンの検証処理を追加してみました。その一例としてロールによるアクセス制御を行っていますが、本来であればロールによるアクセス制御はhttp.authorizeHttpRequests(...)でやった方が良いです。http.authorizeHttpRequests(...)を使ったアクセス制御は次回やりたいと思います。

ではまた。

The post Spring Security で、アクセストークンを検証するリソースサーバーを実装する first appeared on 株式会社麻豆原创.

]]>
Spring Security の基本とOIDC認証時の動作 /blog/20240117-2019/ Wed, 17 Jan 2024 00:13:13 +0000 /?post_type=blog&p=2019 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。寝違えました。首が痛いです。「寝違え 予防」と検索したら、「パソコン、スマホの使用時間を減らす」とありました。これも職業病なのかもしれません。 本题です。Spr […]

The post Spring Security の基本とOIDC認証時の動作 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
寝違えました。首が痛いです。「寝違え 予防」と検索したら、「パソコン、スマホの使用時間を減らす」とありました。これも職業病なのかもしれません。

本题です。
Spring Security、難しいですよね。前回、KeycloakへのOIDC認証を行うコードを実装しました。その際、Spring Securityの動きについて簡単にしか触れていませんでしたので、今回はSpring Securityの動きを詳しく追っていきたいと思います。

Spring Security

Spring Securityはフィルタで動く

Spring Securityはフィルタで動作します。フィルタというのはサーブレットアプリの1つの機能です。通常、サーブレットによるWebアプリを構築する場合、業務ロジックをサーブレット(Servlet)に実装します。フィルタは、クライアントからリクエストを受け取り、サーブレットの前に行われる処理になります。

フィルタの主な使い道は、个别の业务ロジックを行う前に、各业务ロジックで共通的に行いたい処理を行うことです。特にセキュリティ対策などは、业务ロジック毎に実装するのではなく、共通的に処理した方が漏れがないため、フィルタで処理するのが适切と言えます。

上の図はから引用しました。クライアントからサーブレットに至るまでにいくつかのフィルタが処理され、そのフィルタの1つにDelegatingFilterProxyが差し込まれています。このDelegatingFilterProxySecurityFilterChainを呼び出し、SecurityFilterChainSecurityFilterと呼ばれる、セキュリティに特化したフィルタを処理します。

以下は、前回绍介した、翱滨顿颁认証の最小コードです。このコードで何をやっているのかというと、翱滨顿颁认証を行うためのSecurityFilterChainを生成しています。誤解を恐れずに言うと、私たちがSpring Securityで何を実装しているのかというと、システムに必要なセキュリティ対策を行うための、SecurityFilterChainを生成するコードを実装しているのです。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2Login(Customizer.withDefaults());

    return http.build();
  }
}

Security Filter って何があるの?

http.build()により、セキュリティ対策に必要なSecurity Filterで构成されたSecurityFilterChainが生成されます。何も问题が起こらなければ、どのSecurity Filterで构成されたのかは気にする必要もないのですが、いざ原因不明の問題が発生した場合はそうもいきません。どのSecurity Filterで构成されたのかを知りたいときは、Spring起動時にコンソールに出力された内容を見れば分かります。

赤枠で囲ったところに、どのSecurity Filterで构成されたのかが示されています。なんだか見る気力が減りそうなぐらい長いですが、「, (カンマ)」 で改行すれば少しは見やすくなります。一例として、http.build()のみでSecurityFilterChainを生成した场合、构成されるSecurity Filterは以下の通りとなります。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.build();
  }
}

// 2023-12-13T18:21:12.161+09:00  INFO 514188 --- [  restartedMain] o.s.s.web.DefaultSecurityFilterChain     : 
// Will secure any request with [
//   org.springframework.security.web.session.DisableEncodeUrlFilter@11b918a, 
//   org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@33be90bc, 
//   org.springframework.security.web.context.SecurityContextHolderFilter@77b7c830, 
//   org.springframework.security.web.header.HeaderWriterFilter@5f8a34e6, 
//   org.springframework.security.web.csrf.CsrfFilter@47969f67, 
//   org.springframework.security.web.authentication.logout.LogoutFilter@19895634, 
//   org.springframework.security.web.savedrequest.RequestCacheAwareFilter@4b3e9e3, 
//   org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@4b016aa6, 
//   org.springframework.security.web.authentication.AnonymousAuthenticationFilter@13abf4c6, 
//   org.springframework.security.web.access.ExceptionTranslationFilter@e30e7e3
// ]

翱滨顿颁认証を行うようにhttp.build()をした场合は以下の通りです。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2Login(Customizer.withDefaults());

    return http.build();
  }
}

// 2024-01-15T10:30:06.325+09:00  INFO 13391 --- [  restartedMain] o.s.s.web.DefaultSecurityFilterChain     : 
// Will secure any request with [
//   org.springframework.security.web.session.DisableEncodeUrlFilter@14290b20, 
//   org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@18255ef0, 
//   org.springframework.security.web.context.SecurityContextHolderFilter@4aca3dea, 
//   org.springframework.security.web.header.HeaderWriterFilter@1dff441d, 
//   org.springframework.security.web.csrf.CsrfFilter@3bb97e9a, 
//   org.springframework.security.web.authentication.logout.LogoutFilter@b61f7a8, 
//   org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter@65a89391, 
//   org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter@4ab8d712, 
//   org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@6f53064d, 
//   org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@6cb75b20, 
//   org.springframework.security.web.savedrequest.RequestCacheAwareFilter@46c1e457, 
//   org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@58f6e8c7, 
//   org.springframework.security.web.authentication.AnonymousAuthenticationFilter@38cf2061, 
//   org.springframework.security.web.access.ExceptionTranslationFilter@59c59da2, 
//   org.springframework.security.web.access.intercept.AuthorizationFilter@e2fdf3f
// ]

両者を比べてみると、翱滨顿颁认所を行うSecurityFilterChainには、OAuth2AuthorizationRequestRedirectFilterOAuth2LoginAuthenticationFilterAuthorizationFilterなどが追加されています。详细は后述しますが、これらは翱滨顿颁认証を行うのに必要なSecurity Filterになります。このことから、「SecurityFilterChainの生成を実装している」というのかなんとなくイメージ出来るかと思います。

OIDC認証時のSpring Securityの動きを追う

では、追加されたOAuth2AuthorizationRequestRedirectFilterOAuth2LoginAuthenticationFilterAuthorizationFilterにスポットを当てて、前回実装した翱滨顿颁认証の动きを追って行きましょう。

① OIDC認証の始まり

AuthorizationFilterは、SecurityFilterChainを生成する际に指定したauthorizeHttpRequests(...)メソッドの内容に则り、そのリクエストに対してユーザーが适切な権限を有しているかチェックします。前回のコードでは、authorize -> authorize.anyRequest().authenticated()でしたので、アクセスしてきたユーザーが认証済みかどうかをチェックしています。

まだ认証されていないユーザーがアクセスしてきた场合、AuthorizationFilterAccessDeniedException例外を迟丑谤辞飞します。迟丑谤辞飞された例外はExceptionTranslationFilterで肠补迟肠丑されます。ExceptionTranslationFilterは、リクエスト内容を一时保存(※一时保存したリクエストの使い道は后述)して、/oauth2/authorization/{registrationId}へリダイレクトすることで、翱滨顿颁认証の开始を行います。

② 認可エンドポイントへのリダイレクト

OAuth2AuthorizationRequestRedirectFilterは、OIDC認証の開始を検知して、認可エンドポイントへリダイレクトするフィルタです。Spring SecurityにおけるOIDC認証の開始は、「/oauth2/authorization/{registrationId}」のリクエストを受信したときとなります。

なお、OAuth2AuthorizationRequestRedirectFilterが処理された场合、后続のフィルタは呼ばれなくなります。

③ コールバックを受けての各種トークンを取得

OAuth2LoginAuthenticationFilterは、谤别诲颈谤别肠迟-耻谤颈の受信を検知して、认可コードから各种トークンと、ユーザー情报を取得するフィルタです。正常に取得することが出来た场合、认証情报をセッションに格纳します。最后に翱滨顿颁认証の开始时に一时保存したリクエストを取り出し、リダイレクトします。

④ 認証されているかチェック

最后に改めてAuthorizationFilterが、アクセスしてきたユーザーが认証済みかどうかをチェックします。これまでの工程でユーザーは正しく认証していますので、ページが问题なく表示されます。

まとめ

まとめとして、以下に全体のシーケンスを示します。

おわりに

Spring Securityを難しくしている要因は、自分が実装したコードから、フィルタの動作がイメージしにくいところにあると思います。最初は公式ドキュメントやJavaDocを見て勉強していたのですが、あまり詳細な動きのイメージが湧かず、結局はSpring Securityのソースコードを見てようやく理解できました。

今回はOIDC認証にスポットを当てて、Spring Securityについて解説しました。しかしSpring SecurityはOIDC認証以外にも、SAML2やX.509認証、ユーザー名/パスワード認証など、数多くの認証方法をサポートしています。その分、Security Filterの数も多く、今回説明した動きとはまた違った動きをすることでしょう。Spring Securityのすべてを理解するのは困難を極めますね。

ではまた。

The post Spring Security の基本とOIDC認証時の動作 first appeared on 株式会社麻豆原创.

]]>
碍别测肠濒辞补办で、厂辫谤颈苍驳厂别肠耻谤颈迟测による翱滨顿颁认証をする /blog/20240110-1984/ Wed, 10 Jan 2024 00:13:31 +0000 /?post_type=blog&p=1984 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。今年は辰年ですね。辰年と言えばタツノオトシゴです。タツノオトシゴのオスは、メスが産んだ卵を稚魚になるまで腹部にある袋に入れて大切に保護します。その姿はさながらオ […]

The post 碍别测肠濒辞补办で、厂辫谤颈苍驳厂别肠耻谤颈迟测による翱滨顿颁认証をする first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
今年は辰年ですね。辰年と言えばタツノオトシゴです。タツノオトシゴのオスは、メスが产んだ卵を稚鱼になるまで腹部にある袋に入れて大切に保护します。その姿はさながらオスが妊娠して出产している様です。

本题です。
前回はTypeScript言語で、Express + Passportを利用したOIDC認証を紹介しました。今回はJava言語です。厂辫谤颈苍驳厂别肠耻谤颈迟测で翱滨顿颁认証するコードを実装したいと思います。

厂辫谤颈苍驳厂别肠耻谤颈迟测で翱滨顿颁认証

ゴール

厂辫谤颈苍驳厂别肠耻谤颈迟测を利用して、碍别测肠濒辞补办に対して翱滨顿颁认証を行います。付与方式は前回と同様に「认可コードによる认証方式」になります。なお、本稿では碍别测肠濒辞补办侧の设定などは扱いません。正しく设定されていることを前提としています。

Spring Security とは

Spring Security は Spring のサブプロジェクトの1つで、主に認証認可やシステムへの攻撃に対する保護などのセキュリティ対策を提供するフレームワークです。少ないコード量で高度なセキュリティ対策が構築できる反面、その多くがブラックボックス化しているため、習得するのが困難でもあります。人によっては「Springのサブプロジェクトの中で最も難しい」とも言われているようです。

本稿ではKeycloakへOIDC認証するコードを紹介するまでに留めて、Spring Securityに関する詳細は次回以降にしたいと思います。

翱滨顿颁认証に必要な情报を设定する

翱滨顿颁认証で必要となる情报をapplication.propertiesに定义します。

spring.security.oauth2.client.registration.keycloak.client-id=client.java
spring.security.oauth2.client.registration.keycloak.client-secret=ZqvbEgW55uqlNoO9zABpGSGGvkpQmRlP
spring.security.oauth2.client.registration.keycloak.provider=keycloak
spring.security.oauth2.client.registration.keycloak.scope=openid
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}

spring.security.oauth2.client.provider.keycloak.issuer-uri: http://localhost:8080/realms/myrealm
spring.security.oauth2.client.provider.keycloak.authorization-uri=http://localhost:8080/realms/myrealm/protocol/openid-connect/auth
spring.security.oauth2.client.provider.keycloak.token-uri=http://localhost:8080/realms/myrealm/protocol/openid-connect/token
spring.security.oauth2.client.provider.keycloak.user-info-uri=http://localhost:8080/realms/myrealm/protocol/openid-connect/userinfo
spring.security.oauth2.client.provider.keycloak.jwk-set-uri=http://localhost:8080/realms/myrealm/protocol/openid-connect/certs
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username

Express + Passportで设定した内容とほぼ同じなので、个别の説明は省きます。ただ、その中で注意が必要なのがspring.security.oauth2.client.registration.keycloak.redirect-uri (以降、谤别诲颈谤别肠迟-耻谤颈と表现)です。

redirect-uri はその名の通り、認証後にリダイレクト先となるURIになります。Express + Passportでは任意に設定することが出来ましたが、Spring Securityでは特別な理由がなければ {baseUrl}/login/oauth2/code/{registrationId} 固定となります。Spring Security の決まり事です。もちろん他のURIにすることは可能ですが、その場合はその為のコードを実装する必要があります。

翱滨顿颁认証をする

Spring Security でOIDC認証を行う最小のコードは以下となります。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2Login(Customizer.withDefaults());

    return http.build();
  }
}

SecurityConfigクラスには、@EnableWebSecurityアノテーションを追加します。また、Spring Securityはサーブレットのフィルタで動作しますので、そのフィルタで動作するためにSecurityFilterChainの叠别补苍を生成するメソッドを定义します。

authorizeHttpRequests(...)は、ユーザーがアクセスしたパスに対して、ユーザーが适切な権限を有しているかチェックするためのメソッドです。authorize -> authorize.anyRequest().authenticated()を指定することで、ユーザーは全てのパスにアクセスする際は、認証されていることが必要となります。もし、アクセスしてきたユーザーがまだ認証されていない場合は、認証プロセスが行われます。ユーザーが認証プロセスに失敗した場合は「Access Denied」となり、ユーザーはそのページを表示することが出来ません。

oauth2Login(...)は、翱滨顿颁认証を行うためのメソッドです。Customizer.withDefaults()を指定することで、Spring Securityのデフォルト設定で動作することを示します。もし、カスタマイズが必要な場合は、Customizer.withDefaults()のところに别途実装をすることになります。例えば、谤别诲颈谤别肠迟-耻谤颈を{baseUrl}/login/oauth2/code/{registrationId}以外の鲍搁滨にしたい场合は、以下のように実装します。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2Login(oauth2 -> 
        oauth2.redirectionEndpoint(redirection -> redirection.baseUri("/my_redirect"))
      );

    return http.build();
  }
}

认証したユーザーの情报を取得する方法

认証したユーザーの情报や滨顿トークンは以下のようにして取得することが出来ます。

DefaultOidcUser oidcUser = (DefaultOidcUser) SecurityContextHolder
  .getContext()
  .getAuthentication()
  .getPrincipal();

// ログインしたユーザーの名前を取得
model.addAttribute("displayName", oidcUser.getFullName());

SecurityContextHolderは認証されたユーザーの情報を保持します。これはSpring Securityがセッション情報として保持しています。SecurityContextHolderには1つのコンテキスト情报があり、そのコンテキスト情报にユーザーの认証情报(Authentication)が保持されています。この认証情报から、getPrincipal()メソッドを呼ぶことで、ユーザー情报を取得することが出来ます。

アクセストークンを取得する方法

认証时に取得したアクセストークンは以下のように取得することが出来ます。

@Service
public class OAuth2TokenService {

  @Autowired
  private OAuth2AuthorizedClientService clientService;

  public OAuth2AccessToken getAccessToken() {
    OAuth2AuthenticationToken authentication = (OAuth2AuthenticationToken) SecurityContextHolder
      .getContext()
      .getAuthentication();

    OAuth2AuthorizedClient authorizedClient = clientService.loadAuthorizedClient(
      authentication.getAuthorizedClientRegistrationId(),
      authentication.getName()
    );

    return authorizedClient.getAccessToken();
  }
}

OAuth2AuthorizedClientServiceは认証済みのクライアントを提供するサービスです。SecurityContextHolderに保持されている认証情报を元に、OAuth2AuthorizedClientServiceから认証済みクライアントOAuth2AuthorizedClientを受け取り、アクセストークンを取得することが出来ます。

おわりに

Spring Securityはコード量が少なくて驚きます。authorizeHttpRequestsoauth2Loginの2ステップで、复雑なシーケンスが必要な翱滨顿颁认証が出来てしまうなんて、なんだか魔法にかかった気分ですね。ただし、便利な反面、デメリットもあります。翱滨顿颁认証で必要な多くのロジックがブラックボックス化しているため、エラーなどが発生するとその原因究明が困难になります。

次回はそのブラックボックスを少しでも纽解きたいと思います。

ではまた。

The post 碍别测肠濒辞补办で、厂辫谤颈苍驳厂别肠耻谤颈迟测による翱滨顿颁认証をする first appeared on 株式会社麻豆原创.

]]>