์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ณต๊ฒฉํ๋ ์ธ์ ๊ณ ์ ๊ณต๊ฒฉ, ํด๋ฆญ์ฌํน ๊ณต๊ฒฉ, CSRF ๋ฑ์ ๋ณด์ ์ทจ์ฝ์ ์ ๊ณ ๋ คํ๊ธฐ ์ํด์๋ Spring Security๋ผ๋ ๋ณด์ ํ๋ ์์ํฌ๋ฅผ ์ด์ฉํ ์ ์๋ค.
*์ธ์ ๊ณ ์ ๊ณต๊ฒฉ(Session Fixation) : ๊ณต๊ฒฉ์๊ฐ ์ ํจํ ์ฌ์ฉ์์ ์ธ์ ์ ๊ฐ๋ก์ฑ๋๋ก ํ์ฉํ๋ ๊ณต๊ฒฉ. ์ด ๊ณต๊ฒฉ์ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ด session ID, ํนํ ์ทจ์ฝํ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ด๋ฆฌํ๋ ๋ฐฉ์์ ํ๊ณ๋ฅผ ํ์ํ๋ค. ์ฌ์ฉ์ ์ธ์ฆ ์ ์๋ก์ด session ID๋ฅผ ํ ๋นํ์ง ์์ผ๋ฏ๋ก(๋ก๊ทธ์ธ ์ ๋ฐ๊ธ๋ฐ์ ์ธ์ ID๊ฐ ๋ก๊ทธ์ธ ์ /ํ ๋ชจ๋ ๋์ผํ๊ฒ ์ฌ์ฉ๋จ) ๊ธฐ์กด session ID๋ฅผ ์ฌ์ฉํ ์ ์์ผ๋ฉฐ, ๊ณต๊ฒฉ์ ์ ํจํ session ID๋ฅผ ์ป์ ํ ํด๋น ์ธ์ ID๋ก ์ฌ์ฉ์๋ฅผ ์ธ์ฆํ๋๋ก ์ ๋ํ๊ณ , ์ฌ์ฉ๋ session ID ๋ฅผ ์์๋ด์ด ์ฌ์ฉ์๊ฐ ์ธ์ฆํ ์ธ์ ์ ํ์ทจํ๋ ๊ฒ์ผ๋ก ๊ตฌ์ฑ๋๋ค.
์ธ์ ๊ณ ์ ๊ณต๊ฒฉ์ ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ํ ํ์ ํด๋ผ์ด์ธํธ์ ์น ์๋ฒ ๊ฐ์ ์๋ฆฝ๋ ์ธ์ ์ ๋์ฒญํ๋ ์ธ์ ํ์ด์ฌํน์ ์ผ์ข ์ด ์๋๋ฉฐ, ํผํด์์ ๋ธ๋ผ์ฐ์ ์ established๋ ์ธ์ ์ fix ์ํค๋ฏ๋ก ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํ๊ธฐ ์ ์ ๊ณต๊ฒฉ์ด ์์๋๋ค.
*ํด๋ฆญ์ฌํน ๊ณต๊ฒฉ : ์น ์ฌ์ฉ์๊ฐ ์์ ์ด ํด๋ฆญํ๊ณ ์๋ค๊ณ ์ธ์งํ๋ ๊ฒ๊ณผ ๋ค๋ฅธ ์ด๋ค ๊ฒ์ ํด๋ฆญํ๊ฒ ์์ด๋ ์ ์์ ์ธ ๊ธฐ๋ฒ์ผ๋ก์จ ์ ์ฌ์ ์ผ๋ก ๊ณต๊ฒฉ์๋ ๋น๋ฐ์ ๋ณด๋ฅผ ์ ์ถ์ํค๊ฑฐ๋ ๊ทธ๋ค์ ์ปดํจํฐ์ ๋ํ ์ ์ด๋ฅผ ํ๋ํ ์ ์๊ฒ ๋๋ค.(์ฌ์ฉ์๋ฅผ ์์ฌ ๋ณด์ด์ง ์๋ ์์๋ฅผ ํด๋ฆญํ๋๋ก ํ ๋ ๋ฐ์ํจ)
ํดํน๋ ์ฌ์ดํธ๋ฅผ ํด๋ฆญํ๋ฉด ๋ค๋ฅธ ๋ถ๋ iframe ์น ์ฌ์ดํธ์์ ์์ ์ด ์คํ๋๊ณ , ์ด๋ฌํ ํด๋ฆญ์ผ๋ก ์ธํด ์ฌ์ฉ์ ์ญ์ , ๊ถํ ์ ๋ฐ์ดํธ ๋๋ ๊ธฐํ ์์ ์ด ๋ฐ์ํ ์ ์๋ค.
์๋ฅผ ๋ค์ด, ์ด๋ค ๋น๋์ค์ ๋งํฌ๋ ์ด๋ฉ์ผ์ ๋ฐ์์ ๊ฒฝ์ฐ, ์ฌ์ฉ์๊ฐ ํ๋ ์ด ๋ฒํผ์ ๋๋ฅผ ๋ ์ด๋ ์๋ง์กด์ ์ํ ํ์ด์ง์ด ์ํ ๊ตฌ๋งค ๋ฒํผ์ ๋๋ฅด๋ ๊ฒ์ผ ์ ์๋ค.
*CSRF๋ https://nothing-is-on-my-way.tistory.com/83 ์ฐธ๊ณ .
Spring Security๋ Spring MVC ๊ธฐ๋ฐ ์ ํ๋ฆฌ์ผ์ด์ ์ธ์ฆ๊ณผ ์ธ๊ฐ ๊ธฐ๋ฅ์ ์ง์ํ๋ ๋ณด์ ํ๋ ์์ํฌ์ด๋ค.
Spring MVC ๊ธฐ๋ฐ ์ ํ๋ฆฌ์ผ์ด์ ๋ณด์์ ์ ์ฉํ๊ธฐ ์ํ ์ฌ์ค์์ ํ์ค์ด๋ค.
Spring์์ ์ง์ํ๋ Interceptor๋ Servlet Filter๋ฅผ ์ฌ์ฉํด์ ๋ณด์ ๊ธฐ๋ฅ์ ์ง์ ๊ตฌํํ ์๋ ์์ง๋ง, ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ณด์์ ๋๋ถ๋ถ์ ๊ธฐ๋ฅ์ Spring Security์์ ์์ ์ ์ผ๋ก ์ง์ํ๊ณ ์์ผ๋ฏ๋ก ๊ตฌ์กฐ์ ์ผ๋ก ์ ๋ง๋ค์ด์ง ๊ฒ์ฆ๋ Spring Security๋ฅผ ์ด์ฉํ๋ ๊ฒ์ด ์์ ํ ์ ํ์ด๋ผ๊ณ ๋ณผ ์ ์๋ค.
Spring Security๋ก ํ ์ ์๋ ๋ณด์ ๊ฐํ ๊ธฐ๋ฅ์ ๋ค์๊ณผ ๊ฐ๋ค.
- ๋ค์ํ ์ ํ(ํผ ๋ก๊ทธ์ธ ์ธ์ฆ, ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ, OAuth 2 ๊ธฐ๋ฐ ์ธ์ฆ, LADP ์ธ์ฆ)์ ์ฌ์ฉ์ ์ธ์ฆ ๊ธฐ๋ฅ ์ ์ฉ
- ์ ํ๋ฆฌ์ผ์ด์ ์ฌ์ฉ์์ ์ญํ (Role)์ ๋ฐ๋ฅธ ๊ถํ ๋ ๋ฒจ ์ ์ฉ
- ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ ๊ณตํ๋ ๋ฆฌ์์ค์ ๋ํ ์ ๊ทผ ์ ์ด
- ๋ฏผ๊ฐํ ์ ๋ณด์ ๋ํ ๋ฐ์ดํฐ ์ํธํ
- SSL ์ ์ฉ
- ์ผ๋ฐ์ ์ผ๋ก ์๋ ค์ง ์น ๋ณด์ ๊ณต๊ฒฉ ์ฐจ๋จ
์ด ์ธ์๋ SSO, ํด๋ผ์ด์ธํธ ์ธ์ฆ์ ๊ธฐ๋ฐ ์ธ์ฆ, ๋ฉ์๋ ๋ณด์, ์ ๊ทผ ์ ์ด ๋ชฉ๋ก ๊ฐ์ ๋ณด์์ ์ํ ๊ธฐ๋ฅ์ ์ง์ํ๋ค.
Spring Security์์ ์ฌ์ฉํ๋ ์ฉ์ด ์ ๋ฆฌ
- Principal(์ฃผ์ฒด)
- Spring Security์์ ์ฌ์ฉ๋๋ Principal ์ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์์ ์ ์ํํ ์ ์๋ ์ฌ์ฉ์, ๋๋ฐ์ด์ค ๋๋ ์์คํ ๋ฑ์ด ๋ ์ ์์ผ๋ฉฐ, ์ผ๋ฐ์ ์ผ๋ก ์ธ์ฆ ํ๋ก์ธ์ค๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์ํ๋ ์ฌ์ฉ์์ ๊ณ์ ์ ๋ณด๋ฅผ ์๋ฏธ
- Authentication(์ธ์ฆ)
- ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ์ฉํ๋ ์ฌ์ฉ์๊ฐ ๋ณธ์ธ์ด ๋ง์์ ์ฆ๋ช ํ๋ ์ ์ฐจ๋ฅผ ์๋ฏธ.
- Authentication (์ธ์ฆ)์ ์ํด์๋ Credential(์์ ์ฆ๋ช ์ ๋ณด)๊ฐ ํ์ํ๋ค.
- ํน์ ์ฌ์ดํธ์ ๋ก๊ทธ์ธ์ ์ํด ์ ๋ ฅํ๋ ํจ์ค์๋ ์ญ์ Credential
- Authorization(์ธ๊ฐ, ๊ถํ ๋ถ์ฌ)
- Authentication์ด ์ ์์ ์ผ๋ก ์ํ๋ ์ฌ์ฉ์์๊ฒ ํ๋ ์ด์์ ๊ถํ(authority)์ ๋ถ์ฌํ์ฌ ํน์ ์ ํ๋ฆฌ์ผ์ด์ ์ ํน์ ๋ฆฌ์์ค์ ์ ๊ทผํ ์ ์๊ฒ ํ๊ฐํ๋ ๊ณผ์ ์ ์๋ฏธ.
- Authorization์ Authentication ๊ณผ์ ์ดํ ์ํ๋์ด์ผ ํ๋ฉฐ ๊ถํ์ ์ผ๋ฐ์ ์ผ๋ก ์ญํ (Role) ํํ๋ก ๋ถ์ฌ๋๋ค.
- Access Control
- ์ฌ์ฉ์๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฆฌ์์ค์ ์ ๊ทผํ๋ ํ์๋ฅผ ์ ์ดํ๋ ๊ฒ์ ์๋ฏธํ๋ค.
Hello, Spring Security ์ํ ์ ํ๋ฆฌ์ผ์ด์ ์ผ๋ก Spring Security์ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฅผ ์์๋ณด์.
Security์ ๊ธฐ๋ณธ ๊ตฌ์กฐ์ ๋์ ๋ฐฉ์์ ๊ฐ์ฅ ์ดํดํ๊ธฐ ์ฌ์ด SSR ๋ฐฉ์์ผ๋ก ์ํ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค์ด๋ณด์.
Hello Spring Security ์ํ ์ ํ๋ฆฌ์ผ์ด์ ์ Home ํ๋ฉด์ ๋ค์๊ณผ ๊ฐ๋ค.
ํ์ ๊ฐ์ ํ๋ฉด์ ๋ค์๊ณผ ๊ฐ๋ค.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="layouts/common-layout">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Hello Spring Security Coffee Shop</title>
</head>
<body>
<hr />
<div class="container" layout:fragment="content">
<!-- (1) ํ์ ๊ฐ์
ํผ -->
<form action="/members/register" method="post">
<div class="row">
<div class="col-xs-2">
<input type="text" name="fullName" class="form-control" placeholder="User Name"/>
</div>
</div>
<div class="row" style="margin-top: 20px">
<div class="col-xs-2">
<input type="email" name="email" class="form-control" placeholder="Email"/>
</div>
</div>
<div class="row" style="margin-top: 20px">
<div class="col-xs-2">
<input type="password" name="password" class="form-control" placeholder="Password"/>
</div>
</div>
<button class="btn btn-outline-secondary" style="margin-top: 20px">ํ์ ๊ฐ์
</button>
</form>
</div>
</body>
</html>
(1)์ HTML form ํ๊ทธ๋ฅผ ์ด์ฉํด์ ํ์ ๊ฐ์ ํผ์ ๊ตฌ์ฑํ๋ฉฐ ํด๋น ํผ์์ ์ ๋ ฅํ ์ ๋ณด๋ ์๋์ ์ฝ๋์์ MemberController์ registerMember() ํธ๋ค๋ฌ ๋ฉ์๋๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋๋ค.
package com.codestates.member;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@Controller
@RequestMapping("/members")
public class MemberController {
private final MemberService memberService;
private final MemberMapper mapper;
public MemberController(MemberService memberService, MemberMapper mapper) {
this.memberService = memberService;
this.mapper = mapper;
}
@GetMapping("/register")
public String registerMemberForm() {
return "member-register";
}
@PostMapping("/register")
public String registerMember(@Valid MemberDto.Post requestBody) {
Member member = mapper.memberPostToMember(requestBody);
memberService.createMember(member);
System.out.println("Member Registration Successfully");
return "login";
}
@GetMapping("/my-page")
public String myPage() {
return "my-page";
}
}
๋ก๊ทธ์ธ ํ๋ฉด์ ๋ค์๊ณผ ๊ฐ๋ค.
์ปคํผ ๋ณด๊ธฐ ํ๋ฉด์ ๋ค์๊ณผ ๊ฐ๋ค.
์ ์ฒด ์ฃผ๋ฌธ ๋ชฉ๋ก ๋ณด๊ธฐ ํ๋ฉด์ ๋ค์๊ณผ ๊ฐ๋ค.
Spring Security๋ฅผ ์ ์ฉํ์ ๋, ๊ด๋ฆฌ์๋ง ์ ๊ทผํ ์ ์๋๋ก ์ค์ ํด์ฃผ๋ฉด ๋๋ค.
๋ง์ดํ์ด์ง ํ๋ฉด์ ๋ค์๊ณผ ๊ฐ๋ค.
๋ง์ดํ์ด์ง ํ๋ฉด์ Spring Security๋ฅผ ์ ์ฉํ์ ๋, ์ผ๋ฐ ์ฌ์ฉ์๋ง ์ ๊ทผํ ์ ์๋ ํ์ด์ง์ด์ด์ผ ํ๋ค.
์ด์ ์ํ ์ ํ๋ฆฌ์ผ์ด์ ์ Spring Security ์์กด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ถ๊ฐํด๋ณด์.
build.gradle์ dependencies์ ๋ค์๊ณผ ๊ฐ์ด spring-boot-starter-security ๋ฅผ ์ถ๊ฐํ๋ค.
dependencies {
...
...
implementation 'org.springframework.boot:spring-boot-starter-security' // (1)
...
...
}
๊ทธ๋ฆฌ๊ณ ์๋ฒ๋ฅผ ์คํํ์ฌ localhost:8080/login์ ์ ์ํ๋ฉด ์๋์ ๊ฐ์ ํ๋ฉด์ด ๋ํ๋๋ค.
์ด ํ๋ฉด์ ์ฐ๋ฆฌ๊ฐ ์ง์ ๋ง๋ ๋ก๊ทธ์ธ ํ์ด์ง๊ฐ ์๋๋ฉฐ, ๋ฐ๋ก Spring Security์์ ์๋ ๊ตฌ์ฑ์ ํตํด ๋ด๋ถ์ ์ผ๋ก ์ ๊ณตํด์ฃผ๋ ๋ํดํธ ๋ก๊ทธ์ธ ํ์ด์ง์ด๋ค. ๊ทธ๋ฆฌ๊ณ Username๊ณผ password ์ญ์ ๋ํดํธ ๊ฐ์ด ์๋์ ๊ฐ์ด ์ ํด์ ธ ์๋ค.
โ๏ธ Spring Security์์ ์ ๊ณตํด์ฃผ๋ ๋ํดํธ ๋ก๊ทธ์ธ ์ ๋ณด
- Username : 'user'
- Password : ์๋์ ๊ฐ์ด ์๋ฒ์ ๋ก๊ทธ์์ ํ์ธ ๊ฐ๋ฅ
ํ์ง๋ง ์ด ๋ฐฉ์์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์คํํ ๋๋ง๋ค ํจ์ค์๋๊ฐ ๋ณ๊ฒฝ๋๋ฉฐ, Spring Security์์ ์ ๊ณตํ๋ ๋ํดํธ ์ธ์ฆ ์ ๋ณด๋ง์ผ๋ก๋ ํ์ ๊ฐ์์ ์ธ์ฆ ์ ๋ณด๋ก ๋ก๊ทธ์ธ ํ ์ ์๋ค.
๋ฐ๋ผ์ Spring Security์ Configuration์ ํตํด ์ฐ๋ฆฌ์๊ฒ ๋ง๋ ์ธ์ฆ ๋ฐฉ์์ ์ค์ ํด๋ณด์.
Spring Security Configuration์ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ๋ค.
package com.codestates.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SecurityConfiguration {
// ์ฌ๊ธฐ์ Spring Security์ ์ค์ ์ ์งํํฉ๋๋ค.
}
์ฌ๊ธฐ์ InMemory User๋ก ์ธ์ฆํ ์ ์๋๋ก ์ค์ ์ ๋ณด๋ฅผ ์์ฑํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ ์ ์๋ค.
package com.codestates.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
@Configuration
public class SecurityConfiguration {
@Bean
public UserDetailsManager userDetailsService(){
UserDetails userDetails =
User.withDefaultPasswordEncoder()
.username("jeein@naver.com")
.password("1111")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
}
์ ์ฝ๋๋ฅผ ์์ฑํ๋ฉฐ, ์ฌ์ฉ์์ ๊ณ์ ์ ๋ณด๋ฅผ ๋ฉ๋ชจ๋ฆฌ์์ ์ง์ ํ๊ธฐ ๋๋ฌธ์ ์ ํ๋ฆฌ์ผ์ด์ ์ด ์คํ๋ ๋๋ง๋ค ์ฌ์ฉ์ ๊ณ์ ์ ๋ณด๊ฐ ๋ฐ๋ ์ผ์ ์๋ค.
UserDetails ์ธํฐํ์ด์ค๋ ์ธ์ฆ๋ ์ฌ์ฉ์์ ํต์ฌ ์ ๋ณด๋ฅผ ๋ด๊ณ ์์ผ๋ฉฐ, UserDetails ๊ตฌํ์ฒด์ธ User ํด๋์ค๋ฅผ ์ด์ฉํด ์ฌ์ฉ์์ ์ธ์ฆ ์ ๋ณด๋ฅผ ์์ฑํ๊ณ ์๋ค.
- withDefaultPasswordEncoder()๋ ๋ํดํธ ํจ์ค์๋ ์ธ์ฝ๋๋ฅผ ์ด์ฉํด ์ฌ์ฉ์์ ํจ์ค์๋๋ฅผ ์ํธํํ๋ค.
- username() ๋ฉ์๋๋ ์ฌ์ฉ์์ username์ ์ค์ ํ๋ค. (์ฌ๊ธฐ์ username์ ํ์๋ช ์ด ์๋๋ผ, ํ์์ ์๋ณํ ์ ์๋ ์ฌ์ฉ์ ์์ด๋ ๊ฐ์ ๊ฐ)
- password() ๋ฉ์๋๋ ์ฌ์ฉ์์ password๋ฅผ ์ค์ ํ๋ค.
- roles() ๋ฉ์๋๋ ์ฌ์ฉ์์ Role, ์ฆ ์ญํ ์ ์ง์ ํ๋ ๋ฉ์๋์ด๋ค.
Spring Security์์๋ ์ฌ์ฉ์์ ํต์ฌ ์ ๋ณด๋ฅผ ํฌํจํ UserDetails๋ฅผ ๊ด๋ฆฌํ๋ UserDetailsManager๋ผ๋ ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํ๋ค.
๊ทธ๋ฐ๋ฐ ์ฐ๋ฆฌ๋ ๋ฉ๋ชจ๋ฆฌ์์์ UserDetails๋ฅผ ๊ด๋ฆฌํ๋ฏ๋ก InMemoryUserDetailsManager๋ผ๋ ๊ตฌํ์ฒด๋ฅผ ์ฌ์ฉํ๋ค.
*UseDetails ์ธํฐํ์ด์ค
UserDetails ์ธํฐํ์ด์ค๋ ํต์ฌ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ฉฐ, ์ด๋ฅผ ๊ตฌํํ ํด๋์ค๋ Spring Security์์ ์ง์ ์ ์ผ๋ก ๋ณด์์ ๋ชฉ์ ์ผ๋ก ์ฌ์ฉ๋์ง๋ ์๋๋ค. ๊ตฌํ ํด๋์ค๋ ๋จ์ํ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ ๊ฒ์ด๊ณ , ์ด ์ ๋ณด๋ ๋์ค์ ์ธ์ฆ(Authentication) ๊ฐ์ฒด๋ก ์บก์ํ๋๋ค. ์ด๋ฅผ ํตํด ๋ณด์๊ณผ ๊ด๋ จ์ด ์๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ํธ๋ฆฌํ ์์น์ ์ ์ฅํ ์ ์๋ค.
*withDefaultPasswordEncoder : withDefaultPasswordEncoder() ๋ฉ์๋์ Deprecated๋ ํน์ดํ๊ฒ๋ ํฅํ ๋ฒ์ ์์ ์ ๊ฑฐ๋จ์ ์๋ฏธํ๊ธฐ ๋ณด๋ค๋ Production ํ๊ฒฝ์์ ์ธ์ฆ์ ์ํ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ณ ์ ํด์ ์ฌ์ฉํ์ง ๋ง๋ผ๋ ๊ฒฝ๊ณ ์ ์๋ฏธ๋ฅผ ๋ํ๋ด๊ณ ์๋ ๊ฒ์ด๋ ๋ฐ๋์ ํ ์คํธ ํ๊ฒฝ์ด๋ ๋ฐ๋ชจ ํ๊ฒฝ์์๋ง ์ฌ์ฉํ๊ธฐ ๋ฐ๋๋ค.
์์ ๋์ ์๋ฏ์ด UserDetails ์์ฑํ ๋์ User.withDefaultPasswordEncoder()๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ฉด ์์ค์ฝ๋์ "password"๊ฐ ์ปดํ์ผ๋๊ณ ์์ฑ ์์ ์ ๋ฉ๋ชจ๋ฆฌ์ ํฌํจ๋๊ธฐ ๋๋ฌธ์ ์ํธ๊ฐ plain text๋ก ๋ ธ์ถ๋ ์ ์์ด์ ํ๋ก๋์ ํ๊ฒฝ์์๋ ๋ฏธ๋ฆฌ ๋น๋ฐ๋ฒํธ๋ฅผ ํด์ํ๋ ๊ฒ์ด ์ข๋ค.
์๋ฅผ ๋ค์ด
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
// outputs {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
// remember the password that is printed out and use in the next step
System.out.println(encoder.encode("password"));
UserDetails user = User.withUsername("user")
.password("{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG")
.roles("USER")
.build();
์ด๋ ๊ฒ ๊ฐ์ด new InMemoryUserDetailsManager(userDetails)๋ฅผ ํตํด UserDetailsManager ๊ฐ์ฒด๋ฅผ Bean์ผ๋ก ๋ฑ๋กํ๋ฉด Spring ์์๋ ํด๋น Bean์ด ๊ฐ์ง๊ณ ์๋ ์ฌ์ฉ์์ ์ธ์ฆ ์ ๋ณด๊ฐ ํด๋ผ์ด์ธํธ์ ์์ฒญ์ด ๋์ด์ฌ ๊ฒฝ์ฐ, ์ ์์ ์ธ ์ธ์ฆ ํ๋ก์ธ์ค๋ฅผ ์ํํ๋ค.
๋ค์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์คํ์ํค๊ณ ๋ก๊ทธ์ธ ์ ๋ณด๋ฅผ ์ ๋ ฅํ๋ฉด ์ ์์ ์ผ๋ก ๋ก๊ทธ์ธ์ด ์ฑ๊ณตํ๋ ๊ฒ์ ํ์ธ ๊ฐ๋ฅํ๋ค.
ํ์ง๋ง ์ค์ ์๋น์ค์์ ์ด๋ ๊ฒ ์ฌ์ฉ์ ๊ณ์ ์ ๋ณด๋ฅผ ๊ณ ์ ํ์ฌ ์ฌ์ฉํ ์๋ ์๋ค. ์ค์ ๋ก๋ ์ฌ์ฉ์ ๊ณ์ ์ ๋ณด๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋์ด์ผ ํ๋ค.
๋ค์์ผ๋ก Spring Security์์ HTTP ๋ณด์์ ์ค์ ํ๊ธฐ ์ํ ๊ธฐ๋ณธ ์ฝ๋๋ฅผ ์์ฑํด ๋ณด์.
์ด๋ ์ฐ๋ฆฌ๊ฐ ๋ง๋ custom ๋ก๊ทธ์ธ ํ์ด์ง๋ฅผ ์ฌ์ฉํ ์ ์๋๋ก ์ค์ ์ ์ถ๊ฐํด๋ณด์.
package com.codestates.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
//HttpSecurity๋ฅผ ํตํด HTTP ์์ฒญ์ ๋ํ ๋ณด์ ์ค์ ๊ตฌ์ฑ
http
.csrf().disable() //(1)
.formLogin() //(2)
.loginPage("/auths/login-form") //(3)
.loginProcessingUrl("/process_login") //(4)
.failureUrl("/auths/login-form?error") //(5)
.and() //(6)
.authorizeHttpRequests() //(7)
.anyRequest() //(8)
.permitAll(); //(9)
return http.build();
}
@Bean
public UserDetailsManager userDetailsService(){
UserDetails userDetails =
User.withDefaultPasswordEncoder()
.username("jeein@naver.com")
.password("1111")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
}
- (1)์์๋ CSRF(Cross-Site Reqeust Forgery) ๊ณต๊ฒฉ์ ๋ํ Spring Security ์ค์ ์ ๋นํ์ฑํํ๊ณ ์๋ค. Spring Security๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์๋ฌด ์ค์ ํ์ง ์์ผ๋ฉด csrf() ๊ณต๊ฒฉ์ ๋ฐฉ์งํ๊ธฐ ์ํด ํด๋ผ์ด์ธํธ๋ก๋ถํฐ CSRF Token์ ์์ ํ ๊ฒ์ฆํ๋ค.
- (2)์์๋ formLogin()์ ํตํด ๊ธฐ๋ณธ์ ์ธ ์ธ์ฆ ๋ฐฉ๋ฒ์ ํผ ๋ก๊ทธ์ธ ๋ฐฉ์์ผ๋ก ์ง์ ํ๋ค.
- (3)์ loginPage()์์๋ ์ฐ๋ฆฌ๊ฐ ๋ง๋ค์ด ๋ custom ๋ก๊ทธ์ธ ํ์ด์ง๋ฅผ ์ฌ์ฉํ๋๋ก ์ค์ ํ๋ค.
- (4)์ loginProcessingUrl("/process_login") ๋ฉ์๋๋ฅผ ํตํด ๋ก๊ทธ์ธ ์ธ์ฆ ์์ฒญ์ ์ํํ ์์ฒญ URL์ ์ง์ ํ๋ค. loginProcessing()์ ํ๋ผ๋ฏธํฐ์ธ "/process_login"์ ์ฐ๋ฆฌ๊ฐ ๋ง๋ค์ด ๋ login.html์ form ํ๊ทธ์ action ์์ฑ์ ์ง์ ํ URL๊ณผ ๋์ผํ๋ค. ์์ง ์ฐ๋ฆฌ๋ AuthController์ ๋ณ๋์ ์ธ์ฆ ํ๋ก์ธ์ค ๋ก์ง์ ์ง์ ํ์ง ์์๋ค. ๊ฒฐ๊ตญ /process_login URL๋ก ์์ฒญ์ ์ ์กํ๋ฉด ์ฌ์ ํ Spring Security์์ ๋ด๋ถ์ ์ผ๋ก ์ธ์ฆ ํ๋ก์ธ์ค๋ฅผ ์งํํ๋ ๊ฒ์ด๋ค.
- (5)์ failureUrl("/auth/login-form?error") ๋ฉ์๋๋ฅผ ํตํด ๋ก๊ทธ์ธ ์ธ์ฆ์ด ์คํจํ ๊ฒฝ์ฐ ์ด๋ค ํ๋ฉด์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธํ ๊ฒ์ธ๊ฐ๋ฅผ ์ง์ ํ๋ค.
๋ก๊ทธ์ธ์ ์คํจํ ๊ฒฝ์ฐ, ๋ก๊ทธ์ธ ํ๋ฉด์ ํ์ํ๊ณ ๋ก๊ทธ์ธ ์ธ์ฆ์ ์คํจํ๋ค๋ ๋ฉ์์ง๋ฅผ ํ์ํด ์ฃผ๋ ๊ฒ์ด ๊ฐ์ฅ ์์ฐ์ค๋ฌ์์ผ๋ก failureUrl()์ ํ๋ผ๋ฏธํฐ๋ก ์ปค์คํ ๋ก๊ทธ์ธ ํ์ด์ง์ URL์ธ "/auths/login-form?error"๋ฅผ ์ง์ ํ๋ค.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="layouts/common-layout">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Hello Spring Security Coffee Shop</title>
</head>
<body>
<div class="container" layout:fragment="content">
<form action="/process_login" method="post">
<!-- (1) ๋ก๊ทธ์ธ ์คํจ์ ๋ํ ๋ฉ์์ง ํ์ -->
<div class="row alert alert-danger center" role="alert" th:if="${param.error != null}">
<div>๋ก๊ทธ์ธ ์ธ์ฆ์ ์คํจํ์ต๋๋ค.</div>
</div>
<div class="row">
<div class="col-xs-2">
<input type="email" name="username" class="form-control" placeholder="Email" />
</div>
</div>
<div class="row" style="margin-top: 20px">
<div class="col-xs-2">
<input type="password" name="password" class="form-control" placeholder="Password" />
</div>
</div>
<button class="btn btn-outline-secondary" style="margin-top: 20px">๋ก๊ทธ์ธ</button>
</form>
<div style="margin-top: 20px">
<a href="/members/register">ํ์๊ฐ์
</a>
</div>
</div>
</body>
</html>
(1)์ ๋ณด๋ฉด ${param.error} ์ ๊ฐ์ ํตํด ๋ก๊ทธ์ธ ์ธ์ฆ ์คํจ ๋ฉ์์ง ํ์ ์ฌ๋ถ๋ฅผ ๊ฒฐ์ ํ๊ณ ์๋ค.
${param.error} ๋ Spring Security Configuration์์ failureUrl(”/auths/login-form?error”)์ ?error ๋ถ๋ถ์ ํด๋นํ๋ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ฅผ ์๋ฏธํ๋ค.
- (7)์ authorizeHttpRequests() ๋ฉ์๋๋ฅผ ํตํด ํด๋ผ์ด์ธํธ์ ์์ฒญ์ด ๋ค์ด์ค๋ฉด ์ ๊ทผ ๊ถํ์ ํ์ธํ๊ฒ ๋ค๊ณ ์ ์ํ๋ค.
- (8), (9)์ anyRequest().permitAll() ๋ฉ์๋๋ฅผ ํตํด ํด๋ผ์ด์ธํธ์ ๋ชจ๋ ์์ฒญ์ ๋ํ ์ ๊ทผ์ ํ์ฉํ๋ค.
๊ทธ๋ผ ๋ค์์ผ๋ก request URI์ ์ ๊ทผ ๊ถํ์ ๋ถ์ฌํด๋ณด์.
์ฌ์ฉ์์๊ฒ ๋ถ์ฌ๋ Role์ ์ด์ฉํด์ ์ํ ์ ํ๋ฆฌ์ผ์ด์ ์ request URI์ ์ ๊ทผ ๊ถํ์ ๋ถ์ฌํด๋ณด๋๋ก ํ์.
package com.codestates.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
//HttpSecurity๋ฅผ ํตํด HTTP ์์ฒญ์ ๋ํ ๋ณด์ ์ค์ ๊ตฌ์ฑ
http
.csrf().disable()
.formLogin()
.loginPage("/auths/login-form")
.loginProcessingUrl("/process_login")
.failureUrl("/auths/login-form?error")
.and()
.exceptionHandling().accessDeniedPage("/auth/access-denied") //(1)
.and()
.authorizeHttpRequests(authorize -> authorize //(2)
.antMatchers("/orders/**").hasRole("ADMIN") //(2-1)
.antMatchers("/members/my-page").hasRole("USER") //(2-2)
.antMatchers("/**").permitAll() //(2-3)
);
return http.build();
}
@Bean
public UserDetailsManager userDetailsService(){
UserDetails userDetails =
User.withDefaultPasswordEncoder()
.username("jeein@naver.com")
.password("1111")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
}
(1)์์๋ exceptionHandling().accessDeniedPage("auth/access-denied")๋ฅผ ํตํด ๊ถํ์ด ์๋ ์ฌ์ฉ์๊ฐ ํน์ request URI์ ์ ๊ทผํ ๊ฒฝ์ฐ, ๋ฐ์ํ๋ 403 ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํ ํ์ด์ง๋ฅผ ์ค์ ํจ.
exceptionHandling() ๋ฉ์๋๋ ๋ฉ์๋์ ์ด๋ฆ ๊ทธ๋๋ก Exception์ ์ฒ๋ฆฌํ๋ ๊ธฐ๋ฅ์ ํ๋ฉฐ, ๋ฆฌํดํ๋ ExceptionHandlingConfigurer ๊ฐ์ฒด๋ฅผ ํตํด ๊ตฌ์ฒด์ ์ธ Exception ์ฒ๋ฆฌ๋ฅผ ํ ์ ์๋ค.
accessDeniedPage() ๋ฉ์๋๋ 403 ์๋ฌ ๋ฐ์ ์, ํ๋ผ๋ฏธํฐ๋ก ์ง์ ํ URL๋ก ๋ฆฌ๋ค์ด๋ ํธ ๋๋๋ก ํด์ค๋ค.
authorizeHttpRequest() ๋ฉ์๋๋ (2)์ ๊ฐ์ด ๋๋ ํํ์์ ํตํด request URI ์ ๋ํ ์ ๊ทผ ๊ถํ์ ๋ถ์ฌ ๊ฐ๋ฅํ๋ค.
antMatchers() ๋ฉ์๋๋ ์ด๋ฆ ๊ทธ๋๋ก ant ๋ผ๋ ๋น๋ ํด์์ ์ฌ์ฉ๋๋ Path Pattern์ ์ด์ฉํ์ฌ ๋งค์น๋๋ URL์ ํํํ๋ค.
(2-1)์ .antMatchers(”/orders/**”).hasRole(”ADMIN”)์ ADMIN Role์ ๋ถ์ฌ๋ฐ์ ์ฌ์ฉ์๋ง /orders๋ก ์์ํ๋ ๋ชจ๋ URL์ ์ ๊ทผํ ์ ์๋ค๋ ์๋ฏธ.
/orders/**์์ **์ /orders๋ก ์์ํ๋ ๋ชจ๋ ํ์ URL์ ํฌํจํ๋ค.
์๋ฅผ ๋ค์ด, /orders/1, /orders/1/coffees, /orders/1/coffee/1 ๊ณผ ๊ฐ์ ๋ชจ๋ ํ์ URL์ ํฌํจํ๋ค.
(2-2)์ antMatchers("/members/my-page").hasRole("USER")์ USER Role์ ๋ถ์ฌ๋ฐ์ ์ฌ์ฉ์๋ง /members/my-page URL์ ์ ๊ทผํ ์ ์๋ค.
(2-3)์ .anyMatchers("/**").permitAll()์ ์์์ ์ง์ ํ URL ์ธ์ ๋๋จธ์ง ๋ชจ๋ URL์ Role ์๊ด์์ด ์ ๊ทผ ๊ฐ๋ฅํจ์ ๋ํ๋.
antMatchers()๋ฅผ ์ด์ฉํ ์ ๊ทผ ๊ถํ ๋ถ์ฌ ์์๋ ๋ค์์ ์ฃผ์ํ์ฌ์ผ ํ๋ค.
.authorizeHttpRequests(authorize -> authorize
.antMatchers("⁄**").permitAll() // ์ด ํํ์์ด ์ ์ผ ์์ ์ค๋ฉด?
.antMatchers("/orders/**").hasRole("ADMIN")
.antMatchers("/members/my-page").hasRole("USER")
);
์ด์ฒ๋ผ .antMatchers("/**").permitAll()์ด ๋จผ์ ์ค๋ฉด Spring Security์์๋ Role์ ์๊ด์์ด ๋ชจ๋ request URL์ ๋ํ ์ ๊ทผ์ ํ์ฉํ๊ธฐ ๋๋ฌด์ ๋ค์์ ์ค๋ .antMatchers("/orders/**").hasRole("ADMIN")๊ณผ .antMatchers("/members/my-page").hasRole("USER")๋ ์ ๊ธฐ๋ฅ์ ํ์ง ๋ชปํ๊ฒ ๋๊ณ , ๊ฒฐ๊ณผ์ ์ผ๋ก ์ฌ์ฉ์์ Role๊ณผ๋ ๋ฌด๊ดํ๊ฒ ๋ชจ๋ request URL์ ์ ๊ทผํ ์ ์๊ฒ ๋๋ค.
ํ์ฌ ์ฐ๋ฆฌ๊ฐ ์์ฑํ SecurityConfiguration ํด๋์ค์๋ USER Role์ ๊ฐ์ง๋ ํ๋์ ์ฌ์ฉ์๋ง InMemory User๋ก ๋ฑ๋ก๋ ์ํ์ด๋ค.
์ด์ ADMIN Role์ ๊ฐ์ง๋ ์ฌ์ฉ์๋ฅผ ํ๋ ๋ ์ถ๊ฐํด์ .antMatchers("/orders/**").hasRole("ADMIN")๋ก ์ค์ ํ ํ๋ฉด์ ์ ๊ทผํ ์ ์๋์ง ํ์ธ์ ํด๋ณด๋๋ก ํ์.
@Bean
public UserDetailsManager userDetailsService(){
UserDetails user =
User.withDefaultPasswordEncoder()
.username("jeein@naver.com")
.password("1111")
.roles("USER")
.build();
//(1)
UserDetails admin =
User.withDefaultPasswordEncoder()
.username("admin@naver.com")
.password("2222")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user,admin);
}
(1)๊ณผ ๊ฐ์ด admin@naver.com์ด๋ผ๋ InMemory User๋ฅผ ํ๋ ๋ ์ถ๊ฐํ์์ผ๋ฉฐ, admin@naver.com ์๊ฒ๋ ADMIN Role์ด ๋ถ๊ฐ๋์๋ค.
์ ํ๋ฆฌ์ผ์ด์ ์ ๋ค์ ์คํํ๊ณ ์ํ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฉ์ธ ํ๋ฉด์์ [์ ์ฒด ์ฃผ๋ฌธ ๋ชฉ๋ก ๋ณด๊ธฐ] ๋ฉ๋ด๋ฅผ ํด๋ฆญํ๋ฉด ์ ์์ ์ผ๋ก ์ ๊ทผ์ด ๊ฐ๋ฅํ ๊ฒ์ ํ์ธํ ์ ์๋ค.
ํ์ฌ ํ๋ฉด์์๋ ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํ ํ์ ์ด๋ค ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํ๋์ง ์ ์ ์๋ค. ๋ํ ๋ก๊ทธ์ธํ ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์์์ ํ ์ ์๋ ๊ธฐ๋ฅ๋ ์๋ค. ๊ทธ๋ฆฌ๊ณ ๋ง์ดํ์ด์ง ๋งํฌ ์ญ์ ๋ก๊ทธ์ธ ํ ์ฌ์ฉ์์๊ฒ๋ง ๋ณด์ด๋ ๊ฒ์ด ์ข์ ๊ฒ ๊ฐ๋ค.
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"> <!-- (1) -->
<body>
<div align="right" th:fragment="header">
<a href="/members/register" class="text-decoration-none">ํ์๊ฐ์
</a> |
<span sec:authorize="isAuthenticated()"> <!-- (2) -->
<span sec:authorize="hasRole('USER')"> <!-- (3) -->
<a href="/members/my-page" class="text-decoration-none">๋ง์ดํ์ด์ง</a> |
</span>
<a href="/logout" class="text-decoration-none">๋ก๊ทธ์์</a> <!-- (4) -->
<span th:text="${#authentication.name}">ํ๊ธธ๋</span>๋ <!-- (5) -->
</span>
<span sec:authorize="!isAuthenticated()"> <!-- (6) -->
<a href="/auths/login-form" class="text-decoration-none">๋ก๊ทธ์ธ</a>
</span>
</div>
</body>
</html>
๋ก๊ทธ์์ ๋ฐ ๊ถํ๋ณ๋ก ๋ฉ๋ด ํ์๋ฅผ ํ๊ธฐ ์ํด ์์ ํ header.html
ํ์๋ฆฌํ ๊ธฐ๋ฐ์ HTML ํ ํ๋ฆฟ์์ ์ฌ์ฉ์์ ์ธ์ฆ ์ ๋ณด๋ ๊ถํ ์ ๋ณด๋ฅผ ์ด์ฉํด ์ด๋ค ๋ก์ง์ ์ฒ๋ฆฌํ๊ธฐ ์ํด์๋ ๋จผ์ (1)๊ณผ ๊ฐ์ด sec ํ๊ทธ๋ฅผ์ฌ์ฉํ๊ธฐ ์ํ XML ๋ค์์คํ์ด์ค๋ฅผ ์ง์ ํ๋ค.
๋ค์์คํ์ด์ค๋ฅผ ์ง์ ํ์ง ์์๋ ๋์ํ์ง๋ง, IDE์์ ๋นจ๊ฐ๊ฒ ํ์๋๋ค.
(2)์ ๊ฐ์ด ํ๊ทธ ๋ด๋ถ์์ sec:authorize="isAuthenticated()"์ ์ง์ ํ๋ฉด ํ์ฌ ํ์ด์ง์ ์ ๊ทผํ ์ฌ์ฉ์๊ฐ ์ธ์ฆ์ ์ฑ๊ณตํ ์ฌ์ฉ์์ธ์ง๋ฅผ ์ฒดํฌํ๋ค. ์ฆ, isAuthenticated()์ ๊ฐ์ด true์ด๋ฉด ํ๊ทธ ํ์์ ํฌํจ๋ ์ฝํ ์ธ ๋ฅผ ํ๋ฉด์ ํ์ํ๋ค.
๋ง์ดํ์ด์ง์ ๊ฒฝ์ฐ, ADMIN Role์ ๊ฐ์ง ์ฌ์ฉ์๋ ํ์ ์๋ ๊ธฐ๋ฅ์ด๋ฏ๋ก (3)๊ณผ ๊ฐ์ด sec:authorize="hasRole('USER')"์ ์ง์ ํด์ User Role์ ๊ฐ์ง ์ฌ์ฉ์์๊ฒ๋ง ํ์๋๋๋ก ํ๋ค.
(2)์์ isAuthenticated()์ ๊ฐ์ด true๋ผ๋ ์๋ฏธ๋ ์ด๋ฏธ ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ผ๋ ์๋ฏธ๋ก [๋ก๊ทธ์ธ] ๋ฉ๋ด ๋์ ์ (4)์ ๊ฐ์ด [๋ก๊ทธ์์] ๋ฉ๋ด๋ฅผ ํ์ํ๋ค. (4)์ href="/logout"์์ "/logout" URL์ SecurityConfiguration ํด๋์ค์์ ์ค์ ํ ๊ฐ๊ณผ ๊ฐ์์ผ ํ๋ค.
(5)์์๋ th:text="${#authentication.name}"๋ฅผ ํตํด ๋ก๊ทธ์ธ ์ฌ์ฉ์์ username์ ํ์ํ๊ณ ์๋ค. ์ด๊ณณ์๋ ์ฐ๋ฆฌ๊ฐ ๋ก๊ทธ์ธํ ๋ ์ฌ์ํ username์ด ํ์๋๋ค.
(6)์์๋ sec:authorize-"!isAuthenticated()"๋ฅผ ํตํด ๋ก๊ทธ์ธํ ์ฌ์ฉ์๊ฐ ์๋๋ผ๋ฉด [๋ก๊ทธ์ธ] ๋ฒํผ์ด ํ์๋๋๋ก ํ๋ค.
*sec ํ๊ทธ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋ build.gradle์ dependencies()์ ๋ค์์ ์์กด๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ถ๊ฐํด์ผ ํ๋ค.
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5’
๋ง์ง๋ง์ผ๋ก Spring Security์์ ๋ก๊ทธ์์ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๊ธฐ ์ํ ์ค์ ์ ํด๋ณด์.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
//HttpSecurity๋ฅผ ํตํด HTTP ์์ฒญ์ ๋ํ ๋ณด์ ์ค์ ๊ตฌ์ฑ
http
.csrf().disable()
.formLogin()
.loginPage("/auths/login-form")
.loginProcessingUrl("/process_login")
.failureUrl("/auths/login-form?error")
.and()
.logout() //(1)
.logoutUrl("/logout") //(2)
.logoutSuccessUrl("/") //(3)
.and()
.exceptionHandling().accessDeniedPage("/auth/access-denied")
.and()
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/orders/**").hasRole("ADMIN")
.antMatchers("/members/my-page").hasRole("USER")
.antMatchers("/**").permitAll()
);
return http.build();
}
๋ก๊ทธ์์์ ๋ํ ์ถ๊ฐ ์ค์ ์ ์ํด์๋ (1)๊ณผ ๊ฐ์ด logout()์ ๋จผ์ ํธ์ถํด์ผ ํ๋ค. logout() ๋ฉ์๋๋ ๋ก๊ทธ์์ ์ค์ ์ ์ํ LogoutConfigurer ๋ฅผ ๋ฆฌํดํ๋ค.
(2)์์๋ logoutUrl("/logout")์ ํตํด ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์์์ ์ํํ๊ธฐ ์ํ request URL์ ์ง์ ํ๋ค.
(3)์์๋ ๋ก๊ทธ์์์ ์ฑ๊ณต์ ์ผ๋ก ์ํํ ์ดํ ๋ฆฌ๋ค์ด๋ ํธํ URL์ ์ง์ ํ๋ค. ์ฌ๊ธฐ์๋ ๋ก๊ทธ์์ ์ดํ ์ํ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฉ์ธ ํ๋ฉด์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ ํ๋๋ก ์ง์ ํ๋ค.
์ฌ๊ธฐ๊น์ง InMemory User๋ฅผ ์ด์ฉํด ์ ํ๋ฆฌ์ผ์ด์ ์คํ ์, ๋ฉ๋ชจ๋ฆฌ์ ๋ ๊ฐ์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฏธ๋ฆฌ ๋ฑ๋กํ์ฌ ์ฌ์ฉ์ ๊ถํ ๋ณ๋ก request URL์ ์ ๊ทผ์ด ์ ํ๋๋์ง ์ฌ๋ถ๋ฅผ ์ดํด๋ณด์๋ค.
ํ์ง๋ง ์ ์์ ์ผ๋ก ๋์ํ์ง ์๋ ๊ธฐ๋ฅ์ด ์๋๋ฐ, ๋ฐ๋ก ํ์๊ฐ์ ์ด๋ค.
์ฐ๋ฆฌ๋ ์์ง ์ํ ์ ํ๋ฆฌ์ผ์ด์ ์์ ํ์ ๊ฐ์ ์์ฒญ์ผ๋ก ์ ๋ฌ๋ฐ์ ํ์ ์ ๋ณด๋ฅผ Spring Security๊ฐ ์ ์ ์๋ ์ด๋ค ์ฒ๋ฆฌ๋ ํ์ง ์์๋ค.
ํ์ ๊ฐ์ ํผ์ ํตํ InMemory User ๋ฅผ ๋ฑ๋กํ๊ธฐ ์ํ ์์ ์์๋ ๋ค์๊ณผ ๊ฐ๋ค.
- PasswordEncoder Bean ๋ฑ๋ก
- MemberService Bean ๋ฑ๋ก์ ์ํ JavaConfiguration ์์ฑ
- InMemoryMemberService ํด๋์ค ๊ตฌํ
๋จผ์ PasswordEncoder๋ฅผ Bean ๋ฑ๋กํด๋ณด์.
PasswordEncoder๋ Spring Security์์ ์ ๊ณตํ๋ ํจ์ค์๋ ์ํธํ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ์ปดํฌ๋ํธ์ด๋ค.
์ฐ๋ฆฌ๊ฐ ํ์ ๊ฐ์ ํผ์ ํตํด ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ๋ฌํ๋ ํจ์ค์๋๋ ์ํธํ๋์ง ์์ plain text์ด๋ค.
๋ฐ๋ผ์ ํ์ ๊ฐ์ ํผ์์ ์ ๋ฌ๋ฐ์ ํจ์ค์๋๋ InMemory User๋ก ๋ฑ๋ก๋๊ธฐ ์ ์ ์ํธํ๋์ด์ผ ํ๋ค.
PasswordEncoder๋ ๋ค์ํ ์ํธํ ๋ฐฉ์์ ์ฌ์ฉํ๋ฉฐ, Spring Security์์ ์ง์ํ๋ PasswordEncoder์ ๋ํดํธ ์ํธํ ์๊ณ ๋ฆฌ์ฆ์ bcrypt ์ด๋ค.
package com.codestates.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
//HttpSecurity๋ฅผ ํตํด HTTP ์์ฒญ์ ๋ํ ๋ณด์ ์ค์ ๊ตฌ์ฑ
http
.csrf().disable()
.formLogin()
.loginPage("/auths/login-form")
.loginProcessingUrl("/process_login")
.failureUrl("/auths/login-form?error")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.and()
.exceptionHandling().accessDeniedPage("/auth/access-denied")
.and()
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/orders/**").hasRole("ADMIN")
.antMatchers("/members/my-page").hasRole("USER")
.antMatchers("/**").permitAll()
);
return http.build();
}
@Bean
public UserDetailsManager userDetailsService(){
UserDetails user =
User.withDefaultPasswordEncoder()
.username("jeein@naver.com")
.password("1111")
.roles("USER")
.build();
//(1)
UserDetails admin =
User.withDefaultPasswordEncoder()
.username("admin@naver.com")
.password("2222")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user,admin);
}
@Bean
public PasswordEncoder passwordEncoder(){
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
SecurityConfiguration ํด๋์ค์์ PasswordEncoder๋ฅผ Bean์ผ๋ก ๋ฑ๋กํ๊ณ ์๋ค.
PasswordEncoderFactories.createDelegatingPasswordEncoder(); ๋ฅผ ํตํด์ DelegatingPasswordEncoder๋ฅผ ๋จผ์ ์์ฑํ๋๋ฐ, ์ด DelegatingPasswordEncoder๊ฐ ์ค์ง์ ์ผ๋ก PasswordEncoder ๊ตฌํ ๊ฐ์ฒด๋ฅผ ์์ฑํด์ค๋ค.
์ฐ๋ฆฌ๊ฐ userDetailsService() ๋ฉ์๋์์ ๋ฏธ๋ฆฌ ์์ฑํ๋ InMemoryUser ํจ์ค์๋๋ ๋ด๋ถ์ ์ผ๋ก ๋ํดํธ PasswordEncoder๋ฅผ ํตํด ์ํธํ๋๋ค.
MemberServcie Bean ๋ฑ๋ก์ ์ํ JavaConfiguration ์์ฑ
package com.codestates.member;
public interface MemberService {
Member createMember(Member member);
}
Hello Spring Security ์ํ ์ ํ๋ฆฌ์ผ์ด์ ์ ํ์ ๊ฐ์ ํผ์์ ์ ๋ฌ๋ฐ์ ์ ๋ณด๋ฅผ ์ด์ฉํด ์๋ก์ด ์ฌ์ฉ์๋ฅผ ์ถ๊ฐํ๋ ๊ธฐ๋ฅ๋ง ์์ผ๋ฉด ๋๋ฏ๋ก createMember() ํ๋๋ง ๊ตฌํํ๋ ๊ตฌํ์ฒด๊ฐ ์์ผ๋ฉด ๋๋ค.
InMemory User ๋ฑ๋ก์ ์ํ InMemoryMemberService ํด๋์ค
package com.codestates.member;
public class InMemoryMemberService implements MemberService {
public Member createMember(Member member) {
return null;
}
}
๋ฐ์ดํฐ๋ฒ ์ด์ค์ User๋ฅผ ๋ฑ๋กํ๊ธฐ ์ํ DBMemberService ํด๋์ค
package com.codestates.member;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public class DBMemberService implements MemberService {
public Member createMember(Member member) {
return null;
}
}
JavaConfiguration ๊ตฌ์ฑ
package com.codestates.config;
import com.codestates.member.InMemoryMemberService;
import com.codestates.member.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.UserDetailsManager;
@Configuration
public class JavaConfiguration {
// (1)
@Bean
public MemberService inMemoryMemberService(UserDetailsManager userDetailsManager,
PasswordEncoder passwordEncoder) {
return new InMemoryMemberService(userDetailsManager, passwordEncoder);
}
}
JavaConfiguration ํด๋์ค๋ MemberService ์ธํฐํ์ด์ค์ ๊ตฌํ ํด๋์ค์ธ InMemoryMemberService๋ฅผ Spring Bean์ผ๋ก ๋ฑ๋กํ๋ค.
(1)์์๋ MemberService ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด์ธ InMemoryMemberService ํด๋์ค์ Bean ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ค.
InMemoryMemberService ํด๋์ค๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋ ์์ด ๋ฉ๋ชจ๋ฆฌ์ Spring Security์ User๋ฅผ ๋ฑ๋กํด์ผ ํ๋ฏ๋ก UserDetailsManager๊ฐ์ฒด๊ฐ ํ์ํ๋ค. ๋ํ User ๋ฑ๋ก ์ ํจ์ค์๋๋ฅผ ์ํธํํ ํ์ ๋ฑ๋กํด์ผ ํ๋ฏ๋ก Spring Security์์ ์ ๊ณตํ๋ PasswordEncoder ๊ฐ์ฒด๊ฐ ํ์ํ๋ค.
์ด์ InMemoryMemberService๋ฅผ ๊ตฌํํด๋ณด์.
package com.codestates.member;
import com.codestates.auth.utils.AuthorityUtils;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.UserDetailsManager;
import java.util.List;
public class InMemoryMemberService implements MemberService { // (1)
private final UserDetailsManager userDetailsManager;
private final PasswordEncoder passwordEncoder;
// (2)
public InMemoryMemberService(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) {
this.userDetailsManager = userDetailsManager;
this.passwordEncoder = passwordEncoder;
}
public Member createMember(Member member) {
// (3)
List<GrantedAuthority> authorities = createAuthorities(Member.MemberRole.ROLE_USER.name());
// (4)
String encryptedPassword = passwordEncoder.encode(member.getPassword());
// (5)
UserDetails userDetails = new User(member.getEmail(), encryptedPassword, authorities);
// (6)
userDetailsManager.createUser(userDetails);
return member;
}
private List<GrantedAuthority> createAuthorities(String... roles) {
// (3-1)
return Arrays.stream(roles)
.map(role -> new SimpleGrantedAuthority(role))
.collect(Collectors.toList());
}
}
InMemoryMemberService ํด๋์ค๋ MemberService ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๋ ๊ตฌํ ํด๋์ค์ด๋ฏ๋ก (1)๊ณผ ๊ฐ์ด implements MemberService๋ฅผ ์ง์ ํ๋ค.
(2)์์๋ UserDetailsManager์ PasswordEncoder๋ฅผ DI ๋ฐ๋๋ค.
- UserDetailsManager๋ Spring Security์ User๋ฅผ ๊ด๋ฆฌํ๋ ์ญํ ์ ํ๋ค. ์ฐ๋ฆฌ๊ฐ SecurityConfiguration์์ Bean์ผ๋ก ๋ฑ๋กํ UserDetailsManager๋ InMemoryUserDetailsManager ์ด๋ฏ๋ก ์ฌ๊ธฐ์ DI ๋ฐ์ UserDetailsManager์ ํ์ ํ์ ์ InMemoryUserDetailsManager์ด๋ค.
- PasswordEncoder๋ Spring Secrutiy User๋ฅผ ๋ฑ๋กํ ๋ ํจ์ค์๋๋ฅผ ์ํธํํด์ฃผ๋ ํด๋์ค์ด๋ค. Spring Security 5์์๋ InMemory User๋ ํจ์ค์๋ ์ํธํ๊ฐ ํ์์ด๋ค.
Spring Security์์ User๋ฅผ ๋ฑ๋กํ๊ธฐ ์ํด์๋ ํด๋น User์ ๊ถํ(Authority)๋ฅผ ์ง์ ํด ์ฃผ์ด์ผ ํ๋ค.
๋ฐ๋ผ์ createAuthorities(Member.MemberRole.ROLE_USER.name());์ ์ด์ฉํด User์ ๊ถํ ๋ชฉ๋ก์ List<GrantedAuthority>๋ก ์์ฑํ๊ณ ์๋ค.
Member ํด๋์ค์๋ MemberRole์ด๋ผ๊ณ ํ๋ enum์ด ์ ํด์ ธ ์๊ณ , ROLE_USER, ROLE_ADMIN ์ด๋ผ๋ enum ํ์ ์ด ์ ์๋์ด ์๋ค.
Spring Security์์๋ SimpleGrantedAuthority๋ฅผ ์ฌ์ฉํด Role ๋ฒ ์ด์ค ํํ์ ๊ถํ์ ์ง์ ํ ๋, 'ROLE_'+๊ถํ๋ช ํํ๋ก ์ง์ ํด ์ฃผ์ด์ผ ํ๋ค. (๊ทธ๋ ์ง ์์ ๊ฒฝ์ฐ, ์ ์ ํ ๊ถํ ๋งคํ์ด ์ด๋ฃจ์ด์ง์ง ์์)
(3-1)์์๋ Java์ Stream API๋ฅผ ์ฌ์ฉํด ์์ฑ์ ํ๋ผ๋ฏธํฐ๋ก ํด๋น User์ Role์ ์ ๋ฌํ๋ฉด์ SimpleGrantedAuthority ๊ฐ์ฒด๋ฅผ ์์ฑํ ํ, List<SimpleGrantedAuthority> ํํ๋ก ๋ฆฌํดํด ์ค๋ค.
(4)์์๋ PasswordEncoder๋ฅผ ์ด์ฉํด ๋ฑ๋กํ User์ ํจ์ค์๋๋ฅผ ์ํธํํ๊ณ ์๋ค. ๋ง์ฝ ํจ์ค์๋๋ฅผ ์ํธํํ์ง ์๊ณ User๋ฅผ ๋ฑ๋กํ๋ค๋ฉด User ๋ฑ๋ก์ ๋์ง๋ง, ๋ก๊ทธ์ธ ์ธ์ฆ ์์ ๋ค์๊ณผ ๊ฐ์ ์๋ฌ๋ฅผ ๋ง๋๊ฒ ๋๋ค.
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null”
(5)์์๋ Spring Security User๋ก ๋ฑ๋กํ๊ธฐ ์ํด UserDetails๋ฅผ ์์ฑํ๋ค. (Spring Security์์๋ User ์ ๋ณด๋ฅผ UserDetails๋ก ๊ด๋ฆฌํ๋ค.)
์ด์ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ค์ ์คํํ๊ณ ํ์ ๊ฐ์ ๋ฉ๋ด์์ ํ์ ์ ๋ณด๋ฅผ ๋ฑ๋กํ ํ, ๋ฑ๋กํ ํ์ ์ ๋ณด๋ก ๋ก๊ทธ์ธ์ ์ํํ๋ฉด ์ ์์ ์ผ๋ก ๋ก๊ทธ์ธ์ด ๋๋ ๊ฒ์ ํ์ธ ๊ฐ๋ฅํ๋ค.
์ง๊ธ๊น์ง ๋ก๊ทธ์ธ ์ธ์ฆ ๋ฐฉ์์ ์ค๋ก์ง Spring Security์์ ์ ๊ณตํ๋ InMemory User๋ฅผ ์ฌ์ฉํ๋ ์ธ์ฆ ๋ฐฉ์์ด๋ค.
InMemory User๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ค์ ์์ํ๋ฉด ๋ฑ๋กํ๋ User ์ ๋ณด๊ฐ ๋ค ์ฌ๋ผ์ง๋ฏ๋ก, ๊ฐ๋จํ Member ์ํฐํฐ ํด๋์ค๋ฅผ ์ด์ฉํด์ ํ์ ์ธ์ฆ ์ ๋ณด๋ฅผ ํฌํจํ ํ์ ์ ๋ณด๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ ์ด๋ธ์์ ๊ด๋ฆฌํ๋๋ก ํด๋ณด์.
Spring Security์์๋ User ์ธ์ฆ ์ ๋ณด๋ฅผ ํ ์ด๋ธ์ ์ ์ฅํ๊ณ , ํ ์ด๋ธ์ ์ ์ฅ๋ ์ธ์ฆ ์ ๋ณด๋ฅผ ์ด์ฉํด ์ธ์ฆ ํ๋ก์ธ์ค๋ฅผ ์งํํ ์ ์๋ ๋ช๊ฐ์ง ๋ฐฉ๋ฒ์ด ์๋๋ฐ, ๊ทธ ์ค ํ๊ฐ์ง ๋ฐฉ๋ฒ์ด ๋ฐ๋ก Custom UserDetailsService๋ฅผ ์ด์ฉํ๋ ๋ฐฉ๋ฒ์ด๋ค.
*์ผ๋ฐ์ ์ผ๋ก Spring Security์์๋ ์ธ์ฆ์ ์๋ํ๋ ์ฃผ์ฒด๋ฅผ User(๋น์ทํ ์๋ฏธ๋ก Principal)๋ผ๊ณ ๋ถ๋ฅธ๋ค. Principal์ User์ ๊ตฌ์ฒด์ ์ธ ์ ๋ณด๋ฅผ ์๋ฏธํ๋ฉฐ, ์ผ๋ฐ์ ์ผ๋ก Spring Security์์์ Username์ ์๋ฏธํ๋ค. ์ํ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ Member ์ํฐํฐ ํด๋์ค๊ฐ ๋ก๊ทธ์ธ ์ธ์ฆ ์ ๋ณด๋ฅผ ํฌํจํ ํ ๋ฐ ์ด Member ์ํฐํฐ๊ฐ Spring Security์ User ์ ๋ณด๋ฅผ ํฌํจํ๋ค๊ณ ๋ณด๋ฉด ๋๋ค.
๋ก๊ทธ์ธ ์ธ์ฆ์ ์ํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋์ด ์๋ ์ธ์ฆ ์ ๋ณด๋ฅผ ์ฌ์ฉํ ๊ฒ์ด๋ฏ๋ก, InMemory User๋ฅผ ์ํ ์ค์ ๋ค์ ๋ ์ด์ ํ์์์ผ๋ฏ๋ก ์ ๊ฑฐํด์ผ ํ๋ค.
package com.codestates.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
//HttpSecurity๋ฅผ ํตํด HTTP ์์ฒญ์ ๋ํ ๋ณด์ ์ค์ ๊ตฌ์ฑ
http
.headers().frameOptions().sameOrigin() //(1)
.and()
.csrf().disable()
.formLogin()
.loginPage("/auths/login-form")
.loginProcessingUrl("/process_login")
.failureUrl("/auths/login-form?error")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.and()
.exceptionHandling().accessDeniedPage("/auth/access-denied")
.and()
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/orders/**").hasRole("ADMIN")
.antMatchers("/members/my-page").hasRole("USER")
.antMatchers("/**").permitAll()
);
return http.build();
}
//InMemory User๋ฅผ ์ํ ์ค์ ์ด๋ฏ๋ก ์ ๊ฑฐ ๋์...
// @Bean
// public UserDetailsManager userDetailsService(){
// UserDetails user =
// User.withDefaultPasswordEncoder()
// .username("jeein@naver.com")
// .password("1111")
// .roles("USER")
// .build();
//
// //(1)
// UserDetails admin =
// User.withDefaultPasswordEncoder()
// .username("admin@naver.com")
// .password("2222")
// .roles("ADMIN")
// .build();
//
//
// return new InMemoryUserDetailsManager(user,admin);
// }
@Bean
public PasswordEncoder passwordEncoder(){
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
SecurityConfiguration ํด๋์ค์์ (1)์ ์น ๋ธ๋ผ์ฐ์ ์์ H2 ์น ์ฝ์์ ์ ์์ ์ผ๋ก ์ฌ์ฉํ๊ธฐ ์ํ ์ค์ ์ด๋ค.
frameOptions()๋ HTML ํ๊ทธ ์ค์์ <frame>์ด๋ <iframe>,<object> ํ๊ทธ์์ ํ์ด์ง๋ฅผ ๋ ๋๋งํ ์ง์ ์ฌ๋ถ๋ฅผ ๊ฒฐ์ ํ๋ ๊ธฐ๋ฅ์ ํ๋ค.
Spring Security์์๋ ํด๋ฆญ์ฌํน ๊ณต๊ฒฉ์ ๋ง๊ธฐ ์ํด์ ๊ธฐ๋ณธ์ ์ผ๋ก frameOptions() ๊ธฐ๋ฅ์ด ํ์ฑํ๋์ด ์์ผ๋ฉฐ ๋ํดํธ ๊ฐ์ DENY์ด๋ค.
์ฆ, ์์์ ์ธ๊ทผํ HTML ํ๊ทธ๋ฅผ ์ด์ฉํ ํ์ด์ง ๋ ๋๋ง์ ํ์ฉํ์ง ์๊ฒ ๋ค๋ ๋ป์ด๋ฉฐ, (1)๊ณผ ๊ฐ์ด .frameOptions().sameOrigin()์ ํธ์ถํ๋ฉด ๋์ผ ์ถ์ฒ๋ก๋ถํฐ ๋ค์ด์ค๋ request๋ง ํ์ด์ง ๋ ๋๋ง์ ํ์ฉํ๋ค.
H2 ์น ์ฝ์์ ํ๋ฉด ์์ฒด๊ฐ ๋ด๋ถ์ ์ผ๋ก ํ๊ทธ๋ฅผ ์ฌ์ฉํ๊ณ ์์ผ๋ฏ๋ก ๊ฐ๋ฐ ํ๊ฒฝ์์๋ H2 ์น ์ฝ์์ ์ ์์ ์ผ๋ก ์ฌ์ฉํ ์ ์๋๋ก (1)๊ณผ ๊ฐ์ด ์ค์ ํ๋ฉด ๋๋ค.
(2)์ userDetailsService() ๋ฉ์๋๋ InMemory User๋ฆ ๋ฑ๋กํ๋ ์ญํ ์ ํ์ง๋ง, ์ด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ User๋ฅผ ๋ฑ๋กํ๊ณ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋ User์ ์ธ์ฆ ์ ๋ณด๋ฅผ ์ฌ์ฉํ ๊ฒ์ด๋ฏ๋ก ํ์๊ฐ ์๋ค.
JavaConfiguration์ Bean ๋ฑ๋ก ๋ณ๊ฒฝ
@Configuration
public class JavaConfiguration {
// (1)
@Bean
public MemberService dbMemberService(MemberRepository memberRepository,
PasswordEncoder passwordEncoder) {
return new DBMemberService(memberRepository, passwordEncoder); (1-1)
}
}
DBMemberService๋ User ์ธ์ฆ ์ ๋ณด๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๋ ์ญํ ์ ํ๋๋ฐ, Spring Security ์ ์ฅ์์ User๋ผ๊ณ ๋ถ๋ฅด๋ ์ ๋ณด๋ ์ฐ๋ฆญ ํ์ ๊ฐ์ ์ ๋ฑ๋กํ๋ ํ์ ์ ๋ณด ์์ ํฌํจ๋์ด ์๋ค๊ณ ๋ณด๋ฉด๋๋ค.
package com.codestates.member;
import com.codestates.exception.BusinessLogicException;
import com.codestates.exception.ExceptionCode;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Transactional
public class DBMemberService implements MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
public DBMemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
this.memberRepository = memberRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public Member createMember(Member member) {
verifyExistsEmail(member.getEmail());
String encryptedPassword = passwordEncoder.encode(member.getPassword());
member.setPassword(encryptedPassword);
Member savedMember = memberRepository.save(member);
System.out.println("# Create Member in DB");
return savedMember;
}
@Override
public Member findMember(String email){
return null;
}
private void verifyExistsEmail(String email){
Optional<Member> member = memberRepository.findByEmail(email);
if(member.isPresent())
throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS);
}
}
(1)์ ์์ฑ์๋ฅผ ํตํด MemberRepository์ PasswordEncoder Bean ๊ฐ์ฒด๋ฅผ DI ๋ฐ๋๋ค.
(2)์์ PasswordEncoder๋ฅผ ์ด์ฉํด ํจ์ค์๋๋ฅผ ์ํธํํ๋ค.
(3)์์ ์ํธํ๋ ํจ์ค์๋๋ฅผ password ํ๋์ ๋ค์ ํ ๋นํ๋ค.
๋ค์์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์กฐํํ User ์ธ์ฆ ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ธ์ฆ์ ์ฒ๋ฆฌํ๋ Custom UserDetailsService๋ฅผ ๊ตฌํํด ๋ณด์.
*UserDetailsService๋ Spring Security์์ ์ ๊ณตํ๋ ์ปดํฌ๋ํธ ์ค ํ๋๋ก User ์ ๋ณด๋ฅผ ๋ก๋ํ๋ ํต์ฌ ์ธํฐํ์ด์ค์ด๋ค. ์ฌ๊ธฐ์ '๋ก๋'์ ์๋ฏธ๋ ์ธ์ฆ์ ํ์ํ User ์ ๋ณด๋ฅผ ์ด๋๊ฐ์์ ๊ฐ์ ธ์จ๋ค๋ ์๋ฏธ์ด๋ฉฐ, ์ฌ๊ธฐ์ ๋งํ๋ '์ด๋๊ฐ'๋ ๋ฉ๋ชจ๋ฆฌ๊ฐ ๋ ์๋ ์๊ณ , DB ๋ฑ์ ์๊ตฌ ์ ์ฅ์๊ฐ ๋ ์๋ ์๋ค.
์ฐ๋ฆฌ๊ฐ InMemory User๋ฅผ ๋ฑ๋กํ๋๋ฐ ์ฌ์ฉํ๋ InMemoryUserDetailsManager๋ UserDetailsManager ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด์ด๊ณ , UserDetailsManager๋ UserDetailsService๋ฅผ ์์ํ๋ ํ์ฅ ์ธํฐํ์ด์ค์ด๋ค.
package com.codestates.auth;
import com.codestates.auth.utils.HelloAuthorityUtils;
import com.codestates.exception.BusinessLogicException;
import com.codestates.exception.ExceptionCode;
import com.codestates.member.Member;
import com.codestates.member.MemberRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Optional;
@Component
public class HelloUserDetailsServiceV1 implements UserDetailsService { //(1)
private final MemberRepository memberRepository;
private final HelloAuthorityUtils authorityUtils;
//(2)
public HelloUserDetailsServiceV1(MemberRepository memberRepository, HelloAuthorityUtils authorityUtils) {
this.memberRepository = memberRepository;
this.authorityUtils = authorityUtils;
}
//(3)
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
//(4)
Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember.getEmail());
//(5)
return new User(findMember.getEmail(), findMember.getPassword(), authorities);
}
}
๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์กฐํํ ์ธ์ฆ ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ธ์ฆ์ ์ฒ๋ฆฌํ๋ Custom UserDetailsService์ธ HelloUserDetailsService์ ํด๋์ค ์ฝ๋์ด๋ค,
HelloUserDetailsService์ ๊ฐ์ Custom UserDetailsService๋ฅผ ๊ตฌํํ๊ธฐ ์ํด์๋ (1)๊ณผ ๊ฐ์ด UserDetailsService ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํด์ผ ํ๋ค.
HelloUserDetailsService๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ User๋ฅผ ์กฐํํ๊ณ , ์กฐํํ User์ ๊ถํ(Role) ์ ๋ณด๋ฅผ ์์ฑํ๊ธฐ ์ํด (2)์ ๊ฐ์ด MemberRepository์ HelloAuthorityUtils ํด๋์ค๋ฅผ DI ๋ฐ๋๋ค.
UserDetailsService ์ธํฐํ์ด์ค๋ฅผ implements ํ๋ ๊ตฌํ ํด๋์ค๋ (3)๊ณผ ๊ฐ์ด loadUserByUserName(String username)์ด๋ผ๋ ์ถ์ ๋ฉ์๋๋ฅผ ๊ตฌํํด์ผ ํ๋ค.
(4)์์๋ HelloAuthorityUtils๋ฅผ ์ด์ฉํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์กฐํํ ํ์์ ์ด๋ฉ์ผ ์ ๋ณด๋ฅผ ์ด์ฉํด Role ๊ธฐ๋ฐ์ ๊ถํ ์ ๋ณด(GrantedAuthority) ์ปฌ๋ ์ ์ ์์ฑํ๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์กฐํํ ์ธ์ฆ ์ ๋ณด์ (4)์์ ์์ฑํ ๊ถํ ์ ๋ณด๋ฅผ Spring Security๊ฐ ์์ง ๋ชปํ๊ธฐ ๋๋ฌธ์ Spring Security์ ์ด ์ ๋ณด๋ค์ ์ ๊ณตํด ์ฃผ์ด์ผ ํ๋ฉฐ, (5)์์๋ UserDetails ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด์ธ User ํด๋์ค์ ๊ฐ์ฒด๋ฅผ ํตํด ์ ๊ณตํ๊ณ ์๋ค.
(5)์ ๊ฐ์ด ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์กฐํํ User ํด๋์ค์ ๊ฐ์ฒด๋ฅผ ๋ฆฌํดํ๋ฉด Spring Security๊ฐ ์ด ์ ๋ณด๋ฅผ ์ด์ฉํด ์ธ์ฆ ์ ์ฐจ๋ฅผ ์ํํ๋ค.
์ฆ, ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ User ์ธ์ฆ ์ ๋ณด๋ง Spring Security์๊ฒ ๋๊ฒจ์ฃผ๊ณ , ์ธ์ฆ ์ฒ๋ฆฌ๋ Spring Security๊ฐ ๋์ ํด์ค๋ค.
UserDetails๋ UserDetailsService์ ์ํด ๋ก๋๋์ด ์ธ์ฆ์ ์ํด ์ฌ์ฉ๋๋ ํต์ฌ User ์ ๋ณด๋ฅผ ํํํ๋ ์ธํฐํ์ด์ค์ด๋ค. UserDetails ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด๋ Spring Security์์ ๋ณด์ ์ ๋ณด ์ ๊ณต์ ๋ชฉ์ ์ผ๋ก ์ง์ ์ฌ์ฉ๋์ง๋ ์๊ณ , Authentication ๊ฐ์ฒด๋ก ์บก์ํ๋์ด ์ ๊ณต๋๋ค.
HelloAuthorityUtils์ ์ฝ๋๋ ์๋์ ๊ฐ๋ค.
package com.codestates.auth.utils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class HelloAuthorityUtils {
// (1)
@Value("${mail.address.admin}")
private String adminMailAddress;
// (2)
private final List<GrantedAuthority> ADMIN_ROLES = AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER");
// (3)
private final List<GrantedAuthority> USER_ROLES = AuthorityUtils.createAuthorityList("ROLE_USER");
public List<GrantedAuthority> createAuthorities(String email) {
// (4)
if (email.equals(adminMailAddress)) {
return ADMIN_ROLES;
}
return USER_ROLES;
}
}
HelloUserDetailsService์์ Role ๊ธฐ๋ฐ์ User ๊ถํ์ ์์ฑํ๊ธฐ ์ํ HelloAuthorityUtils ์ฝ๋์ด๋ค.
(1)์ application.yml์์ ์ถ๊ฐํ ํ๋กํผํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ํํ์์ด๊ณ , (1)๊ณผ ๊ฐ์ด @Value("${ํ๋กํผํฐ ๊ฒฝ๋ก}") ํํ์ ํํ๋ก ์์ฑํ๋ฉด application.yml ์ ์ ์๋์ด ์๋ ํ๋กํผํฐ์ ๊ฐ์ ํด๋์ค ๋ด์์ ์ฌ์ฉํ ์ ์๋ค.
(1)์์๋ applicaiton.yml ์ ๋ฏธ๋ฆฌ ์ ์ํ ๊ด๋ฆฌ์ ๊ถํ์ ๊ฐ์ง ์ ์๋ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ๋ถ๋ฌ์ค๊ณ ์๋ค.
application.yml ํ์ผ์ ์ ์ํ ๊ด๋ฆฌ์์ฉ ์ด๋ฉ์ผ ์ฃผ์๋ ํ์ ๋ฑ๋ก ์, ํน์ ์ด๋ฉ์ผ ์ฃผ์์ ๊ด๋ฆฌ์ ๊ถํ์ ๋ถ์ฌํ ์ ์๋์ง๋ฅผ ๊ฒฐ์ ํ๊ธฐ ์ํด ์ฌ์ฉ๋๋ค.
application.yml ํ์ผ์๋ ๋ค์๊ณผ ๊ฐ์ด ๊ด๋ฆฌ์ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์ ์ํด์ผ ํ๋ค.
mail:
address:
admin: admin@naver.com
(2)์์๋ Spring Security์์ ์ง์ํ๋ AuthorityUtils ํด๋์ค๋ฅผ ์ด์ฉํ์ฌ ๊ด๋ฆฌ์์ฉ ๊ถํ ๋ชฉ๋ก์ List<GrantedAuthority> ๊ฐ์ฒด๋ก ๋ฏธ๋ฆฌ ์์ฑํ๋ค.
๊ด๋ฆฌ์ ๊ถํ์ผ ๊ฒฝ์ฐ, ์ผ๋ฐ ์ฌ์ฉ์์ ๊ถํ๊น์ง ์ถ๊ฐ๋ก ํฌํจ๋์ด ์๋ค.
(3)์์๋ Spring Security์์ ์ง์ํ๋ AuthorityUtils ํด๋์ค๋ฅผ ์ด์ฉํ์ฌ ์ผ๋ฐ ์ฌ์ฉ ๊ถํ ๋ชฉ๋ก์ List<GrantedAuthority> ๊ฐ์ฒด๋ก ๋ฏธ๋ฆฌ ์์ฑํ๋ค.
(4)์์๋ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌ๋ฐ์ ์ด๋ฉ์ผ ์ฃผ์๊ฐ application.yml ํ์ผ์์ ๊ฐ์ ธ์จ ๊ด๋ฆฌ์์ฉ ์ด๋ฉ์ผ ์ฃผ์์ ๋์ผํ๋ค๋ฉด ๊ด๋ฆฌ์์ฉ ๊ถํ์ธ List<GrantedAuthority> ADMIN_ROLES๋ฅผ ๋ฆฌํดํ๋ค.
์ค๋ฌด์์๋ ๊ด๋ฆฌ์ ๊ถํ ๋ถ์ฌ๋ฅผ ์ํ ์ถ๊ฐ์ ์ธ ์ธ์ฆ ์ ์ฐจ๊ฐ ํ์ํ๋ค.
์กฐ๊ธ ๋ ์ ์ฐํ๊ณ ๊น๋ํ ์ฝ๋์ ๊ตฌ์ฑ์ ์ํด ์์์ ์์ฑํ HelloUserDetailsService ํด๋์ค๋ฅผ ์ด์ง ๊ฐ์ ํด๋ณด์.
package com.codestates.auth;
import com.codestates.auth.utils.HelloAuthorityUtils;
import com.codestates.exception.BusinessLogicException;
import com.codestates.exception.ExceptionCode;
import com.codestates.member.Member;
import com.codestates.member.MemberRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Optional;
@Component
public class HelloUserDetailsServiceV2 implements UserDetailsService {
private final MemberRepository memberRepository;
private final HelloAuthorityUtils authorityUtils;
public HelloUserDetailsServiceV2(MemberRepository memberRepository, HelloAuthorityUtils authorityUtils) {
this.memberRepository = memberRepository;
this.authorityUtils = authorityUtils;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
//(1) ๊ฐ์
๋ ๋ถ๋ถ
return new HelloUserDetails(findMember);
}
//(2) HelloUserDetails ํด๋์ค ์ถ๊ฐ
private final class HelloUserDetails extends Member implements UserDetails{ //(2-1)
//(2-2)
HelloUserDetails(Member member){
setMemberId(member.getMemberId());
setFullName(member.getFullName());
setEmail(member.getEmail());
setPassword(member.getPassword());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorityUtils.createAuthorities(this.getEmail()); //(2-3) ๋ฆฌํฉํ ๋ง ํฌ์ธํธ
}
// (2-4)
@Override
public String getUsername() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
}
๊ธฐ์กด์๋ loadUserByUsername() ๋ฉ์๋์ ๋ฆฌํด๊ฐ์ผ๋ก new User(findMember.getEmail(), findMember.getPassword(), authorities);๋ฅผ ๋ฆฌํดํ์ง๋ง ๊ฐ์ ๋ ์ฝ๋์์๋ (1)๊ณผ ๊ฐ์ด new HelloUserDetails(findMember); ๋ผ๋ Custom UserDetails ํด๋์ค์ ์์ฑ์๋ก findMember๋ฅผ ์ ๋ฌํ๋ฉด์ ์ฝ๋๊ฐ ์กฐ๊ธ ๋ ๊น๋ํด์ก๋ค.
๊ทธ๋ฆฌ๊ณ ๊ธฐ์กด์ loadUserByUsername() ๋ฉ์๋ ๋ด๋ถ์์ User ๊ถํ ์ ๋ณด๋ฅผ ์์ฑํ๋ Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember); ์ฝ๋๊ฐ ์ฌ๋ผ์ก๋ค. ์ด ์ฝ๋๋ (2)์์ ์ ์ํ HelloUserDetails ํด๋์ค ๋ด๋ถ๋ก ํฌํจ๋์๋ค.
(2)์ HelloUserDetails ํด๋์ค๋ UserDetails ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๊ณ ์๊ณ , ๋ํ Member ์ํฐํฐ ํด๋์ค๋ฅผ ์์ํ๊ณ ์๋ค.
์ด๋ ๊ฒ ๊ตฌ์ฑํ๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์กฐํํ ํ์ ์ ๋ณด๋ฅผ Spring Security์ User ์ ๋ณด๋กค ๋ณํํ๋ ๊ณผ์ ๊ณผ User์ ๊ถํ ์ ๋ณด๋ฅผ ์์ฑํ๋ ๊ณผ์ ์ ์บก์ํํ ์ ์๋ค.
๋ํ, HelloUserDetails ํด๋์ค๋ Member ์ํฐํฐ ํด๋์ค๋ฅผ ์์ํ๊ณ ์์ผ๋ฏ๋ก HelloUserDetails๋ฅผ ๋ฆฌํด๋ฐ์ ์ฌ์ฉํ๋ ์ธก์์๋ ๋ ๊ฐ์ ํด๋์ค์ ๊ฐ์ฒด๋ฅผ ๋ค ์์ฝ๊ฒ ์บ์คํ ํด์ ์ฌ์ฉ ๊ฐ๋ฅํ๋ค๋ ์ฅ์ ์ด ์๋ค.
(2-3)์์๋ HelloAuthorityUtils์ createAuthorities()๋ฉ์๋๋ฅผ ์ด์ฉํด User์ ๊ถํ ์ ๋ณด๋ฅผ ์์ฑํ๊ณ ์๋ค.
์ด ์ฝ๋๋ ๊ธฐ์กด์์ loadUserByUsername() ๋ฉ์๋ ๋ด๋ถ์ ์์์ง๋ง ์ง๊ธ์ HelloUserDetails ํด๋์ค ๋ด๋ถ์์ ์ฌ์ฉ๋๋๋ก ์บก์ํ๋์๋ค.
(2-4)์์๋ Spring Security์์ ์ธ์ํ ์ ์๋ username์ Member ํด๋์ค์ email ์ฃผ์๋ก ์ฑ์ฐ๊ณ ์๋ค. getUsername()์ ๋ฆฌํด๊ฐ์ null์ผ ์ ์๋ค. User์ ์๋ณ์๋ ๋ฌด์กฐ๊ฑด ์์ด์ผ ํ๋ฏ๋ก!
์ง๊ธ๊น์ง์ ์ฝ๋๋ User์ ๊ถํ ์ ๋ณด๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๊ด๋ฆฌํ๋ ๊ฒ์ด ์๋๋ผ, ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์กฐํํ User ์ ๋ณด๋ฅผ ๊ธฐ์ค์ผ๋ก ์ฝ๋ ์์์ ์กฐ๊ฑด์ ๋ง๊ฒ ์์ฑํ๊ณ ์๋ค.
User์ ๊ถํ ์ ๋ณด๋ฅผ DB์์ ๊ด๋ฆฌํ๊ธฐ ์ํ ๊ณผ์ ์ ๋ค์๊ณผ ๊ฐ๋ค.
- User์ ๊ถํ ์ ๋ณด๋ฅผ ์ ์ฅํ๊ธฐ ์ํ ํ ์ด๋ธ ์์ฑ
- ํ์ ๊ฐ์ ์, User์ ๊ถํ ์ ๋ณด(Role)๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๋ ์์
- ๋ก๊ทธ์ธ ์ธ์ฆ ์, User์ ๊ถํ ์ ๋ณด๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์กฐํํ๋ ์์
์ฐ์ , User์ ๊ถํ ์ ๋ณด ํ ์ด๋ธ๋ถํฐ ์์ฑํด๋ณด์.
User์ ๊ถํ ์ ๋ณด ํ ์ด๋ธ์ ์์ฑํ๊ธฐ ์ ์ User์ User์ ๊ถํ ์ ๋ณด ๊ฐ์ ๊ด๊ณ๋ฅผ ๋จผ์ ์๊ฐํด๋ณด์.
package com.codestates.member;
import com.codestates.audit.Auditable;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@NoArgsConstructor
@Getter
@Setter
@Entity
public class Member extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
@Column(length = 100, nullable = false)
private String fullName;
@Column(nullable = false, updatable = false, unique = true)
private String email;
@Column(length = 100, nullable = false)
private String password;
@Enumerated(value = EnumType.STRING)
@Column(length = 20, nullable = false)
private MemberStatus memberStatus = MemberStatus.MEMBER_ACTIVE;
//(1) User์ ๊ถํ ์ ๋ณด ํ
์ด๋ธ๊ณผ ๋งคํ๋๋ ์ ๋ณด
@ElementCollection(fetch = FetchType.EAGER)
private List<String> roles = new ArrayList<>();
public Member(String email) {
this.email = email;
}
public Member(String email, String fullName, String password) {
this.email = email;
this.fullName = fullName;
this.password = password;
}
public enum MemberStatus {
MEMBER_ACTIVE("ํ๋์ค"),
MEMBER_SLEEP("ํด๋ฉด ์ํ"),
MEMBER_QUIT("ํํด ์ํ");
@Getter
private String status;
MemberStatus(String status) {
this.status = status;
}
}
public enum MemberRole {
ROLE_USER,
ROLE_ADMIN
}
}
Member ์ํฐํฐ ํด๋์ค์ User ๊ถํ ์ ๋ณด๋ฅผ ๋งคํํ๋ ๊ฒ์ (1)๊ณผ ๊ฐ์ด ๊ฐ๋จํ๊ฒ ์ฒ๋ฆฌํ ์ ์๋ค.
(1)๊ณผ ๊ฐ์ด List, Set๊ณผ ๊ฐ์ ์ปฌ๋ ์ ํ์ ์ ํ๋๋ @ElementCollection ์ ๋ํ ์ด์ ์ ์ถ๊ฐํ์ฌ User ๊ถํ ์ ๋ณด์ ๊ด๋ จ๋ ๋ณ๋์ ์ํฐํฐ ํด๋์ค๋ฅผ ์์ฑํ์ง ์์๋ ๊ฐ๋จํ๊ฒ ๋งคํ ์ฒ๋ฆฌ๊ฐ ๋๋ค.
์ฆ, ์ด ์ํ์์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์คํํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ํ ์ด๋ธ์ด ์์ฑ๋๋ค.
Member ์ํฐํฐ ํด๋์ค์ ์ฐ๊ด ๊ด๊ณ ๋งคํ์ ๋ํ ํ ์ด๋ธ์ด ์์ฑ๋๋ค.
ํ ๋ช ์ ํ์์ด ํ ๊ฐ ์ด์์ Role์ ๊ฐ์ง ์ ์์ผ๋ฏ๋ก, MEMBER ํ ์ด๋ธ๊ณผ MEMBER_ROLES ํ ์ด๋ธ์ 1 ๋ N ๊ด๊ณ์ด๋ค.
ํ์ ๊ฐ์ ์ ํตํด ํ์ ์ ๋ณด๊ฐ MEMBER ํ ์ด๋ธ์ ์ ์ฅ๋ ๋, MEMBER_ROLES ํ ์ด๋ธ์ MEMBER_MEMBER_ID ์ด์๋ MEMBER ํ ์ด๋ธ์ ๊ธฐ๋ณธํค ๊ฐ์ด ๊ทธ๋ฆฌ๊ณ ROLES ์ด์๋ ๊ถํ ์ ๋ณด๊ฐ ์ ์ฅ๋๋ค.
ํ์ ๊ฐ์ ์ User์ ๊ถํ ์ ๋ณด๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํด๋ณด์.
package com.codestates.member;
import com.codestates.auth.utils.HelloAuthorityUtils;
import com.codestates.exception.BusinessLogicException;
import com.codestates.exception.ExceptionCode;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Transactional
public class DBMemberService implements MemberService {
...
...
private final HelloAuthorityUtils authorityUtils;
...
...
public Member createMember(Member member) {
verifyExistsEmail(member.getEmail());
String encryptedPassword = passwordEncoder.encode(member.getPassword());
member.setPassword(encryptedPassword);
// (1) Role์ DB์ ์ ์ฅ
List<String> roles = authorityUtils.createRoles(member.getEmail());
member.setRoles(roles);
Member savedMember = memberRepository.save(member);
return savedMember;
}
...
...
}
DBMemberService์์ ํ์ ๋ฑ๋ก ์, ํ์์ ๊ถํ ์ ๋ณด๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๋ ์ฝ๋๊ฐ ์ถ๊ฐ๋๋ค.
(1)์์๋ authorityUtils.createRoles(member.getEmail());๋ฅผ ํตํด ํ์์ ๊ถํ ์ ๋ณด(List<String> roles)๋ฅผ ์์ฑํ ๋ค member ๊ฐ์ฒด์๊ฒ ๋๊ฒจ์ฃผ๊ณ ์๋ค.
์๋์ ์ฝ๋๋ createRoles() ๋ฉ์๋๊ฐ ์ถ๊ฐ๋ HelloAuthorityUtils ํด๋์ค์ ์ฝ๋์ด๋ค.
package com.codestates.auth.utils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class HelloAuthorityUtils {
@Value("${mail.address.admin}")
private String adminMailAddress;
...
...
private final List<String> ADMIN_ROLES_STRING = List.of("ADMIN", "USER");
private final List<String> USER_ROLES_STRING = List.of("USER");
...
...
// (1) DB ์ ์ฅ์ฉ
public List<String> createRoles(String email) {
if (email.equals(adminMailAddress)) {
return ADMIN_ROLES_STRING;
}
return USER_ROLES_STRING;
}
}
(1)์์๋ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌ๋ ์ด๋ฉ์ผ ์ฃผ์๊ฐ application.yml ํ์ผ์ mail.address.admin ํ๋กํผํฐ์ ์ ์๋ ์ด๋ฉ์ผ ์ฃผ์์ ๋์ผํ๋ฉด ๊ด๋ฆฌ์ Role ๋ชฉ๋ก(ADMIN_ROLES_STRING)์ ๋ฆฌํดํ๊ณ , ๊ทธ ์ธ์๋ ์ผ๋ฐ ์ฌ์ฉ์ Role ๋ชฉ๋ก(USER_ROLES_STRING)์ ๋ฆฌํดํ๋ค.
๋ก๊ทธ์ธ ์ธ์ฆ ์, User์ ๊ถํ ์ ๋ณด๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์กฐํํ๋ ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ๋ค.
package com.codestates.auth;
import com.codestates.auth.utils.HelloAuthorityUtils;
import com.codestates.exception.BusinessLogicException;
import com.codestates.exception.ExceptionCode;
import com.codestates.member.Member;
import com.codestates.member.MemberRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Optional;
@Component
public class HelloUserDetailsServiceV3 implements UserDetailsService {
private final MemberRepository memberRepository;
private final HelloAuthorityUtils authorityUtils;
public HelloUserDetailsServiceV3(MemberRepository memberRepository, HelloAuthorityUtils authorityUtils) {
this.memberRepository = memberRepository;
this.authorityUtils = authorityUtils;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
return new HelloUserDetails(findMember);
}
private final class HelloUserDetails extends Member implements UserDetails {
HelloUserDetails(Member member) {
setMemberId(member.getMemberId());
setFullName(member.getFullName());
setEmail(member.getEmail());
setPassword(member.getPassword());
setRoles(member.getRoles()); // (1)
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// (2) DB์ ์ ์ฅ๋ Role ์ ๋ณด๋ก User ๊ถํ ๋ชฉ๋ก ์์ฑ
return authorityUtils.createAuthorities(this.getRoles());
}
...
...
}
}
๋ฐ์ดํฐ๋ฒ ์ด์ค์ MEMBER_ROLES ํ ์ด๋ธ์์ ์กฐํํ Role์ ๊ธฐ๋ฐ์ผ๋ก User ๊ถํ ๋ชฉ๋ก(List<GrantedAuthority>)์ ์์ฑํ๋ ๋ก์ง์ด ์ถ๊ฐ๋ HelloUserDetailsService ํด๋์ค.
๋จผ์ (1)์์๋ HelloUserDetails๊ฐ ์์ํ๊ณ ์๋ Member(extends Member)์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์กฐํํ List<String> roles๋ฅผ ์ ๋ฌํ๋ค.
๊ทธ๋ฆฌ๊ณ (2)์์ ๋ค์ Member(extends Member)์ ์ ๋ฌํ Role ์ ๋ณด๋ฅผ authorityUtils.createAuthorities() ๋ฉ์๋์ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌํด์ ๊ถํ ๋ชฉ๋ก(List<GrantedAuthority>์ ์์ฑํ๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค์์ Role ์ ๋ณด๋ฅผ ๊ฐ์ง๊ณ ์ค์ง ์์์ ๋์๋ authorityUtils.createAuthorities(this.getEmail()); ์ด์์.
์ด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์กฐํํ Role ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก User ๊ถํ ๋ชฉ๋ก์ ์์ฑํ๋ createAuthorities(List<String> roles) ๋ฉ์๋๊ฐ ์ถ๊ฐ๋ HelloAuthorityUtils๋ฅผ ๋ณด์
package com.codestates.auth.utils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class HelloAuthorityUtils {
@Value("${mail.address.admin}")
private String adminMailAddress;
private final List<GrantedAuthority> ADMIN_ROLES = AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER");
private final List<GrantedAuthority> USER_ROLES = AuthorityUtils.createAuthorityList("ROLE_USER");
private final List<String> ADMIN_ROLES_STRING = List.of("ADMIN", "USER");
private final List<String> USER_ROLES_STRING = List.of("USER");
// ๋ฉ๋ชจ๋ฆฌ ์์ Role์ ๊ธฐ๋ฐ์ผ๋ก ๊ถํ ์ ๋ณด ์์ฑ.
public List<GrantedAuthority> createAuthorities(String email) {
if (email.equals(adminMailAddress)) {
return ADMIN_ROLES;
}
return USER_ROLES;
}
// (1) DB์ ์ ์ฅ๋ Role์ ๊ธฐ๋ฐ์ผ๋ก ๊ถํ ์ ๋ณด ์์ฑ
public List<GrantedAuthority> createAuthorities(List<String> roles) {
List<GrantedAuthority> authorities = roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role)) // (2)
.collect(Collectors.toList());
return authorities;
}
...
...
}
(1)์ ๋ณด๋ฉด ๊ธฐ์กด์๋ application.yml ํ์ผ์ mail.address.admin ํ๋กํผํฐ์ ์ ์๋ ๊ด๋ฆฌ์ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ๊ธฐ์ค์ผ๋ก ๊ด๋ฆฌ์ Role์ ์ถ๊ฐํ์์ผ๋ ์ด์ ๋ ๊ทธ๋ด ํ์๊ฐ ์๋ค.
๋จ์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๊ฐ์ง๊ณ ์จ Role ๋ชฉ๋ก(List<String> roles)์ ๊ทธ๋๋ก ์ฌ์ฉํด์ ๊ถํ ๋ชฉ๋ก์ ๋ง๋ค๋ฉด ๋๊ธฐ ๋๋ฌธ์
์ฃผ์ํด์ผ ํ ๊ฒ์ (2)์ ๊ฐ์ด SimpleGrantedAuthority ๊ฐ์ฒด๋ฅผ ์์ฑํ ๋ ์์ฑ์ ํ๋ผ๋ฏธํฐ๋ก ๋๊ฒจ์ฃผ๋ ๊ฐ์ด "USER" ๋๋ "ADMIN"์ผ๋ก ๋๊ฒจ์ฃผ๋ฉด ์๋๊ณ "ROLE_USER", "ROLE_ADMIN" ํํ๋ก ๋๊ฒจ์ฃผ์ด์ผ ํ๋ค๋ ๊ฒ.
Custom AuthenticationProvider๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ
์์์ Custom UserDetailsService๋ฅผ ์ด์ฉํด ๋ก๊ทธ์ธ ์ธ์ฆ์ ํ๋ ๋ฐฉ์์ Spring Security๊ฐ ๋ด๋ถ์ ์ผ๋ก ์ธ์ฆ์ ๋์ ์ฒ๋ฆฌํด ์ฃผ๋ ๋ฐฉ์์ด๋ค.
์ด๋ฒ์๋ Custom AuthenticationProvider๋ฅผ ์ด์ฉํด ์ฐ๋ฆฌ๊ฐ ์ง์ ๋ก๊ทธ์ธ ์ธ์ฆ์ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ณด์.
Custom AuthenticationProvider๋ฅผ ์ดํดํ๋ ๋ฐ ๋์์ด ๋๊ณ , ๋์ ๋ณด์ ์๊ตฌ ์ฌํญ์ ๋ถํฉํ๋ ์ ์ ํ ์ธ์ฆ ๋ฐฉ์ ์๋ฅผ ๋ค์ด (2 Factor ์ธ์ฆ ๋ฑ)์ ์ง์ ๊ตฌํํด์ผ ํ ๊ฒฝ์ฐ, Custom AuthenticationProvider๊ฐ ํ์ํ ์ ์๋ค.
package com.codestates.auth;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Optional;
@Component
public class HelloUserAuthenticationProvider implements AuthenticationProvider { // (1)
private final HelloUserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
public HelloUserAuthenticationProvider(HelloUserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
// (3)
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication; // (3-1)
// (3-2)
String username = authToken.getName();
Optional.ofNullable(username).orElseThrow(() -> new UsernameNotFoundException("Invalid User name or User Password"));
// (3-3)
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String password = userDetails.getPassword();
verifyCredentials(authToken.getCredentials(), password); // (3-4)
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities(); // (3-5)
// (3-6)
return UsernamePasswordAuthenticationToken.authenticated(username, password, authorities);
}
// (2) HelloUserAuthenticationProvider๊ฐ Username/Password ๋ฐฉ์์ ์ธ์ฆ์ ์ง์ํ๋ค๋ ๊ฒ์ Spring Security์ ์๋ ค์ค๋ค.
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
private void verifyCredentials(Object credentials, String password) {
if (!passwordEncoder.matches((String)credentials, password)) {
throw new BadCredentialsException("Invalid User name or User Password");
}
}
}
(1)๊ณผ ๊ฐ์ด AuthenticationProvide์ ๊ตฌํ ํด๋์ค๋ก์จ HelloUserAuthenticationProvider๋ฅผ ๊ตฌํํด์ผ ํ๋ค.
Spring Security๋ ์ ์ฝ๋์ ๊ฐ์ด AuthentictionProvider๋ฅผ ๊ตฌํํ ํด๋์ค๊ฐ Spring Bean์ผ๋ก ๋ฑ๋ก๋์ด ์๋ค๋ฉด ํด๋น AuthenticationProvider๋ฅผ ์ด์ฉํด์ ์ธ์ฆ์ ์งํํ๋ค.
AuthenticationProvider ์ธํฐํ์ด์ค์ ๊ตฌํ ํด๋์ค๋ authenticate(Authentication authentication) ๋ฉ์๋์ supports(Class<?> authentication) ๋ฉ์๋๋ฅผ ๊ตฌํํด์ผ ํ๋ค.
๊ทธ ์ค (2)์ supports(Class<?> authentication) ๋ฉ์๋๋ ์ฐ๋ฆฌ๊ฐ ๊ตฌํํ๋ Custom AuthenticationProvider(HelloUserAthenticationProvider)๊ฐ Username/Password ๋ฐฉ์์ ์ธ์ฆ์ ์ง์ํ๋ค๋ ๊ฒ์ Spring Securityํํ ์๋ ค์ฃผ๋ ์ญํ ์ ํ๋ค.
supports() ์ ๊ฐ์ด true์ธ ๊ฒฝ์ฐ, Spring Security๋ ํด๋น AuthenticationProvider์ authenticate() ๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ ์ธ์ฆ์ ์งํํ๋ค.
(3)์ authenticate(Authentication authentication)์์ ์ฐ๋ฆฌ๊ฐ ์ง์ ์์ฑํ ์ธ์ฆ ์ฒ๋ฆฌ ๋ก์ง์ ์ด์ฉํด ์ฌ์ฉ์์ ์ธ์ฆ ์ฌ๋ถ๋ฅผ ๊ฒฐ์ ํ๋ค.
(3-1)์์ authentication์ ์บ์คํ ํ์ฌ UsernamePasswordAuthenticationToken์ ์ป๋๋ค.
์ด UsernamePasswordAuthenticationToken ๊ฐ์ฒด์์ (3-2)์ ๊ฐ์ด ํด๋น ์ฌ์ฉ์์ Username์ ์ป์ ํ, ์กด์ฌํ๋์ง ์ฒดํฌํ๋ค.
Username์ด ์กด์ฌํ๋ฉด (3-3)๊ณผ ๊ฐ์ด userDetailsService๋ฅผ ์ด์ฉํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ํด๋น ์ฌ์ฉ์๋ฅผ ์กฐํํ๋ค.
(3-4)์์ ๋ก๊ทธ์ธ ์ ๋ณด์ ํฌํจ๋ ํจ์ค์๋(authToken.getCredentials())์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋ ์ฌ์ฉ์์ ํจ์ค์๋ ์ ๋ณด์ ์ผ์นํ๋์ง๋ฅผ ๊ฒ์ฆํ๋ค.
(3-4)์ ์ธ์ฆ ๊ณผ์ ์ ํต๊ณผํ๋ค๋ฉด ๋ก๊ทธ์ธ ์ธ์ฆ์ ์ฑ๊ณตํ ์ฌ์ฉ์์ด๋ฏ๋ก (3-5)์ ๊ฐ์ด ํด๋น ์ฌ์ฉ์์ ๊ถํ์ ์์ฑํ๋ค.
๋ง์ง๋ง์ผ๋ก (3-6)๊ณผ ๊ฐ์ด ์ธ์ฆ๋ ์ฌ์ฉ์์ ์ธ์ฆ ์ ๋ณด๋ฅผ ๋ฆฌํด๊ฐ์ผ๋ก ์ ๋ฌํ๋ค. ์ด ์ธ์ฆ ์ ๋ณด๋ ๋ด๋ถ์ ์ผ๋ก Spring Security์์ ๊ด๋ฆฌํ๊ฒ ๋๋ค.
*SecurityConfiguration์ .loginProcessingUrl("/process_login")์์ "/process_login"์ ๋ํ ์์ฒญ์ด ๋ค์ด์ค๋ฉด HelloUserAuthenticationProvider๊ฐ ๋์ํ์ฌ ๋ก๊ทธ์ธ ์ธ์ฆ์ ์ฒ๋ฆฌํ๊ฒ ๋๊ณ , ๋ง์ฝ Custom AuthenticationProvider๋ฅผ ์์ฑํ์ง ์์ผ๋ฉด ๋ด๋ถ์ ์ผ๋ก AuthenticationProvider๋ฅผ ํธ์ถํ์ฌ ์ธ์ฆ ์ฒ๋ฆฌ๋ฅผ ํ๊ณ , ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณต๋๋ DaoAuthenticationProvider๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ค.
์ด์ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ค์ ์คํํ๊ณ , ํ์ ๊ฐ์ ์ ํ ๋ค, ๋ก๊ทธ์ธ์ ํด๋ณด์.
ํ์ ๊ฐ์ ์ ๋ฑ๋กํ ์ด๋ฉ์ผ ์ฃผ์/ํจ์ค์๋๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์ ๋ ฅํ๋ฉด ์ ์์ ์ผ๋ก Hello, Spring Security์ ๋ฉ์ธ ํ๋ฉด์ด ๋ธ๋ผ์ฐ์ ์ ํ์๋๋ค.
๋ง์ฝ ํ์ ๊ฐ์ ์ ํ์ง ์๊ณ ๋ก๊ทธ์ธ์ ํ ๊ฒฝ์ฐ ์ธ์ฆ์ ์คํจํ๊ณ , ๋ค์๊ณผ ๊ฐ์ ํ์ ์๋ฌ ํ๋ฉด์ ๋ง๋๊ฒ ๋๋ค.
์ฐ๋ฆฌ๊ฐ ์์์ HelloUserDetailsService๋ฅผ ํตํด ์ธ์ฆ์ ์ฒ๋ฆฌํ ๊ฒฝ์ฐ์๋ ์ธ์ฆ ์คํจ ์, Spring Security ๋ด๋ถ์์ ์ธ์ฆ ์คํจ์ ๋ํ ์ ์ฉ Exception์ธ AuthenticationException์ throwํ๊ฒ ๋๊ณ , ์ด AuthenticationException์ด throw ๋๋ฉด ๊ฒฐ๊ณผ์ ์ผ๋ก SecurityCofiguration์์ ์ค์ ํ .failureUrl("auths/login-form?error")๋ฅผ ํตํด ๋ก๊ทธ์ธ ํผ์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธํ๋ฉด์ ์๋์ ๊ฐ์ด "๋ก๊ทธ์ธ ์ธ์ฆ์ ์คํจํ์ต๋๋ค."๋ผ๋ ์ธ์ฆ ์คํจ ํ์ด์ง๋ฅผ ํ์ํ๋ค.
๊ทธ๋ฐ๋ฐ Custom AuthenticationProvider๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ, ํ์ ๊ฐ์ ์ ์ธ์ฆ ์คํจ ์, ์ ๊ทธ๋ฆผ๊ณผ ๊ฐ์ ํ๋ฉด์ด ์๋ "Whitelebel Error Page" ๊ฐ ํ์๋๋ ๊ฒ์ผ๊น?
์ด์ ๋ MemberService์์ ๋ฑ๋ก๋ ํ์ ์ ๋ณด๊ฐ ์์ผ๋ฉด, BusinessLogicException์ throw ํ๋๋ฐ ์ด๋ BusinessLogicException์ด Custom AuthenticationProvider๋ฅผ ๊ฑฐ์ณ์ ๊ทธ๋๋ก Spring Security ๋ด๋ถ ์์ญ์ผ๋ก throw๋๊ธฐ ๋๋ฌธ์ด๋ค.
Spring Security์์๋ ์ธ์ฆ ์คํจ ์, authenticationException์ด throw๋์ง ์์ผ๋ฉด Exception์ ๋ํ ๋ณ๋์ ์ฒ๋ฆฌ๋ฅผ ํ์ง ์๊ณ , ์๋ธ๋ฆฟ ์ปจํ ์ด๋์ ํฐ์บฃ ์ชฝ์ผ๋ก ์ฒ๋ฆฌ๋ฅผ ๋๊ธด๋ค.
๊ฒฐ๊ตญ ์๋ธ๋ฆฟ ์ปจํ ์ด๋ ์์ญ์์ ํด๋น Exception์ ๋ํ "/error" URL๋ก ํฌ์๋ฉํ๋๋ฐ ์ฐ๋ฆฌ๊ฐ ํน๋ณํ "/error" URL๋ก ํฌ์๋ฉ๋์์ ๋ ๋ณด์ฌ์ค ๋ทฐ ํ์ด์ง๋ฅผ ๋ณ๋๋ก ๊ตฌ์ฑํ์ง ์๊ธฐ ๋๋ฌธ์ ๋ํดํธ ํ์ด์ง์ธ "Whitelebel Error Page"๋ฅผ ๋ธ๋ผ์ฐ์ ์ ํ์ํ๋ ๊ฒ.
โญ๏ธ ํด๊ฒฐ์ฑ ์ ๊ฐ๋จํจ. Custom AuthenticationProvider์์ Exception์ด ๋ฐ์ํ ๊ฒฝ์ฐ, ์ด Exception์ catchํด์ AuthenticationException์ผ๋ก rethrow๋ฅผ ํด์ฃผ๋ฉด ๋๋ค.
package com.codestates.auth;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Optional;
@Component
public class HelloUserAuthenticationProvider implements AuthenticationProvider {
private final HelloUserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
public HelloUserAuthenticationProvider(HelloUserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
// V2: AuthenticationException์ rethrow ํ๋ ๊ฐ์ ์ฝ๋
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication;
String username = authToken.getName();
Optional.ofNullable(username).orElseThrow(() -> new UsernameNotFoundException("Invalid User name or User Password"));
try {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String password = userDetails.getPassword();
verifyCredentials(authToken.getCredentials(), password);
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
return UsernamePasswordAuthenticationToken.authenticated(username, password, authorities);
} catch (Exception ex) {
throw new UsernameNotFoundException(ex.getMessage()); // (1) AuthenticationException์ผ๋ก ๋ค์ throw ํ๋ค.
}
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
private void verifyCredentials(Object credentials, String password) {
if (!passwordEncoder.matches((String)credentials, password)) {
throw new BadCredentialsException("Invalid User name or User Password");
}
}
}
(1)์์ UsernameNotFoundException์ throw ํ๋๋ก ์์ ๋์๋๋ฐ, UsernameNotFoundException์ throwํ๋๋ก ์์ ๋์๋๋ฐ, UsernameNoFoundException์ AuthenticationException์ ์์ํ๋ ํ์ Exception์ด๊ธฐ ๋๋ฌธ์ ์ด UsernameNotFoundException์ด throwํ๊ฒ ๋๋ฉด Spring Security์ชฝ์์ ์ ์์ ์ผ๋ก catchํด์ ์ ์์ ์ธ ์ธ์ฆ ์คํจ ํ๋ฉด์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ ์์ผ์ค๋ค.
โญ Custom AuthenticationProvider์์ AuthenticationException์ด ์๋ Exception์ด ๋ฐ์ํ ๊ฒฝ์ฐ์๋ ๊ผญ AuthenticationException์ rethrow ํ๋๋ก ์ฝ๋๋ฅผ ๊ตฌ์ฑํด์ผ ํ๋ค
*AuthenticationProvider๋ Spring Security์์ ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ์ ๋ฌ๋ฐ์ ์ธ์ฆ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ์ธ์ฆ๋ ์ฌ์ฉ์์ธ์ง์ ๋ํ ์ธ์ฆ ์ฒ๋ฆฌ๋ฅผ ์ํํ๋ Spring Security ์ปดํฌ๋ํธ์ด๋ค. AuthenticationProvider๋ ์ธํฐํ์ด์ค ํํ.
๋ค์ ํฌ์คํ ์ ํตํด Srpgin Security์์ ๋ด๋ถ์ ์ผ๋ก ์ผ์ด๋๋ User ์ธ์ฆ๊ณผ ๊ถํ์ ๋ํ ์ฒ๋ฆฌ ๊ณผ์ ์ ๊ตฌ์ฒด์ ์ผ๋ก ์์๋ณด๋๋ก ํ์.

'Spring๐ธ' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Spring Security - JWT๋ฅผ ์ด์ฉํ ์๊ฒฉ ์ฆ๋ช ๋ฐ ๊ฒ์ฆ ๊ตฌํ (0) | 2023.06.07 |
---|---|
Spring Security - JWT ์๊ฒฉ ์ฆ๋ช ์ ์ํ ๋ก๊ทธ์ธ ์ธ์ฆ ๊ตฌํ (0) | 2023.06.07 |
Spring Security - JWT ์ธ์ฆ(Authentication) (0) | 2023.05.17 |
Spring MVC - JUnit์ ์ฌ์ฉํ ๋จ์ ํ ์คํธ (0) | 2023.04.30 |
Spring MVC - JDBC ๊ธฐ๋ฐ ๋ฐ์ดํฐ ์ก์ธ์ค ๊ณ์ธต (0) | 2023.04.18 |