Skip to content

Latest commit

ย 

History

History
1309 lines (884 loc) ยท 46.3 KB

README.md

File metadata and controls

1309 lines (884 loc) ยท 46.3 KB

SpringBoot ์™€ Spring Security ์—ฐ๋™

โœ”๏ธ ํ•™์Šตํ•  ๋‚ด์šฉ

  • ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ ์ œ๊ณตํ•˜๋Š” ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์˜ ์ดํ•ด

  • 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


  • 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. ์‚ฌ์šฉ์ž๋Š” ์€ํ–‰์— ๊ฐ€์„œ ์ž์‹ ์ด ์–ด๋–ค ์‚ฌ๋žŒ์ธ์ง€ ์ž์‹ ์˜ ์‹ ๋ถ„์ฆ์œผ๋กœ ์ฆ๋ช…ํ•œ๋‹ค.
  2. ์€ํ–‰์—์„œ๋Š” ์‚ฌ์šฉ์ž์˜ ์‹ ๋ถ„์„ ํ™•์ธํ•œ๋‹ค.
  3. ์€ํ–‰์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ๊ธˆ๊ณ ๋ฅผ ์—ด์–ด๋ณผ ์ˆ˜ ์žˆ๋Š” ์‚ฌ๋žŒ์ธ์ง€๋ฅผ ํŒ๋‹จํ•œ๋‹ค.
  4. ๋งŒ์ผ ์ ์ ˆํ•œ ๊ด€๋ฆฌ๋‚˜ ๊ถŒํ•œ์ด ์žˆ๋Š” ์‚ฌ์šฉ์ž์˜ ๊ฒฝ์šฐ ๊ธˆ๊ณ ๋ฅผ ์—ด์–ด์ค€๋‹ค.

์œ„์˜ ๊ณผ์ •์—์„œ 1์€ ์ธ์ฆ์— ํ•ด๋‹นํ•˜๋Š” ์ž‘์—…์œผ๋กœ ์ž์‹ ์„ '์ฆ๋ช…'ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. 3์—์„œ๋Š” ์‚ฌ์šฉ์ž๋ฅผ '์ธ๊ฐ€'ํ•˜๋Š” ์ผ์ข…์˜ ํ—ˆ๊ฐ€๋ฅผ ํ•ด ์ฃผ๋Š” ๊ณผ์ •์œผ๋กœ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ์—ญ์‹œ ๋‚ด๋ถ€์ ์œผ๋กœ ์œ„์™€ ์œ ์‚ฌํ•œ ๊ณผ์ •์„ ๊ฑฐ์ณ์„œ ๋™์ž‘ํ•œ๋‹ค.


โœ” ํ•„ํ„ฐ์™€ ํ•„ํ„ฐ ์ฒด์ด๋‹

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ ํ•„ํ„ฐ (Filter)๋Š” ์„œ๋ธ”๋ฆฟ์ด๋‚˜ JSP ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํ•„ํ„ฐ์™€ ๊ฐ™์€ ๊ฐœ๋…์ด์ง€๋งŒ, ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ๋Š” ์Šคํ”„๋ง์˜ ๋นˆ๊ณผ ์—ฐ๋™ํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ๋กœ ์„ค๊ณ„๋˜์–ด ์žˆ๋‹ค. ์ผ๋ฐ˜์ ์ธ ํ•„ํ„ฐ๋Š” ์Šคํ”„๋ง์˜ ๋นˆ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„์˜ ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›๋Š” ํ˜•ํƒœ๊ฐ€ ๋งŽ๋‹ค.

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์˜ ๋‚ด๋ถ€์—๋Š” ์—ฌ๋Ÿฌ ๊ฐœ์˜ ํ•„ํ„ฐ๊ฐ€ Filter Chain ์ด๋ผ๋Š” ๊ตฌ์กฐ๋กœ Request ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋œ๋‹ค. ์•ž์—์„œ ์‹คํ–‰๋˜์—ˆ๋˜ ๋กœ๊ทธ๋ฅผ ์‚ดํŽด๋ณด๋ฉด 15๊ฐœ ์ •๋„์˜ ํ•„ํ„ฐ๊ฐ€ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ฐœ๋ฐœ ์‹œ์— ํ•„ํ„ฐ๋ฅผ ํ™•์žฅํ•˜๊ณ  ์„ค์ •ํ•˜๋ฉด ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋ฅผ ์ด์šฉํ•ด์„œ ๋‹ค์–‘ํ•œ ํ˜•ํƒœ์˜ ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋œ๋‹ค. ์•„๋ž˜๋Š” ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ๋‚ด๋ถ€์— ์‚ฌ์šฉ๋˜๋Š” ์ฃผ์š” ํ•„ํ„ฐ์ด๋‹ค.


