๋ณธ ๊ธ€์€ OKKY์— ๊ณต์œ ๋œ ์ดˆ๋ณด๊ฐ€ ์ดํ•ดํ•˜๋Š” ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ์ž…๋‹ˆ๋‹ค.

๋“ค์–ด๊ฐ€๋ฉฐ

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋ฅผ ์ดํ•ดํ•˜๊ธฐ ์œ„ํ•ด์„œ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๊ฐ€ ๋ฌด์—‡์ธ์ง€๋ฅผ ์•Œ์•„์•ผํ•ฉ๋‹ˆ๋‹ค. ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ๋ ˆํผ๋Ÿฐ์Šค์—์„œ๋Š” ์ž๋ฐ” EE ๊ธฐ๋ฐ˜์˜ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ์†Œํ”„ํŠธ์›จ์–ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ํฌ๊ด„์ ์ธ ๋ณด์•ˆ ์„œ๋น„์Šค๋“ค์„ ์ œ๊ณตํ•˜๊ณ  ์˜คํ”ˆ ํ”Œ๋žซํผ์ด๋ฉด์„œ ์ž์‹ ๋งŒ์˜ ์ธ์ฆ ๋งค์ปค๋‹ˆ์ฆ˜์„ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ž๋ž‘(?)ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ, ์‹ ์ž… ๊ฐœ๋ฐœ์ž๋“ค ์ˆ˜์ค€์—์„œ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์™€ ๊ฐ™์€ ๋ณด์•ˆ ๊ธฐ์ˆ ์„ ์ดํ•ดํ•˜๊ธฐ๋ž€ ์ฐธ ํž˜๋“  ๊ณผ์ •์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

์ €๋งŒ ์ดํ•ดํ•˜๊ธฐ ํž˜๋“ค ์ˆ˜ ์žˆ์–ด์š” ใ… ใ… 

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋ฅผ ์ดํ•ดํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ณด์•ˆ์„ ๊ตฌ์„ฑํ•˜๋Š” ๋‘๊ฐ€์ง€ ์˜์—ญ์— ๋Œ€ํ•ด์„œ ์•Œ์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ”๋กœ ์ธ์ฆ(Authentication)๊ณผ ๊ถŒํ•œ(Authorization)์ด๋ผ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด ๋‘ ์˜์—ญ์€ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์˜ ๋ชฉํ‘œ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ˜๋“œ์‹œ ์ดํ•ดํ•˜๊ณ  ๋„˜์–ด๊ฐ€์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ธ์ฆ์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ์ฃผ์ฒด(์‚ฌ์šฉ์ž)๋ผ๊ณ  ์ฃผ์žฅํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์„ ๋งํ•˜๋ฉฐ ๊ถŒํ•œ์€ ์ธ์ฆ๋œ ์ฃผ์ฒด๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ—ˆ๋ฝ๋˜์žˆ๋Š”์ง€๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ๊ฒƒ์„ ๋งํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, ๊ถŒํ•œ ์Šน์ธ์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„์œผ๋กœ ์ ‘๊ทผํ•˜๋ ค๋ฉด ์ธ์ฆ ๊ณผ์ •์„ ํ†ตํ•ด ์ฃผ์ฒด๊ฐ€ ์ฆ๋ช… ๋˜์–ด์•ผ๋งŒ ํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์•ž์„œ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ์ž์‹ ๋งŒ์˜ ์ธ์ฆ ๋งค์ปค๋‹ˆ์ฆ˜์„ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ž๋ž‘ํ•œ๋‹ค๊ณ  ํ–ˆ๋Š”๋ฐ์š”. ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ์ด์™€ ๊ด€๋ จํ•ด์„œ ์ธ์ฆ ๋งค์ปค๋‹ˆ์ฆ˜๊ณผ ๊ด€๊ณ„์—†์ด ์ƒ๋‹นํžˆ ๊นŠ์€ ๊ถŒํ•œ ๋ถ€์—ฌ๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ถŒํ•œ ๋ถ€์—ฌ์—๋„ ๋‘๊ฐ€์ง€ ์˜์—ญ์ด ์กด์žฌํ•˜๋Š”๋ฐ ์›น ์š”์ฒญ ๊ถŒํ•œ, ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ ๋ฐ ๋„๋ฉ”์ธ ์ธ์Šคํ„ด์Šค์— ๋Œ€ํ•œ ์ ‘๊ทผ ๊ถŒํ•œ ๋ถ€์—ฌ์ž…๋‹ˆ๋‹ค. ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ์ด๋ ‡๊ฒŒ ๋ชจ๋“  ์ค‘์š”ํ•œ ์˜์—ญ์— ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ œํƒ€๊ฑด๋‹ด๋‹˜์˜ ๊ธ€์—์„œ๋Š” ๋กœ๊ทธ์ธ ํ™”๋ฉด์„ ํ†ตํ•ด์„œ ์•„์ด๋””์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅ๋ฐ›์•„ ๋กœ๊ทธ์ธํ•˜๋Š” ๊ณผ์ •์„ ์ธ์ฆ์ด๋ผ๊ณ  ์„œ์ˆ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด์™€ ๊ด€๋ จ๋œ ์šฉ์–ด๊ฐ€ ๋ฐ”๋กœ HTTP ๊ธฐ๋ณธ ์ธ์ฆ(HTTP Basic Authentication) ๋งค์ปค๋‹ˆ์ฆ˜์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด HTTP ๊ธฐ๋ณธ ์ธ์ฆ ๋งค์ปค๋‹ˆ์ฆ˜์„ ์ด์šฉํ•˜๋Š” ๋ฐฉ์‹์ด ๋ฐ”๋กœ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ๋ ˆํผ๋Ÿฐ์Šค์—์„œ ์„ค๋ช…ํ•˜๋Š” ํผ ๊ธฐ๋ฐ˜ ๋กœ๊ทธ์ธ์ด ๋ฉ๋‹ˆ๋‹ค.

