kotlin springboot exposedで構築する
MybatisはKotlinからxmlを触ろうとするとエラーがでる。その対処法がわからずとりあえずexposedでやろうという方針になった。
おすすめの参考書
↓これはうまくまとまっていて、ものすごく参考になる。
springboot 3.0系情報で詰まった際にGithub上のコードなどを参照していたのだが、結局この本に書かれていた内容だったみたいなことがたくさんあったので、数回読み直したほうが良さそう。
より詳細
UserDetailsがよくわからないときに細かく書いてあったので迷走した際に一度読み直すと書いてあると思われる。
intellij Unlimitedを利用している場合
Developper Tools
- Spring Boot DevTools
Web
- Spring Web
Template
- Thymleaf
Security
- Spring Security
Database
- Mysql
を利用する
application.properties
spring.datasource.password=docker
spring.datasource.username=docker
spring.datasource.url=jdbc:mysql://localhost:3306/demo
exposed
implementation 'org.jetbrains.exposed:exposed-spring-boot-starter:0.41.1'
だけを追加すればいい。
exposedでSchamaUtils.createする。
これは@Beanで作成している部分は、SchemaUtils.create()をすると、Database.connect()が未完了なので、というエラーメッセージが出力されるため。
@SpringBootApplication
class Demo4Application(private val dataSource: DataSource) {
@Bean
fun database(): Database {
val db = Database.connect(dataSource)
TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_READ_COMMITTED
return db
}
}
fun main(args: Array<String>) {
runApplication<Demo4Application>(*args)
transaction {
SchemaUtils.create(Cities)
}
}
パッケージは、
controller
repository
datasource←interface
model
があれば良い
参考
thymleafの使い方
th宣言
<html lang="en" xmlns:th="http://www.thymeleaf.org">
text
<td th:text="${cities}"></td>
テキストの結合 "'a'+${}"
<span th:text="'The name of the user is ' + ${user.name}">
each
<table>
<tr>
<td>id</th>
<td>name</th>
</tr>
<tr th:each="city: ${cities}">
<td th:text="${city.id}"></td>
<td th:text="${city.name}"></td>
</tr>
</table>
object
<p>th:object="${city}"</p>
<p>th:text="*{id}"</p>
<p>th:text="*{name}"</p>
if
kotlin側
model.addAttribute("page", page)
html側
<table th:if="${page.size > 0}" class="table">
with
<div th:with="x=1,y=2">
<p th:text="${x}"></p>
<p th:text="${y}"></p>
</div>
link href <a>
<p><a th:href="@{'/customers/edit/' + ${param.id[0]}}">link</a></p>
参照 フラグメントの解説
フラグメント
htmlの要素を部品化して、共通する部品を作成することができる。
th:fragment
を宣言することで、部品を作る (今回はcopyと宣言した)
<div th:fragment="copy" th:remove="tag">
</div>
th:insert
th:replace
で利用することができる ( th:include
は3.0で非推奨 )
insert
はタグの中にフラグメント化されたタグを生成する
replace
はタグをフラグメント化されたタグに置き換える
<body>
<!-- th:insertで読み込む -->
<div th:insert="footer :: copy"></div>
<!-- th:replaceで読み込む -->
<div th:replace="footer :: copy"></div>
<!-- th:includeで読み込む -->
<div th:include="footer :: copy"></div>
</body>
(参考 filedの挙動)[https://qiita.com/usedbookhappy/items/683fd17ec1eb3f9aa0e5]
基本的にth:filed="*{}"を利用すればいい
filed formで利用できる objectが必要。
id, name, value の役割を果たす
formの場合
<form th:action="@{/}" method="post" th:object="${}">
<input type="text" th:field="*{}"><br>
<button></button>
</form>
ラジオボタンのときは th:filed, th:value, th:textが必要
th:textがラジオボタンの横に文字を出力する。
<input type="radio" th:field="*{gender}" th:value="${gender.key}" th:text="${gender.value}"><br>
シンプルなログインフォーム
<form th:action="@{/login}" method="post">
<input type="text" name="username" placeholder="ユーザ名"><br>
<input type="text" name="password" placeholder="パスワード"><br>
<button>foo</button>
</form>
spirng boot アノテーション
@Controller
@Service
@Repository
@GetMapping("/edit/{id}")
fun editTeacher(
@PathVariable id: Long,
model: Model
): String {
model.addAttribute("teacher", service.selectByPrimaryKey(id))
return "form"
}
@ModelAttribute
勝手に指定したクラスをModelに渡してくれる。
fun addTeacher(@ModelAttribute teacher:Teacher, model: Model): String {
return "form"
}
@PostMapping("/register")
fun registerPost(
@RequestParam("username") username: String,
@RequestParam("password") password: String,
): String {
println("$username ${encoder.encode(password)}")
return "redirect:/"
}
@DeleteMapping("/{id}")
@PreAuthorize("!hasAuthority('USER')")
fun delete(@PathVariable id: Long) {
if (service.exists(id)) {
repository.deleteById(id)
} else {
throw EntityNotFoundException(User::class.java, "id", id.toString())
}
}
@PreAuthorize
などは参考になる。
@Configuration
のfilterChainを利用している場合はそちらを優先してそちらで制御したほうが良さそうではある。
exposedをの説明
基本的にSQL DSLを利用する
理由はSQLライクのほうがわかりやすい
公式サンプル
objectの定義は複数形が基本
テーブル
object Users : Table() {
val id: Column<String> = varchar("id", 10)
val name: Column<String> = varchar("name", length = 50)
val cityId: Column<Int?> = (integer("city_id") references Cities.id).nullable()
override val primaryKey = PrimaryKey(id, name = "PK_User_ID") // name is optional here
}
object Cities : Table() {
val id: Column<Int> = integer("id").autoIncrement()
val name: Column<String> = varchar("name", 50)
override val primaryKey = PrimaryKey(id, name = "PK_Cities_ID")
}
select
val name = Users.select { User.id eq "Narikake" }.single()[User.name]
insert
Users.insert {
it[id] = ""
it[name] = ""
it[Users.cityId] = null
}
update
Users.update({ Users.id eq "" }) {
it[name] = ""
}
アップデートでわけわからんことになっている。
@Configurationですべて記述することができる
@EnableWebSecurityはそもそも必要ない
@Serviceを継承したUserDetailServiceにつけるとDIが勝手に働いて、そもそもConfigurationに記述する必要がない
userDetailServiceは3.0以前はauth.serviceDetailsService()のメソッドの引数に追加していたみたい。
@Configuration
class SampleConfig {
@Bean
fun passWordEncoder(): BCryptPasswordEncoder {
return BCryptPasswordEncoder()
}
/*
複数のSecurityChainを定義することができるらしい
リスクエストパターンごとに定義することができる。 /api や /uiなどのリソースに対して。
*/
@Bean
@Throws(Exception::class)
fun filterChain(http: HttpSecurity?): DefaultSecurityFilterChain {
http!!
.authorizeHttpRequests { auth ->
/* register 以降をすべて許可する。 */
auth.requestMatchers("/register/**").permitAll()
auth.requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
auth.anyRequest().authenticated()
}
.formLogin { login ->
login.loginPage("/login")
login.defaultSuccessUrl("/")
login.permitAll()
}
.logout { logout ->
logout.logoutRequestMatcher(AntPathRequestMatcher("/logout"))
logout.permitAll()
}
.rememberMe()
return http.build()
}
}
継承したUserDetailsServiceの詳細
@Service
class SecurityService(
val repository: AuthUserRepository
): UserDetailsService {
override fun loadUserByUsername(username: String?): UserDetails {
val userName = UserName(username ?: "default")
val user = repository.findBy(userName) ?: throw IllegalArgumentException("Not found")
return User(
user.userName.name,
user.password.password,
AuthorityUtils.createAuthorityList("User")
)
}
}
起動したときに、動作するコード
Repositoryを実装しているデータベースに対して、データを保存する
@Component
class DataLoader(
val encoder: PasswordEncoder,
val repository: AuthUserRepository
): ApplicationRunner {
override fun run(args: ApplicationArguments?) {
val user = User (
userName = UserName("Narikake"),
password = PassWord(encoder.encode("password")),
userId = UserId("fooo")
)
repository.register(user)
}
}
#ThymeleafでSecurity
<htmlxmlns:sec="http://www.thymeleaf.org/extras/springsecurity">
と宣言して
<htmlxmlns:sec>
で利用する
UserDetailsで定義したフィールド(name)を利用できる
<div sec:authentication="name">
The value of the "name" property of the authentication object should appear here.
</div>
ROLE_ADMIN
を所有していれば出力される
サイドバーでADMIN
だけが閲覧できる要素を出力するなどの使いみち
<div sec:authorize="hasRole('ROLE_ADMIN')">
This will only be displayed if authenticated user has role ROLE_ADMIN.
</div>
自作Validation
html側でエラーメッセージを出したいときにSpringValidationを利用するが@annotation
Validationを自作できる。
@Bean
と@Autowired
の違い
@Bean
はここに元インスタンスがあることを知らせるが、
@Autowired
は予め@Bean
などで宣言されたDIを利用する際に使う。
@Component
と@Bean
の違い
大差はないみたいだが、外部のライブラリなどをDIしたい際には@Component
を利用するべき
springboot DIの種類
@Slf4j
- Classに定義する
-
private static
の↓を自動生成してくれる
val log = LoggerFactory.getLogger(SpringIocAopApplication.class);
@Scope
- BeanのDIの範囲を指定する
- Session毎の動作ややSingletonなどを定義できる
SpringBootのSecurityのSession
について詳しく書いてある。
RememerMeなどのクッキー☆に保存しているセッションを利用する方法なども。
ファイルアップロード
html form
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Upload</title>
</head>
<body>
<form th:action="@{/upload}" method="post" enctype="multipart/form-data">
<p><input type="file" name="file"></p>
<p><input type="submit" id="upload" value="アップロード"></p>
</form>
</body>
</html>
kotlin
@PostMapping("/upload")
fun upload(@RequestParam file: MultipartFile): String {
val path = System.getProperty("user.dir").plus("/images")
val dst = Path.of(path, file.originalFilename);
Files.copy(file.inputStream, dst)
return "redirect:/";
}
# ファイルサイズの合計の最大値
spring.servlet.multipart.max-file-size=20MB
# リクエストサイズの最大値
spring.servlet.multipart.max-request-size=20MB
ユーザ情報を取得する
permitAllなどを指定した際には、エラーが出力される
base64の文字列から、InputStreamを作成する方法
val base64String = "base64の文字列がここに来る"
val bytes = Base64.getDecoder().decode(base64String)
val inputStream = ByteArrayInputStream(bytes)
InputStreamからPNGファイルを作成する方法
val path: String = System.getProperty("user.dir").plus("/aaaa.png")
Files.copy(inputStream, Path.of(path))
ChatGPT
bytesでimageを表示する方法
@RestController
public class VideoController {
@GetMapping(value = "/video", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
@ResponseBody
public byte[] getVideo() throws IOException {
Resource resource = new ClassPathResource("videos/sample.mp4");
return Files.readAllBytes(resource.getFile().toPath());
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Video Player</title>
</head>
<body>
<h1>動画再生</h1>
<video width="640" height="480" controls>
<source th:src="@{/video}" type="video/mp4">
Your browser does not support the video tag.
</video>
<!-- 変数を利用した -->
<img th:src=@{/thumbnails/{videoid}(videoid=${1})}">
</body>
</html>
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ViewController {
@GetMapping("/view")
public String videoView() {
return "video-view";
}
}
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE
について
不明なデータにつけるらしい。
MediaType.IMAGE_PNG
などもあるみたい。
@RestController
public class VideoController {
@GetMapping(value = "/video", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<Resource> getVideo() throws IOException {
Resource resource = new ClassPathResource("videos/sample.mp4");
InputStream inputStream = resource.getInputStream();
InputStreamResource inputStreamResource = new InputStreamResource(inputStream);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(inputStreamResource);
}
}
thymeleafでforを利用する
<th:block th:each="video : ${videos}">
<p th:text="${video.title}"></p>
<p th:text="${video.description}"></p>
<p th:text="${video.uploadTime}"></p>
<img width="360" height="640" th:src="@{/thumbnails/{videoid}(videoid=${video.id})}">
</th:block>
s3で明示的に利用する方法
private val awsCreds = AwsBasicCredentials.create(
"your_access_key_id",
"your_secret_access_key"
)
private val s3Client = S3Client.builder()
.region(Region.AP_NORTHEAST_1)
.credentialsProvider(
StaticCredentialsProvider.create(awsCreds)
)
.build()
cloudflareR2をAWS SDK for java 2.0で利用するためのコード
サンプルとしてInputStream
を利用して、空のファイルをアップロードする
interface S3ClientSample {
fun upload(bucketName: String, s3Key: String, inputStream: InputStream)
}
class S3ClientSampleJavaSdk: S3ClientSample {
private val accountID = ""
private val url = URI("https://$accountID.r2.cloudflarestorage.com");
private val accessKey = ""
private val secretAccessKey = ""
private val awsCreds = AwsBasicCredentials.create(
accessKey,
secretAccessKey
)
private val s3Client = S3Client.builder()
.region(Region.AWS_GLOBAL)
.credentialsProvider(
StaticCredentialsProvider.create(awsCreds)
)
.endpointOverride(
url
)
.build()
override fun upload(bucketName: String, s3Key: String, inputStream: InputStream) {
s3Client.putObject(
PutObjectRequest.builder()
.bucket(bucketName)
.key(s3Key).build(),
RequestBody.fromInputStream(inputStream, inputStream.readBytes().size.toLong())
)
}
}
class S3ClientTest {
@Test
fun `アップロードできることを確認する`() {
val client = S3ClientSampleJavaSdk()
val emptyInputStream = InputStream.nullInputStream()
val bucketName = "sample-bucket"
client.upload(bucketName, "emptyfile.txt", emptyInputStream)
}
}
exposedで時間を扱うときに利用する
JWT認証は、一度きりで、短い期間だけ利用する場合にのみ有効であるので、それ以外の場合は利用するべきではない。そもそもcookieなどに保存されることを意図していない可能性が高い。
徳丸本の人が
『サーバーサイドフレームワークが提供する通常のセッションを使用すればよい』とするならば、JWTをローカルストレージやCookie等に保存する必要はないように思いますが、どのような目的でJWTを保存する想定でしょうか?
と指摘を入れていた。
JWTの使い所
ってことでcsrf対策をどのようにすればいいのか振り出しに戻った。
SPAは無理。セキュリティを担保しきれいない。シンプルにThymeleafを使うか....
プロセスキル
lsof -i:8080
SecurityConfigを実装したい。
Reactでログイン実装
import React, {useEffect} from 'react';
import './App.css';
function App() {
useEffect(() => {
const data = new FormData()
data.append("username", "user")
data.append("password", "password")
fetch('http://localhost:8080/login', {
credentials: 'include',
method: 'POST',
body: data
}).then(res =>
res.text()
).then(text =>
console.log(text)
)
})
return (
<>
</>
);
}
export default App;
@Configuration
class SecurityConfig {
@Bean
@Throws(Exception::class)
fun filterChain(http: HttpSecurity): SecurityFilterChain? {
http.authorizeHttpRequests {
it
.anyRequest()
.authenticated()
}
.formLogin { }
.csrf { it.disable() }
.cors { it.configurationSource(corsConfigurationSource()) }
return http.build()
}
private fun corsConfigurationSource(): CorsConfigurationSource {
val corsConfiguration = CorsConfiguration()
corsConfiguration.addAllowedMethod(CorsConfiguration.ALL)
corsConfiguration.addAllowedHeader(CorsConfiguration.ALL)
corsConfiguration.addAllowedOrigin("http://localhost:3000")
corsConfiguration.allowCredentials = true
val corsConfigurationSource = UrlBasedCorsConfigurationSource()
corsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration)
return corsConfigurationSource
}
@Bean
fun passwordEncoder(): PasswordEncoder {
return PasswordEncoderFactories.createDelegatingPasswordEncoder()
}
@Bean
fun userDetailsService(): UserDetailsService? {
val user = User.withUsername("user")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build()
return InMemoryUserDetailsManager(user)
}
}
credentialsをinclude
してあげる必要性がある。
import React, {useEffect, useState} from 'react';
import './App.css';
function App() {
useEffect(() => {
fetch('http://localhost:8080/csrf',{ credentials: `include`}).then(res =>
res.text()
).then( (token) => {
const data = new FormData()
data.append("username", "user")
data.append("password", "password")
data.append("_csrf", token)
fetch('http://localhost:8080/login', {
credentials: 'include',
method: 'POST',
body: data,
}).then(res =>
res.text()
).then(text =>
console.log(text)
)
}
)
}, [])
return (
<>
</>
);
}
export default App;
CSRF token
@Controller
class MiniController {
@ResponseBody
@GetMapping("/csrf")
fun preLogin(request: HttpServletRequest): String {
val token = request.getAttribute("_csrf") as CsrfToken? ?: return ""
println("token was generated: ${token.token}")
return token.token
}
}
ResponseEntity<SomethingDao> SomethingDaoのJsonマッピングをSnakeCase形式にしてくれる。
spring.jackson.property-naming-strategy=SNAKE_CASE
kotlin exposedの groupByとinnerJoinを利用したクエリの発行の仕方。
テーブルの定義
object Auths: IntIdTable()
object Properties: IntIdTable() {
val authId = integer("auth_id").references(Auths.id)
val name = varchar("name", 20)
}
object Comments: IntIdTable() {
val authId = integer("auth_id").references(Auths.id)
}
object Replies: IntIdTable() {
val commentId = integer("comment_id").references(Comments.id)
}
実行する方法
コツは、val count = Replies.commentId.count().alias("count")
の部分で、このAliasを.slice()
で利用してあげないと、なぜか取得することができない。
.slice
なしで取得しようとするとcount関数の部分だけが表示されなくなる。原因は不明。
// コメントのリプライの数と、コメントのを投稿したユーザのプロパティを取得する
val count = Replies.commentId.count().alias("count")
val subTable = Replies
.slice(Replies.commentId, count)
.selectAll()
.groupBy(Replies.commentId)
.alias("subTable")
val foo = Comments
.join(subTable, JoinType.INNER, additionalConstraint = {Comments.id eq subTable[Replies.commentId]})
.join(Properties, JoinType.INNER, additionalConstraint = {Comments.authId eq Properties.authId})
.slice(
Comments.id,
Comments.authId,
subTable[count],
Properties.id,
Properties.name,
)
.selectAll()
.map { it }
println(foo)
出力結果
[Comments.id=4, Comments.auth_id=7, subTable.count=1, Properties.id=1, Properties.name=Narikake]
データベース用語
データベースはNULLの扱いが面倒なので、極力使わないこと!
集合演算氏(和集合union, 差集合except, 積集合intersect)
家計簿
と家計簿アーカイブ
などの同じデータ型、列の数同士を組み合わせることができる
(イメージ的には、テーブルを縦にくっつける)
列が増えるわけではない。
集約関数(Sum, Countなど) グループ化(GroupBy)
テーブル内の行数などを数えたり合計を調べることができる。
グループ化を行うことで、特定の列の値の合計を調べられる。
単一の値を返すために使用される。
Window関数
集約関数とにているが、行のグループに対して計算を行い、結果セットの各行に結果を返す。
集約関数は単一の値を返すが、Window関数は各行に値を返す。
副問合せ・サブクエリ
メインのクエリ(select, update, insert, delete)の他にクエリを定義してメインクエリ内で利用することができる。
相関副問合せ
サブクエリの中で、メインクエリ内の列などを利用すること。サブクエリと比較しても処理が重くなる事がある。
結合・Join
異なるテーブル同士を組み合わせることができる。 (イメージ的には、テーブルを横にくっつける)
列が増える。
AuthとProfile内のAuth.idなどの外部を参照している。
INNER JOIN
結合する相手がいなければ消滅する
内部結合テーブル1 join テーブル2
で、
テーブル2に要素が複数あった場合、テーブル1の行数が増える。
逆にテーブル1が参照しているデータがテーブル2に見当たらなかった場合、テーブル1の行数が減る。
ただこれは、外部参照を行っていない場合に発生する(と思われる)
外部結合 勝手に消滅してほしくない場合に利用する
外部結合は結合する相手がいない場合でも、強制的にNULLで新しく行を追加する。
LEFT JOIN
RIGHT JOIN
FULL JOIN
などがある
LEFT JOIN
は利用されなかった行を強制的にテーブル2に生成する。
RIGHT JOIN
は利用されなかった行を強制的にテーブル1に生成する。(NULLだらけの項目ができる)
FULLJOIN
はどちらの正質も兼ね備えている。
3つ以上の結合
3つ以上の結合の場合でも一つ一つ結合が行われる。
副問合せの結合をする場合
as で別名をつける必要性がある。
同じテーブルの結合 (自己結合、再帰結合)
自分のテーブルを参照して再帰的に利用することができる。
コメント機能のサンプルコード
CREATE TABLE posts (
id INT PRIMARY KEY,
content VARCHAR(255),
parent_id INT,
FOREIGN KEY (parent_id) REFERENCES posts(id)
);
SELECT p.id, p.content, c.id AS comment_id, c.content AS comment_content
FROM posts p
LEFT JOIN posts c ON c.parent_id = p.id
イコール以外の結合条件 非等価結合
比較演算子も結合条件にすることができる(ただし処理が重くなる)
Viewを利用すると、毎回クエリが生成されるが、マテリアライズド・ビューの場合はキャッシュしてくれるのでおすすめである。
EXPLAINでSELECT
などの速度を計測することができる。
中間テーブル 多対多のテーブルを1対多にすることができる。
タグを作成する際に利用できそうですね。
正規化
第一正規化
セルの結合
や、繰り返し列
を排除する
「一つのセルの中には一つの値しか含まない」ためセル結合は
ピンク色
繰り返し列
青色
で示している
セル結合
と繰り返し列
は別のテーブルとして分割し、primary keyを与えることで正しく正規化することができる。
第二正規化
関数従属
ある列Aが決まれば自ずと列Bも決まる関係
例えば動画IDが決まると、動画のタイトルが決まる、 認証IDが決まると、認証パスワードが決まる。 そんな関係。
部分関数従属
は、複合キーのような一部にしか関数従属しない物を指す。
複合キーでないならそれはすでに第二正規化されている。
部分関数従属している列を切り離して、部分関数従属しているIDをコピーして新しいテーブルを作成する。
第三正規化
主キーに対して間接的に関数従属する場合は排除またはテーブルとして切り出す。
- きれいな関数従属
- VideoテーブルのIDに認証IDがある
- きたない関数従属
- VideoテーブルのIDに認証IDがあり認証パスワードが認証IDに従属している。
きたない関数従属のことを、推移関数従属
という。
推移関数従属
になっている場合は切り抜いて別のテーブルとして切り出す。
スッキリSQLより
Reidsを利用してセッションを管理する。
spring.session.store-type=redis
intellijで何故か認識されない(バージョンの問題?)ので、@EnableRedisHttpSession
を利用している。
class FooUserDetails(
val email: String,
val id: Int,
private val hashedPassword: String,
val role: Role,
) : UserDetails {
# Sessions flush mode.
spring.session.redis.flush-mode=on_save
spring.session.redis.namespace=spring:session # Namespace for keys used to store sessions.
#Redis
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.password=null
spring.data.redis.database=0
// spring boot session
implementation("org.springframework.session:spring-session-data-redis")
implementation "org.springframework.boot:spring-boot-starter-data-redis"
// // https://mvnrepository.com/artifact/org.springframework.session/spring-session-core
implementation 'org.springframework.session:spring-session-core:3.1.2'
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 60 * 60 * 24 * 30) // 30日間利用する
@Configuration
class RedisSessionConfiguration {
@Bean
fun configureRedisAction(): ConfigureRedisAction {
return ConfigureRedisAction.NO_OP
}
}
fun Auth.refreshUserRole() {
val newAuthentication = UsernamePasswordAuthenticationToken(
AniShareUserDetails(this.email, this.id, this.hashedPassword, this.role),
this.hashedPassword,
AuthorityUtils.createAuthorityList(this.role.toString())
)
SecurityContextHolder.getContext().authentication = newAuthentication
}