โœ” ์ธ์ฆ์„ ์œ„ํ•œ AuthenticationManager

ํ•„ํ„ฐ์˜ ํ•ต์‹ฌ์ ์ธ ๋™์ž‘์€ 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 ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.



โœ” ์ธ๊ฐ€ (Authorization)์™€ ๊ถŒํ•œ/ ์ ‘๊ทผ ์ œํ•œ

์ธ์ฆ์ฒ˜๋ฆฌ ๋‹จ๊ณ„ ์ดํ›„ ์‚ฌ์šฉ์ž ๊ถŒํ•œ์„ ํ™•์ธํ•˜์—ฌ ์ ‘๊ทผ ์ œํ•œ์„ ๋‘ . (Access-Control)

  • ๐Ÿ“‹ url ์•„๋ฌด๊ฑฐ๋‚˜ ์ž…๋ ฅ

  • ๐Ÿ“‹ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ ์ธ์ฆ/์ธ๊ฐ€๊ฐ€ ํ•„์š”ํ•˜๋‹ค๊ณ  ํŒ๋‹จ. ์‚ฌ์šฉ์ž๊ฐ€ ์ธ์ฆํ•˜๋„๋ก ๋กœ๊ทธ์ธ ํ™”๋ฉด์œผ๋กœ redirect

  • ๐Ÿ“‹ ์ž„์‹œ ๊ณ„์ •, ๋น„๋ฒˆ ์ž…๋ ฅ

  • ๐Ÿ“‹ ์ •๋ณด๊ฐ€ ์ „๋‹ฌ๋˜๋ฉด AuthenticationManager๊ฐ€ ์ ์ ˆํ•œ AuthenticationProvider๋ฅผ ์ฐพ์•„์„œ ์ธ์ฆ ์‹œ๋„

AuthenticationProvider์˜ ์‹ค์ œ ๋™์ž‘์€ UserDetailService๋ฅผ ๊ตฌํ˜„ํ•œ ๊ฐ์ฒด๋กœ ์ฒ˜๋ฆฌ



โญ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•

โœ” PasswordEncoder

ํŒจ์Šค์›Œ๋“œ๋ฅผ ์ธ์ฝ”๋”ฉํ•˜๋Š” ๊ฐ์ฒด (์•”ํ˜ธํ™”)์ž„. ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ์—ฌ๋Ÿฌ ์ข…๋ฅ˜์˜ PasswordEncoder๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ์œผ๋ฉฐ ๊ทธ ์ค‘ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ BCryptPasswordEncoder๋ผ๋Š” ํด๋ž˜์Šค์ด๋‹ค.

  • BCryptPasswordEncoder 'bcrypt'๋ผ๋Š” ํ•ด์‹œ ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•ด ํŒจ์Šค์›Œ๋“œ๋ฅผ ์•”ํ˜ธํ™”ํ•จ. ์•”ํ˜ธํ™”๋œ ํŒจ์Šค์›Œ๋“œ๋Š” ๋ณตํ˜ธํ™”๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•˜๊ณ  ๋งค๋ฒˆ ์•”ํ˜ธํ™”๋œ ๊ฐ’๋„ ๋‹ค๋ฅด๊ฒŒ ๋จ. ๋Œ€์‹  ํŠน์ •ํ•œ ๋ฌธ์ž์—ด์ด ์•”ํ˜ธํ™”๋œ ๊ฒฐ๊ณผ์ธ์ง€๋งŒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ณ  ์›๋ณธ์˜ ๋‚ด์šฉ์„ ๋ณผ ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ์ตœ๊ทผ์— ๋งŽ์ด ์‚ฌ์šฉ๋จ. SecurityConfig๋Š” @Bean์„ ํ†ตํ•ด BCryptPasswordEncoder๋ฅผ ์ง€์ •

โœ SecurityConfig
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();
    }

}

๐Ÿ“‹ PasswordEncoder ํ…Œ์ŠคํŠธ

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);

    }
}

๐Ÿ“‹ PasswordEncoder ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ

# first ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ
enPW: $2a$10$PHKK0aBGdOWborx8QJfJd.96.OfvKL47wUOuoFpuK2sr0/wzYfgw2
matchResult: true

