SpringBootで監査証跡の記録を自動化する

データを登録、更新する場合に、登録者、登録日時、更新者、更新日時の項目に認証情報を使って自動的にセットします。

少し長くなりますが、出来てしまえば、すべてのテーブルに対して適用できます。

ログインした認証情報を
![image.png](https://qiita-image-store.s3.amazonaws.com/0/246741/74325615-3a6b-bf96-20ca-6d88dc83b56c.png)


テーブルの登録、更新で監査証跡を自動でセットします
![image.png](https://qiita-image-store.s3.amazonaws.com/0/246741/467b3504-b64d-d644-b90b-9a0dd7428785.png)


- 構成

```
sample-websecurity2.0
│  build.gradle
└─src
    └─main
        ├─java
        │  └─com
        │      └─stone
        │          └─tm
        │              │  AccountController.java
        │              │  AccountForm.java
        │              │  AccountListForm.java
        │              │  Application.java
        │              │  AuthController.java
        │              │  SecurityAuditorAwareConfig.java
        │              │  ServletInitializer.java
        │              │  WebSecurityConfig.java
        │              │  
        │              ├─converter
        │              │      LocalDateConverter.java
        │              │      LocalDateTimeConverter.java
        │              │      
        │              └─entity
        │                      Account.java
        │                      AccountRepository.java
        │                      AccountService.java
        │                      BaseEntity.java
        │                      
        └─resources
            │  application.properties
            │  hibernate.properties
            │  logback.xml
            │  
            ├─static
            └─templates
                    account.html
                    login.html
                    top.html
```

- 依存関係

認証は、spring-securityを使います。

```build.gradle
dependencies {
	compile('org.springframework.boot:spring-boot-starter-data-jpa')
	compile('org.springframework.boot:spring-boot-starter-security')
	compile('org.springframework.boot:spring-boot-starter-thymeleaf')
	compile('org.springframework.boot:spring-boot-starter-web')
	compile('org.springframework.security:spring-security-web')
	compile('org.springframework.security:spring-security-config')
	compile('org.springframework.security:spring-security-taglibs')
	compile('org.thymeleaf.extras:thymeleaf-extras-springsecurity4')
	runtime('org.springframework.boot:spring-boot-devtools')
	runtime('org.postgresql:postgresql')
	compileOnly('org.projectlombok:lombok')
	providedRuntime('org.springframework.boot:spring-boot-starter-tomcat')
	testCompile('org.springframework.boot:spring-boot-starter-test')
	testCompile('org.springframework.security:spring-security-test')
}
```

- application.properties

データベースはpostgreSQLを使います。

```application.properties
spring.datasource.driverClassName=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/testdb
spring.datasource.username=testuser
spring.datasource.password=testuser
spring.jpa.hibernate.ddl-auto=none
spring.thymeleaf.mode=HTML
```

- hibernate.properties

postgreSQLでエラーにならないように、hibernate.propertiesを作成します

```hibernate.properties
hibernate.jdbc.lob.non_contextual_creation = true
```

- LocalDate,LocalDateTime

LocalDate,LocalDateTimeをデータベースのTimeStampに変換するコンバーター

```LocalDateConverter.java
import java.time.LocalDate;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public class LocalDateConverter implements AttributeConverter<LocalDate, java.sql.Date> {
    @Override
    public java.sql.Date convertToDatabaseColumn(LocalDate localDateTime) {
        return (localDateTime == null ? null : java.sql.Date.valueOf(localDateTime));
    }
    @Override
    public LocalDate convertToEntityAttribute(java.sql.Date  date) {
        return (date == null ? null : date.toLocalDate());
    }
}
```

```LocalDateTimeConverter.java
import java.time.LocalDateTime;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public class LocalDateTimeConverter
	implements AttributeConverter<LocalDateTime, java.sql.Timestamp> {

    @Override
    public java.sql.Timestamp convertToDatabaseColumn(LocalDateTime localDateTime) {
        return (localDateTime == null ? null : java.sql.Timestamp.valueOf(localDateTime));
    }

    @Override
    public LocalDateTime convertToEntityAttribute(java.sql.Timestamp  timestamp) {
        return (timestamp == null ? null : timestamp.toLocalDateTime());
    }
}
```

- 認証

>認証用のテーブルは、Accountとします。
「SpringBoot + Spring Securityで認証を行う」の投稿を参照してください。
https://qiita.com/tiguchi919/items/9d12ab0b260e286ba18c
　Account.java
　AccountRepository.java
　AccountService.java

>WebSecurityも同様です。この記事では割愛します。
　WebSecurityConfig.java

- AuditorAware
監査証跡で認証情報を利用するためにAuditorAwareを設定します。

```SecurityAuditorAwareConfig.java
import java.util.Optional;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import com.stone.tm.entity.Account;

@EnableJpaAuditing
@Configuration
public class SecurityAuditorAwareConfig
{
    @Bean
    public AuditorAware<String> auditorAware()
    {
        return new SecurityAuditorAware();
    }

    public static class SecurityAuditorAware implements AuditorAware<String>
    {
        @Override
        public Optional<String> getCurrentAuditor()
        {
            Authentication authentication =
            		SecurityContextHolder.getContext().getAuthentication();
            if (authentication == null || !authentication.isAuthenticated())
            {
                return null;
            }
            Account account = (Account) authentication.getPrincipal();

            return Optional.ofNullable(account.getUsername());
        }
    }
}
```

- Controller

```AccountController.java
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import com.stone.tm.entity.Account;
import com.stone.tm.entity.AccountRepository;
import com.stone.tm.entity.Account.Authority;

@Controller
public class AccountController {
	@Autowired
	AccountRepository repo;
    @RequestMapping(name="/account", method = RequestMethod.GET)
    public ModelAndView account(Model model) {
    	ModelAndView mv=new ModelAndView();
    	mv.setViewName("account");
    	List<Account> accountlist=repo.findAll();
    	AccountListForm form=new AccountListForm();
    	form.setAccoutlist(accountlist);
    	mv.addObject("form", form);
    	return mv;
    }
    @RequestMapping(name="/account", method = RequestMethod.POST)
    public ModelAndView account(@ModelAttribute("form") AccountForm form,Model model) {
    	if (form.getUsername()!=null && !form.getUsername().isEmpty()) {
    		Account acc = repo.findById(form.getUsername()).orElse(new Account());
    		acc.setUsername(form.getUsername());
    		acc.setEmail(form.getEmail());
    		acc.setAuthority(Authority.ROLE_USER);
    		repo.save(acc);
    	}
    	return account(model);
    }
}
```

- form

```AccountForm
import lombok.Data;
@Data
public class AccountForm {
	private String username;
	private String email;
}
```

```
import java.util.List;
import com.stone.tm.entity.Account;
import lombok.Data;

@Data
public class AccountListForm {
	private List<Account> accoutlist;
}
```

10.Entity

監査証跡は親クラスで定義します。

```BaseEntity.java
import java.time.LocalDateTime;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import lombok.Data;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Data
public class BaseEntity
{
    @CreatedBy
    private String createon;

    @CreatedDate
    private LocalDateTime createat;

    @LastModifiedBy
    private String modifiedon;

    @LastModifiedDate
    private LocalDateTime modifiedat;
}
```

@MappedSuperclass
Entityの親クラスとしたいときに、@MappedSuperclassをつけます。
今回は、監査証跡を記録したい全てのテーブルで使えるように、親クラスに定義しています。

@EntityListeners(AuditingEntityListener.class)
コールバックリスナはAuditingEntityListenerとします

@CreatedBy,@CreatedDate,@LastModifiedBy,@LastModifiedDate
これらのアノテーションをつけておけばOK


- HTML

```login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<title>Login page</title>
<style>
.alert-danger{color:red;}
</style>
</head>
<body>
<h2>ログイン画面</h2>
<form th:action="@{/login}" method="post">
	<div th:if="${session['SPRING_SECURITY_LAST_EXCEPTION']} != null"
			class="alert-danger">
		<span th:text="ユーザ名またはパスワードに誤りがあります"></span>
	</div>
	<div style="width:160px;"><label for="username">ユーザ名：</label></div>
	<input type="text" name="username" autofocus="autofocus" />
	<br/>
	<div style="width:160px;"><label for="password">パスワード：</label></div>
	<input type="password" name="password" />
	<br/>
	<p><input type="submit" value="ログイン" /></p>
</form>
</body>
</html>
```

```account.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta charset="UTF-8" />
<title>メニュー画面</title>
<style>
table, td, th {
	border: 1px gray solid;border-collapse: collapse; padding:5px;
}
span{margin-right:10px;}
</style>
</head>
<body>
	<div th:fragment="logout" sec:authorize="isAuthenticated()">
		<div style="background:pink;">
			<label>user:</label>
			<span sec:authentication="name"></span>
			<label class="ml-2">mail:</label>
			<span sec:authentication="principal.email"></span>
			<label class="ml-2">role:</label>
			<span sec:authentication="principal.authorities"></span>
		</div>
		<form th:action="@{/logout}" method="post" id="logout">
			<button type="submit" class="ml-2" id="btn-logout" value="">logout</button>
		</form>
	</div>
	<form th:action="@{/account}" th:method="post">
		<div>
			<label>アカウント：</label><input type="text" name="username"/>
		</div>
		<div>
			<label>Ｅメール　：</label><input type="text" name="email"/>
		</div>
		<div>
			<button type="submit">更新</button>
		</div>
	</form>
	<table th:object="${form}">
		<thead>
			<tr>
				<th>ユーザ名</th>
				<th>メールアドレス</th>
				<th>ロール</th>
				<th>登録日時</th>
				<th>登録ユーザ</th>
				<th>更新日時</th>
				<th>更新ユーザ</th>
			</tr>
		</thead>
		<tbody>
			<tr th:each="row,rowStat : *{accoutlist}">
				<td th:text="*{accoutlist[__${rowStat.index}__].username}">
				</td>
				<td th:text="*{accoutlist[__${rowStat.index}__].email}">
				</td>
				<td th:text="*{accoutlist[__${rowStat.index}__].authority}">
				</td>
				<td th:text="*{accoutlist[__${rowStat.index}__].createat}">
				</td>
				<td th:text="*{accoutlist[__${rowStat.index}__].createon}">
				</td>
				<td th:text="*{accoutlist[__${rowStat.index}__].modifiedat}">
				</td>
				<td th:text="*{accoutlist[__${rowStat.index}__].modifiedon}">
				</td>
			</tr>
		</tbody>
	</table>
</body>
</html>
```