์•„ ๋ฌผ๋ก , HTTP ๊ธฐ๋ณธ ์ธ์ฆ == ํผ ๊ธฐ๋ฐ˜ ๋กœ๊ทธ์ธ์ด๋ผ๋Š” ๋ง์€ ์•„๋‹™๋‹ˆ๋‹ค. ํผ ์–‘์‹์„ ํ†ตํ•œ ๋กœ๊ทธ์ธ๋„ username๊ณผ password๋ฅผ ํ†ตํ•ด ์ธ์ฆํ•˜๊ธฐ ๋•Œ๋ฌธ์— HTTP ๊ธฐ๋ณธ ์ธ์ฆ ๋งค์ปค๋‹ˆ์ฆ˜์„ ์ด์šฉํ•˜๋Š” ๋ฐฉ์‹์ด๋ผ๊ณ  ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์‹ค์ œ๋กœ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ๋ ˆํผ๋Ÿฐ์Šค ๋ฌธ์„œ์˜ Basic and Digest Authentication์—์„œ๋Š” ๋‹ค์Œ์„ ์–ธ๊ธ‰ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

Basic authentication is often used with stateless clients which pass their credentials on each request. Itโ€™s quite common to use it in combination with form-based authentication where an application is used through both a browser-based user interface and as a web-service.

์ธ์ฆ๊ณผ ๊ถŒํ•œ์ด๋ผ๋Š” ๊ฐœ๋…์„ ์ดํ•ดํ•˜์…จ๋‹ค๋ฉด ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์— ๋Œ€ํ•ด์„œ ๋ฐฐ์›Œ๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ์‹œ์ž‘ํ•˜๊ธฐ

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ๋ฉ”์ด๋ธ์ด๋‚˜ ๊ทธ๋ž˜๋“ค๊ฐ™์€ ๋นŒ๋“œ๋„๊ตฌ๋ฅผ ํ†ตํ•ด ์‰ฝ๊ฒŒ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

with Maven

<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-web</artifactId>
	<version>4.2.2.RELEASE</version>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-config</artifactId>
	<version>4.2.2.RELEASE</version>
</dependency>
</dependencies>

with Gradle

dependencies {
	compile 'org.springframework.security:spring-security-web:4.2.2.RELEASE'
	compile 'org.springframework.security:spring-security-config:4.2.2.RELEASE'
}

์ €๋Š” ๋ฉ”์ด๋ธ ๋Œ€์‹  ๊ทธ๋ž˜๋“ค์„ ์‚ฌ์šฉํ•ด๋ดค์Šต๋‹ˆ๋‹ค.

Java Configuration

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ๋ ˆํผ๋Ÿฐ์Šค์—์„œ๋Š” ์ž๋ฐ” ๊ธฐ๋ฐ˜์˜ ์„ค์ •์œผ๋กœ ์„ค๋ช…ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ์ด์œ ๋Š” ๋ฌด์—‡์ผ๊นŒ์š”?

์Šคํ”„๋ง ํ”„๋ ˆ์ž„์›Œํฌ 3.1์—์„œ๋ถ€ํ„ฐ ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•œ ์ž๋ฐ” ์„ค์ •์„ ์ง€์›ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ 3.2๋ถ€ํ„ฐ๋Š” XML๋กœ ์„ค์ •ํ•˜์ง€ ์•Š๊ณ ๋„ ๊ฐ„๋‹จํ•˜๊ฒŒ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง€์›ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์›๋ž˜ XML ๊ธฐ๋ฐ˜์˜ ์„ค์ •์—์„œ๋Š” web.xml์— org.springframework.web.filter.DelegatingFilterProxy๋ผ๋Š” springSecurityFilterChain์„ ๋“ฑ๋กํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค๋งŒ, ์ž๋ฐ” ๊ธฐ๋ฐ˜์˜ ์„ค์ •์—์„œ๋Š” WebSecurityConfigurerAdapter๋ฅผ ์ƒ์†๋ฐ›์€ ํด๋ž˜์Šค์— @EnableWebSecurity ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ช…์‹œํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ๋„ springSecurityFilterChain๊ฐ€ ์ž๋™์œผ๋กœ ํฌํ•จ๋˜์–ด์ง‘๋‹ˆ๋‹ค.

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}

๊ทธ๋ฆฌ๊ณ  ํฌํ•จ๋œ springSecurityFilterChain์„ ๋“ฑ๋กํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” AbstractSecurityWebApplicationInitializer๋ฅผ ์ƒ์†๋ฐ›์€ WebApplicationInitializer๋ฅผ ๋งŒ๋“ค์–ด๋‘๋ฉด ๋ฉ๋‹ˆ๋‹ค.

public class SecurityWebApplicationInitializer
	extends AbstractSecurityWebApplicationInitializer {

}

XML์„ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค๋Š” ์ž๋ฐ” ๊ธฐ๋ฐ˜์˜ ๊ตฌ์„ฑ์ด ๋”์šฑ ์‰ฝ์Šต๋‹ˆ๋‹ค.

๊ทธ ๋‹ค์Œ์—๋Š” ๋ณดํ†ต ์Šคํ”„๋ง MVC๋ฅผ ์ด์šฉํ•ด์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์„ฑํ•˜๊ธฐ ๋•Œ๋ฌธ์— ApplicationIniticalizer์— WebSecurityConfigurerAdapter๋ฅผ ์ƒ์†๋ฐ›์€ ํด๋ž˜์Šค๋ฅผ getRootConfigClasses() ๋ฉ”์†Œ๋“œ์— ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์— ๋Œ€ํ•œ ๊ธฐ๋ณธ์ ์ธ ์ ์šฉ์€ ๋์ž…๋‹ˆ๋‹ค.

