下記のURLにアクセスして、(レイアウトはイマイチですが...)データを挿入する方法について書いていきます。
http://localhost:8080/StrutsApp/userRegister.action
技術とバージョン
| 技術 | バージョン |
|---|---|
| Tomcat | 9系 |
| Java | 21 |
| Spring Framework | 5.3.x系 |
| Servlet API(javax) | 5.8.x系 |
| spring-security-core | 5.8.x系 |
使用したMavenライブラリ
Spring Security Core
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-core -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.8.13</version>
</dependency>
Spring Transaction
👇下記をコピペしてください。👇
<!-- https://mvnrepository.com/artifact/org.springframework/spring-tx -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.39</version>
</dependency>
少し整理しながら、「confirmUserInformation.jsp → JavaScript(saveFiles)→ Struts2 Action → MyBatis → 2テーブルINSERT」の王道構成を説明します。
(※ いまのJSPは form submit なので、JavaScriptでPOSTする版 と Struts2フォーム版 の両方を示します)
① confirmUserInformation.jsp(JavaScriptでPOSTする場合)
JavaScript(FormData を使う)
<script>
function saveFiles() {
const formData = new FormData();
formData.append("userName", "${userName}");
formData.append("email", "${email}");
formData.append("password", "${password}");
formData.append("gender", "${gender eq '男性' ? 'M' : 'F'}");
formData.append("office", "${office}");
<s:iterator value="uploadedFileNames">
formData.append("uploadedFileNames", "<s:property/>");
</s:iterator>
fetch("<s:url action='userRegister_saveFiles'/>", {
method: "POST",
body: formData
}).then(res => {
if (res.ok) {
location.href = "userRegisterComplete.jsp";
}
});
}
</script>
<button onclick="saveFiles()">登録完了</button>
※ 画像自体はすでにサーバに一時保存されている前提(session管理)
② Struts2 Action
UserRegisterAction.java
package com.example.StrutsApp.action;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import org.apache.commons.io.FileUtils;//追加
import org.apache.struts2.ServletActionContext;//追加
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.transaction.annotation.Transactional;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;
import mapper.AdminUserMapper;
import mapper.AdminUserPhotoMapper;
import model.AdminUser;
import model.AdminUserPhoto;
public class UserRegisterAction extends ActionSupport {
private String userName;
private String email;
private String password;
private String gender;
private String office;
private Boolean adminrole;
// 可変ファイルアップロード用
private File[] photos;
private String[] photosFileName;
private String[] photosContentType;
@Autowired
private AdminUserMapper adminUserMapper;
@Autowired
private AdminUserPhotoMapper adminUserPhotoMapper;
// 永続保存先(サーバ上のフォルダ)
//private static final String UPLOAD_DIR = "/WEB-INF/upload/path/";
// getter / setter
public String getUserName() {return userName;}
public void setUserName(String userName) {
this.userName = userName;
}
public String getEmail() {return email;}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {return password;}
public void setPassword(String password) {
this.password = password;
}
public String getGender() {
if("M".equals(gender)) return "男性";
if("F".equals(gender)) return "女性";
return "";
}
public void setGender(String gender) {
this.gender = gender;
}
public String getOffice() {
return office;
}
public void setOffice(String office) {
this.office = office;
}
public File[] getPhotos() {
return photos;
}
public void setPhotos(File[] photos) {
this.photos = photos;
}
public String[] getPhotosFileName() {
return photosFileName;
}
public void setPhotosFileName(String[] photosFileName) {
this.photosFileName = photosFileName;
}
public String[] getPhotosContentType() {
return photosContentType;
}
public void setPhotosContentType(String[] photosContentType) {
this.photosContentType = photosContentType;
}
private List<String> uploadedFileNames;
public List<String> getUploadedFileNames(){return uploadedFileNames;}
@Override
public String execute() throws IOException{
uploadedFileNames = new ArrayList<>();
if(photos !=null && photosFileName !=null) {
File tmpDir = new File(System.getProperty("java.io.tmpdir"),"Struts2AppTmpFolder");
tmpDir.mkdir();
for(int i=0; i<photos.length; i++) {
File tmpFile = new File(tmpDir,photosFileName[i]);
FileUtils.copyFile(photos[i], tmpFile);
uploadedFileNames.add(tmpFile.getAbsolutePath());
}
}
// セッションに保存
Map<String, Object> session = ActionContext.getContext().getSession();
session.put("uploadedFileNames", uploadedFileNames);
/*
// 確認画面用にファイル名コピー
if(photosFileName != null) {
uploadedFileNames = new ArrayList<>();
for(String fileName : photosFileName) {
uploadedFileNames.add(fileName);
}
}
*/
return "confirm";
}
/*
private String getUploadDir() {
return ServletActionContext.getServletContext().getRealPath("/uploads");
}
*/
// 永続保存用メソッド(確認画面から「登録完了」ボタンを押した時)
@Transactional
public String saveFiles() throws IOException{
// ===== 1. adminusers INSERT =====
String adminUserId = UUID.randomUUID().toString();
// パスワードのハッシュ化処理
String hasedpassword = createHashedPassword(password);
AdminUser user = new AdminUser();
user.setId(adminUserId);
user.setName(userName);
user.setEmail(email);
user.setPassword(hasedpassword);
user.setGender(gender);
user.setOffice(office);
user.setAdminrole(adminrole);
//adminUserMapper.insert(user);
// ===== 2. ファイルコピー & adminuser_photos INSERT =====
File uploadDir = new File(getUploadDirFromProperties());
if(!uploadDir.exists()) {
uploadDir.mkdir();
}
int sortOrder = 1;
for(String tmpPath : uploadedFileNames) {
File tmpFile = new File(tmpPath);
File dest = new File(uploadDir, tmpFile.getName());
FileUtils.copyFile(tmpFile, dest);
AdminUserPhoto photo = new AdminUserPhoto();
photo.setAdminuserId(adminUserId);
photo.setFilePath(dest.getAbsolutePath());
photo.setSortOrder(sortOrder++);
//adminUserPhotoMapper.insert(photo);
}
/*
//==================
// セッションから取得
Map<String, Object> session = ActionContext.getContext().getSession();
uploadedFileNames = (List<String>) session.get("uploadedFileNames");
if(uploadedFileNames == null) {
addActionError("アップロードファイルが存在しません。");
return ERROR;
}
//⇩本番用
//File uploadDir = new File(ServletActionContext.getServletContext().getRealPath("/uploads"));
File uploadDir = new File(getUploadDirFromProperties());
uploadDir.mkdir();
for(String tmpPath : uploadedFileNames) {
File tmpFile = new File(tmpPath);
File dest = new File(uploadDir, tmpFile.getName());
FileUtils.copyFile(tmpFile, dest);
}
*/
// セッションから削除
//session.remove("uploadedFileNames");
return SUCCESS;
}
// 保存先パスをプロパティから読み込むメソッド
private String getUploadDirFromProperties()throws IOException{
Properties properties = new Properties();
// クラスパスからsrc/main/resources 配下のプロパティファイルを読み込む
try(InputStream inputStream = getClass().getClassLoader().getResourceAsStream("filesForDevelopment.properties")){
if(inputStream == null) {
return ServletActionContext.getServletContext().getRealPath("/uploads");
}
properties.load(inputStream);
}
/*
// プロジェクトのルートに properties がある場合
FileInputStream fileInputStream = new FileInputStream("filesForDevelopment.properties");
properties.load(fileInputStream);
fileInputStream.close();
*/
String uploadDir = properties.getProperty("uploadedUrl");
if(uploadDir == null || uploadDir.isEmpty()) {
// プロパティに値がなければデフォルト
uploadDir = ServletActionContext.getServletContext().getRealPath("/uploads");
}
return uploadDir;
}
// パスワードハッシュ化処理
private String createHashedPassword(String password) {
return BCrypt.hashpw(password, BCrypt.gensalt());
}
}
③ MyBatis Mapper(例)
AdminUsersMapper.xml
<insert id="insert" parameterType="AdminUsers">
INSERT INTO adminusers
(id, name, email, password, gender, office, admin_role)
VALUES
(#{id}, #{name}, #{email}, #{password}, #{gender}, #{office}, #{adminRole})
</insert>
<insert id="insert" parameterType="AdminUserPhotos">
INSERT INTO adminuser_photos
(adminuser_id, file_path, sort_order)
VALUES
(#{adminuserId}, #{filePath}, #{sortOrder})
</insert>
全体のpom.xmlは下記です。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>StrutsApp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>StrutsApp</name>
<description>Struts 2 Starter</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<struts2.version>6.1.1</struts2.version>
<log4j2.version>2.19.0</log4j2.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>${struts2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-sitemesh-plugin</artifactId>
<version>${struts2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-spring-plugin</artifactId>
<version>${struts2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-config-browser-plugin</artifactId>
<version>${struts2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId><!--javax-->
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId><!--javax-->
<artifactId>jsp-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>org.directwebremoting</groupId>
<artifactId>dwr</artifactId>
<version>3.0.2-RELEASE</version>
</dependency>
<!-- Tests -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.5.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.19</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis.generator/mybatis-generator-core -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.2</version>
</dependency>
<!--<dependency>
<groupId>com.example</groupId>
<artifactId>Backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>-->
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-core -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.8.13</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-tx -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.39</version>
</dependency>
</dependencies>
<build>
<!--<finalName>struts2-archetype-starter</finalName>-->
<finalName>StrutsApp</finalName>
<plugins>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.4.46.v20220331</version>
<configuration>
<stopKey>CTRL+C</stopKey>
<stopPort>8999</stopPort>
<scanIntervalSeconds>10</scanIntervalSeconds>
<webAppSourceDirectory>${basedir}/src/main/webapp/</webAppSourceDirectory>
<webAppConfig>
<contextPath>/StrutsApp</contextPath>
<descriptor>${basedir}/src/main/webapp/WEB-INF/web.xml</descriptor>
</webAppConfig>
</configuration>
</plugin>
<!--MyBatis追加-->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.1</version>
<configuration>
<configurationFile>${project.basedir}/src/main/resources/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<includeAllDependencies>true</includeAllDependencies>
</configuration>
</plugin>
</plugins>
</build>
</project>
④ 重要ポイント(よくハマる所)
✅ 1. 画像は「2回送らない」
アップロード時:一時ディレクトリ
確認画面:ファイルパスのみ保持
登録完了時:本番へコピー+DB登録
✅ 2. トランザクション管理
Spring + MyBatisなら @Transactional を推奨
@Transactional
public String saveFiles() { ... }
→ adminusers 失敗時に photos もロールバックされる
✅ 3. パスワードは必ずハッシュ化(超重要)
String hashed = BCrypt.hashpw(password, BCrypt.gensalt());
user.setPassword(hashed);
まとめ(結論)
✔ confirm → POST
✔ Actionで adminusers → adminuser_photos の順でINSERT
✔ UUIDで adminuser_id を共有
✔ 画像は「コピー+DB登録」
サイト
Javaでパスワードを安全にハッシュ化する方法〜Apache Commons Codecを使ってみよう〜
【Java】多要素認証実装方法の紹介①
チップ集
BCrypt.hashpw()
JavaでBCrypt.hashpw()を使うには、org.springframework.security.crypto.bcrypt.BCryptクラスを使用し、BCrypt.hashpw(plainPassword, BCrypt.gensalt())のように平文パスワードとランダムなソルトをgensalt()で生成して渡します。これにより、パスワードは「$2a(10)...」のような形式でハッシュ化され、安全に保存・比較できるようになります。
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.x.x</version> <!-- 最新バージョンに合わせる -->
</dependency>
import org.springframework.security.crypto.bcrypt.BCrypt;
public class PasswordHasher {
public static void main(String[] args) {
String password = "mySecretPassword123";
// 1. ソルトを生成 (デフォルトのコストファクター10)
String salt = BCrypt.gensalt();
System.out.println("Salt: " + salt); // 例: $2a$10$nU.P7j.hC5wB5aP.cWp3/u
// 2. パスワードをハッシュ化
String hashed = BCrypt.hashpw(password, salt);
System.out.println("Hashed Password: " + hashed); // 例: $2a$10$nU.P7j.hC5wB5aP.cWp3/u.sVq8m0sZ9tK5n3e4cE5j7sY8dJ7
// 3. ハッシュ化されたパスワードと平文パスワードの照合
boolean matches = BCrypt.checkpw(password, hashed);
System.out.println("Password matches: " + matches); // true
// 別のパスワードで試す
boolean notMatches = BCrypt.checkpw("wrongPassword", hashed);
System.out.println("Wrong password matches: " + notMatches); // false
}
}
maven
トラブルシューティング
spring-security-coreバージョン7.0.2を入れてアプリ起動すると404エラーになった。
(原因)
Tomcat9とspring-security-coreバージョン違いによるエラーです。
Tomcat 9 を使い続けるなら、spring-security-core は 5.8.x に下げる必要があります。
❌ spring-security-core 7.x
→Spring Framework 6 / Java 17 / jakarta.servlet前提
✅ spring-security-core 5.8.x
→ Spring Framework 5 / Java 8〜17 / javax.servlet 対応
今の構成(Tomcat 9 + javax.servlet)では
5.8.x が事実上の最終ラインです。
(解決方法)
pom.xml の修正例
① spring-security-core を5.8.xに変更
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.8.13</version>
</dependency>
※ 5.8.x の中で最新安定系(2024年以降もセキュリティ修正あり)
② Spring Framework の衝突を防ぐ(重要)
Struts2 6.1.1 は内部で Spring を使います。
Security が勝手に別バージョンの Spring を引き込まないよう固定します。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>5.3.39</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
パスワードのハッシュ化用途だけなら(Web セキュリティ機能は不要な場合)、主に使うのは下記のライブラリです。
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
「Transactional を型に解決できません」というエラー
@Transactionalを使用すると下記のエラーが出た。
Transactional を型に解決できません
これは ライブラリ不足ではなく「import 先が無い」ことが原因です。
つまり@Transactionalのクラスがクラスパス上に存在していません。
(解決方法)
今の pom.xml には Spring Transaction モジュールが無いです。
@Transactional はspring-tx に含まれています。
なので、pom.xmlに下記を追加しましょう。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.39</version>
</dependency>
その後、下記のインポートしましょう。
import org.springframework.transaction.annotation.Transactional;