# second ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ
enPW: $2a$10$pvtnrZLWPHqGZ/7xF5FxEO29x.UgF6lV21L16NtVfxuUtQzMQG9Nu
matchResult: true


โœ” AuthenticationManager ์„ค์ •

์•”ํ˜ธํ™”๋œ ํŒจ์Šค์›Œ๋“œ๋ฅผ ์ด์šฉํ•˜๊ธฐ ์œ„ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ํ•„์š”ํ•จ. ์ด๋ฅผ ์œ„ํ•ด AuthenticationManager์˜ ์„ค์ •์„ ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” Configure() ๋ฉ”์„œ๋“œ ์˜ค๋ฒ„๋ผ์ด๋”ฉ ์ฒ˜๋ฆฌ


๐Ÿ“‹ SecurityConfig

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");
    }

}

๐Ÿ“‹ PasswordEncoder ํ…Œ์ŠคํŠธ




โœ” ์ธ๊ฐ€(Authorization)๊ฐ€ ํ•„์š”ํ•œ ๋ฆฌ์†Œ์Šค ์„ค์ •

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋ฅผ ์ด์šฉํ•œ ํŠน์ • ๋ฆฌ์†Œ์Šค (์›น์˜ ๊ฒฝ์šฐ์—๋Š” ํŠน์ •ํ•œ URL)์— ์ ‘๊ทผ ์ œํ•œ์„ ํ•˜๋Š” ๋ฐฉ์‹์—๋Š” ํฌ๊ฐœ 2๊ฐ€์ง€ ์žˆ์Œ.

  1. ์„ค์ •์„ ํ†ตํ•ด ํŒจํ„ด ์ง€์ •
  2. ์–ด๋…ธํ…Œ์ด์…˜์„ ์ด์šฉํ•ด ์ ์šฉ

๐Ÿ“‹ SecurityConfig

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' ๋Š” ๋กœ๊ทธ์ธ ์—†์ด๋„ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•จ


๐Ÿ“‹ ๊ฒฐ๊ณผ



๐Ÿ“‹ SecurityConfig

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() ๋“ฑ์„ ์ด์šฉํ•ด์„œ ํ•„์š”ํ•œ ์„ค์ •์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Œ.


๐Ÿ“‹ ๊ฒฐ๊ณผ




โœ” CSRF ์„ค์ • (Cross Site Request Forgery, ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ ์š”์ฒญ ์œ„์กฐ)

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 ๋“ฑ๊ธ‰์˜ ์‚ฌ์šฉ์ž๋กœ ๋ณ€๊ฒฝ๋จ. ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋Š” ์„œ๋ฒ„์—์„œ ๋ฐ›์•„๋“ค์ด๋Š” ์š”์ฒญ์„ ํ•ด์„ํ•˜๊ณ  ์ฒ˜๋ฆฌํ•  ๋•Œ ์–ด๋–ค ์ถœ์ฒ˜์—์„œ ํ˜ธ์ถœ์ด ์ง„ํ–‰๋˜์—ˆ๋Š”์ง€ ๋”ฐ์ง€์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ƒ๊ธฐ๋ฉฐ, ํ•˜๋‚˜์˜ ์‚ฌ์ดํŠธ ๋‚ด์—์„œ๋„ ๊ฐ€๋Šฅํ•จ.


๐Ÿ“‹ ํ˜„์žฌ login form

<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 ํ† ํฐ์˜ ๋ฐœํ–‰์„ ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๋„ ์žˆ์Œ.



โœ” CSRF ํ† ํฐ ๋น„ํ™œ์„ฑํ™”

๐Ÿ“‹ SecurityConfig

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>



โœ” logout ์„ค์ •

๐Ÿ“‹ SecurityConfig

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()๋ฅผ ์ด์šฉํ•ด์„œ๋„ ์ฟ ํ‚ค๋‚˜ ์„ธ์…˜์„ ๋ฌดํšจํ™” ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.




โœ” ํ”„๋กœ์ ํŠธ๋ฅผ ์œ„ํ•œ JPA ์ฒ˜๋ฆฌ

์ตœ๊ทผ์—” ์•„์ด๋”” ๋Œ€์‹  ์ด๋ฉ”์ผ์„ ์•„์ด๋””๋กœ ๊ตฌ์„ฑํ•ด์„œ ํšŒ์› (ClubMember) ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Œ. ํšŒ์› ์ •๋ณด ๊ตฌ์„ฑ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

