โ๏ธ ํ์ตํ ๋ด์ฉ
-
์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ ๊ณตํ๋ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ๋ฐฉ์์ ์ดํด
-
JPA ์ ์ฐ๋ํ๋ ์ปค์คํ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ
-
Thymeleaf ์์ ๋ก๊ทธ์ธ ์ ๋ณด ํ์ฉํ๊ธฐ
-
์คํ๋ง ์ํ๋ฆฌํฐ ๊ฐ์ฒด๋ค์ ์ฒ๋ฆฌํ๊ธฐ ์ํ Thymeleaf ํ์ฅ ํ๋ก๊ทธ์ธ ์ถ๊ฐ
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
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.thymeleaf.extras:thymeleaf-extras-springsecurity5'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
implementation group: 'org.mariadb.jdbc', name: 'mariadb-java-client'
implementation group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-springsecurity5'
implementation group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-java8time'
}
- ๊ธฐํ ์ค์ ์ถ๊ฐ
๋ฐ์ดํฐ๋ฒ ์ด์ค, JPA ๊ด๋ จ ์ค์ ๊ณผ ํ์ผ ์ ๋ก๋ ๊ด๋ จ ์ค์ ์ถ๊ฐ, ์ํ๋ฆฌํฐ์ ๊ด๋ จ๋ ๋ก๊ทธ ๋ ๋ฒจ ๋ฎ๊ฒ ์ค์ ํด์ ์์ธํ ๋ก๊ทธ ํ์ธํ ์ ์๊ฒ ์ค์
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/bootex
spring.datasource.username=bootuser
spring.datasource.password=bootuser
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true
spring.thymeleaf.cache=false
spring.servlet.multipart.enabled=true
spring.servlet.multipart.location=C:\\upload
spring.servlet.multipart.max-request-size=30MB
spring.servlet.multipart.max-file-size=10MB
logging.level.org.springframework.security.web=trace
logging.level.org.example=debug
- ์คํ
17:48:47.839 [Thread-0] DEBUG org.springframework.boot.devtools.restart.classloader.RestartClassLoader - Created RestartClassLoader org.springframework.boot.devtools.restart.classloader.RestartClassLoader@2271d2cb
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.3)
2022-09-08 17:48:48.542 INFO 4160 --- [ restartedMain] c.e.s.SpringSecurityApplication : Starting SpringSecurityApplication using Java 17.0.2 on KunYoung with PID 4160 (C:\Users\ur2ku\OneDrive\๋ฐํ ํ๋ฉด\WORKSPACE\[Spring]workspace\SpringSecurity\build\classes\java\main started by ur2ku in C:\Users\ur2ku\OneDrive\๋ฐํ ํ๋ฉด\WORKSPACE\[Spring]workspace\SpringSecurity)
2022-09-08 17:48:48.544 INFO 4160 --- [ restartedMain] c.e.s.SpringSecurityApplication : No active profile set, falling back to 1 default profile: "default"
2022-09-08 17:48:48.585 INFO 4160 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2022-09-08 17:48:48.585 INFO 4160 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2022-09-08 17:48:49.032 INFO 4160 --- [ restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2022-09-08 17:48:49.043 INFO 4160 --- [ restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 4 ms. Found 0 JPA repository interfaces.
2022-09-08 17:48:49.484 INFO 4160 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2022-09-08 17:48:49.492 INFO 4160 --- [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2022-09-08 17:48:49.492 INFO 4160 --- [ restartedMain] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.65]
2022-09-08 17:48:49.543 INFO 4160 --- [ restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2022-09-08 17:48:49.543 INFO 4160 --- [ restartedMain] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 958 ms
2022-09-08 17:48:49.670 INFO 4160 --- [ restartedMain] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2022-09-08 17:48:49.715 INFO 4160 --- [ restartedMain] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.6.10.Final
2022-09-08 17:48:49.828 INFO 4160 --- [ restartedMain] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2022-09-08 17:48:49.906 INFO 4160 --- [ restartedMain] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2022-09-08 17:48:49.966 INFO 4160 --- [ restartedMain] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2022-09-08 17:48:49.989 INFO 4160 --- [ restartedMain] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MariaDB106Dialect
2022-09-08 17:48:50.229 INFO 4160 --- [ restartedMain] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2022-09-08 17:48:50.235 INFO 4160 --- [ restartedMain] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2022-09-08 17:48:50.262 WARN 4160 --- [ restartedMain] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2022-09-08 17:48:50.524 WARN 4160 --- [ restartedMain] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: 9a7fb52e-4c3a-4a73-82f3-7d15067e5cca
This generated password is for development use only. Your security configuration must be updated before running your application in production.
2022-09-08 17:48:50.595 DEBUG 4160 --- [ restartedMain] edFilterInvocationSecurityMetadataSource : Adding web access control expression [authenticated] for any request
2022-09-08 17:48:50.612 TRACE 4160 --- [ restartedMain] o.s.s.w.a.i.FilterSecurityInterceptor : Validated configuration attributes
2022-09-08 17:48:50.612 TRACE 4160 --- [ restartedMain] o.s.s.w.a.i.FilterSecurityInterceptor : Validated configuration attributes
2022-09-08 17:48:50.615 INFO 4160 --- [ restartedMain] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@4cc47b75, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@37391e7b, org.springframework.security.web.context.SecurityContextPersistenceFilter@6cb9892a, org.springframework.security.web.header.HeaderWriterFilter@15b0ec06, org.springframework.security.web.csrf.CsrfFilter@1e5a0d82, org.springframework.security.web.authentication.logout.LogoutFilter@3e1da4fe, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@55c80996, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@371888a0, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@2930a576, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@57559a6c, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7cd98d0, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@68dabf46, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@24f804c1, org.springframework.security.web.session.SessionManagementFilter@6c2d5330, org.springframework.security.web.access.ExceptionTranslationFilter@572d32b6, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@3aea7a0e]
2022-09-08 17:48:50.646 INFO 4160 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
2022-09-08 17:48:50.674 INFO 4160 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2022-09-08 17:48:50.685 INFO 4160 --- [ restartedMain] c.e.s.SpringSecurityApplication : Started SpringSecurityApplication in 2.827 seconds (JVM running for 3.659)
2022-09-08 17:49:06.249 INFO 4160 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2022-09-08 17:49:06.251 INFO 4160 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2022-09-08 17:49:06.253 INFO 4160 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
Process finished with exit code 130
- ์ค๊ฐ์ ํจ์ค์๋ ํ๋๊ฐ ์ถ๋ ฅ๋๋ ๊ฒ์ ๋ณผ ์ ์์
Using generated security password: 9a7fb52e-4c3a-4a73-82f3-7d15067e5cca
์์ฑ๋ ํจ์ค์๋๋ ๊ธฐ๋ณธ์ผ๋ก ์ฌ์ฉํด ๋ณผ ์ ์๋ 'user' ๊ณ์ ์ ํจ์ค์๋๋ก ํ๋ก์ ํธ ์ด๊ธฐ์ ์๋ฌด ๊ณ์ ๋ ์์ ๋ ์ฌ์ฉํ ์ ์๋ ์์ ํจ์ค์๋ ์ญํ ์ ํจ
- http://localhost:8080/login ์ ๊ฒฝ๋ก๋ก ์ ๊ทผํด์ ๋ก๊ทธ์ธ ํ ์คํธ
์คํ๋ง ๋ถํธ๋ ์๋ ์ค์ ๊ธฐ๋ฅ์ด ์์ด ๋ณ๋์ ์ค์ ์์ด๋ ์ฐ๋ ์ฒ๋ฆฌ๋ ์์ ๊ฐ์ด ๊ฐ๋ฅํ์ง๋ง ์คํ๋ง ์ํ๋ฆฌํฐ๋ฅผ ์ด์ฉํ๋ ๋ชจ๋ ํ๋ก์ ํธ๋ ํ๋ก์ ํธ์ ๋ง๋ ์ค์ ์ ์ถ๊ฐํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์. ๋ฐ๋ผ์ ๋ณ๋์ ์ํ๋ฆฌํฐ ์ค์ ํด๋์ค๋ฅผ ๋ง๋ค์ด๋ณด์
- SecurityConfig ํด๋์ค ์ถ๊ฐ
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@Configuration
@Log4j2
@EnableWebSecurity
public class SecurityConfig {}
์๋๋ WebSecurityConfigurerAdapter ๋ฅผ ์์๋ฐ๊ณ ์ค๋ฒ๋ผ์ด๋๋ฅผ ํตํด์ ์ค์ ์ ์กฐ์ ํ๋๋ฐ deprecate ๋๊ณ @EnableWebSecurity ์ฌ์ฉ.
ํค์๋ | ๋ด์ฉ |
---|---|
SecurityConfig.class | ์์ผ๋ก ๋ชจ๋ ์ํ๋ฆฌํฐ ๊ด๋ จ ์ค์ ์ ์ฌ๊ธฐ๋ค ์ถ๊ฐํ ๊ฒ์ |
- SampleController
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@Log4j2
@RequestMapping("/sample/")
public class SampleController {
@GetMapping("/all")
public void exAll() {
log.info("exAll........");
}
@GetMapping("/member")
public void exMember() {
log.info("exMember.......");
}
@GetMapping("/admin")
public void exAdmin() {
log.info("exAdmin.......");
}
}
SampleController ์๋ ํ์ฌ ์ฌ์ฉ์์ ๊ถํ์ ๋ฐ๋ผ ์ ๊ทผํ ์ ์๋ ๊ฒฝ๋ก๋ฅผ ์ง์ ํ ๊ฒ์
- ๋ก๊ทธ์ธ์ ํ์ง ์์ ์ฌ์ฉ์๋ ์ ๊ทผํ ์ ์๋ '/sample/all'
- ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ง์ด ์ ๊ทผํ ์ ์๋ '/sample/member'
- ๊ด๋ฆฌ์ (admin) ๊ถํ์ด ์๋ ์ฌ์ฉ์๋ง์ด ์ ๊ทผํ ์ ์๋ '/sample/admin'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>admin</title>
</head>
<body>
<h2>admin</h2>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>all</title>
</head>
<body>
<h2>all</h2>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>member</title>
</head>
<body>
<h2>member</h2>
</body>
</html>
ํ๋ก์ ํธ๋ฅผ ์คํํ๊ณ '/sample/all' ๊ณผ ๊ฐ์ ๊ฒฝ๋ก๋ฅผ ํธ์ถํ๋ฉด ์ํ๋ฆฌํฐ๋ก ์ธํด ๋ก๊ทธ์ธ ํ๋ฉด์ด ๋ณด์ด๋ ๊ฒ์ ํ์ธํ ์ ์์. ์ด๋ฅผ ์๋ฒ ๋ก๊ทธ๋ฅผ ์ค์ฌ์ผ๋ก ์ดํด๋ณด์
๋ก๊ทธ์ธ ์ธ์ ์ด ์์ ๊ฒฝ์ฐ 'localhost:8080/sample/admin' ์ผ๋ก ์ ์ํ๋ 'localhost:8080/sample/all'์ผ๋ก ์ ์ํ๋ 'localhost:8080/sample/member' ๋ก ์ ์ํ๋ 'http://localhost:8080/login' ํ์ด์ง๊ฐ ํธ์ถ๋๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
- '/sample/all' ๋ฑ์ ํธ์ถํ ๊ฒฝ์ฐ ๋ด๋ถ์ ์ผ๋ก ์ฌ๋ฌ ๊ฐ์ ํํฐ (filter)๊ฐ ๋์ํ๋ ๊ฒ์ ํ์ธํ ์ ์์.
- ์คํ๋ง ์ํ๋ฆฌํฐ์ ๋์์๋ ์ฌ๋ฌ ๊ฐ์ ๊ฐ์ฒด๊ฐ ์๋ก ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ผ๋ฉด์ ์ด๋ฃจ์ด์ง
ํต์ฌ ์ญํ ์ AuthenticationProvider (์ธ์ฆ ๋งค๋์ )๋ฅผ ํตํด์ ์ด๋ฃจ์ด์ง๋ค. AuthenticationProvider ๋ ์ธ์ฆ ๋งค๋์ ๊ฐ ์ด๋ป๊ฒ ๋์ํด์ผ ํ๋์ง๋ฅผ ๊ฒฐ์ ํ๊ณ ์ต์ข ์ ์ผ๋ก ์ค์ ์ธ์ฆ์ UserDetailService ์ ์ํด์ ์ด๋ฃจ์ด์ง๋ค.
์คํ๋ง ์ํ๋ฆฌํฐ์ ๊ฐ์ฅ ํต์ฌ ๊ฐ๋ ์ ์ธ์ฆ (Authentication)๊ณผ ์ธ๊ฐ (Authorization) ์ด๋ค. ์๋ฅผ ๋ค์ด ์ํ์ ๊ธ๊ณ ๊ฐ ํ๋ ์๊ณ , ์ฌ์ฉ์๊ฐ ๊ธ๊ณ ์ ๋ด์ฉ์ ์ด์ด ๋ณธ๋ค๊ณ ๊ฐ์ ํด ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ ๊ณผ์ ์ ๊ฑฐ์น๊ฒ ๋๋ค.
- ์ฌ์ฉ์๋ ์ํ์ ๊ฐ์ ์์ ์ด ์ด๋ค ์ฌ๋์ธ์ง ์์ ์ ์ ๋ถ์ฆ์ผ๋ก ์ฆ๋ช ํ๋ค.
- ์ํ์์๋ ์ฌ์ฉ์์ ์ ๋ถ์ ํ์ธํ๋ค.
- ์ํ์์ ์ฌ์ฉ์๊ฐ ๊ธ๊ณ ๋ฅผ ์ด์ด๋ณผ ์ ์๋ ์ฌ๋์ธ์ง๋ฅผ ํ๋จํ๋ค.
- ๋ง์ผ ์ ์ ํ ๊ด๋ฆฌ๋ ๊ถํ์ด ์๋ ์ฌ์ฉ์์ ๊ฒฝ์ฐ ๊ธ๊ณ ๋ฅผ ์ด์ด์ค๋ค.
์์ ๊ณผ์ ์์ 1์ ์ธ์ฆ์ ํด๋นํ๋ ์์ ์ผ๋ก ์์ ์ '์ฆ๋ช 'ํ๋ ๊ฒ์ด๋ค. 3์์๋ ์ฌ์ฉ์๋ฅผ '์ธ๊ฐ'ํ๋ ์ผ์ข ์ ํ๊ฐ๋ฅผ ํด ์ฃผ๋ ๊ณผ์ ์ผ๋ก ์คํ๋ง ์ํ๋ฆฌํฐ ์ญ์ ๋ด๋ถ์ ์ผ๋ก ์์ ์ ์ฌํ ๊ณผ์ ์ ๊ฑฐ์ณ์ ๋์ํ๋ค.
์คํ๋ง ์ํ๋ฆฌํฐ์์ ํํฐ (Filter)๋ ์๋ธ๋ฆฟ์ด๋ JSP ์์ ์ฌ์ฉํ๋ ํํฐ์ ๊ฐ์ ๊ฐ๋ ์ด์ง๋ง, ์คํ๋ง ์ํ๋ฆฌํฐ์์๋ ์คํ๋ง์ ๋น๊ณผ ์ฐ๋ํ ์ ์๋ ๊ตฌ์กฐ๋ก ์ค๊ณ๋์ด ์๋ค. ์ผ๋ฐ์ ์ธ ํํฐ๋ ์คํ๋ง์ ๋น์ ์ฌ์ฉํ ์ ์๊ธฐ ๋๋ฌธ์ ๋ณ๋์ ํด๋์ค๋ฅผ ์์๋ฐ๋ ํํ๊ฐ ๋ง๋ค.
์คํ๋ง ์ํ๋ฆฌํฐ์ ๋ด๋ถ์๋ ์ฌ๋ฌ ๊ฐ์ ํํฐ๊ฐ Filter Chain ์ด๋ผ๋ ๊ตฌ์กฐ๋ก Request ๋ฅผ ์ฒ๋ฆฌํ๊ฒ ๋๋ค. ์์์ ์คํ๋์๋ ๋ก๊ทธ๋ฅผ ์ดํด๋ณด๋ฉด 15๊ฐ ์ ๋์ ํํฐ๊ฐ ๋์ํ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค. ๊ฐ๋ฐ ์์ ํํฐ๋ฅผ ํ์ฅํ๊ณ ์ค์ ํ๋ฉด ์คํ๋ง ์ํ๋ฆฌํฐ๋ฅผ ์ด์ฉํด์ ๋ค์ํ ํํ์ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ๊ฒ ๋๋ค. ์๋๋ ์คํ๋ง ์ํ๋ฆฌํฐ ๋ด๋ถ์ ์ฌ์ฉ๋๋ ์ฃผ์ ํํฐ์ด๋ค.
ํํฐ์ ํต์ฌ์ ์ธ ๋์์ AuthenticationManager ๋ฅผ ํตํด์ ์ธ์ฆ (Authentication) ์ด๋ผ๋ ํ์ ์ ๊ฐ์ฒด๋ก ์์ ์ ํ๊ฒ ๋๋ค. AuthenticationManager๊ฐ ๊ฐ์ง ์ธ์ฆ ์ฒ๋ฆฌ ๋ฉ์๋๋ ํ๋ผ๋ฏธํฐ๋ Authentication ํ์ ์ผ๋ก ๋ฐ๊ณ ๋ฆฌํด ํ์ ๋ํ Authentication ์ด๋ค.
์ธ์ฆ (Authentication) ๋ '์ฃผ๋ฏผ๋ฑ๋ก์ฆ' ๊ณผ ๋น์ทํ ๊ฐ๋ ์ผ๋ก ์๊ฐํ๋ฉด ๋๋ค. '์ธ์ฆ' ์ด๋ผ๋ ์ฉ์ด๋ '์ค์ค๋ก ์ฆ๋ช ํ๋ค' ๋ผ๋ ์๋ฏธ์ด๋ค. ์๋ฅผ ๋ค์ด ๋ก๊ทธ์ธํ๋ ๊ณผ์ ์์๋ ์ฌ์ฉ์์ ์์ด๋/ํจ์ค์๋๋ก ์์ ์ด ์ด๋ค ์ฌ๋์ธ์ง๋ฅผ ์ ๋ฌํ๋ค. ์ ๋ฌ๋ ์์ด๋/ํจ์ค์๋๋ก ์ค์ ์ฌ์ฉ์์ ๋ํด์ ๊ฒ์ฆํ๋ ํ์๋ AuthenticationManager (์ธ์ฆ ๋งค๋์ )๋ฅผ ํตํด์ ์ด๋ฃจ์ด์ง๋ค.
์ค์ ๋์์์ ์ ๋ฌ๋๋ ํ๋ผ๋ฏธํฐ๋ UsernamePasswordAuthenticationToken ๊ณผ ๊ฐ์ด ํ ํฐ์ด๋ผ๋ ์ด๋ฆ์ผ๋ก ์ ๋ฌ๋๋ค. ์ฆ, ์คํ๋ง ์ํ๋ฆฌํฐ ํํฐ์ ์ฃผ์ ์ญํ ์ด ์ธ์ฆ ๊ด๋ จ๋ ์ ๋ณด๋ฅผ ํ ํฐ์ด๋ผ๋ ๊ฐ์ฒด๋ก ๋ง๋ค์ด ์ ๋ฌํ๋ค๋ ์๋ฏธ์ด๋ค. ์๋๋ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณต๋๋ ํํฐ ์ค UsernamePasswordAuthenticationFilter ํด๋์ค ์ฝ๋ ์ค ์ผ๋ถ์ด๋ค.
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password ! = null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subckasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
request ๋ฅผ ์ด์ฉํด์ ์ฌ์ฉ์์ ์์ด๋์ ํจ์ค์๋๋ฅผ ๋ฐ์์ UsernamePasswordAuthenticationToken ์ด๋ผ๋ ๊ฐ์ฒด๋ฅผ ๋ง๋ค๊ณ ์ด๋ฅผ AuthenticationManager ์ authenticate() ์ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌํ๋ ๊ฒ์ ํ์ธํ ์ ์์. AuthenticationManager ๋ ์ด๋ฌํ ์ฒ๋ฆฌ๋ฅผ AuthenticationProvider ๋ก ์ฒ๋ฆฌํ๋ค.
์ธ์ฆ์ฒ๋ฆฌ ๋จ๊ณ ์ดํ ์ฌ์ฉ์ ๊ถํ์ ํ์ธํ์ฌ ์ ๊ทผ ์ ํ์ ๋ . (Access-Control)
- ๐ url ์๋ฌด๊ฑฐ๋ ์ ๋ ฅ
- ๐ ์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ธ์ฆ/์ธ๊ฐ๊ฐ ํ์ํ๋ค๊ณ ํ๋จ. ์ฌ์ฉ์๊ฐ ์ธ์ฆํ๋๋ก ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก redirect
- ๐ ์์ ๊ณ์ , ๋น๋ฒ ์ ๋ ฅ
- ๐ ์ ๋ณด๊ฐ ์ ๋ฌ๋๋ฉด AuthenticationManager๊ฐ ์ ์ ํ AuthenticationProvider๋ฅผ ์ฐพ์์ ์ธ์ฆ ์๋
AuthenticationProvider์ ์ค์ ๋์์ UserDetailService๋ฅผ ๊ตฌํํ ๊ฐ์ฒด๋ก ์ฒ๋ฆฌ
ํจ์ค์๋๋ฅผ ์ธ์ฝ๋ฉํ๋ ๊ฐ์ฒด (์ํธํ)์. ์คํ๋ง ์ํ๋ฆฌํฐ๋ ์ฌ๋ฌ ์ข ๋ฅ์ PasswordEncoder๋ฅผ ์ ๊ณตํ๊ณ ์์ผ๋ฉฐ ๊ทธ ์ค ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉํ๋ ๊ฒ์ BCryptPasswordEncoder๋ผ๋ ํด๋์ค์ด๋ค.
- BCryptPasswordEncoder 'bcrypt'๋ผ๋ ํด์ ํจ์๋ฅผ ์ด์ฉํด ํจ์ค์๋๋ฅผ ์ํธํํจ. ์ํธํ๋ ํจ์ค์๋๋ ๋ณตํธํ๊ฐ ๋ถ๊ฐ๋ฅํ๊ณ ๋งค๋ฒ ์ํธํ๋ ๊ฐ๋ ๋ค๋ฅด๊ฒ ๋จ. ๋์ ํน์ ํ ๋ฌธ์์ด์ด ์ํธํ๋ ๊ฒฐ๊ณผ์ธ์ง๋ง์ ํ์ธํ ์ ์๊ณ ์๋ณธ์ ๋ด์ฉ์ ๋ณผ ์ ์์ผ๋ฏ๋ก ์ต๊ทผ์ ๋ง์ด ์ฌ์ฉ๋จ. SecurityConfig๋ @Bean์ ํตํด BCryptPasswordEncoder๋ฅผ ์ง์
package com.example.springsecurity.config;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@Log4j2
@EnableWebSecurity
public class SecurityConfig {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
package com.example.springsecurity.security;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
@SpringBootTest
public class PasswordTests {
@Autowired
private PasswordEncoder passwordEncoder;
@Test
public void testEncode() {
String password = "1111";
String enPw = passwordEncoder.encode(password);
System.out.println("enPW: "+enPw);
boolean matchResult = passwordEncoder.matches(password, enPw);
System.out.println("matchResult: "+matchResult);
}
}
# first ํ
์คํธ ๊ฒฐ๊ณผ
enPW: $2a$10$PHKK0aBGdOWborx8QJfJd.96.OfvKL47wUOuoFpuK2sr0/wzYfgw2
matchResult: true
# second ํ
์คํธ ๊ฒฐ๊ณผ
enPW: $2a$10$pvtnrZLWPHqGZ/7xF5FxEO29x.UgF6lV21L16NtVfxuUtQzMQG9Nu
matchResult: true
์ํธํ๋ ํจ์ค์๋๋ฅผ ์ด์ฉํ๊ธฐ ์ํ ์ฌ์ฉ์๊ฐ ํ์ํจ. ์ด๋ฅผ ์ํด AuthenticationManager์ ์ค์ ์ ์ฝ๊ฒ ์ฒ๋ฆฌํ ์ ์๋๋ก ๋์์ฃผ๋ Configure() ๋ฉ์๋ ์ค๋ฒ๋ผ์ด๋ฉ ์ฒ๋ฆฌ
package com.example.springsecurity.config;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@Log4j2
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
// ์ฌ์ฉ์ ๊ณ์ ์ user1
authenticationManagerBuilder.inMemoryAuthentication().withUser("user1")
.password("$2a$10$pvtnrZLWPHqGZ/7xF5FxEO29x.UgF6lV21L16NtVfxuUtQzMQG9Nu") // 1111 ํจ์ค์๋ ์ธ์ฝ๋ฉ ๊ฒฐ๊ณผ
.roles("USER");
}
}
์คํ๋ง ์ํ๋ฆฌํฐ๋ฅผ ์ด์ฉํ ํน์ ๋ฆฌ์์ค (์น์ ๊ฒฝ์ฐ์๋ ํน์ ํ URL)์ ์ ๊ทผ ์ ํ์ ํ๋ ๋ฐฉ์์๋ ํฌ๊ฐ 2๊ฐ์ง ์์.
- ์ค์ ์ ํตํด ํจํด ์ง์
- ์ด๋ ธํ ์ด์ ์ ์ด์ฉํด ์ ์ฉ
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@Log4j2
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.antMatchers("/sample/all").permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
// ์ฌ์ฉ์ ๊ณ์ ์ user1
authenticationManagerBuilder.inMemoryAuthentication().withUser("user1")
.password("$2a$10$pvtnrZLWPHqGZ/7xF5FxEO29x.UgF6lV21L16NtVfxuUtQzMQG9Nu") // 1111 ํจ์ค์๋ ์ธ์ฝ๋ฉ ๊ฒฐ๊ณผ
.roles("USER");
}
}
ํค์๋ | ์ค๋ช |
---|---|
httpSecurity.authorizeRequests() | ์ธ์ฆ์ด ํ์ํ ์์๋ค์ ์ค์ ํ๋ค |
antMatchers() | '**/*'์ ๊ฐ์ ์ํธ ์คํ์ผ์ ํจํด์ผ๋ก ์ํ๋ ์์์ ์ ํ |
permitAll() | ๋ชจ๋ ์ฌ์ฉ์์๊ฒ ์ ๊ทผ ํ๋ฝ |
๋ฐ๋ผ์ '/sample/all' ๋ ๋ก๊ทธ์ธ ์์ด๋ ์ ๊ทผ ๊ฐ๋ฅํจ
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@Log4j2
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
/*TODO-ISSUE ๋ชจ๋ URL์์ Security ํ๋ฆผ*/
httpSecurity.authorizeRequests()
.antMatchers("/sample/all").permitAll()
.antMatchers("/sample/member").hasRole("USER");
httpSecurity.formLogin(); // ์ธ๊ฐ, ์ธ์ฆ์ ๋ฌธ์ ์ ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก
}
@Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
// ์ฌ์ฉ์ ๊ณ์ ์ user1
authenticationManagerBuilder.inMemoryAuthentication().withUser("user1")
.password("$2a$10$pvtnrZLWPHqGZ/7xF5FxEO29x.UgF6lV21L16NtVfxuUtQzMQG9Nu") // 1111 ํจ์ค์๋ ์ธ์ฝ๋ฉ ๊ฒฐ๊ณผ
.roles("USER");
}
}
ํค์๋ | ์ค๋ช |
---|---|
httpSecurity.formLogin() | ์ธ๊ฐ, ์ธ์ฆ์ ๋ฌธ์ ์ ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก |
formLogin()์ ์ด์ฏ์๋ ๊ฒฝ์ฐ ๋ณ๋์ ๋์์ธ์ ์ ์ฉํ๊ธฐ ์ํด ์ถ๊ฐ์ ์ธ ์ค์ ์ด ํ์ํ๋ฉฐ loginPage()๋ loginProcessUrl(), defaultSuccessURL(), failureUrl() ๋ฑ์ ์ด์ฉํด์ ํ์ํ ์ค์ ์ ์ง์ ํ ์ ์์.
CRSF๋ ์๋ฒ์์ ๋ฐ์๋ค์ด๋ ์ ๋ณด๊ฐ ํน๋ณํ ์ฌ์ ์กฐ๊ฑด์ ๊ฒ์ฆํ์ง ์๋๋ค๋ ์ ์ ์ด์ฉํ ๊ณต๊ฒฉ ๋ฐฉ์์ด๋ค. CSRF์ ํตํด ๋จ์ ๊ฒ์๋ฌผ์ ์กฐํ์๋ฅผ ๋๋ฆฌ๋ ๋ฑ์ ์กฐ์๋ถํฐ, ํผํด์์ ๊ณ์ ์ ์ด์ฉํ ๋ค์ํ ๊ณต๊ฒฉ์ด ๊ฐ๋ฅํ๋ค.
์๋ฅผ ๋ค์ด A ์ฌ์ดํธ์๋ ํน์ ์ฌ์ฉ์ ๋ฑ๊ธ์ ๋ณ๊ฒฝํ๋ URI๊ฐ ์กด์ฌํ๋ ๊ฒ์ ๊ณต๊ฒฉ์๊ฐ ์์๊ณ ํด๋น URI์ ์ฝ๊ฐ์ ํ๋ผ๋ฏธํฐ๊ฐ ํ์ํ๋ค๋ ๊ฒ์ ์์๋ค๊ณ ๊ฐ์ ํ์.
www.aaa.xxx?update? grade=admin&account=123
๊ณต๊ฒฉ์๋ A ์ฌ์ดํธ ๊ด๋ฆฌ์๊ฐ ์์ฃผ ๋ฐฉ๋ฌธํ๋ B์ฌ์ดํธ์ ํ๊ทธ๋
๋ฅผ ์ด์ฉํ์ฌ ์์ URI๋ฅผ ์ถ๊ฐํ ๊ฒ์๋ฌผ์ ์์ฑํ๋ค.<form action="www.aaa.xxx?update? grade=admin&account=123">
<input type="submit" value="์ถ ์ด๋ฒคํธ ๋น์ฒจ">
</form>
<!--or-->
<img scr="www.aaa.xxx?update? grade=admin&account=123">
A ์ฌ์ดํธ ๊ด๋ฆฌ์๋ A์ฌ์ดํธ์ ๋ก๊ทธ์ธ๋ ์ํ๋ก ์์ ์ด ํ์์ ๋ฐฉ๋ฌธํ๋ B ์ฌ์ดํธ๋ฅผ ๋ฐฉ๋ฌธํ๊ฒ ๋๊ณ ๊ณต๊ฒฉ์๊ฐ ์์ฑํ ๊ฒ์๋ฌผ์ ๋ณด๊ฒ๋๋ฉด <img>
ํ๊ทธ ๋ฑ์ ์ฌ์ฉ๋ URI๊ฐ ํธ์ถ๋๊ณ ์๋ฒ์๋ ๋ก๊ทธ์ธํ ๊ด๋ฆฌ์์ ์์ฒญ์ ์ํด ๊ณต๊ฒฉ์๋ admin ๋ฑ๊ธ์ ์ฌ์ฉ์๋ก ๋ณ๊ฒฝ๋จ.
์ด๋ฌํ ๋ฌธ์ ๋ ์๋ฒ์์ ๋ฐ์๋ค์ด๋ ์์ฒญ์ ํด์ํ๊ณ ์ฒ๋ฆฌํ ๋ ์ด๋ค ์ถ์ฒ์์ ํธ์ถ์ด ์งํ๋์๋์ง ๋ฐ์ง์ง ์๊ธฐ ๋๋ฌธ์ ์๊ธฐ๋ฉฐ, ํ๋์ ์ฌ์ดํธ ๋ด์์๋ ๊ฐ๋ฅํจ.
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Please sign in</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous">
</head>
<body>
<div class="container">
<form class="form-signin" method="post" action="/login">
<h2 class="form-signin-heading">Please sign in</h2>
<p>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required="" autofocus="">
</p>
<p>
<label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required="">
</p>
<input name="_csrf" type="hidden" value="6df225a0-163b-4e5f-8bb9-a6421bdd9e27">
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
</div>
</body></html>
ํ์ฌ ์๋์ผ๋ก ๋ง๋ค์ด์ง ๋ก๊ทธ์ธ ํ์ด์ง์์ CSRF ํ ํฐ๊ฐ์ด ๋ฐํ๋ ๊ฒ์ ํ์ธํ ์ ์์. CSRF ํ ํฐ์ ์ธ์ ๋น ํ๋ ์์ฑ๋๋ค.
์ผ๋ฐ์ ์ธ ์ธ์
์ ์ด์ฉํ๊ณ , <form>
ํ๊ทธ๋ฅผ ์ด์ฉํ๋ ๋ฐฉ์์๋ CSRF ํ ํฐ์ด ๋ณด์์์ผ๋ก ๊ถ์ฅ๋๋, REST ๋ฐฉ์ ๋ฑ์์ ๋งค๋ฒ CSRF ํ ํฐ ๊ฐ์ ์์๋ด์ผ ํ๋ ๋ถํธํจ์ด ์๊ธฐ ๋๋ฌธ์ ๊ฒฝ์ฐ์ ๋ฐ๋ผ์ CSRF ํ ํฐ์ ๋ฐํ์ ํ์ง ์๋ ๊ฒฝ์ฐ๋ ์์.
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@Log4j2
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.antMatchers("/sample/all").permitAll()
.antMatchers("/sample/member").hasRole("USER");
httpSecurity.formLogin(); // ์ธ๊ฐ, ์ธ์ฆ์ ๋ฌธ์ ์ ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก
httpSecurity.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
// ์ฌ์ฉ์ ๊ณ์ ์ user1
authenticationManagerBuilder.inMemoryAuthentication().withUser("user1")
.password("$2a$10$pvtnrZLWPHqGZ/7xF5FxEO29x.UgF6lV21L16NtVfxuUtQzMQG9Nu") // 1111 ํจ์ค์๋ ์ธ์ฝ๋ฉ ๊ฒฐ๊ณผ
.roles("USER");
}
}
ํค์๋ | ์ค๋ช |
---|---|
httpSecurity.csrf().disable() | CSRF ํ ํฐ ๋นํ์ฑํ |
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Please sign in</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous">
</head>
<body>
<div class="container">
<form class="form-signin" method="post" action="/login">
<h2 class="form-signin-heading">Please sign in</h2>
<p>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required="" autofocus="">
</p>
<p>
<label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required="">
</p>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
</div>
</body></html>
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@Log4j2
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.antMatchers("/sample/all").permitAll()
.antMatchers("/sample/member").hasRole("USER");
httpSecurity.formLogin(); // ์ธ๊ฐ, ์ธ์ฆ์ ๋ฌธ์ ์ ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก
httpSecurity.csrf().disable();
httpSecurity.logout();
}
@Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
// ์ฌ์ฉ์ ๊ณ์ ์ user1
authenticationManagerBuilder.inMemoryAuthentication().withUser("user1")
.password("$2a$10$pvtnrZLWPHqGZ/7xF5FxEO29x.UgF6lV21L16NtVfxuUtQzMQG9Nu") // 1111 ํจ์ค์๋ ์ธ์ฝ๋ฉ ๊ฒฐ๊ณผ
.roles("USER");
}
}
์ฌ์ฉ์๊ฐ ๋ณ๋์ ๋ก๊ทธ์์ ๊ด๋ จ ์ค์ ์ ์ถ๊ฐํ๊ณ ์ถ๋ค๋ฉด logoutURL(), logoutSuccessUrl() ๋ฑ์ ์ง์ ํ ์ ์์.
์คํ๋ง ์ํ๋ฆฌํฐ๋ HttpSession์ ์ด์ฉํ๋๋ฐ invalidatedHttpSession()๊ณผ deleteCookies()๋ฅผ ์ด์ฉํด์๋ ์ฟ ํค๋ ์ธ์ ์ ๋ฌดํจํ ์ํฌ ์ ์๋ค.
์ต๊ทผ์ ์์ด๋ ๋์ ์ด๋ฉ์ผ์ ์์ด๋๋ก ๊ตฌ์ฑํด์ ํ์ (ClubMember) ์ฒ๋ฆฌ๋ฅผ ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์. ํ์ ์ ๋ณด ๊ตฌ์ฑ์ ๋ค์๊ณผ ๊ฐ๋ค.
- ์ด๋ฉ์ผ(ID)
- ํจ์ค์๋
- ์ด๋ฆ (๋๋ค์)
- ์์ ๊ฐ์ ์ฌ๋ถ (OAuth๋ก ํ์ ๊ฐ์ ๋ ๊ฒฝ์ฐ)
- ๊ธฐํ (๋ฑ๋ก์ผ/์์ ์ผ)
ํ์์ ๊ถํ์ ์๋์ ๊ฐ์ด ๋์๋ค.
- USER: ์ผ๋ฐ ํ์
- MANAGER: ์ค๊ฐ ๊ด๋ฆฌ ํ์
- ADMIN: ์ด๊ด ๊ด๋ฆฌ์
ํ ๋ช ์ ํด๋ฝ ํ์์ ์ฌ๋ฌ ๊ฐ์ ๊ถํ์ ๊ฐ์ง ์ ์๋ค.
ClubMember์ ClubMemberRole์ 1:N ๊ด๊ณ์ด๋, ์ฌ์ค์ ClubMemberRole ์์ฒด๋ ํต์ฌ์ ์ธ ์ญํ ์ ํ์ง ์๊ธฐ ๋๋ฌธ์ ๋ณ๋์ ์ํฐํฐ ์์ฑ์ด ์๋ @ElementCollection ์ ์ด์ฉํด PK ์์ด ๊ตฌ์ฑํ ๊ฒ์.
package com.example.springsecurity.entity;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@MappedSuperclass /*ํด๋น ์ด๋
ธํ
์ด์
์ด ์ ์ฉ๋ ํด๋์ค๋ ํ
์ด๋ธ๋ก ์์ฑ๋์ง ์๊ณ ์ด ํด๋์ค๋ฅผ ์์๋ฐ์ ์ํฐํฐ ํด๋์ค๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ
์ด๋ธ์ด ์์ฑ๋จ*/
@EntityListeners(value = {AuditingEntityListener.class}) /*์ํฐํฐ ๊ฐ์ฒด๊ฐ ์์ฑ/๋ณ๊ฒฝ๋๋ ๊ฒ์ ๊ฐ์ง - AuditingEntityListener*/
@Getter
public class BaseEntity {
@CreatedDate /*์ํฐํฐ ์์ฑ ์๊ฐ ์ฒ๋ฆฌ*/
@Column(name = "regdate", updatable = false)
private LocalDateTime regDate;
@LastModifiedDate /*์ต์ข
์์ ์๊ฐ ์ฒ๋ฆฌ*/
@Column(name = "moddate")
private LocalDateTime modDate;
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@SpringBootApplication
@EnableJpaAuditing
public class SpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityApplication.class, args);
}
}
package com.example.springsecurity.entity;
public enum ClubMemberRole {
USER, MANAGER, ADMIN
}
package com.example.springsecurity.entity;
import lombok.*;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import java.util.HashSet;
import java.util.Set;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class ClubMember {
@Id
private String email;
private String password;
private String name;
private boolean fromSocial;
@ElementCollection(fetch = FetchType.LAZY)
@Builder.Default
private Set<ClubMemberRole> roleSet = new HashSet<>();
public void addMemberRole(ClubMemberRole clubMemberRole) {
roleSet.add(clubMemberRole);
}
}
package com.example.springsecurity.repository;
import com.example.springsecurity.entity.ClubMember;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ClubMemberRepository extends JpaRepository<ClubMember, String> {
}
package com.example.springsecurity.security;
import com.example.springsecurity.entity.ClubMember;
import com.example.springsecurity.entity.ClubMemberRole;
import com.example.springsecurity.repository.ClubMemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.stream.IntStream;
@SpringBootTest
public class ClubMemberTests {
@Autowired
private ClubMemberRepository repository;
@Autowired
private PasswordEncoder passwordEncoder;
@Test
public void insertDummies() {
// 1 - 80 : USER
// 81 - 90: USER, MANAGER
// 91 - 100: USER, MANAGER, ADMIN
IntStream.rangeClosed(1,100).forEach(i -> {
ClubMember clubMember = ClubMember.builder()
.email("user"+i+"@outlook.com")
.name("์ฌ์ฉ์"+i)
.fromSocial(false)
.password(passwordEncoder.encode("1111"))
.build();
// default role
clubMember.addMemberRole(ClubMemberRole.USER);
if(i > 80) clubMember.addMemberRole(ClubMemberRole.MANAGER);
if(i > 90) clubMember.addMemberRole(ClubMemberRole.ADMIN);
repository.save(clubMember);
});
}
}
ClubMember์ ์กฐํ ์์ ์ด๋ฉ์ผ์ ๊ธฐ์ค์ผ๋ก ์กฐํํ๊ณ , ์ผ๋ฐ ๋ก๊ทธ์ธ ์ฌ์ฉ์์ ๋ค์ ์ถ๊ฐ๋๋ ์์ ๋ก๊ทธ์ธ ์ฌ์ฉ์๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ํด ClubMemberRepository์ ๋ณ๋์ ๋ฉ์๋๋ก ์ฒ๋ฆฌ
package com.example.springsecurity.repository;
import com.example.springsecurity.entity.ClubMember;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Optional;
public interface ClubMemberRepository extends JpaRepository<ClubMember, String> {
@EntityGraph(attributePaths = {"roleSet"}, type = EntityGraph.EntityGraphType.LOAD)
@Query("select clubmember from ClubMember clubmember where clubmember.fromSocial = :social and clubmember.email =:email")
Optional<ClubMember> findByEmail(@Param("email") String email, @Param("social") boolean social);
}
ํค์๋ | ์ค๋ช |
---|---|
@EntityGraph | Lazy ๋ก๋ฉ ์ฌ์ฉํ ๋ ํ๋ฒ์ ํจ์นํ ๋ |
package com.example.springsecurity.security;
import com.example.springsecurity.entity.ClubMember;
import com.example.springsecurity.entity.ClubMemberRole;
import com.example.springsecurity.repository.ClubMemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Optional;
import java.util.stream.IntStream;
@SpringBootTest
public class ClubMemberTests {
@Autowired
private ClubMemberRepository repository;
@Autowired
private PasswordEncoder passwordEncoder;
@Test
public void insertDummies() {
// 1 - 80 : USER
// 81 - 90: USER, MANAGER
// 91 - 100: USER, MANAGER, ADMIN
IntStream.rangeClosed(1,100).forEach(i -> {
ClubMember clubMember = ClubMember.builder()
.email("user"+i+"@outlook.com")
.name("์ฌ์ฉ์"+i)
.fromSocial(false)
.password(passwordEncoder.encode("1111"))
.build();
// default role
clubMember.addMemberRole(ClubMemberRole.USER);
if(i > 80) clubMember.addMemberRole(ClubMemberRole.MANAGER);
if(i > 90) clubMember.addMemberRole(ClubMemberRole.ADMIN);
repository.save(clubMember);
});
}
@Test
public void testRead() {
Optional<ClubMember> result = repository.findByEmail("user95@outlook.com", false);
ClubMember clubMember = result.get();
System.out.println(clubMember);
}
}
ClubMember(email=user95@outlook.com, password=$2a$10$toaBcYTzSfXApxTPNtqFU.F2WFocOBl0qSpGwouektXBwJI2GVH4O, name=์ฌ์ฉ์95, fromSocial=false, roleSet=[MANAGER, USER, ADMIN])
์คํ๋ง ์ํ๋ฆฌํฐ๊ฐ ClubMemberRepository๋ฅผ ์ด์ฉํด์ ํ์ ์ฒ๋ฆฌํ๋ ๋ถ๋ถ ์ ์
๊ธฐ์กด์ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ๊ฐ๋ฐ ๋ฐฉ์ (ํ์ ID์ ํจ์ค์๋๋ฅผ DB์์ ์กฐํํ๊ณ , ์ฌ๋ฐ๋ฅธ ๋ฐ์ดํฐ๊ฐ ์๋ฐ๋ฉด ์ธ์ ์ด๋ ์ฟ ํค๋ก ์ฒ๋ฆฌ)๊ณผ ๋ฌ๋ฆฌ ์คํ๋ง ์ํ๋ฆฌํฐ๋ ๋ค์ ๋ฐฉ์์ผ๋ก ๋์ํ๋ค.
๐ด ์คํ๋ง ์ํ๋ฆฌํฐ์์ ํ์์ด๋ ๊ณ์ ์ ๋ํด User๋ผ๋ ์ฉ์ด๋ฅผ ์ฌ์ฉ. ๋ฐ๋ผ์ User๋ผ๋ ๋จ์ด ์ฌ์ฉ์ ์ฃผ์ํ์ (์์์๋ ClubMember๋ผ๋ ๋จ์ด๋ฅผ ์ฌ์ฉํจ)
๐ด ํ์ ID ๋ผ๋ ์ฉ์ด ๋์ username์ด๋ผ๋ ๋จ์ด ์ฌ์ฉ. ์คํ๋ง ์ํ๋ฆฌํฐ์์ username ๋จ์ด ์์ฒด๊ฐ ํ์์ ๊ตฌ๋ณํ ์ ์๋ ์๋ณ ๋ฐ์ดํฐ๋ฅผ ์๋ฏธํจ. ๋ฌธ์์ด๋ก ์ฒ๋ฆฌํ๋ ์ ์ ๊ฐ์ผ๋ ์ผ๋ฐ์ ์ผ๋ก ์ฌ์ฉํ๋ ํ์์ ์ด๋ฆ์ด ์๋ id์ ํด๋นํจ.
๐ด username๊ณผ password๋ฅผ ๋์์ ์ฌ์ฉํ์ง ์๋๋ค. ์คํ๋ง ์ํ๋ฆฌํฐ๋ UserDetailService๋ฅผ ์ด์ฉํด ํ์์ ์กด์ฌ๋ง์ ์ฐ์ ์ ์ผ๋ก ๊ฐ์ ธ์ค๊ณ ์ดํ password๊ฐ ํ๋ฆฌ๋ฉด 'Bad Cridential (์๋ชป๋ ์๊ฒฉ์ฆ๋ช )'์ด๋ผ๋ ๊ฒฐ๊ณผ๋ฅผ ๋ง๋ค์ด ๋ (์ธ์ฆ)
๐ด ์ฌ์ฉ์์ username๊ณผ password๋ก ์ธ์ฆ ๊ณผ์ ์ด ๋๋๋ฉด ์ํ๋ ์์ (URL)์ ์ ๊ทผํ ์ ์๋ ์ ์ ํ ๊ถํ์ด ์๋์ง ํ์ธํ๊ณ ์ธ๊ฐ ๊ณผ์ ์ ์คํํจ. ์ด ๊ณผ์ ์์ 'Access Denied'์ ๊ฐ์ ๊ฒฐ๊ณผ๊ฐ ๋ง๋ค์ด์ง
loadUserName()์ username๋ฅผ ์๋ณ ๊ฐ์ผ๋ก ํ์ ์ ๋ณด (UserDetails)๋ฅผ ๊ฐ์ง๊ณ ์ค๋๋ฐ ์ด๋ฅผ ํตํด ๋ค์ ์ ๋ณด๋ฅผ ์ ์ ์์
ํค์๋ | ์ค๋ช |
---|---|
getAuthorities() | ์ฌ์ฉ์๊ฐ ๊ฐ์ง๋ ๊ถํ์ ๋ํ ์ ๋ณด |
getPassword() | ์ธ์ฆ์ ๋ง๋ฌด๋ฆฌํ๊ธฐ ์ํ ํจ์ค์๋ ์ ๋ณด |
getUsername() | ์ธ์ฆ์ ํ์ํ ์์ด๋์ ๊ฐ์ ์ ๋ณด |
๊ณ์ ๋ง๋ฃ ์ฌ๋ถ | ๋ ์ด์ ์ฌ์ฉ์ด ๋ถ๊ฐ๋ฅํ ๊ณ์ ์ธ์ง ์ ์ ์๋ ์ ๋ณด |
๊ณ์ ์ ๊น ์ฌ๋ถ | ํ์ฌ ๊ณ์ ์ ์ ๊น ์ฌ๋ถ |
์ด๋ฅผ ์ํด ClubMember๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์ฒ๋ฆฌํ ์ ์์
- ๊ธฐ์กด DTO ํด๋์ค์ UserDetails ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๋ ๋ฐฉ๋ฒ
- DTO์ ๊ฐ์ ๊ฐ๋ ์ผ๋ก ๋ณ๋์ ํด๋์ค๋ฅผ ๊ตฌ์ฑํ๊ณ ์ด๋ฅผ ํ์ฉํ๋ ๋ฐฉ๋ฒ
package com.example.springsecurity.security.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
@Log4j2
@Getter
@Setter
@ToString
public class ClubAuthMemberDTO extends User {
private String email;
private String name;
private boolean fromSocial;
public ClubAuthMemberDTO(String username, String password, boolean fromSocial, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
this.email = username;
this.fromSocial = fromSocial;
}
}
ClubAuthMemberDTO๋ DTO ์ญํ ์ ์ํํ๋ ํด๋์ค์ธ ๋์์ ์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ธ๊ฐ/์ธ์ฆ ์์ ์ ์ฌ์ฉํ ์ ์๋ค. password๋ ๋ถ๋ชจ ํด๋์ค๋ฅผ ์ฌ์ฉํ๋ฏ๋ก ๋ณ๋์ ๋ฉค๋ฒ ๋ณ์๋ก ์ ์ธํ์ง ์๋๋ค.