์ปค์คํ ๋ก๊ทธ์ธ ํ์ด์ง
์ด์ ํ์ด์ง
์ด์ ํ์ด์ง ๋ด์ฉ์ ์ด์ด์ ์งํํฉ๋๋ค.
Code
Preview
1.
๋ก๊ทธ์ธ ํ๋ฉด
2.
๋ฉ์ธ ํ๋ฉด
์์ ํ๋ก์ธ์ค
1.
ํ๋ก์ ํธ ์์ฑ
2.
ํ๋ก์ ํธ ์ค์
3.
์คํ๋ง ์ํ๋ฆฌํฐ ์ค์
4.
์์ฒญ ๊ฒฝ๋ก ๋งคํ
Preview
๋ก๊ทธ์ธ ํ๋ฉด
๋ฉ์ธ ํ๋ฉด
์์ ํ๋ก์ธ์ค
1.
ํ๋ก์ ํธ ์์ฑ
2.
ํ๋ก์ ํธ ์ค์
3.
์คํ๋ง ์ํ๋ฆฌํฐ ์ค์
4.
์์ฒญ ๊ฒฝ๋ก ๋งคํ
ํ๋ก์ ํธ ์์ฑ
build.gradle
spring boot 3.x.x
spring security 6.x.x
plugins {
id 'java'
id 'war'
id 'org.springframework.boot' version '3.3.5'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com.aloha'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.3'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
Java
๋ณต์ฌ
ํ๋ก์ ํธ ์ค์
application.properties
spring.application.name=form-custom
# ๋ฐ์ดํฐ ์์ค - MySQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/aloha?serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&useSSL=false&autoReconnection=true&autoReconnection=true
spring.datasource.username=aloha
spring.datasource.password=123456
# Mybatis ์ค์
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.type-aliases-package=com.aloha.security6.domain
mybatis.mapper-locations=classpath:mybatis/mapper/**/**.xml
Markdown
๋ณต์ฌ
์คํ๋ง ์ํ๋ฆฌํฐ ์ค์
โข
SecurityConfig.java
์ปค์คํ
๋ก๊ทธ์ธ ํ์ด์ง ๊ฒฝ๋ก์ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ๊ฒฝ๋ก๋ฅผ ์ง์ ํฉ๋๋ค.
๋ฉ์๋ | ์ค๋ช
|
loginPage(โ/๊ฒฝ๋กโ) | - โ/๊ฒฝ๋กโ ๋ฅผ ๋ก๊ทธ์ธ ํ๋ฉด ๊ฒฝ๋ก๋ก ์ง์ ํฉ๋๋ค. ์ด ๊ฒฝ๋ก๋ฅผ ์ง์ ํ๋ฉด ์คํ๋ง ์ํ๋ฆฌํฐ์ ๊ธฐ๋ณธ ๋ก๊ทธ์ธ ํ๋ฉด์ ๋ ์ด์ ์ ๊ณต๋์ง ์์ต๋๋ค.
- [GET] ๋ฐฉ์์ผ๋ก ๋ก๊ทธ์ธ ํ๋ฉด์ ์๋ํฌ์ธํธ๋ฅผ ์ง์ ํด์ผํฉ๋๋ค. |
loginProcessingUrl(โ/๊ฒฝ๋กโ ) | - โ/๊ฒฝ๋กโ ๋ฅผ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ์์ฒญ ๊ฒฝ๋ก๋ก ์ง์ ํฉ๋๋ค. ์ง์ ํ์ง ์์ ๊ฒฝ์ฐ, ๊ธฐ๋ณธ ๊ฒฝ๋ก๋ [POST] ๋ฐฉ์์ โ/loginโ ๊ฒฝ๋ก๋ก ์ง์ ๋์ด ์์ต๋๋ค.
- form ํ๊ทธ์ action=โ/๊ฒฝ๋กโ ๋ฅผ ์ผ์นํ๋๋ก ์์ฑํด์ผํฉ๋๋ค. |
// ๐ ํผ ๋ก๊ทธ์ธ ์ค์
// โ
์ปค์คํ
๋ก๊ทธ์ธ ํ์ด์ง
http.formLogin(login -> login.loginPage("/login")
.loginProcessingUrl("/login"));
Java
๋ณต์ฌ
loginPage() ๋ก ๋ก๊ทธ์ธ ํ์ด์ง๋ฅผ ์ค์ ํ๋ฉด, ์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ ๊ณตํด์ฃผ๋ ๊ธฐ๋ณธ ๋ก๊ทธ์ธ ํ๋ฉด์ ๋ ์ด์ ์ ๊ณต๋์ง ์์ต๋๋ค. ๊ทธ๋์ ์ง์ ๋ง๋ ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก ๋ก๊ทธ์ธ ์์ฒญ์ ๋ณด๋ผ ์ ์์ต๋๋ค.
/login ๊ฒฝ๋ก๋ก ๋ก๊ทธ์ธ ํ๋ฉด ์์ฒญ ๊ฒฝ๋ก ๋งคํ์ ํด์ฃผ์ด์ผํฉ๋๋ค.
SecurityConfig.java
@Slf4j
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailServiceImpl userDetailServiceImpl;
// ์คํ๋ง ์ํ๋ฆฌํฐ ์ค์ ๋ฉ์๋
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// โ
์ธ๊ฐ ์ค์
http.authorizeRequests(requests -> requests
.antMatchers("/**").permitAll()
.anyRequest().permitAll()
);
// ๐ ํผ ๋ก๊ทธ์ธ ์ค์
// โ
์ปค์คํ
๋ก๊ทธ์ธ ํ์ด์ง
http.formLogin(login -> login.loginPage("/login")
.loginProcessingUrl("/login"));
// โ
์ฌ์ฉ์ ์ ์ ์ธ์ฆ ์ค์
http.userDetailsService(userDetailServiceImpl);
// ๐ ์๋ ๋ก๊ทธ์ธ ์ค์
http.rememberMe(me -> me.key("aloha")
.tokenRepository(tokenRepository())
.tokenValiditySeconds(60 * 60 * 24 * 7));
return http.build();
}
/**
* ๐ ์๋ ๋ก๊ทธ์ธ ์ ์ฅ์ ๋น ๋ฑ๋ก
* โ
๋ฐ์ดํฐ ์์ค
* โญ persistent_logins ํ
์ด๋ธ ์์ฑ
create table persistent_logins (
username varchar(64) not null
, series varchar(64) primary key
, token varchar(64) not null
, last_used timestamp not null
);
* ๐ ์๋ ๋ก๊ทธ์ธ ํ๋ก์ธ์ค
* โ
๋ก๊ทธ์ธ ์
* โก ๐ฉโ๐ผ(ID, ์๋ฆฌ์ฆ, ํ ํฐ) ์ ์ฅ
* โ
๋ก๊ทธ์์ ์,
* โก ๐ฉโ๐ผ(ID, ์๋ฆฌ์ฆ, ํ ํฐ) ์ญ์
* @return
*/
@Bean
public PersistentTokenRepository tokenRepository() {
// JdbcTokenRepositoryImpl : ํ ํฐ ์ ์ฅ ๋ฐ์ดํฐ ๋ฒ ์ด์ค๋ฅผ ๋ฑ๋กํ๋ ๊ฐ์ฒด
JdbcTokenRepositoryImpl repositoryImpl = new JdbcTokenRepositoryImpl();
// โ
ํ ํฐ ์ ์ฅ์๋ฅผ ์ฌ์ฉํ๋ ๋ฐ์ดํฐ ์์ค ์ง์
// - ์ํ๋ฆฌํฐ๊ฐ ์๋ ๋ก๊ทธ์ธ ํ๋ก์ธ์ค๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํ DB๋ฅผ ์ง์ ํฉ๋๋ค.
repositoryImpl.setDataSource(dataSource);
// persistent_logins ํ
์ด๋ธ ์์ฑ
try {
repositoryImpl.getJdbcTemplate().execute(JdbcTokenRepositoryImpl.CREATE_TABLE_SQL);
}
catch (BadSqlGrammarException e) {
log.error("persistent_logins ํ
์ด๋ธ์ด ์ด๋ฏธ ์กด์ฌํฉ๋๋ค.");
}
catch (Exception e) {
log.error("์๋ ๋ก๊ทธ์ธ ํ
์ด๋ธ ์์ฑ ์ค , ์์ธ ๋ฐ์");
}
return repositoryImpl;
}
}
Java
๋ณต์ฌ
๋ก๊ทธ์ธ ์์ฒญ ํ๋ผ๋ฏธํฐ
์์ฒญ ํ๋ผ๋ฏธํฐ๋ฅผ ์คํ๋ง ์ํ๋ฆฌํฐ ์ค์ ์์ ๋ณ๊ฒฝํ ์ ์์ต๋๋ค.
โข
๊ธฐ๋ณธ ์์ฒญ ํ๋ผ๋ฏธํฐ
์์ | ์์ฒญ ํ๋ผ๋ฏธํฐ |
์์ด๋ | username |
๋น๋ฐ๋ฒํธ | password |
์๋ ๋ก๊ทธ์ธ | remember-me |
์์ฒญ ํ๋ผ๋ฏธํฐ ๋ณ๊ฒฝํ๋ ๋ฐฉ๋ฒ
โข
์์ด๋/๋น๋ฐ๋ฒํธ ์์ฒญ ํ๋ผ๋ฏธํฐ
โข
์๋ ๋ก๊ทธ์ธ ํ๋ผ๋ฏธํฐ
์์ด๋/๋น๋ฐ๋ฒํธ ์์ฒญ ํ๋ผ๋ฏธํฐ
http.formLogin(login -> login.loginPage("/login")
.loginProcessingUrl("/login")
.usernameParameter("id")
.passwordParameter("pw")
);
Java
๋ณต์ฌ
โข
usernameParameter(โ์์ด๋ ์์ฒญ ํ๋ผ๋ฏธํฐโ)
โข
passwordParameter(โ๋น๋ฐ๋ฒํธ ์์ฒญ ํ๋ผ๋ฏธํฐโ)
์์ ๊ฐ์ด ํผ ๋ก๊ทธ์ธ ์ค์ ์์ ์์ด๋ ๋น๋ฐ๋ฒํธ์ ๊ฐ๊ฐ ์์ฒญ ํ๋ผ๋ฏธํฐ ์ด๋ฆ์ ๋ณ๊ฒฝ ์ค์ ํ ์ ์๋ค.
์๋ ๋ก๊ทธ์ธ ํ๋ผ๋ฏธํฐ
http.rememberMe(me -> me.key("aloha")
.tokenRepository(tokenRepository())
.tokenValiditySeconds(60 * 60 * 24 * 7)
.rememberMeParameter("auto-login")
);
Java
๋ณต์ฌ
โข
rememberMeParameter(โ์๋๋ก๊ทธ์ธ ์์ฒญ ํ๋ผ๋ฏธํฐโ)
์์ ๊ฐ์ด ์๋ ๋ก๊ทธ์ธ ์ค์ ์์ ์๋ ๋ก๊ทธ์ธ ์ฌ๋ถ์ ์์ฒญ ํ๋ผ๋ฏธํฐ ์ด๋ฆ์ ๋ณ๊ฒฝ ์ค์ ํ ์ ์๋ค.
์์ ์์์ฒ๋ผ ์๋ ๋ก๊ทธ์ธ ํ๋ผ๋ฏธํฐ๋ฅผ โauto-loginโ ์ด๋ผ๊ณ ๋ณ๊ฒฝ ์ค์ ํ๋ค๋ฉด, ์๋์ ๊ฐ์ด input checkbox ํ๊ทธ์์ name=โauto-loginโ ์์ฑ์ ์ผ์น์์ผ์ผํ๋ค.
<input class="form-check-input" type="checkbox" name="auto-login" id="remember-me-check">
Java
๋ณต์ฌ
์์ฒญ ๊ฒฝ๋ก ๋งคํ
โข
HomeController.java
โข
login.html
HomeController.java
/**
* ๋ก๊ทธ์ธ ํ๋ฉด
* @return
*/
@GetMapping("/login")
public String login() {
log.info(":::::::::: ๋ก๊ทธ์ธ ํ์ด์ง ::::::::::");
return "/login";
}
Java
๋ณต์ฌ
login.html
๋ก๊ทธ์ธ ์์ฒญ ํ๋ผ๋ฏธํฐ
โข
๊ธฐ๋ณธ ์์ฒญ ํ๋ผ๋ฏธํฐ
์์ | ์์ฒญ ํ๋ผ๋ฏธํฐ |
์์ด๋ | username |
๋น๋ฐ๋ฒํธ | password |
์๋ ๋ก๊ทธ์ธ | remember-me |
โข
form ์์ ํ๋ผ๋ฏธํฐ ์ง์
์์ | ์์ฒญ ํ๋ผ๋ฏธํฐ |
์์ด๋ | <input type="text" class="form-control" id="username" name="username" value="" placeholder="์์ด๋" autofocus> |
๋น๋ฐ๋ฒํธ | <input type="password" class="form-control" id="password" name="password" placeholder="๋น๋ฐ๋ฒํธ"> |
์๋ ๋ก๊ทธ์ธ | <input class="form-check-input" type="checkbox" name="remember-me" id="remember-me-check"> |
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Form ๋ก๊ทธ์ธ</title>
<!-- bootstrap css -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container col-12 col-md-6 col-lg-4">
<div class="px-4 py-5 mt-5 text-center">
<h1 class="display-5 fw-bold text-body-emphasis">๋ก๊ทธ์ธ</h1>
</div>
<!-- ๋ก๊ทธ์ธ ์์ญ -->
<main class="form-signin login-box w-100 m-auto">
<form action="/login" method="post">
<!-- CSRF TOKEN -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<div class="form-floating">
<input type="text" class="form-control" id="username" name="username" value="" placeholder="์์ด๋"
autofocus th:value="${username}">
<label for="username">์์ด๋</label>
</div>
<div class="form-floating">
<input type="password" class="form-control" id="password" name="password" placeholder="๋น๋ฐ๋ฒํธ">
<label for="password">๋น๋ฐ๋ฒํธ</label>
</div>
<div class="form-check text-start my-3 d-flex justify-content-around">
<div class="item">
<input class="form-check-input" type="checkbox" name="remember-id" id="remember-id-check" th:checked="${rememberId}">
<label class="form-check-label" for="remember-id-check">์์ด๋ ์ ์ฅ</label>
</div>
<div class="item">
<input class="form-check-input" type="checkbox" name="remember-me" id="remember-me-check">
<label class="form-check-label" for="remember-me-check">์๋ ๋ก๊ทธ์ธ</label>
</div>
</div>
<!-- ๋ก๊ทธ์ธ ์๋ฌ -->
<th:block th:if="${param.error}">
<p class="text-center text-danger">์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ๋ฅผ ์๋ชป ์
๋ ฅํ์ต๋๋ค.</p>
</th:block>
<!-- ๋ก๊ทธ์์ ์๋ฃ -->
<th:block th:if="${param.logout}">
<p class="text-center text-success">์ ์์ ์ผ๋ก ๋ก๊ทธ์์ ๋์์ต๋๋ค.</p>
</th:block>
<!-- ๋ฒํผ -->
<div class="d-grid gap-2">
<button class="btn btn-lg btn-primary w-100 py-2" type="submit">๋ก๊ทธ์ธ</button>
<a href="/join" class="btn btn-lg btn-success w-100 py-2">ํ์๊ฐ์
</a>
<hr>
</div>
</form>
</main>
</div>
<!-- bootstrap -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
HTML
๋ณต์ฌ