๋ฐฑ์๋ ํ์ ์ค ํ๋ถ๊ณผ ํ๋ก์ ํธ๋ฅผ ๋ง์น๊ณ ์ฝ๋๋ฆฌ๋ทฐ๋ฅผ ์งํํ์๋ค. ๋ง์ด ๊ฑฐ์ฐฝํด์ ์ฝ๋๋ฆฌ๋ทฐ์ด๊ณ ๋ณธ์ธ์ด ๊ตฌํํ ๋ถ๋ถ์ ๋ํด์ ์๋ก ์ด์ผ๊ธฐ๋ฅผ ๋๋๋ ์๊ฐ์ด์๋ค. ๋๋ ์๊ฒฉ์ฆ ์ ๋ณด๋ฅผ ์ด๋ค์์ผ๋ก ๋ถ๋ฌ์ค๋์ง์ ํ์ถํ๋ ๋ฐฉ๋ฒ๋ฑ์ ๋ํด์ ์ค๋ช ์ ํ๊ณ , ํ์๋ถ์ด ๊ถ๊ธํด ํ์ จ๋ ๊ณต๊ณต๋ฐ์ดํฐ ํฌํธ API๋ก ์ฝ๋๊ฐ์ ๋ฃ์ด์ ์ ๋ฌํ๋ ๋ถ๋ถ์ ์ค์ฌ์ผ๋ก ์ค๋ช ํ๋ค. ๊ทธ๋ฆฌ๊ณ ๋๋ ํ์๋ถ์ด ๊ตฌํํ๋ ์คํ๋ง์ํ๋ฆฌํฐ์ชฝ์ ๊ดํด์ ์ง๋ฌธ์ ํ๊ณ ๋ต๋ณ์ ๋ค์๋ค. ์ด๋ถ๋ถ์ ๋ํด์ ๊ธฐ๋ก์ ๋จ๊ธฐ๋ ค๊ณ ํ๋ค.
SpringSecurity ์ค๋ช
์ํ๋ฆฌํฐ - > ํํฐ์ฒด์ธ ๋ด๋ถ์์ ์ด๋ค์์ผ๋ก ๋์ํ๋์ง๋ฅผ ํ์ธ
์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํ๊ฒ ๋๋ฉด http request๊ฐ ๋ค์ด์ค๊ณ ๊ถํ์ ์์ฑํด์ (Authentication)ํ ํฐ์ ๋ฐ๊ธํ๋ค. ๊ถํ์ ์ ๋ฌํ๋ฉด PoviderManager๋ฅผ ์์๋ฐ์ AuthenticationManager์๊ฒ ์ ๋ฌ์ ํ๋ค. AuthenticationProvider๋ก ์ ๋ฌ์ ํ๊ณ , UserDetail์ ์กฐํ๋ฅผ ํ๊ณ , User์ ์ ๋ณด, ์ด๋ฆ, ํจ์ค์๋๋ฑ์ ์กฐํ๋ฅผ ํ๋ค. ํฌ๋ฆฌ๋ด์
์ ์ฅ์ -> ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ง๋๋ ๋ถ๋ถ์ผ๋ก ์ค๋ช
. ์ฆ MemberRepository์ด๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก ๋ก๊ทธ์ธ์ ํ์๋, ์คํ๋ง ์ํ๋ฆฌํฐ ์์ฒด์์ ์๋ํ๋ ํ๋ฆํ์ธ.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers().frameOptions().sameOrigin() // ๋์ผํ ์ถ์ฒ๋ก ๋ค์ด์ค๋ ์์ฒญ๋ง ๋ ๋๋ง ํ์ฉ
.and()
.csrf().disable() // csrf ํ์ฉ ์ ํจ
.cors().configurationSource(corsConfigurationSource()) // ์ง์ ์์ฑํ corsConfiguration ์ ์ฉ
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // ์ธ์
์ ์งง๊ฒ ๊ฐ์ง๊ณ ๊ฐ๋ ์ค์ ์ถ๊ฐ (๋ฌด์ํ์ฑ ์ ์ง)
.and()
.formLogin().disable() // formLogin(์ฃผ๋ก SSR ๋ฐฉ์์์ ์ฌ์ฉ๋จ) ํ์ฉ ์ ํจ -> JSON ํ์์ผ๋ก ์ ์กํ ๊ฒ
.httpBasic().disable() // httpBasic(username, password๋ฅผ ํค๋์ ์ค์ด์ ์ธ์ฆ) ํ์ฉ ์ ํจ
.exceptionHandling()
.authenticationEntryPoint(new MemberAuthenticationEntryPoint()) // AuthenticationException ๋ฐ์ ์ ํธ์ถ
.accessDeniedHandler(new MemberAccessDeniedHandler()) // ๊ถํ ์์ ๋ ํธ์ถ
.and()
.apply(new CustomFilterconfigurer())
.and()
// .authorizeHttpRequests(authorize -> authorize
// .anyRequest().permitAll() // ๋ชจ๋ ์์ฒญ ์ ๊ทผ ํ์ฉ
// )
.authorizeHttpRequests(authorize -> authorize
/** ---------------------------------- member ์ ๊ทผ ๊ถํ ์ค์ ---------------------------------- **/
.antMatchers(HttpMethod.POST, "/members/signup").permitAll()
.antMatchers(HttpMethod.PATCH, "/members/mypage/edit/**").hasRole("USER")
.antMatchers(HttpMethod.PATCH, "/members/mypage/**").hasRole("USER")
.antMatchers(HttpMethod.GET, "/members").hasRole("ADMIN")
.antMatchers(HttpMethod.GET, "/members/**").hasAnyRole("ADMIN", "USER")
.antMatchers(HttpMethod.DELETE, "/members/delete/**").hasAnyRole("USER")
.antMatchers(HttpMethod.PATCH, "/members/mypage/image/upload/**").hasRole("USER")
.antMatchers(HttpMethod.PATCH, "/members/mypage/image/delete/**").hasRole("USER")
/** ---------------------------------- boards ์ ๊ทผ ๊ถํ ์ค์ ---------------------------------- **/
.antMatchers(HttpMethod.POST, "/boards/create").permitAll()//.hasAnyRole("ADMIN", "USER")
.antMatchers(HttpMethod.PATCH, "/boards/edit/**").permitAll()//.hasAnyRole("ADMIN", "USER")
.antMatchers(HttpMethod.GET, "/boards").permitAll()
.antMatchers(HttpMethod.GET, "/boards/**").permitAll()
.antMatchers(HttpMethod.DELETE, "/boards/delete/**").permitAll()//.hasAnyRole("ADMIN", "USER")
/** ---------------------------------- boards-answers ์ ๊ทผ ๊ถํ ์ค์ ---------------------------------- **/
.antMatchers(HttpMethod.POST, "/boards/**/answers/create").permitAll()//.hasAnyRole("ADMIN", "USER")
.antMatchers(HttpMethod.PATCH, "/boards/**/answers/**").permitAll()//.hasAnyRole("ADMIN", "USER")
.antMatchers(HttpMethod.DELETE, "/boards/**/answers/**/delete").permitAll()//.hasAnyRole("ADMIN", "USER")
/** ---------------------------------- boards-replies ์ ๊ทผ ๊ถํ ์ค์ ---------------------------------- **/
.antMatchers(HttpMethod.POST, "/answers/replies/create").permitAll()//.hasAnyRole("ADMIN", "USER")
.antMatchers(HttpMethod.PATCH, "/answers/replies/**").permitAll()//.hasAnyRole("ADMIN", "USER")
.antMatchers(HttpMethod.DELETE, "/answers/replies/**/delete").permitAll()//.hasAnyRole("ADMIN", "USER")
/** ---------------------------------- licenses ์ ๊ทผ ๊ถํ ์ค์ ---------------------------------- **/
.antMatchers(HttpMethod.GET, "/licenses/**").permitAll()
.antMatchers(HttpMethod.GET, "/licenses").permitAll()
);
// Spring security OAuth2 ์ ์ฉ ๋ฒ์
// .oauth2Login()
// .successHandler(oAuth2SuccessHandler)
// .failureHandler(oAuth2FailureHandler)
// .userInfoEndpoint()
// .userService(oAuth2UserService);
return http.build();
}
๊ตฌํ์ ์ง์ ํ๋ ๋ถ๋ถ - SecurityFilterChain์ ํตํด์ ๊ตฌํ์ ์งํํ๋ค.
CORS ๊ฐ์ ํํฐ๋ฅผ ์ถ๊ฐ๋ก ๋ฃ์ด์ค์ ์๊ณ , handlerMessage ๋ฑ์ ์ถ๊ฐ ์ํฌ์์๋ค.
JWT
- ์ฌ์ฉ์๋ URL /auth/login ๋ก EMAIL ๊ณผ PASSWORD๋ฅผ POST ์์ฒญ์ผ๋ก ๋ณด๋ธ๋ค
- ์คํ๋ง ์ํ๋ฆฌํฐ์ ํต์ฌ ๋ก์ง์ผ๋ก DB๋ก ์ด๋ฉ์ผ๊ณผ ํจ์ค์๋๋ฅผ ์ธ์ฆํ๊ณ ํต๊ณผํ๋ฉด Access Token๊ณผ Refresh Token์ ๋ฐ๊ธํ๋ค.
- ์ฌ์ฉ์๋ ์ผ๋ฐ ๋ฐ์ดํฐ ์์ฒญ์ Access Token๊ณผ ํจ๊ป ๋ณด๋ธ๋ค
- ์๋ฒ๋ Access Token์ ๊ฒ์ฆํ๊ณ ํต๊ณผํ๋ฉด ๋ฐ์ดํฐ ์๋ต์ ๋ณด๋ธ๋ค
- ์ฌ์ฉ์๊ฐ ๋ง๋ฃ๋ Access Token์ ์ด์ฉํด ์์ฒญ์ ๋ณด๋ด๋ฉด ์๋ฒ๋ ์ฌ๋ฐํ์์ฒญ์ ํ๋ค
- ์ฌ๋ฐํ ์์ฒญ์ ๋ง๋ฃ๋ Access Token๊ณผ ์ ํจํ RefreshToken ๊ฐ์ URL /auth/reissue ๋ก POST ์์ฒญ์ ๋ณด๋ธ๋ค.
- ์๋ฒ์์๋ RefreshToken์ ๊ฒ์ฆํ๊ณ ๋ค์ AccessToken๊ณผ RefreshToken ๊ฐ์ ์ฌ์ฉ์์๊ฒ ๋๊ฒจ์ค๋ค
jwt๋ฅผ ํตํด ๋ก๊ทธ์ธ์ ํ๊ฒ ๋๋ฉด. ์ฌ์ฉ์๋ฅผ ํ์ธํ ๋ access, refresh ํ ํฐ์ ๋ฐ๊ธํ๋ค.
๊ทธ๋ฆฌ๊ณ ์ด๊ฒ์ด ์ ํจํ์ง ํ์ธํ์ฌ, ์ฌ์ฉ์๊ฐ ์น์ธ์ด๋๊ณ ์๋๋ฉด ์๋๊ฒ ํ๋ค.
JwtTokenizer ํด๋์ค์์ JWT ํ ํฐ์ ๋ง๋ค์ด์ฃผ๋๊ฒ์ด๋ค.
์ต์ธ์คํ ํฐ ๋ง๋ฃ๊ธฐ๊ฐ๋ ์ฌ๊ธฐ์ ์ค์ ํ๋ค. @value๋ก ์ค์ .
๋ณดํต ์ต์ธ์คํ ํฐ ์๊ฐ์ ์ ์ ์๊ฐ์ 1์๊ฐ ์ด๋ด๋ก ํ๊ณ , ์ค๊ฐ๋ณด์์ 6์๊ฐ์ ๋, ๊ทธ์ด์์ ์ฝํ ๋ณด์์ด๋ค. ํ์ฌ ์ํ๋ฆฌํฐ์์ ์ ์ผ ๋ณด์ํด์ผํ ๋ถ๋ถ์ด๋ค. claims ์ jwt์ ์ ์ฅ๋ ์ ๋ณด๋ก ๋ณด๋ฉด๋๊ณ , ๋์ฝ๋ฉํ ๋ ์ฌ์ฉํ๋ ๊ฒ์ด๋ค.
์ธ์ฝ๋ฉ๋ ํค๋ก๋ถํฐ ํค๊ฐ์ฒด ์์ฑ..!
jwtAuthenticationFilter๋ฅผ ํตํด์ jwt decodeํ์๋ ๋์ค๋ ๊ฐ๋ฑ์ ๋ฃ์ด ์ค์ ์๋ค.
jwtVerificationFilter๋ ํ ํฐ์ ์ด๋ค์์ผ๋ก ๊ฒ์ฆํ ๊ฒ์ธ์ง ํ์ธํด์ฃผ๋ ํํฐ์ด๋ค. jwt๋ฅผ ๊ฒ์ฆํ ๋ ๊ถํ์ด ์๋์ง ์๋์ง๋ฅผ ํ์ธ. Bearer ๊ฐ์ด ๋น ์ก๊ฑฐ๋, authorization์ด null ์ด๊ฑฐ๋ ์ด๋ฐ ๊ฐ๋ค์ ์ถ๊ฐํด์ ๊ฒ์ฆ๋ฐฉ์์ ์ค์ ํ๋ค.
handler๋ ์ฌ์ค์ ์์ธ์ฒ๋ฆฌ๋ถ๋ถ์ด๋ผ๊ณ ๋ณด๋ฉด๋๊ณ , ์ํ๋ฆฌํฐ์์ ๋ฐ์ํ๋ ๋ก๊ทธ์ฐ๊ธฐ ์ํด์ ์์ฑ.
ํํฐ๋ ์ฌ์ฉ์ฉ๋์ ๋ฐ๋ผ์ ๋ผ์์ฃผ๋ ๊ณณ์ด ๋ฌ๋ผ์ง๋ค.
public class MemberDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
private final CustomAuthorityUtils authorityUtils;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// ์ฌ์ฉ์ ์ด๋ฉ์ผ๋ก ํ์ ์ ๋ณด ์กฐํ
Optional<Member> optionalMember = memberRepository.findByEmail(username);
// ์์ผ๋ฉด ์์ธ์ฒ๋ฆฌ ๐จ
Member getMember = optionalMember.orElseThrow(
() -> new RuntimeException("๐จ ํ์ ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. ๐จ"));
// MemberDetails ๊ฐ์ฒด๋ฅผ ์์ฑํ์ฌ ํ์ ์ ๋ณด UserDetails๋ก ํฌ์ฅ
return new MemberDetails(getMember);
}
// MemberDetails ํด๋์ค๋ Member ์ํฐํฐ๋ฅผ UserDetails๋ก ๋ณํ
private class MemberDetails extends Member implements UserDetails {
MemberDetails(Member member) {
setMemberId(member.getMemberId());
setName(member.getName());
setEmail(member.getEmail());
setPassword(member.getPassword());
setPhone(member.getPhone());
setRoles(member.getRoles());
}
// ๊ถํ ์ ๋ณด
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorityUtils.createAuthorities(this.getRoles());
}
// ์ด๋ฉ์ผ ๋ฐํ
@Override
public String getUsername() {
return getEmail();
}
// ๊ณ์ ์ด ๋ง๋ฃ๋์ง ์์์ -> true
@Override
public boolean isAccountNonExpired() {
return true;
}
// ๊ณ์ ์ด ์ ๊ธฐ์ง ์์์(ํ์ฑํ) -> true
@Override
public boolean isAccountNonLocked() {
return true;
}
// credential์ด ๋ง๋ฃ๋์ง ์์ -> true
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// ๊ณ์ ํ์ฑํ -> true
@Override
public boolean isEnabled() {
return true;
}
}
}
UserDetailService๋ฅผ ์์๋ฐ์์, ์ฌ์ฉ์ด๋๋ค. ์ด๋ฅผ ํตํด์ ์ ์ ๋ํ
์ผ์ด ์์ผ๋ฉด ๋ฉค๋ฒ๋ฅผ ๊ฐ์ ธ์ค๊ณ , ์ด๋ฐ์์ผ๋ก ํ์ฌ, Member Entity์ ์คํ๋ง์ํ๋ฆฌํฐ๊ฐ ๋ง๋๋๋ถ๋ถ์ด๋ค. ์ฆ ๋ฉค๋ฒ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์์. MemberDetail์ ๋ง๋ค์ด์ค๋ค.
CustomAuthorityUtils -> ์ ์ ์ ๊ถํ์ ์ค์ ํด์ฃผ๋ ๋ถ๋ถ์ผ๋ก, ์ฌ์ฉ์๊ฐ ๊ฐ์
์ ํ์๋ ๊ถํ์ ์ด๋ค๊ฒ์ ์ค์ง ์ค์ ํ๋ ํด๋์ค์ด๋ค.
์ด๋ ๊ฒ ํด์ ๋น๊ต๋ฅผ ํ๋๊ฑด UserDetails , ๋ก๊ทธ์ธํ ์ ๊ฐ ๊ฐ์ง ์ ๋ณด๋ ๋น๊ต๋ฅผ ํ๋๊ฒ. ํ ํฐ์ ํ์ด์ ํ ํฐ ์์ฒด์ ๋ด๊ธด ์ ๋ณด์ ์ง๊ธ ์ ๊ทผํ๋ ค๊ณ ํ๋ ์ ๋ณด๋ ๋น๊ต๋ฅผ ํ๋ค. ์ด๋ถ๋ถ์ด verifyAuthorizedUser ์ด๋ถ๋ถ์ ํ๋ค.
Outh2๋ก ๋์ด๊ฐ๋ณด๋ฉด, ๋ก๊ทธ์ธ ์์ฒญ์ด ๋ค์ด์ค๋ฉด ์๋ฒ๋ก ๋ค์ด์ค๋ ๊ฒ์ด์๋๊ณ , ์ ํด๋์ ์๋ฒ(์นด์นด์ค, ๊ตฌ๊ธ๋ฑ)์ผ๋ก ์์ฒญ์ด๋์ด๊ฐ๊ณ authorization ์ฝ๋๋ฅผ ๋ฐ๊ธํด์ค๋ค. ๊ทธ๋ฆฌ๊ณ ์ด์ฝ๋๋ฅผ ํ๋ก ํธ์์ ๋ฐ์์ ๋ฐฑ์๋๋ก ๋๊ฒจ์ค๋ค. ๊ทธ๋ฌ๋ฉด ๋ฐฑ์์ ์ด์ฝ๋๋ฅผ ์ ํด๋์ ์๋ฒ(์นด์นด์ค, ๊ตฌ๊ธ)์ ํ์ธ ์์ฒญํ๊ณ ๋ง๋ค๋ฉด ํ๋ก ํธ์ ์๋ต์ ํด์ค๋ค. ์ด์ํฉ์์ ํ ํฐ๊ฐ์ด ๋
ธ์ถ๋ ์ ์๊ธฐ ๋๋ฌธ์ JWT๋ฅผ์ฌ์ฉํด์ ํ๋ก ํธ๋ก ๋๊ฒจ์ค๋ค.
์ด ๊ณผ์ ์ gradle์ outh2๋ฅผ ์ถ๊ฐํด์ฃผ๋ฉด ์๋์ ์ผ๋ก ๊ตฌํ์ด ๊ฐ๋ฅํ๋ค.
์ง๊ธ ์ฝ๋์์ v2 ๋ถ๋ถ์ ์ด๋ ๊ฒ ๊ตฌํ์ ํ ๋ถ๋ถ์ด๋ค.
ํ๊ธฐ
๊ตฌํํ ๋๋ ๋งํ์์ด ํ๋ ๋ถ๋ถ๋ ์ค๋ช ์ ํ๋ ค๋๊น ๊ต์ฅํ ์ด๋ ค์ด๋ถ๋ถ์ด ๋ง์๋ค. ๋ฌด์ธ๊ฐ๋ฅผ ๋๊ตฐ๊ฐ์๊ฒ ์ค๋ช ํ ์์๋ ๊ฒ์ด ๊ทธ๊ฒ์ ๋ํด ์ง์ง ์๋๊ฒ์ด๋ผ๊ณ ํด์ฌํ ํ์ฌ์์ ์ฌ์๋ถ์๊ฒ ๋ง์ด ๋ค์๋ ๋ง์ด๋ค. ๊ทธ๋ ๊ฒ ํ ์์๊ฒ ์ด์ ๊ฐ ์๋ ์ฝ๋๋ฅผ ์ง์ผ๊ฒ ๋ค. ๊ทธ๋ฆฌ๊ณ ์ด๋ฒ ๊ธฐํ๋ฅผ ํตํด์ ํ์๋ถ๊ณผ ํจ๊ป ์ฑ์ฅ ํ ์์๋ ์ข์ ๊ฒฝํ์ด์๋ค.