public class ApplicationInitializer extends
		AbstractAnnotationConfigDispatcherServletInitializer {

	@Override
	protected Class<?>[] getRootConfigClasses() {
		return new Class[] { WebSecurityConfig.class };
	}

	// ... other overrides ...
}

์ด๋กœ์จ ์œ ์ถ” ํ•ด๋ณผ๋•Œ, springSecurityFilterChain์€ ๋ฃจํŠธ ์ปจํ…์ŠคํŠธ์—์„œ ๋“ฑ๋ก๋˜์–ด์•ผํ•˜๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ฐ ํ•„ํ„ฐ์— ๋Œ€ํ•œ ์šฐ์„ ์ˆœ์œ„์— ๋Œ€ํ•ด์„œ๋„ ์ถ”๊ฐ€์ ์œผ๋กœ ์•Œ ํ•„์š”์„ฑ์ด ์ƒ๊ธฐ๋Š” ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค.

HttpSecurity

๊ทธ๋ฆฌ๊ณ ๋‚˜์„œ configure(HttpSecurity http) ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด์„œ ์šฐ๋ฆฌ๋งŒ์˜ ์ธ์ฆ ๋งค์ปค๋‹ˆ์ฆ˜์„ ๊ตฌ์„ฑํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์•„์ง๊นŒ์ง€๋Š” ์–ด๋–ป๊ฒŒ ๊ตฌ์„ฑํ•ด์•ผํ• ์ง€ ๋ง‰๋ง‰ํ•˜๊ธฐ๋งŒ ํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ๋œ ์ •๋ณด๋ฅผ ์ข€๋” ์ฐพ์•„๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.httpBasic()
		.and()
		.authorizeRequests()
			.antMatchers("/users/{userId}").access("@authenticationCheckHandler.checkUserId(authentication,#userId)")
			.antMatchers("/admin/db/**").access("hasRole('ADMIN_MASTER') or hasRole('ADMIN') and hasRole('DBA')")
			.antMatchers("/register/**").hasRole("ANONYMOUS")
		.and()
		.formLogin()
			.loginPage("/login")
			.usernameParameter("email")
			.passwordParameter("password")
			.successHandler(successHandler())
			.failureHandler(failureHandler())
			.permitAll();
}

๋ญ ์‚ฌ์‹ค์ƒ ๋ณ„๊ฒŒ ์—†์Šต๋‹ˆ๋‹ค. HttpSecurity์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ํ†ตํ•ด ์ž์‹ ๋งŒ์˜ ์ธ์ฆ ๋งค์ปค๋‹ˆ์ฆ˜์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด๋ผ๊ณ  ๋ณด๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„์˜ ์„ค์ •์€ ์—ฌ๊ธฐ๋ฅผ ํ†ตํ•˜๊ฒŒ ๋˜๋‹ˆ๊นŒ์š” .authorizeRequests()๋ฅผ ํ†ตํ•ด ์š”์ฒญ์— ๋Œ€ํ•œ ๊ถŒํ•œ์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ธฐ๋ณธ์ ์ธ ํ˜•ํƒœ์˜ .anyRequest().authenticated()๋ผ๋Š” ์˜๋ฏธ๋Š” ์–ด๋– ํ•œ ์š”์ฒญ์ด๋“ ์ง€ ์ธ์ฆ๋˜์–ด์•ผํ•œ๋‹ค๋Š” ๊ฒƒ์ด๊ฒŸ์ฃ ? .formLogin()์€ ํผ์„ ํ†ตํ•œ ๋กœ๊ทธ์ธ์„ ์ด์šฉํ•œ๋‹ค๋Š” ์˜๋ฏธ์ด๋ฉฐ, .loginPage(โ€œ/loginโ€)์„ ํ†ตํ•ด ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋Š” /login ๊ฒฝ๋กœ๋กœ ์ œ๊ณตํ•˜๋ฉฐ /login์˜ POST ์š”์ฒญ์ด ๋ฐ”๋กœ ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ๊ณผ์ •์ด๋ผ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ํ™•์žฅ๋œ ํ˜•ํƒœ์—์„œ์˜ .antMatchers().hasRole() ๋˜๋Š” .antMatchers().access()๋Š” ํ•ด๋‹น ๊ฒฝ๋กœ์— ๋Œ€ํ•ด์„œ ์–ด๋– ํ•œ ๊ถŒํ•œ์„ ๊ฐ€์ ธ์•ผ๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š”๊ฐ€๋ฅผ ํ‘œํ˜„ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋‹ค์Œ์€ antMatchers() ๋‹ค์Œ์œผ๋กœ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋Š” ํ•ญ๋ชฉ๋“ค์ž…๋‹ˆ๋‹ค.

  • anonymous()
    ์ธ์ฆ๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • authenticated()
    ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • fullyAuthenticated()
    ์™„์ „ํžˆ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(?)
  • hasRole() or hasAnyRole()
    ํŠน์ • ๊ถŒํ•œ์„ ๊ฐ€์ง€๋Š” ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • hasAuthority() or hasAnyAuthority()
    ํŠน์ • ๊ถŒํ•œ์„ ๊ฐ€์ง€๋Š” ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • hasIpAddress()
    ํŠน์ • ์•„์ดํ”ผ ์ฃผ์†Œ๋ฅผ ๊ฐ€์ง€๋Š” ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • access()
    SpEL ํ‘œํ˜„์‹์— ์˜ํ•œ ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • not()
    ์ ‘๊ทผ ์ œํ•œ ๊ธฐ๋Šฅ์„ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค.
  • permitAll() or denyAll()
    ์ ‘๊ทผ์„ ์ „๋ถ€ ํ—ˆ์šฉํ•˜๊ฑฐ๋‚˜ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค.
  • rememberMe()
    ๋ฆฌ๋ฉค๋ฒ„ ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Role์€ ์—ญํ• ์ด๊ณ  Authority๋Š” ๊ถŒํ•œ์ด์ง€๋งŒ ์‚ฌ์‹ค์€ ํ‘œํ˜„์˜ ์ฐจ์ด์ž…๋‹ˆ๋‹ค. Role์€ โ€œADMINโ€์œผ๋กœ ํ‘œํ˜„ํ•˜๊ณ  Authority๋Š” โ€œROLE_ADMINโ€์œผ๋กœ ํ‘œ๊ธฐํ•˜๋Š” ๊ฒƒ ๋ฟ์ž…๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ hasRole()์— ROLE_ADMIN์œผ๋กœ ํ‘œ๊ธฐํ•˜๋ฉด ROLE์„ ์ง€์šฐ๋ผ๋Š” ์—๋Ÿฌ๋ฅผ ๋ณผ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ํƒœ๊ทธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋˜ํ•œ SpEL ํ‘œํ˜„์‹์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. <sec:authentication /> ์ด๋ ‡๊ฒŒ ๋ง์ž…๋‹ˆ๋‹ค.

