Search

์ปค์Šคํ…€ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€

์ปค์Šคํ…€ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€

Spring Security 6

์ด์ „ ํŽ˜์ด์ง€

์ด์ „ ํŽ˜์ด์ง€ ๋‚ด์šฉ์— ์ด์–ด์„œ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

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
๋ณต์‚ฌ