ํšŒ์› (ClubMember)
  • ์ด๋ฉ”์ผ(ID)
  • ํŒจ์Šค์›Œ๋“œ
  • ์ด๋ฆ„ (๋‹‰๋„ค์ž„)
  • ์†Œ์…œ ๊ฐ€์ž… ์—ฌ๋ถ€ (OAuth๋กœ ํšŒ์› ๊ฐ€์ž…๋œ ๊ฒฝ์šฐ)
  • ๊ธฐํƒ€ (๋“ฑ๋ก์ผ/์ˆ˜์ •์ผ)

ํšŒ์›์˜ ๊ถŒํ•œ์€ ์•„๋ž˜์™€ ๊ฐ™์ด ๋‘์—ˆ๋‹ค.

๊ถŒํ•œ (ClubMemberRole)
  • USER: ์ผ๋ฐ˜ ํšŒ์›
  • MANAGER: ์ค‘๊ฐ„ ๊ด€๋ฆฌ ํšŒ์›
  • ADMIN: ์ด๊ด„ ๊ด€๋ฆฌ์ž

ํ•œ ๋ช…์˜ ํด๋Ÿฝ ํšŒ์›์€ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๊ถŒํ•œ์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค.

ClubMember์™€ ClubMemberRole์€ 1:N ๊ด€๊ณ„์ด๋‚˜, ์‚ฌ์‹ค์ƒ ClubMemberRole ์ž์ฒด๋Š” ํ•ต์‹ฌ์ ์ธ ์—ญํ• ์„ ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„์˜ ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ์ด ์•„๋‹Œ @ElementCollection ์„ ์ด์šฉํ•ด PK ์—†์ด ๊ตฌ์„ฑํ•  ๊ฒƒ์ž„.


๐Ÿ“‹ BaseEntity

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;

}

๐Ÿ“‹ SpringSecurityApplication

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);
    }
}

๐Ÿ“‹ ClubMemberRole

package com.example.springsecurity.entity;

public enum ClubMemberRole {
    USER, MANAGER, ADMIN
}

๐Ÿ“‹ ClubMember

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);
    }

}

๐Ÿ“‹ ๊ฒฐ๊ณผ




โœ” Repository์™€ ๋”๋ฏธ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ํ•˜๊ธฐ


๐Ÿ“‹ ClubMemberRepository

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> {

}

๐Ÿ“‹ ClubMemberTests

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์— ๋ณ„๋„์˜ ๋ฉ”์„œ๋“œ๋กœ ์ฒ˜๋ฆฌ


๐Ÿ“‹ 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 ๋กœ๋”ฉ ์‚ฌ์šฉํ•  ๋•Œ ํ•œ๋ฒˆ์— ํŒจ์น˜ํ•  ๋•Œ

๐Ÿ“‹ ClubMemberTests

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])



โœ” ์‹œํ๋ฆฌํ‹ฐ๋ฅผ ์œ„ํ•œ UserDetailService

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๊ฐ€ ClubMemberRepository๋ฅผ ์ด์šฉํ•ด์„œ ํšŒ์› ์ฒ˜๋ฆฌํ•˜๋Š” ๋ถ€๋ถ„ ์ œ์ž‘

๊ธฐ์กด์˜ ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ๊ฐœ๋ฐœ ๋ฐฉ์‹ (ํšŒ์› ID์™€ ํŒจ์Šค์›Œ๋“œ๋ฅผ DB์—์„œ ์กฐํšŒํ•˜๊ณ , ์˜ฌ๋ฐ”๋ฅธ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋”ฐ๋ฉด ์„ธ์…˜์ด๋‚˜ ์ฟ ํ‚ค๋กœ ์ฒ˜๋ฆฌ)๊ณผ ๋‹ฌ๋ฆฌ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ๋‹ค์Œ ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค.