AuthenticationManagerBuilder

AuthenticationManagerBuilder๋ฅผ ํ†ตํ•ด ์ธ์ฆ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋„๋ก ์ œ๊ณตํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ๋ ˆํผ๋Ÿฐ์Šค์—์„œ ์•Œ๋ ค์ฃผ๋Š” ๋ฐฉ์‹์ธ๋ฐ AuthenticationManagerBuilder๋ฅผ ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ์ฃผ์ž…๋ฐ›์•„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. WebSecurityConfigurerAdapter์˜ configure(AuthenticationManagerBuilder auth)๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•˜๋Š” ๊ฒƒ์— ๋Œ€ํ•œ ์ฐจ์ด๋Š” ์—†์Šต๋‹ˆ๋‹ค.

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
	auth.inMemoryAuthentication().withUser("scott").password("tiger").roles("ROLE_USER");
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	// TODO Auto-generated method stub
	auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN", "DBA");
	auth.inMemoryAuthentication().withUser("scott").password("tiger").roles("USER", "SETTING");
}

์›ํ•˜๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ, ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ๋ ˆํผ๋Ÿฐ์Šค์™€ ์ƒ˜ํ”Œ์„ ๋งŒ๋“  ๊ฐœ๋ฐœ์ž๊ฐ€ ์™œ ์ฒซ๋ฒˆ์งธ ๋ฐฉ์‹์œผ๋กœ ์„ค๋ช…ํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ์˜๋ฏธ๋ฅผ ์ƒ๊ฐํ•ด๋ด…์‹œ๋‹ค.

์•„์‹œ๋Š” ๋ถ„์ด ์žˆ๋‹ค๋ฉด ์•Œ๋ ค์ฃผ์‹œ๋ฉด ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ์†์œผ๋กœ ๋“ค์–ด๊ฐ€๊ธฐ

์ง€๊ธˆ๊นŒ์ง€ ํ–ˆ๋˜ ์„ค์ •๋“ค์€ ์•„์ฃผ ๊ธฐ๋ณธ์ ์ธ ํ˜•ํƒœ๋“ค์ด๊ธฐ ๋•Œ๋ฌธ์— ์‹ค์ œ๋กœ ์šฐ๋ฆฌ๋งŒ์˜ ์ธ์ฆ ๋งค์ปค๋‹ˆ์ฆ˜์œผ๋กœ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์„ ํ•œ ๋’ค ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ถ€๊ฐ€์ ์ธ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

Method Security

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ 2.0 ์—์„œ๋ถ€ํ„ฐ ์„œ๋น„์Šค ๊ณ„์ธต์˜ ๋ฉ”์†Œ๋“œ์— ๋ณด์•ˆ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. @Secured ์–ด๋…ธํ…Œ์ด์…˜ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ JSR-250 ์–ด๋…ธํ…Œ์ด์…˜๋„ ์ง€์›ํ•˜๋„๋ก ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค. ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ 3.0์—์„œ๋Š” ํ‘œํ˜„ ๊ธฐ๋ฐ˜์˜ ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๋Š” Configuration ํด๋ž˜์Šค์— @EnableGlobalMethodSecurity๋ฅผ ์ ์šฉํ•จ์œผ๋กœ์จ ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜์˜ ๋ณด์•ˆ์„ ํ™œ์„ฑํ™”์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์˜ @Secured ์–ด๋…ธํ…Œ์ด์…˜์„ ํ™œ์„ฑํ™”์‹œํ‚ค๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌ์„ฑํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig {
// ...
}

๊ทธ๋ฆฌ๊ณ ๋‚˜์„œ ํด๋ž˜์Šค ๋˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์˜ ๋ฉ”์†Œ๋“œ์— @Secured ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๊ทธ์— ๋”ฐ๋ฅธ ํ•ด๋‹น ๋ฉ”์†Œ๋“œ์— ๋Œ€ํ•œ ์•ก์„ธ์Šค๊ฐ€ ์ œํ•œ๋ฉ๋‹ˆ๋‹ค.

public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

์—ญํ•  ๋‹จ์œ„๋กœ ์ œ์•ฝ์กฐ๊ฑด์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Œ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ 3.0๋ถ€ํ„ฐ ์ง€์›ํ•˜๋Š” ํ‘œํ˜„์‹ ๊ธฐ๋ฐ˜์˜ ๋ฌธ๋ฒ•์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// ...
}
public interface BankService {

@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnonymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

Remember-Me

๋ฆฌ๋ฉค๋ฒ„-๋ฏธ ๊ธฐ๋Šฅ์€ ๋„๋Œ€์ฒด ๋ฌด์—‡์ผ๊นŒ์š”? ๊ตฌ๊ธ€์— what is remember me ๋ผ๋Š” ํ‚ค์›Œ๋“œ๋กœ ๊ฒ€์ƒ‰์„ ํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค.

๋‹จ์ˆœํžˆ ์•„์ด๋””๋ฅผ ๊ธฐ์–ตํ•ด๋†“๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๋กœ๊ทธ์ธ ์ •๋ณด๋ฅผ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์„ ๋งํ•ฉ๋‹ˆ๋‹ค.

Do NOT use โ€œRemember Meโ€ on any public computer, on campus, in Internet cafeโ€™s, or anywhere else where you cannot control access!

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.rememberMe().rememberMeParameter("remember-me").key(REMEMBER_ME_KEY).rememberMeServices(persistentTokenBasedRememberMeServices());
}

