๋ค์ด๊ฐ๊ธฐ์ ์
์ด๋ฒ ํ๋ก์ ํธ์์ ๋ฉค๋ฒ ๋ถ๋ถ์ ๊ตฌํํ๊ฒ ๋์๋ค. ๊ทธ์ ์ ์คํ๋ง ์ํ๋ฆฌํฐ์ ๋ํด์ ์์ธํ๊ฒ ํ์ตํ๊ณ ๊ฐ๋ณด์!
Spring Security๋?
์คํ๋ง ์ํ๋ฆฌํฐ๊ฐ ์ ์ฉ๋์ง ์์์์์ ๋ฌธ์ ์ ์ ๋จผ์ ์์๋ณด์.
1. ๋ก๊ทธ์ธ๊ธฐ๋ฅ์ Authentication(์ธ์ฆ) ์ด ์์.
2. API์ ๋ํ ๊ถํ ๋ถ์ฌ Authirization(์ธ๊ฐ) ๊ธฐ๋ฅ์ด ์์.
3. ์น ๋ณด์ ์ทจ์ฝ์ ์ ๋ํ ๋๋น(CSRF, ํด๋ฆญ์ ํน ๋ฑ)๊ฐ ์์.
๊ทธ๋ผ ์คํ๋ง ์ํ๋ฆฌํฐ๋ฅผ ์ ์ฉํ๋ฉด ์ด๋ค์ ์ด ๋ฌ๋ผ์ง๊น?
- ๋ค์ํ ์ ํ(ํผ ๋ก๊ทธ์ธ ์ธ์ฆ, ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ, OAuth 2 ๊ธฐ๋ฐ ์ธ์ฆ, LDAP ์ธ์ฆ)์ ์ฌ์ฉ์ ์ธ์ฆ ๊ธฐ๋ฅ ์ ์ฉ
- ์ ํ๋ฆฌ์ผ์ด์ ์ฌ์ฉ์์ ์ญํ (Role)์ ๋ฐ๋ฅธ ๊ถํ ๋ ๋ฒจ ์ ์ฉ
- ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ ๊ณตํ๋ ๋ฆฌ์์ค์ ๋ํ ์ ๊ทผ ์ ์ด
- ๋ฏผ๊ฐํ ์ ๋ณด์ ๋ํ ๋ฐ์ดํฐ ์ํธํ
- SSL ์ ์ฉ
- ์ผ๋ฐ์ ์ผ๋ก ์๋ ค์ง ์น ๋ณด์ ๊ณต๊ฒฉ ์ฐจ๋จ
- SSO, ํด๋ผ์ด์ธํธ ์ธ์ฆ๊ธฐ๋ฐ์ธ์ฆ, ๋ฉ์๋๋ณด์, ์ ๊ทผ ์ ์ด ๋ชฉ๋ก ๋ฑ
๋ค์๋งํด์ ์คํ๋ง์ํ๋ฆฌํฐ๋ ์ธ์ฆ, ๊ถํ๋ถ์ฌ ๋ฐ ๋ณดํธ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ํ๋ ์ ์ํฌ์ด๋ค. ๊ฐ๋ฐ์ ํ ๋ ๋ณด์๋ถ์ผ๋ ์ ๋ง ์ค์ํ์ง๋ง ๋ง์ ์๊ฐ์ด ์์๋๋ค. ์ด๋ฅผ ์ํด ๋ง๋ค์ด์ง ์คํ๋ง ์ํ๋ฆฌํฐ๋ฅผ ํตํด ์ธ์ฆ, ๊ถํ ๊ถํํ์ธ ๋ฑ์ ํ์ํ ๊ธฐ๋ฅ๋ค์ ์ฌ์ฉํ ์ ์๋ค.
์คํ๋ง ์ํ๋ฆฌํฐ ์ด์ธ์ Java ๋ณด์ ์๋ฃจ์ ๋ ์กด์ฌํ๋ค.
- OACC
๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๊ธฐ๋ณธ(InMemory ์ฌ์ฉ)
์ง๊ธ๋ถํฐ ์์๋ณผ ๋ถ๋ถ์ DB๊ฐ ์์ด InMemory๋ก ๊ตฌ์ฑํ๋ ๋ฐฉ๋ฒ์ด๋ค.Inmemory ๋ก ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ํ์ธํ๊ณ DB๋ฅผ ์ฐ๋ํ๋ ๋ฐฉ๋ฒ์ ๋ณด์.
๊ธฐ๋ณธ ์ค์
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.web.SecurityFilterChain;
@Configuration //์ด๋
ธํ
์ด์
์ผ๋ก ์ง์
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // (1) csrf ๊ณต๊ฒฉ ๋นํ์ฑ
.formLogin() // (2) ๋ก๊ทธ์ธ ๋ฐฉ์ ์ง์
.loginPage("/auths/login-form") // (3) ์ง์ ํด๋ ๋ก๊ทธ์ธ ํ์ด์ง ์ฌ์ฉ
.loginProcessingUrl("/process_login") // (4) ๋ก๊ทธ์ธ์์ฒญ ์ํ url ์ง์
.failureUrl("/auths/login-form?error") // (5) ์คํจ์์ redirect ํ ๊ณณ
.and() // (6) ๋ฉ์๋์ฒด์ธํํ๋ก ์ฌ์ฉ
.authorizeHttpRequests() // (7) ํด๋ผ์ด์ธํธ ์์ฒญ ์ ๊ทผ ๊ถํ ํ์ธ
.anyRequest() // (8) ๋ชจ๋ ์์ฒญ์ ์ ๊ทผ ํ์ฉ
.permitAll(); // (9) ํ์ฉ
return http.build();
}
/*
InMemoryUserDetailsManager๋ฅผ ์ด์ฉํด
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋ ์์ด ํ
์คํธ ๋ชฉ์ ์
InMemory User๋ฅผ ์์ฑ
*/
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user =
User.withDefaultPasswordEncoder()
.username("kevin@gmail.com") //๊ณ์ username
.password("1111") //๊ณ์ ๋น๋ฐ๋ฒํธ
.roles("USER") //๊ณ์ ๊ถํ
.build();
return new InMemoryUserDetailsManager(user);
}
}
URI ์ ๊ทผ ๊ถํ ๋ถ์ฌ
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.loginPage("/auths/login-form")
.loginProcessingUrl("/process_login")
.failureUrl("/auths/login-form?error")
.and()
.exceptionHandling().accessDeniedPage("/auths/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();
}
(1)์ ํตํด ์ ๊ทผ๊ถํ์ด ์๋ ๊ฒฝ์ฐ exception์ด ๋ฐ์ํ๊ณ ์ด๊ฒ์ ์ฒ๋ฆฌํ page๋ฅผ ์ง์ ํ๋ค.
(2)๋ฅผ ํตํด ๋๋ค ํํ์์ผ๋ก request URI์ ์ ๊ทผ๊ถํ์ ๋ถ์ฌํ๋ค.
(2-1) .antMatchers("/orders/**").hasRole("ADMIN")์ ADMIN Role์ ๋ถ์ฌ๋ฐ์ ์ฌ์ฉ์๋ง /orders๋ก ์์ํ๋ ๋ชจ๋ URL์ ์ ๊ทผํ ์ ์๋ค๋ ์๋ฏธ์ด๋ค.
/orders/์์ `๋ /orders๋ก ์์ํ๋ ๋ชจ๋ ํ์ URL์ ํฌํจํ๋ค.
์๋ฅผ ๋ค์ด/orders/1,/orders/1/coffees,/orders/1/coffees/1` ๊ฐ์ ๋ชจ๋ ํ์ URL์ ํฌํจํ๋ค. ๋ง์ฝ /orders/*๋ผ๋ URL์ ์ง์ ํ๋ค๋ฉด /orders/1๊ณผ ๊ฐ์ด /orders์ ํ์ URL์ depth๊ฐ 1์ธ URL๋ง ํฌํจํ๋ค.
(2-2)์ antMatchers("/members/my-page").hasRole("USER")์ USER Role์ ๋ถ์ฌ๋ฐ์ ์ฌ์ฉ์๋ง /members/my-page URL์ ์ ๊ทผํ ์ ์์์ ๋ํ๋ธ๋ค.
(2-3)์ .antMatchers("/**").permitAll()์ ์์์ ์ง์ ํ URL ์ด์ธ์ ๋๋จธ์ง ๋ชจ๋ URL์ Role์ ์๊ด์์ด ์ ๊ทผ์ด ๊ฐ๋ฅํจ์ ์๋ฏธํ๋ค.
antMatcher() ๋ฅผ ํตํด ์์ฑ๋ ๋ ๊ตฌ์ฒด์ ์ธ URL ๊ฒฝ๋ก๋ถํฐ ์ ๊ทผ๊ถํ์ ๋ถ์ฌํ๊ณ , ๋๊ตฌ์ฒด์ ์ธ URL ๊ฒฝ๋ก์ ์ ๊ทผ ๊ถํ์ ๋ถ์ฌํด์ผํ๋ค.
๋ก๊ทธ์์ ์ค์
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.loginPage("/auths/login-form")
.loginProcessingUrl("/process_login")
.failureUrl("/auths/login-form?error")
.and()
.logout() // (1) ๋ก๊ทธ์์ ์ค์ ์ ํจ
.logoutUrl("/logout") // (2) ๋ก๊ทธ์์ url์ ์ง์ ํ๋ค.
.logoutSuccessUrl("/") // (3) ๋ก๊ทธ์์ ์ฑ๊ณต์์ redirect url
.and()
.exceptionHandling().accessDeniedPage("/auths/access-denied")
.and()
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/orders/**").hasRole("ADMIN")
.antMatchers("/members/my-page").hasRole("USER")
.antMatchers("⁄**").permitAll()
);
return http.build();
}
ํ์๊ฐ์ ์ค์
ํ์๊ฐ์ ์ ํผ์ ํตํด ๋ฑ๋กํ๊ธฐ ์ํด์๋ ์๋์ ๊ฐ์ ์์ ์ด ํ์ํ๋ค.
- PasswordEncoder Bean ๋ฑ๋ก
- MemberService Bean ๋ฑ๋ก์ ์ํ JavaConfiguration ๊ตฌ์ฑ
- MemberService ํด๋์ค ๊ตฌํ
1. PasswordEncoder Bean ๋ฑ๋ก
PasswordEncoder ๋ ์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ ๊ณตํ๋ ํจ์ค์๋ ์ํธํ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ์ปดํฌ๋ํธ์ด๋ค. ํ์๊ฐ์ ํผ์ ํตํด์ ์ ๋ ฅ๋๋ ๋ฐ์ดํฐ๋ ์ํธํ๋์ง์์ Plain Text ์ด๋ค. ๊ทธ๋ฌ๋ฏ๋ก ์ด๋ฅผ DB์ ๊ธฐ๋กํ๊ธฐ์ ์ ์ํธํ๋ฅผ ํด์ผํ๋ค. ๋ค์ํ ์ํธํ ๋ฐฉ์์ ์ ๊ณตํ์ง๋ง default๋ bycrypt ๋ฐฉ์์ด๋ค.
@Configuration
public class SecurityConfiguration {
...
...
@Bean
public UserDetailsManager userDetailsService() {
UserDetails user =
User.withDefaultPasswordEncoder()
.username("kevin@gmail.com")
.password("1111")
.roles("USER")
.build();
UserDetails admin =
User.withDefaultPasswordEncoder()
.username("admin@gmail.com")
.password("2222")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
// (1)
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder(); // (1-1)
}
}
(1-1)์ ๋ณด๋ฉด PasswordEncoder๋ฅผ Bean์ผ๋ก ๋ฑ๋กํ๋ค. ๊ทธ๋ฆฌ๊ณ createDelegatingPasswordEncoder()๋ฅผ ํตํค์ DelegatingPasswordEncoder๋ฅผ ๋จผ์ ์์ฑํ๋ค. ๊ทธ๋ฆฌ๊ณ ์ด๋ฌํ DelegatingPasswordEncoder๊ฐ ์ค์ง์ ์ผ๋ก PasswordEncoder ๊ตฌํ์ฒด๋ฅผ ์์ฑํด ์ค๋ค.
2.MemberService Bean ๋ฑ๋ก์ ์ํ JavaConfiguration ๊ตฌ์ฑ
@Configuration
public class JavaConfiguration {
// (1)
@Bean
public MemberService inMemoryMemberService(UserDetailsManager userDetailsManager,
PasswordEncoder passwordEncoder) {
return new InMemoryMemberService(userDetailsManager, passwordEncoder);
}
}
(1)์์ Bean์ผ๋ก ๋ฑ๋ก์ ํ๋ค. User ๋ฑ๋ก์์ ํจ์ค์๋๋ฅผ ์ํธํ ํ ํ์ ๋ฑ๋กํด์ผ๋๋ฏ๋ก, PasswordEncoder ๊ฐ์ฒด๊ฐ ํ์ํ๋ค. ๊ทธ๋ฌ๋ฏ๋ก ์ด๋ฌํ ๋๊ฐ์ง ๊ฐ์ฒด๋ฅผ ์์ฑ์์ DI ํด์ค๋ค.
3. MemberService ๊ตฌํ
public class InMemoryMemberService implements MemberService { // (1)
private final UserDetailsManager userDetailsManager;
private final PasswordEncoder passwordEncoder;
// (2) UserDetails, PasswordEncoder๋ฅผ DI ๋ฐ๋๋ค.
public InMemoryMemberService(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) {
this.userDetailsManager = userDetailsManager;
this.passwordEncoder = passwordEncoder;
}
public Member createMember(Member member) {
// (3) User์ ๊ถํ์ ์ง์ ํ๋ค.
List<GrantedAuthority> authorities = createAuthorities(Member.MemberRole.ROLE_USER.name());
// (4) PasswordEncoder๋ฅผ ์ด์ฉํด ๋ฑ๋กํ User์ ํจ์ค์๋๋ฅผ ์ํธํํ๋ค.
String encryptedPassword = passwordEncoder.encode(member.getPassword());
// (5) User๋ก ๋ฑ๋กํ๊ธฐ ์ํด UserDetails๋ฅผ ์์ฑํ๋ค.
UserDetails userDetails = new User(member.getEmail(), encryptedPassword, authorities);
// (6) UserDetailsManager์ create ๋ฉ์๋๋ฅผ ์ด์ฉ, ์ ์ ๋ฅผ ์์ฑํ๋ค.
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());
}
}
DB ์ฐ๋ ๋ก๊ทธ์ธ ๊ตฌํ
User ์ ์ธ์ฆ์ ๋ณด๋ฅผ ํ ์ด๋ธ์ ์ ์ฅํ๊ณ , ํ ์ด๋ธ์ ์ ์ฅ๋ ์ธ์ฆ์ ๋ณด๋ฅผ ์ด์ฉํด์ ์ธ์ฆ ํ๋ก์ธ์ค๋ฅผ ์งํํ ์์๋ ๋ฐฉ๋ฒ์ ์์๋ณด์. ๊ทธ์ค ํ๊ฐ์ง๊ฐ Custom UserDetailService๋ฅผ ์ด์ฉํ๋ ๊ฒ์ด๋ค.
์ํ๋ฆฌํฐ์์ ์ธ์ฆ์ ์๋ํ๋ ์ฃผ์ฒด๋ฅผ User(Principal)์ด๋ผ๊ณ ํ๋ค. Principal์ User์ ๊ตฌ์ฒด์ ์ธ ์ ๋ณด๋ฅผ ์๋ฏธํ๊ณ , ์ผ๋ฐ์ ์ผ๋ก ์ํ๋ฆฌํฐ์์์ Username์ ์๋ฏธํ๋ค.
1. SecurityConfiguration ์ ์ค์ ๋ณ๊ฒฝ ๋ฐ ์ถ๊ฐ
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
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("/auths/access-denied")
.and()
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/orders/**").hasRole("ADMIN")
.antMatchers("/members/my-page").hasRole("USER")
.antMatchers("⁄**").permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
(1)์ .frameOptions().sameOrigin()์ ํธ์ถํ๊ฒ ๋๋ฉด, ๋์ผ ์ถ์ฒ๋ก ๋ถํฐ ๋ค์ด์ค๋ request๋ง ํ์ด์ง ๋ ๋๋ง์ ํ์ฉํ๋ค.
2. Java Configuration์ Bean ๋ฑ๋ก ๋ณ๊ฒฝ
@Configuration
public class JavaConfiguration {
// (1)
@Bean
public MemberService dbMemberService(MemberRepository memberRepository,
PasswordEncoder passwordEncoder) {
return new DBMemberService(memberRepository, passwordEncoder); (1-1)
}
}
(1-1) ๋ฐ์ดํฐ ๋ฒ ์ด์ค์ User์ ์ ๋ณด๋ฅผ ์ ์ฅํ๊ธฐ ์ํด MemberRepository์ PasswordEncoder ๊ฐ์ฒด๋ฅผ DIํ๋ dbMemberService ๋ฅผ ์์ฑํ๋ค.
3. DB MemberService ๊ตฌํ
@Transactional
public class DBMemberService implements MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
// (1) ์์ฑ์๋ฅผ ํตํด Bean๊ฐ์ฒด๋ฅผ DI ๋ฐ์.
public DBMemberService(MemberRepository memberRepository,
PasswordEncoder passwordEncoder) {
this.memberRepository = memberRepository;
this.passwordEncoder = passwordEncoder;
}
public Member createMember(Member member) {
verifyExistsEmail(member.getEmail());
String encryptedPassword = passwordEncoder.encode(member.getPassword()); // (2) ํจ์ค์๋ ์ํธํ
member.setPassword(encryptedPassword); // (3) ์ํธํ๋ ํจ์ค์๋๋ฅผ password ํ๋์ ํ ๋น
Member savedMember = memberRepository.save(member);
System.out.println("# Create Member in DB");
return savedMember;
}
...
...
}
(2), (3)ํจ์ค์๋๋ ์ํธํ๋ ์ํ์์ ๋ณตํธํํ ํ์๊ฐ ์์ผ๋ฏ๋ก ๋จ๋ฐฉํฅ ์ํธํ๋ฐฉ์์ผ๋ก ์ ์ฉ๋์ผ ํ๋ค!
4. Custom UserDetailService ๊ตฌํ
@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)loadUserByUsername ์ถ์ ๋ฉ์๋๋ฅผ ๊ตฌํ
@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)์ด๋ฉ์ผ ์ ๋ณด๋ฅผ ์ด์ฉํด Role๊ธฐ๋ฐ ๊ถํ์ ๋ณด ์ปฌ๋ ์
์์ฑ
Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember.getEmail());
// (5)User๊ฐ์ฒด๋ฅผ ๋ฆฌํดํ๋ฉด Security๊ฐ ๋ฆฌํด๋ ์ ๋ณด๋ก ์ธ์ฆ์ ์ฐจ๋ฅผ ์ํ
return new User(findMember.getEmail(), findMember.getPassword(), authorities);
}
}
์ผ๋จ ๊ณต๋ถ๋ฅผ ์ํด์ V1 ์ผ๋ก ์์ฑ์ ํ์๋ค. UserDetails์ ๊ตฌํํด๋์ค์ธ User๊ฐ์ฒด๋ฅผ (5)์์ ์ง์ ์ ์ผ๋ก ์์ฑํด์ ๋ฆฌํดํ๊ณ ์๋๋ฐ, ์ด๋ถ๋ถ์ ์๋์์ ๊ณ ์ณ๋ณผ๊ฒ์ด๋ค.
User DetailsService?
spring security์์ ์ ๊ณตํ๋ ์ปดํฌ๋ํธ ์ค ํ๋๋ก, User ์ ๋ณด๋ฅผ ๋ก๋ํ๋ ํต์ฌ ์ธํฐํ์ด์ค์ด๋ค. load ๋ ์ธ์ฆ์ ํ์ํ User ์ ๋ณด๋ฅผ DB๋ ๋ฉ๋ชจ๋ฆฌ์์ ๊ฐ์ ธ์จ๋ค. ๋ ์๋ฏธ์ด๋ค.
UserDetails?
UserDetails ๋ UserDetailsService์ ์ํด ๋ก๋ ๋์ด ์ธ์ฆ์ ์ํด ์ฌ์ฉ๋๋ ํต์ฌ User ์ ๋ณด๋ฅผ ํํํ๋ ์ธํฐํ์ด์ค์ด๋ค. UserDetails ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด๋ Spring Security์์ ๋ณด์ ์ ๋ณด ์ ๊ณต์ ๋ชฉ์ ์ผ๋ก ์ง์ ์ ๊ณต๋์ง ์๊ณ , Authentication ๊ฐ์ฒด๋ก ์บก์ํ ๋์ด ์ ๊ณต๋๋ค.
5. HelloAuthorityUtils ์์ฑ
@Component
public class HelloAuthorityUtils {
@Value("${mail.address.admin}") // (1) application.yml์์ ํ๊ฒฝ๋ณ์ ๊ฐ์ ธ์ค๊ธฐ
private String adminMailAddress; //๊ด๋ฆฌ์๊ถํ ์ด๋ฉ์ผ์ ๊ฐ์ง๊ณ ์จ๋ค.
// (2) AuthorityUtils ์ด์ฉ, ๊ด๋ฆฌ์์ฉ ๊ถํ ๋ชฉ๋ก์ List ๊ฐ์ฒด๋ก ์์ฑ
private final List<GrantedAuthority> ADMIN_ROLES = AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER");
// (3)AuthorityUtils ์ด์ฉ, ์ผ๋ฐ ์ฌ์ฉ์์ฉ ๊ถํ ๋ชฉ๋ก์ List ๊ฐ์ฒด๋ก ์์ฑ
private final List<GrantedAuthority> USER_ROLES = AuthorityUtils.createAuthorityList("ROLE_USER");
public List<GrantedAuthority> createAuthorities(String email) {
// (4) ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌ๋ฐ์ ์ด๋ฉ์ผ์ฃผ์์ application.yml์ ๊ด๋ฆฌ์์ ๋์ผํ๋ฉด admin return
if (email.equals(adminMailAddress)) {
return ADMIN_ROLES;
}
return USER_ROLES;
}
}
HelloAuthorityUtils๋ Role ๊ธฐ๋ฐ์ User ๊ถํ์ ์์ฑํ๊ธฐ ์ํด ์ฌ์ฉํ๋ค.
6. CustomUserDetails ๊ตฌํ ๋ฐ ๊ฐ์
@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));
return new HelloUserDetails(findMember); // (1) ๊ฐ์ ๋ ๋ถ๋ถ
}
// (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) ๋ฆฌํฉํ ๋ง ํฌ์ธํธ
} //helloAuthorityUtils์ createAuthorities ์ด์ฉํ์ฌ User ๊ถํ์ ๋ณด๋ฅผ ์์ฑ.
// (2-4)
@Override
public String getUsername() { //getUsername์ ๋ฆฌํด๊ฐ์ null์ผ์ ์์.
return getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
}
๊ธฐ์กด์ loadUserByUsername()๋ฉ์๋์ ๋ฆฌํด๊ฐ์ผ๋ก new User(...)์ ๋ฆฌํดํ์ง๋ง, ๊ฐ์ ๋ ์ฝ๋์์๋ (1)์ฒ๋ผ HelloUserDetails(findMember) ๋ผ๋ Custom UserDetails ํด๋์ค์ ์์ฑ์๋ก findMember๋ฅผ ์ ๋ฌํ๋ค. ์ด๋ฅผ ํตํด ์ฝ๋๊ฐ ํจ์ฌ ๊น๋ํด์ก๊ณ , ๊ธฐ์กด์ ๊ถํ ์ ๋ณด๋ฅผ ์์ฑํ๋Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember) ์ฝ๋๋ฅผ (2)์ HelloUserDetails ํด๋์ค์ ๋ด๋ถ๋ก ํฌํจ์์ผฐ๋ค.
(2)์ HelloUserDetails ํด๋์ค๋ UserDetails ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๊ณ , Member์ํฐํฐ ํด๋์ค๋ฅผ ์์ํ๊ณ ์๋ค(2-1). ์ด๋ ๊ฒ ๊ตฌ์ฑํ๊ฒ ๋๋ฉด, ๋ฐ์ดํฐ ๋ฒ ์ด์ค์์ ์กฐํํ ํ์์ ์ ๋ณด๋ฅผ SpringSecurity์ User ์ ๋ณด๋ก ๋ณํํ๋ ๊ณผ์ ๊ณผ User์ ๊ถํ ์ ๋ณด๋ฅผ ์์ฑํ๋ ๊ณผ์ ์ ์บก์ํ ๊ฐ๋ฅํ๋ค. ๊ทธ๋ฆฌ๊ณ Member๋ฅผ ์์ํ๋ฏ๋ก HelloUserDetails๋ฅผ ๋ฆฌํด๋ฐ์ ์ฌ์ฉํ๋ ์ธก์์ ๋๊ฐ ํด๋์ค์ ๊ฐ์ฒด๋ฅผ ๋ชจ๋ ๋ค ์ฝ๊ฒ ์บ์คํ ํด์ ์ฌ์ฉ ๊ฐ๋ฅํ๋ค๋ ์ฅ์ ์ด ์๋ค.
7.User์ Role์ DB์์ ๊ด๋ฆฌ
์ผ๋ฐ์ ์ธ User์ ์ธ์ฆ์ ๋ณด ๊ฐ์ ๋ณด์๊ด๋ จ ์ ๋ณด๋ DB์ฒ๋ผ ์๊ตฌ์ ์ฅ์์ ์์ ํ๊ฒ ๋ณด๊ด๋๋ค. ํ์ง๋ง ํ์ฌ๊น์ง์ ์ฝ๋๋ User์ ๊ถํ ์ ๋ณด๋ฅผ DB์์ ๊ด๋ฆฌํ๋ ๊ฒ์ด ์๋, DB์์ ์กฐํํ User์ ์ ๋ณด๋ฅผ ๊ธฐ์ค์ผ๋ก ์ฝ๋์์์ ์์ฑํ๊ณ ์๋ค. ์ด๋ฅผ ๋ณ๊ฒฝํ๋ ๋ก์ง์ ์์๋ณด์.
1. User ์ ๊ถํ ์ ๋ณด๋ฅผ ์ ์ฅํ๊ธฐ ์ํ ํ ์ด๋ธ ์์ฑ
2. ํ์ ๊ฐ์ ์ , User์ ๊ถํ ์ ๋ณด(Role)์ DB์ ์ ์ฅํ๋ ์์
3. ๋ก๊ทธ์ธ ์ธ์ฆ์, User์ ๊ถํ ์ ๋ณด๋ฅผ DB์์ ์กฐํํ๋ ์์
7-1. User์ ๊ถํ ์ ๋ณด ํ ์ด๋ธ ์์ฑ
@NoArgsConstructor
@Getter
@Setter
@Entity
public class Member extends Auditable implements Principal{
@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;
}
@Override
public String getName() {
return getEmail();
}
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
}
}
User์ ๊ถํ์ ๋ณด๋ฅผ ๋งคํํ๋ ๊ฒ์ (1)๊ณผ ๊ฐ์ด ์ฒ๋ฆฌํ๋ฉด๋๋ค. List, Set ๋ฑ์ ์ปฌ๋ ์ ํ์ ์ ํ๋๋ @ElementCollection ์ ๋ํ ์ด์ ์ ์ถ๊ฐํ์ฌ User ๊ถํ ์ ๋ณด์ ๊ด๋ จ๋ ๋ณ๋์ ์ํฐํฐ ํด๋์ค๋ฅผ ์์ํ์ง ์์๋ ๊ฐ๋จํ ๋งคํ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ค.
์ด๋ฅผ DB์์ ์กฐํํ๊ฒ ๋๋ฉด, ์๋ ๊ทธ๋ฆผ๊ณผ ๊ฐ์ด Member ์ํฐํฐ ํด๋์ค์ ์ฐ๊ด๊ด๊ณ ๋งคํ์ ๋ํ ํ ์ด๋ธ์ด ์์ฑ๋๋ค.
์ด๋ ํ๋ช ์ ํ์์ด ํ๊ฐ์ด์์ Role์ ๊ฐ์ง์์์ผ๋ฏ๋ก, Member์ Member_ROLES ํ ์ด๋ธ์ 1:N ์ ๊ด๊ณ์ด๋ค. ํ์๊ฐ์ ์ ํตํด ํ์ ์ ๋ณด๊ฐ ์์ฑ๋๋ฉด์, Member_ROLES ํ ์ด๋ธ์ MEMBER_MEMBER_ID ์ด์ Member ํ ์ด๋ธ์ ๊ธฐ๋ณธํค ๊ฐ, ROLES ์ด์๋ ๊ถํ ์ ๋ณด๊ฐ ์ ์ฅ๋ ๊ฒ์ด๋ค.
7-2. ํ์๊ฐ์ ์, User์ ๊ถํ ์ ๋ณด(Role)์ DB์ ์ ์ฅ
@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์ admin ์ด๋ฉ์ผ ์ฃผ์์ ์ผ์นํ์ง ํ์ธํ๊ณ , ๋์ผํ๋ค๋ฉด, ADMIN ROLE, ์๋๋ฉด USER ROLE ์ ๋ฆฌํดํ๋ค.
@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;
}
...
...
}
์์์ ์์ฑํ๋ createRoles๋ฅผ ์ด์ฉํด์ ํ์์ ๊ถํ์ ์์ฑํ๊ณ , member์ ๊ฐ์ฒด์ Set ํ๋ค. ์ด๋ ๊ฒ Role๊น์ง ์ธํ ๋ Member ๊ฐ์ฒด๋ฅผ DB์ ์ ์ฅ์ํจ๋ค.
7-3. ๋ก๊ทธ์ธ ์ธ์ฆ์, User์ ๊ถํ ์ ๋ณด๋ฅผ DB์์ ์กฐํํ๋ ์์
@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());
}
...
...
}
}
์์์ ์์ฑํ์๋ HelloUserDetails๋ฅผ ํ๋ฒ๋ ๊ฐ์ ํด์ V3๋ก ๋ง๋ค์๋ค.
(1)์ ๋ณด๋ฉด, HelloUserDetails๊ฐ ์์ํ๊ณ ์๋ Member์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์กฐํํ List<String> roles๋ฅผ ์ ๋ฌํ๋ค.
(2)๋ฅผ ๋ณด๋ฉด, Member์ ์ ๋ฌํ Role์ ์ ๋ณด๋ฅผ authorityUtils.createAuthorities() ๋ฉ์๋์ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌํด์ ๊ถํ๋ชฉ๋ก(List<GrantedAuthority>)๋ฅผ ์์ฑํ๋ค. ๊ธฐ์กด(V2)์๋ DB์์ Role์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค์ง ์์๊ธฐ ๋๋ฌธ์ , authorityUtils.createAuthorities(this.getEmail())๋ก ์์ฑ๋์์๋ค.
@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;
}
...
...
}
DB์์ ์กฐํํ Role ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก User์ ๊ถํ ๋ชฉ๋ก์ ์์ฑํ๋ createAuthorities(List<String> roles) ๋ฉ์๋๋ฅผ ์ถ๊ฐํ์๋ค.
(1)์ ๋ณด๋ฉด ๊ธฐ์กด์๋ application.yml์ ํ๊ฒฝ๋ณ์์ ์ ์๋ ๊ด๋ฆฌ์์ฉ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ๊ธฐ์ค์ผ๋ก ๊ด๋ฆฌ์์ Role์ ์ถ๊ฐ ํ์์ง๋ง, ์ด์ ๋ ๋จ์ํ DB์์ ๊ฐ์ ธ์จ Role ๋ชฉ๋ก์ ๊ทธ๋๋ก ์ด์ฉํด์ ๊ถํ ๋ชฉ๋ก์ ๋ง๋ค์ด์ค๋ค. ์ด๋, ์์ฑ์์ ํ๋ผ๋ฏธํฐ๋ก ROLE_USER , ROLE_ADMIN ๋ฑ์ ํํ๋ก ๋๊ฒจ์ฃผ์ด์ผํ๋ค.
์ฌ๊ธฐ๊น์ง ์์ฑ์ด ๋์๋ค๋ฉด, ์ ์์ ์ผ๋ก ํ์๊ฐ์ ๊ณผ ๋ก๊ทธ์ธ์ด ์๋ํ ๊ฒ์ด๋ค.