๐ŸŒด ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์…˜ ํšŒ์›์ด๋‚˜ ๊ณ„์ •์— ๋Œ€ํ•ด User๋ผ๋Š” ์šฉ์–ด๋ฅผ ์‚ฌ์šฉ. ๋”ฐ๋ผ์„œ User๋ผ๋Š” ๋‹จ์–ด ์‚ฌ์šฉ์„ ์ฃผ์˜ํ•˜์ž (์•ž์—์„œ๋„ ClubMember๋ผ๋Š” ๋‹จ์–ด๋ฅผ ์‚ฌ์šฉํ•จ)
๐ŸŒด ํšŒ์› ID ๋ผ๋Š” ์šฉ์–ด ๋Œ€์‹  username์ด๋ผ๋Š” ๋‹จ์–ด ์‚ฌ์šฉ. ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„  username ๋‹จ์–ด ์ž์ฒด๊ฐ€ ํšŒ์›์„ ๊ตฌ๋ณ„ํ•  ์ˆ˜ ์žˆ๋Š” ์‹๋ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ์˜๋ฏธํ•จ. ๋ฌธ์ž์—ด๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ์ ์€ ๊ฐ™์œผ๋‚˜ ์ผ๋ฐ˜์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ํšŒ์›์˜ ์ด๋ฆ„์ด ์•„๋‹Œ id์— ํ•ด๋‹นํ•จ.
๐ŸŒด username๊ณผ password๋ฅผ ๋™์‹œ์— ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค. ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” UserDetailService๋ฅผ ์ด์šฉํ•ด ํšŒ์›์˜ ์กด์žฌ๋งŒ์„ ์šฐ์„ ์ ์œผ๋กœ ๊ฐ€์ ธ์˜ค๊ณ  ์ดํ›„ password๊ฐ€ ํ‹€๋ฆฌ๋ฉด 'Bad Cridential (์ž˜๋ชป๋œ ์ž๊ฒฉ์ฆ๋ช…)'์ด๋ผ๋Š” ๊ฒฐ๊ณผ๋ฅผ ๋งŒ๋“ค์–ด ๋ƒ„ (์ธ์ฆ)
๐ŸŒด ์‚ฌ์šฉ์ž์˜ username๊ณผ password๋กœ ์ธ์ฆ ๊ณผ์ •์ด ๋๋‚˜๋ฉด ์›ํ•˜๋Š” ์ž์› (URL)์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ์ ์ ˆํ•œ ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์ธ๊ฐ€ ๊ณผ์ •์„ ์‹คํ–‰ํ•จ. ์ด ๊ณผ์ •์—์„  'Access Denied'์™€ ๊ฐ™์€ ๊ฒฐ๊ณผ๊ฐ€ ๋งŒ๋“ค์–ด์ง




โœ” UserDetails ์ธํ„ฐํŽ˜์ด์Šค

loadUserName()์€ username๋ฅผ ์‹๋ณ„ ๊ฐ’์œผ๋กœ ํšŒ์› ์ •๋ณด (UserDetails)๋ฅผ ๊ฐ€์ง€๊ณ  ์˜ค๋Š”๋ฐ ์ด๋ฅผ ํ†ตํ•ด ๋‹ค์Œ ์ •๋ณด๋ฅผ ์•Œ ์ˆ˜ ์žˆ์Œ

ํ‚ค์›Œ๋“œ ์„ค๋ช…
getAuthorities() ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ€์ง€๋Š” ๊ถŒํ•œ์— ๋Œ€ํ•œ ์ •๋ณด
getPassword() ์ธ์ฆ์„ ๋งˆ๋ฌด๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ํŒจ์Šค์›Œ๋“œ ์ •๋ณด
getUsername() ์ธ์ฆ์— ํ•„์š”ํ•œ ์•„์ด๋””์™€ ๊ฐ™์€ ์ •๋ณด
๊ณ„์ • ๋งŒ๋ฃŒ ์—ฌ๋ถ€ ๋” ์ด์ƒ ์‚ฌ์šฉ์ด ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ณ„์ •์ธ์ง€ ์•Œ ์ˆ˜ ์žˆ๋Š” ์ •๋ณด
๊ณ„์ • ์ž ๊น€ ์—ฌ๋ถ€ ํ˜„์žฌ ๊ณ„์ •์˜ ์ž ๊น€ ์—ฌ๋ถ€

์ด๋ฅผ ์œ„ํ•ด ClubMember๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ

  • ๊ธฐ์กด DTO ํด๋ž˜์Šค์— UserDetails ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•
  • DTO์™€ ๊ฐ™์€ ๊ฐœ๋…์œผ๋กœ ๋ณ„๋„์˜ ํด๋ž˜์Šค๋ฅผ ๊ตฌ์„ฑํ•˜๊ณ  ์ด๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•

๐Ÿ“‹ ClubAuthMemberDTO

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๋Š” ๋ถ€๋ชจ ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ๋ณ„๋„์˜ ๋ฉค๋ฒ„ ๋ณ€์ˆ˜๋กœ ์„ ์–ธํ•˜์ง€ ์•Š๋Š”๋‹ค.