@Bean
public PersistentTokenBasedRememberMeServices persistentTokenBasedRememberMeServices(){
	PersistentTokenBasedRememberMeServices persistentTokenBasedRememberMeServices =
			new PersistentTokenBasedRememberMeServices(REMEMBER_ME_KEY, userDetailsService, persistentTokenRepository());
	return persistentTokenBasedRememberMeServices;
}

@Bean
public PersistentTokenRepository persistentTokenRepository(){
	TokenRepositoryImpl tokenRepositoryImpl = new TokenRepositoryImpl();
	return tokenRepositoryImpl;
}

์ €๋Š” ์˜์†์„ฑ ๊ธฐ๋ฐ˜์˜ ๋ฆฌ๋ฉค๋ฒ„-๋ฏธ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด๋•Œ, ๋ฆฌ๋ฉค๋ฒ„-๋ฏธ ํ† ํฐ์„ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋„๋ก TokenRepository ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•ด์•ผํ•˜๋Š”๋ฐ์š”. ์ผ์ข…์˜ ์„œ๋น„์Šค ๊ฐ์ฒด๋ผ๊ณ  ์ƒ๊ฐํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

@Transactional
public class TokenRepositoryImpl  implements PersistentTokenRepository {

	@Autowired
	private TokenRepository tokenRepository;

	@Override
	public void createNewToken(PersistentRememberMeToken token) {
		// TODO Auto-generated method stub
		Token newToken = new Token();
		newToken.setEmail(token.getUsername());
		newToken.setToken(token.getTokenValue());
		newToken.setLast_used(token.getDate());
		newToken.setSeries(token.getSeries());
		tokenRepository.save(newToken);
	}

	@Override
	public void updateToken(String series, String tokenValue, Date lastUsed) {
		// TODO Auto-generated method stub

		Token updateToken = tokenRepository.findOne(series);
		updateToken.setToken(tokenValue);
		updateToken.setLast_used(lastUsed);
		updateToken.setSeries(series);
		tokenRepository.save(updateToken);
	}

	@Override
	public PersistentRememberMeToken getTokenForSeries(String series) {
		// TODO Auto-generated method stub
		Token token = tokenRepository.findOne(series);
		PersistentRememberMeToken persistentRememberMeToken = new PersistentRememberMeToken(token.getEmail(), series, token.getToken(), token.getLast_used());
		return persistentRememberMeToken;
	}

	@Override
	public void removeUserTokens(String username) {
		// TODO Auto-generated method stub
		tokenRepository.deleteByEmail(username);
	}
}

๋ณธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ์˜ Email์€ ์œ ๋‹ˆํฌํ•œ ์†์„ฑ์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค.

Password Encoding

AuthenticationManagerBuilder.userDetailsService().passwordEncoder()๋ฅผ ํ†ตํ•ด ํŒจ์Šค์›Œ๋“œ ์•”ํ˜ธํ™”์— ์‚ฌ์šฉ๋  PasswordEncoder ๊ตฌํ˜„์ฒด๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. PasswordEncoder ์ธํ„ฐํŽ˜์ด์Šค๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

public interface PasswordEncoder {

	/**
	 * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
	 * greater hash combined with an 8-byte or greater randomly generated salt.
	 */
	String encode(CharSequence rawPassword);

	/**
	 * Verify the encoded password obtained from storage matches the submitted raw
	 * password after it too is encoded. Returns true if the passwords match, false if
	 * they do not. The stored password itself is never decoded.
	 *
	 * @param rawPassword the raw password to encode and match
	 * @param encodedPassword the encoded password from storage to compare with
	 * @return true if the raw password, after encoding, matches the encoded password from
	 * storage
	 */
	boolean matches(CharSequence rawPassword, String encodedPassword);
}

๋‹จ์ˆœํžˆ ์ธ์ฝ”๋”ฉํ•˜๋Š” ํ•จ์ˆ˜์™€ ํ‰๋ฌธ์œผ๋กœ ์ œ๊ณต๋˜๋Š” ํŒจ์Šค์›Œ๋“œ์™€ ์ธ์ฝ”๋”ฉ๋˜์–ด์žˆ๋Š” ํŒจ์Šค์›Œ๋“œ(์˜ˆ๋ฅผ๋“ค์–ด, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ธ์ฝ”๋”ฉ๋˜์–ด ์ €์žฅ๋˜์–ด ์žˆ๋Š” ํŒจ์Šค์›Œ๋“œ)๋ฅผ ๋น„๊ตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ €๋Š” PasswordEncoder ๊ตฌํ˜„์ฒด์ธ BCryptPasswordEncoder๋ฅผ ์ง€์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹น์—ฐํžˆ ์ง์ ‘ ๊ตฌํ˜„์ฒด๋ฅผ ๋งŒ๋“ค์–ด์„œ ์ ์šฉํ•ด๋„ ๋ฉ๋‹ˆ๋‹ค! (์•„๋ฌด๋ž˜๋„ ์ง์ ‘ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ์ข‹๊ฒŸ์ฃ ?)

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	// TODO Auto-generated method stub
	auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}

@Bean
public PasswordEncoder passwordEncoder(){
	return new BCryptPasswordEncoder();
}

PasswordEncoder๋ฅผ ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•ด๋‘๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ €์žฅ๋œ ํŒจ์Šค์›Œ๋“œ๋ฅผ ๋น„๊ตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ €์žฅ๋œ ํŒจ์Šค์›Œ๋“œ๋Š” PasswordEncoder์— ์˜ํ•ด ์•”ํ˜ธํ™”๋œ ํ‰๋ฌธ์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

@RequestMapping(value="/users/{userId}", method=RequestMethod.POST)
@PreAuthorize("#updateUser.email == authentication.name")
public String update(@PathVariable("userId") Long id, @ModelAttribute @Valid UserDTO.Update updateUser, Model model){
	User currentUser = userService.findOne(id);
	if(!passwordEncoder.matches(updateUser.getPassword(), currentUser.getPassword())){
		throw new RuntimeException("Not password equals...");
	}
	//...
}

WebSecurity Ignoring

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ๋ ˆํผ๋Ÿฐ์Šค์™€๋Š” ๋‹ฌ๋ฆฌ ๋ฆฌ์†Œ์Šค์™€ ๊ด€๋ จํ•ด์„œ๋Š” WebSecurity.ignoring()๋ฅผ ์ด์šฉํ•ด์„œ ๋ณด์•ˆ์ด ์ ์šฉ๋˜์ง€ ์•Š๋„๋ก ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ API ๋ฌธ์„œ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ ๋ ˆํผ๋Ÿฐ์Šค์— ์„ค๋ช…์ด ์—†๋‹ค๋Š” ๊ฒƒ์ด ์ข€ ์•„์‰ฌ์šด ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค.

์Œ... permitAll๊ณผ ignoring์˜ ์ฐจ์ด๊ฐ€ ์žˆ๋Š”๊ฐ€๋ฅผ ์•„์‹œ๋Š”๋ถ„์€ ๋Œ“๊ธ€ ๋‚จ๊ฒจ์ฃผ์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค ใ… ใ… 

Localization

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ๋ฉ”์‹œ์ง€์— ๋Œ€ํ•œ ํ˜„์ง€ํ™”๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ๋ฉ”์‹œ์ง€ ์†Œ์Šค ๊ด€๋ จ ํ”„๋กœํผํ‹ฐ ํŒŒ์ผ๋“ค์€ spring-security-core.jar์— ํฌํ•จ ๋˜์–ด์ ธ์žˆ์Šต๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๋Š” ๋ฉ”์‹œ์ง€ ํ”„๋กœํผํ‹ฐ ํŒŒ์ผ๋“ค์„ ๋ฉ”์‹œ์ง€์†Œ์Šค๋กœ ๋“ฑ๋กํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

@Configuration
public class ApplicationConfiguration extends WebMvcConfigurerAdapter {

	@Bean
	public MessageSource messageSource(){
		ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
		messageSource.addBasenames("security/messages");
		messageSource.setDefaultEncoding("UTF-8");
		return messageSource;
	}

	@Bean
	public LocaleChangeInterceptor localeChangeInterceptor(){
		LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
		localeChangeInterceptor.setParamName("lang");
		return localeChangeInterceptor;
	}

	@Bean
	public LocaleResolver localeResolver(){
		SessionLocaleResolver localeResolver = new SessionLocaleResolver();
		localeResolver.setDefaultLocale(Locale.KOREAN);
		return localeResolver;
	}

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		// TODO Auto-generated method stub
		registry.addInterceptor(localeChangeInterceptor());
	}
}

๋ฉ”์‹œ์ง€ ํ”„๋กœํผํ‹ฐ ํŒŒ์ผ๋“ค์€ src/main/resource/security ํด๋”์— ์œ„์น˜ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

AuthenticationSuccessHandler & AuthenticationFailureHandler

OKKY์˜ ์งˆ๋ฌธ๊ฒŒ์‹œํŒ์—์„œ ๋กœ๊ทธ์ธ ์‹คํŒจ๋ฅผ ์–ด๋–ป๊ฒŒ ์ฒดํฌํ•˜๋Š”๊ฐ€์— ๋Œ€ํ•ด์„œ ์งˆ๋ฌธํ•˜๋Š” ๊ธ€์„ ๋ณด์•˜์Šต๋‹ˆ๋‹ค. ์ €๋Š” ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ ์‚ฌ์šฉํ•˜๋Š” AuthenticationFailureHandler ๊ตฌํ˜„์ฒด๋ฅผ ์ƒ์†๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

public class AuthenticationSuccessHandlerImpl extends SavedRequestAwareAuthenticationSuccessHandler  {
	private static final Logger logger = LoggerFactory.getLogger(AuthenticationSuccessHandlerImpl.class);

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		logger.info("Login Success... - {}", authentication.getPrincipal());
		response.sendRedirect("/?login");
	}

}

AuthenticationSuccessHandler ๊ตฌํ˜„์ฒด์—์„œ๋Š” ๋กœ๊ทธ์ธ์„ ์„ฑ๊ณตํ–ˆ์„๋•Œ ํ˜ธ์ถœ(์ธ์ฆ ๊ฐ์ฒด๊ฐ€ ์ƒ์„ฑ๋˜์–ด์ง„ ํ›„)๋˜๊ธฐ ๋•Œ๋ฌธ์— Authentication ์ธ์Šคํ„ด์Šค ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. authentication.getPrincipal()์„ ํ˜ธ์ถœํ•˜๊ฒŒ ๋˜๋ฉด ์ € ๊ฐ™์€ ๊ฒฝ์šฐ์—๋Š” org.springframework.security.core.userdetails.User ๊ฐ€ ์•„๋‹Œ com.kdev.app.security.userdetails.UserDetails๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

public class AuthenticationFailureHandlerImpl extends SimpleUrlAuthenticationFailureHandler {
	private static final Logger logger = LoggerFactory.getLogger(AuthenticationFailureHandlerImpl.class);

	public AuthenticationFailureHandlerImpl() {
		this.setDefaultFailureUrl("/login?error");
	}

	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		logger.info("Login Failed... - {}",request.getParameter("email"));
		super.onAuthenticationFailure(request, response, exception);
	}

}

๋กœ๊ทธ์ธ ์‹คํŒจ ์‹œ SimpleUrlAuthenticationFailureHandler์˜ defaultFailureUrl๋ฅผ ์ง€์ •ํ•˜๋ฉด SPRING_SECURITY_LAST_EXCEPTION์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๊ฐ€์ง€๋ฉด์„œ ํ•ด๋‹น ๊ฒฝ๋กœ๋กœ ์ด๋™ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

AuthenticationException์œผ๋กœ ๋กœ๊ทธ์ธ ์‹คํŒจ์˜ ์ด์œ ๋„ ์ฒดํฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์ฃ 

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ํŽ˜์ด์ง€

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ ์ œ๊ณตํ•ด์ฃผ๋Š” ๊ธฐ๋ณธ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ๋Œ€์‹ ์— ์šฐ๋ฆฌ๋งŒ์˜ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="ko">
<head>
	<meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<!--
		<meta id="_csrf" name="_csrf" content="${_csrf.token}" />
		<meta id="_csrf_header" name="_csrf_header" content="${_csrf.headerName}" />
	 -->
	<sec:csrfMetaTags />
	<title>Spring Security + JPA</title>

	<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

</head>
<body>
<div class="container">
	<div class="page-header">
	  <h1>Login Page <small>with Bootstrap</small></h1>
	</div>
	<div class="container-fluid">
		<form method="post" action="/login">
		<%-- <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> --%>
		  <sec:csrfInput />
		  <div class="form-group">
		    <label for="email">์ด๋ฉ”์ผ</label>
		    <input type="text" class="form-control" name="email" placeholder="Email" required>
		  </div>
		  <div class="form-group">
		    <label for="password">๋น„๋ฐ€๋ฒˆํ˜ธ</label>
		    <input type="password" class="form-control" name="password" placeholder="Password" required>
		  </div>
		  <div class="checkbox">
		    <label>
		      <input type="checkbox" name="remember-me" > Remember me
		    </label>
		  </div>
		  <div class="text-center">
		  	<c:if test="${not empty SPRING_SECURITY_LAST_EXCEPTION}">
		  		<span class="text-danger"><c:out value="${SPRING_SECURITY_LAST_EXCEPTION.message}"/></span>
		  		<c:remove var="SPRING_SECURITY_LAST_EXCEPTION" scope="session"/>
		  	</c:if>
		  </div>
		  <hr>
		  <button type="submit" class="btn btn-default">๋กœ๊ทธ์ธ</button>
		  <a class="btn btn-default" href="/register">ํšŒ์›๊ฐ€์ž…</a>
		</form>
	</div>
	<div class="container">

	</div>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!--[if lt IE 9]>
   <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
   <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
</body>
</html>

SPRING_SECURITY_LAST_EXCEPTION์˜ ์กด์žฌ ์—ฌ๋ถ€์— ๋”ฐ๋ผ์„œ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ์— ๋Œ€ํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ๋ ˆํผ๋Ÿฐ์Šค์—์„œ๋Š” ๋‹จ์ˆœํžˆ ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ์กด์žฌ์—ฌ๋ถ€์— ๋”ฐ๋ผ์„œ ์ถœ๋ ฅํ˜•ํƒœ๋ฅผ ์ง€์ •ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ถ€๋ถ„๋„ ์ฐธ ์•„์‰ฝ์Šต๋‹ˆ๋‹ค.

UserDetails & UserDetailsService

์•„ ์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. UserDetails ์ธํ„ฐํŽ˜์ด์Šค์— ๋Œ€ํ•ด์„œ ์„ค๋ช…์„ ์•ˆํ–ˆ๋„ค์š” ใ… ใ… 

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ UserDetails ๊ตฌํ˜„์ฒด๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” org.springframework.security.core.userdetails.User๋ผ๋Š” ํด๋ž˜์Šค๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜, ์ด๋ฆ„๊ณผ ํŒจ์Šค์›Œ๋“œ ๊ทธ๋ฆฌ๊ณ  ๊ถŒํ•œ๋“ค์— ๋Œ€ํ•œ ํ•„๋“œ๋งŒ ์กด์žฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฉ”์ผ ์ •๋ณด ๋˜๋Š” ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ ๋“ฑ๊ณผ ๊ฐ™์€ ๋ถ€๊ฐ€์ ์ธ ์ •๋ณด๋ฅผ ๋‹ด์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ, UserDetails ๊ตฌํ˜„์ฒด๋ฅผ ์ง์ ‘ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. org.springframework.security.core.userdetails.User ์ž์ฒด๋„ UserDetails ๊ตฌํ˜„์ฒด์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฅผ ์ƒ์†๋ฐ›์•„์„œ ๊ตฌํ˜„ํ•ด๋„ ๋ฉ๋‹ˆ๋‹ค.

public class UserDetails extends org.springframework.security.core.userdetails.User {

	private static final long serialVersionUID = -4855890427225819382L;

	private Long id;
	private String nickname;
	private String email;
	private Date createdAt;


	public UserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
		super(username, password, authorities);
		// TODO Auto-generated constructor stub
	}

	public UserDetails(User user){
		super(user.getEmail(), user.getPassword(), user.isAccountNonExpired(), user.isAccountNonLocked(), user.isCredentialsNonExpired(), user.isEnabled(), authorities(user));

		this.id = user.getId();
		this.nickname = user.getNickname();
		this.email = user.getEmail();
		this.createdAt = user.getCreatedAt();
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getNickname() {
		return nickname;
	}

	public void setNickname(String nickname) {
		this.nickname = nickname;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public Date getCreatedAt() {
		return createdAt;
	}

	public void setCreatedAt(Date createdAt) {
		this.createdAt = createdAt;
	}

	private static Collection<? extends GrantedAuthority> authorities(User user) {
		// TODO Auto-generated method stub
		List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
		user.getAuthorities().forEach(a -> {
			authorities.add(new SimpleGrantedAuthority(a.getAuthority()));
		});
		return authorities;
	}

	public UserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities,
			String nickname) {
		super(username, password, authorities);
		this.nickname = nickname;
		this.email = username;
	}

	public UserDetails(String username, String password, boolean enabled, boolean accountNonExpired,
			boolean credentialsNonExpired, boolean accountNonLocked,
			Collection<? extends GrantedAuthority> authorities) {
		super(username, password, authorities);
		// TODO Auto-generated constructor stub
	}
}

๊ทธ๋Ÿฌ๋ฉด ์šฐ๋ฆฌ๋Š” Authentication.getPrincipal() ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ์–ป์€ Principal ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด์„œ๋„ ๋ถ€๊ฐ€์ ์ธ ํ•„๋“œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

<sec:authentication property="principal.email"/>

org.springframework.security.core.userdetails.UserDetailsService ๊ตฌํ˜„์ฒด๋Š” ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ์ธ์ฆ ์‹œ์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. UserRepository๋ฅผ ํ†ตํ•ด ์˜์†์„ฑ์œผ๋กœ ์ €์žฅ๋œ ์ธ์ฆ์ •๋ณด๋ฅผ ๊ฒ€์ƒ‰ํ•œ ํ›„ ์กด์žฌํ•˜์ง€ ์•Š๋‹ค๋ฉด UsernameNotFoundException์„ ๋˜์ง€๊ณ  ์žˆ๋‹ค๋ฉด UserDetails ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

@Service
public class UserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService {

	@Autowired
	private UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
		// TODO Auto-generated method stub
		User user = userRepository.findByEmail(email);
		if(user == null){
			throw new UsernameNotFoundException(email);
		}
		com.kdev.app.security.userdetails.UserDetails userDetails = new com.kdev.app.security.userdetails.UserDetails(user);
		return userDetails;
	}
}

๊ทธ๋Ÿฐ๋ฐ ์ข€ ์ด์ƒํ•˜๋‹ค๊ณ  ๋Š๊ปด์ง‘๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ํŒจ์Šค์›Œ๋“œ ๊ฒ€์ฆ์€ ์–ธ์ œํ•˜๋Š”๊ฒƒ์ผ๊นŒ์š”? ๋ฐ”๋กœ AuthenticationProvider ๊ตฌํ˜„์ฒด์—์„œ ์ง„ํ–‰ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. AuthenticationProvider ๊ตฌํ˜„์ฒด์—์„œ๋Š” authenticate() ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด์„œ Authentication ๊ฐ์ฒด(UsernamePasswordAuthentication)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ๋ฐ˜ํ™˜ํ•˜๊ธฐ ์ง์ „์— ํŒจ์Šค์›Œ๋“œ๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๋Š” AuthenticationProvider๋ฅผ ์ง์ ‘์ ์œผ๋กœ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์•˜์œผ๋‹ˆ๊นŒ ์ด ๋ถ€๋ถ„์— ๋Œ€ํ•ด์„œ ๋ชจ๋ฅด๊ณ  ๋„˜์–ด๊ฐˆ ๋ป” ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ƒค์ฟ ๋‹˜์˜ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ์ปค์Šคํ…€ ๋กœ๊ทธ์ธ์ด๋ผ๋Š” ๊ธ€์—์„œ ์ง์ ‘ AuthenticationProvider๋ฅผ ๊ตฌํ˜„ํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋๋งˆ์น˜๋ฉฐ

์šฐ๋ฆฌ๊ฐ€ ์•Œ์•„๋ณธ ๊ฒƒ ์ด์™ธ์—๋„ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ๋“ค์ด ๋” ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์›น์†Œ์ผ“, RESTful API๋“ฑ์—๋„ ๋ณด์•ˆ์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ณต๋ถ€ํ•˜๋ฉด์„œ ์•Œ๊ฒŒ๋œ ๋ถ€๋ถ„์ธ๋ฐ, ์Šคํ”„๋ง ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ์ œ๊ณตํ•˜๋Š” ์ธ-๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜ ์ธ์ฆ ๊ฐ์ฒด(inMemoryAuthentication())๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ œ๊ณต๋˜๋Š” UserDetails์˜ ์ •๋ณด๋ฅผ ํ™œ์šฉํ•˜์—ฌ User ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ๋•Œ๋ฌธ์— UserDetails์˜ ๊ตฌํ˜„์ฒด๋ฅผ ๋งŒ๋“ ๋‹ค ํ•˜๋”๋ผ๋„ ์˜๋ฏธ๊ฐ€ ์—†๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ, ์ธ ๋ฉ”๋ชจ๋ฆฌ ๋ฐฉ์‹๊ณผ ์˜์†์„ฑ ๋ฐฉ์‹์„ ํ˜ผ์šฉํ•ด์„œ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด ๋ถ€๋ถ„์— ๋Œ€ํ•ด์„œ ์—ฐ๊ตฌํ•ด๋ณผ ํ•„์š”๊ฐ€ ์žˆ๊ฒ ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์€ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณผ ๋•Œ ์ข‹์€ ํฌ์ŠคํŠธ ๋ฐ ๋™์˜์ƒ๋“ค์ž…๋‹ˆ๋‹ค.

์•„๋Š”๊ฒŒ ์—†์œผ๋‹ˆ ์‰ฝ๊ฒŒ ์ดํ•ดํ•˜์ง€ ๋ชปํ•˜๋Š” ๋ถ€๋ถ„์ด ๋งŽ์Šต๋‹ˆ๋‹ค. ๋งŽ์€ ์‹ ์ž… ๊ฐœ๋ฐœ์ž๋“ค์„ ์‘์›ํ•ฉ๋‹ˆ๋‹ค ^ใ…ก^