Spring Security - JWT๋ฅผ ์ด์ฉํ ์๊ฒฉ ์ฆ๋ช ๋ฐ ๊ฒ์ฆ ๊ตฌํ
์ด์ ํฌ์คํ ์์ ํ์ ๊ฐ์ ์ ๋ฑ๋กํ ์ด๋ฉ์ผ ์ฃผ์, ํจ์ค์๋๋ฅผ ํตํด ๋ก๊ทธ์ธ ์ธ์ฆ์ ์ฑ๊ณต์ ์ผ๋ก ์ํํ๋ฉด response header(Authorization, Refresh)๋ฅผ ํตํด JWT๋ฅผ ์ ๋ฌ๋ฐ์ ์ ์์๋ค.
์ด๋ฒ ํฌ์คํ ์์๋ ํด๋ผ์ด์ธํธ ์ธก์์ JWT๋ฅผ ์ด์ฉํด ์๊ฒฉ ์ฆ๋ช ์ด ํ์ํ ๋ฆฌ์์ค์ ๋ํ request ์ ์ก ์, request header๋ฅผ ํตํด ์ ๋ฌ๋ฐ์ JWT๋ฅผ ์๋ฒ ์ธก์์ ๊ฒ์ฆํ๋ ๊ธฐ๋ฅ์ ๊ตฌํํด ๋ณด์.
1๏ธโฃ JWT ๊ฒ์ฆ ํํฐ ๊ตฌํ
JWT๋ฅผ ๊ฒ์ฆํ๋ ์ ์ฉ Security Filter๋ฅผ ๊ตฌํํ๋ ๊ฒ.
package com.codestates.todoapp.auth.filter;
import com.codestates.todoapp.auth.jwt.JwtTokenizer;
import com.codestates.todoapp.auth.utils.CustomAuthorityUtils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
//ํด๋ผ์ด์ธํธ ์ธก์์ ์ ์ก๋ request header์ ํฌํจ๋ JWT์ ๋ํด ๊ฒ์ฆ ์์
์ ์ํํ๋ JwtVerificationFilter
public class JwtVerificationFilter extends OncePerRequestFilter { //request ๋น ํ ๋ฒ๋ง ์คํ๋๋ Security Filter๋ฅผ ๊ตฌํํ ์ ์๋ค.
private final JwtTokenizer jwtTokenizer; //JWT๋ฅผ ๊ฒ์ฆํ๊ณ Claims๋ฅผ ์ป๋ ๋ฐ ์ฌ์ฉ๋จ.
private final CustomAuthorityUtils authorityUtils;//JWT ๊ฒ์ฆ์ ์ฑ๊ณตํ๋ฉด Authentication ๊ฐ์ฒด๋ฅผ ์ฑ์จ ์ฌ์ฉ์์ ๊ถํ์ ์์ฑํ๋ ๋ฐ ์ฌ์ฉ๋จ.
public JwtVerificationFilter(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils) {
this.jwtTokenizer = jwtTokenizer;
this.authorityUtils = authorityUtils;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Map<String, Object> claims = verifyJws(request);
setAuthenticationToContext(claims);
//JWT์ ์๋ช
๊ฒ์ฆ์ ์ฑ๊ณตํ๊ณ , Security Context์ Authentication์ ์ ์ฅํ ๋ค์๋ ๋ค์ Security Filter๋ฅผ ํธ์ถํ๋ค.
filterChain.doFilter(request, response);
}
//ํน์ ์กฐ๊ฑด์ ๋ถํฉํ๋ฉด(true์ด๋ฉด) ํด๋น Filter์ ๋์์ ์ํํ์ง ์๊ณ ๋ค์ Filter๋ก ๊ฑด๋๋ฐ๋๋ก ํด์ค๋ค.
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException{
//JWT๊ฐ Authorization header์ ํฌํจ๋์ง ์์๋ค๋ฉด JWT ์๊ฒฉ ์ฆ๋ช
์ด ํ์ํ์ง ์์ ๋ฆฌ์์ค์ ๋ํ ์์ฒญ์ด๋ผ๊ณ ํ๋จํ๊ณ ๋ค์ Filter๋ก ์ฒ๋ฆฌ๋ฅผ ๋๊ธด๋ค.
String authorization = request.getHeader("Authorization");
return authorization == null || !authorization.startsWith("Bearer");
}
//JWT๋ฅผ ๊ฒ์ฆํ๋ ๋ฐ ์ฌ์ฉ๋๋ private ๋ฉ์๋
private Map<String, Object> verifyJws(HttpServletRequest request) {
//request์ header์์ JWT๋ฅผ ์ป๊ณ ์๋ค.
String jws = request.getHeader("Authorization").replace("Bearer ", "");
//JWT ์๋ช
์ ๊ฒ์ฆํ๊ธฐ ์ํ Secret Key๋ฅผ ์ป๋๋ค.
String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());
//JWT์์ Claims๋ฅผ ํ์ฑ -> ์ ์์ ์ผ๋ก ํ์ฑ๋๋ฉด ์๋ช
๊ฒ์ฆ ์ญ์ ์์ฐ์ค๋ฝ๊ฒ ์ฑ๊ณตํ ๊ฒ.
Map<String, Object> claims = jwtTokenizer.getClaims(jws, base64EncodedSecretKey).getBody();
return claims;
}
//Authentication ๊ฐ์ฒด๋ฅผ SecurityContext์ ์ ์ฅํ๊ธฐ ์ํ priavte ๋ฉ์๋
private void setAuthenticationToContext(Map<String, Object> claims){
String username = (String) claims.get("username");
List<GrantedAuthority> authorities = authorityUtils.createAuthorities((List) claims.get("roles"));
Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
2๏ธโฃ SecurityConfiguration ์ค์ ์ ๋ฐ์ดํธ
์ธ์ ์ ์ฑ ์ค์ ์ ์ถ๊ฐ, JwtVerificationFilter ์ถ๊ฐ
package com.codestates.todoapp.config;
import com.codestates.todoapp.auth.filter.JwtAuthenticationFilter;
import com.codestates.todoapp.auth.filter.JwtVerificationFilter;
import com.codestates.todoapp.auth.handler.MemberAuthenticationFailureHandler;
import com.codestates.todoapp.auth.handler.MemberAuthenticationSuccessHandler;
import com.codestates.todoapp.auth.jwt.JwtTokenizer;
import com.codestates.todoapp.auth.utils.CustomAuthorityUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfiguration {
private final JwtTokenizer jwtTokenizer;
private final CustomAuthorityUtils authorityUtils;
public SecurityConfiguration(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils) {
this.jwtTokenizer = jwtTokenizer;
this.authorityUtils = authorityUtils;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.headers().frameOptions().sameOrigin()
.and()
.csrf().disable()
.cors(Customizer.withDefaults())
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //์ธ์
์ ์์ฑํ์ง ์๋๋ก ์ค์ ํ๋ค.
.and()
.formLogin().disable()
.httpBasic().disable()
.apply(new CustomFilterConfigurer())//์ปค์คํฐ๋ง์ด์ง๋ Configuration ์ถ๊ฐ
.and()
.authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder(){
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
//JwtAuthenticationFilter๋ฅผ ๋ฑ๋กํ๋ ์ญํ
public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity>{
@Override
public void configure(HttpSecurity builder) throws Exception{
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer);
jwtAuthenticationFilter.setFilterProcessesUrl("/auth/login");
jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler());
jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler());
JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, authorityUtils);
builder
.addFilter(jwtAuthenticationFilter) //JwtAuthenticationFilter๋ฅผ Spring Security Filter Chain์ ์ถ๊ฐํ๋ค.
.addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class); //JwtAuthenticationFilter์์ ๋ก๊ทธ์ธ ์ธ์ฆ์ ์ฑ๊ณตํ ํ ๋ฐ๊ธ๋ฐ์ JWT๊ฐ ํด๋ผ์ด์ธํธ์ request header์ ํฌํจ๋์ด ์์ ๊ฒฝ์ฐ์๋ง ๋์ํ๋ค.
}
}
}
๋ก๊ทธ์ธ ์ธ์ฆ ํ, JWT๋ ์ ๋ฐ๊ธ๋๊ณ JWT๋ฅผ ์ด์ฉํด์ ํด๋ผ์ด์ธํธ์ ์๊ฒฉ ์ฆ๋ช ์ ๋ํ ๊ฒ์ฆ๊น์ง ์ ๋๋ ๊ฒ ํ์ธํ๋ค.
๊ทธ๋ฐ๋ฐ Spring Security ์ชฝ์์ ์๋ฒ ์ธก ๋ฆฌ์์ค์ ์ ์ ํ ์ ๊ทผ ๊ถํ ์ค์ ์ ํ์ง ์๋๋ค๋ฉด ์๋ฌด๋ฆฌ JWT๋ฅผ ์ฌ์ฉํ์ฌ ํด๋ผ์ด์ธํธ์ ์๊ฒฉ ์ฆ๋ช ์ด ํ์ธ๋๋ค๊ณ ํ๋๋ผ๋ ๊ทธ ์๋ฏธ๊ฐ ํด์๋๋ค.
JWT๋ฅผ ์ด์ฉํ ์๊ฒฉ ์ฆ๋ช ์ด๋ผ๋ ์๋ฏธ์๋ ํน์ ๋ฆฌ์์ค์ ์ ๊ทผํ ์ ์๋ ์ ์ ํ ๊ถํ์ ๊ฐ์ก๋์ง๋ฅผ ํ๋จํด์ผ ํ๋ค๋ ์๋ฏธ๋ ํฌํจํ๊ณ ์๋ค.
ํ์ฌ๊น์ง SecurityConfiguration์๋ ์ฌ์ฉ์์ ๊ถํ ์ฌ๋ถ์ ์๊ด์์ด ํด๋ผ์ด์ธํธ ์ธก์์ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ชจ๋ ๋ฆฌ์์ค์ ์ ๊ทผํ ์ ์๋๋ก ์ค์ ๋ ์ํ์ด๋ค.
3๏ธโฃ MemberController๋ฅผ ํตํด ์ ๊ทผํ ์ ์๋ ๋ฆฌ์์ค์ ๋ํ ์ ๊ทผ ๊ถํ ๋ถ์ฌ
package com.codestates.todoapp.config;
import com.codestates.todoapp.auth.filter.JwtAuthenticationFilter;
import com.codestates.todoapp.auth.filter.JwtVerificationFilter;
import com.codestates.todoapp.auth.handler.MemberAuthenticationFailureHandler;
import com.codestates.todoapp.auth.handler.MemberAuthenticationSuccessHandler;
import com.codestates.todoapp.auth.jwt.JwtTokenizer;
import com.codestates.todoapp.auth.utils.CustomAuthorityUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfiguration {
private final JwtTokenizer jwtTokenizer;
private final CustomAuthorityUtils authorityUtils;
public SecurityConfiguration(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils) {
this.jwtTokenizer = jwtTokenizer;
this.authorityUtils = authorityUtils;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.headers().frameOptions().sameOrigin()
.and()
.csrf().disable()
.cors(Customizer.withDefaults())
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //์ธ์
์ ์์ฑํ์ง ์๋๋ก ์ค์ ํ๋ค.
.and()
.formLogin().disable()
.httpBasic().disable()
.apply(new CustomFilterConfigurer())//์ปค์คํฐ๋ง์ด์ง๋ Configuration ์ถ๊ฐ
.and()
.authorizeHttpRequests(authorize -> authorize
.antMatchers(HttpMethod.POST, "/members").permitAll()
.antMatchers(HttpMethod.PATCH, "/members/**").hasRole("USER")
.antMatchers(HttpMethod.GET, "/members").hasRole("ADMIN")
.antMatchers(HttpMethod.GET, "/members/**").hasAnyRole("USER", "ADMIN")
.antMatchers(HttpMethod.DELETE, "/members").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/members/**").hasAnyRole("USER", "ADMIN")
.anyRequest().permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder(){
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
//JwtAuthenticationFilter๋ฅผ ๋ฑ๋กํ๋ ์ญํ
public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity>{
@Override
public void configure(HttpSecurity builder) throws Exception{
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer);
jwtAuthenticationFilter.setFilterProcessesUrl("/auth/login");
jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler());
jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler());
JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, authorityUtils);
builder
.addFilter(jwtAuthenticationFilter) //JwtAuthenticationFilter๋ฅผ Spring Security Filter Chain์ ์ถ๊ฐํ๋ค.
.addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class); //JwtAuthenticationFilter์์ ๋ก๊ทธ์ธ ์ธ์ฆ์ ์ฑ๊ณตํ ํ ๋ฐ๊ธ๋ฐ์ JWT๊ฐ ํด๋ผ์ด์ธํธ์ request header์ ํฌํจ๋์ด ์์ ๊ฒฝ์ฐ์๋ง ๋์ํ๋ค.
}
}
}
์ด์ ์กฐ๊ธ ๋ ๊น๋ํ ๋ง๋ฌด๋ฆฌ๋ฅผ ์ํด ์์ธ ์ฒ๋ฆฌ์ ๊ด๋ จ๋ ๋ก์ง์ ์ถ๊ฐํด ๋ณด์.
4๏ธโฃ JwtVerificationFilter์ ์์ธ ์ฒ๋ฆฌ ๋ก์ง ์ถ๊ฐ
JwtVerificationFilter์ ๊ฒฝ์ฐ, ํด๋ผ์ด์ธํธ๋ก ์ ๋ฌ๋ฐ์ JWT์ Claims๋ฅผ ์ป๋ ๊ณผ์ ์์ ๋ด๋ถ์ ์ผ๋ก JWT์ ๋ํ ์๋ช (Signature)์ ๊ฒ์ฆํ๋ค.
๊ทธ๋ฐ๋ฐ ํ์ฌ JwtVerificationFilter์์๋ JWT์ ๋ํ ์๋ช ๊ฒ์ฆ์ ์คํจํ ๊ฒฝ์ฐ throw ๋๋ SignatureException์ ๋ํด์ ์ด๋ค ์ฒ๋ฆฌ๋ ํ์ง ์๊ณ ์๋ค.
๊ทธ๋ฆฌ๊ณ JWT๊ฐ ๋ง๋ฃ๋ ๊ฒฝ์ฐ, ๋ฐ์ํ๋ ExpiredJwtException์ ๋ํ ์ฒ๋ฆฌ๋ ์ด๋ฃจ์ด์ง์ง ์๊ณ ์๋ค.
JWT ๊ฒ์ฆ ๊ณผ์ ์์ ๋ฐ์ํ ์ ์๋ Exception์ ์ฒ๋ฆฌํ ์ ์๋ ์์ธ ์ฒ๋ฆฌ ๋ก์ง์ JwtVerificationFilter์ ์ถ๊ฐํด๋ณด๋๋ก ํ์.
package com.codestates.todoapp.auth.filter;
import io.jsonwebtoken.security.SignatureException;
import com.codestates.todoapp.auth.jwt.JwtTokenizer;
import com.codestates.todoapp.auth.utils.CustomAuthorityUtils;
import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
//ํด๋ผ์ด์ธํธ ์ธก์์ ์ ์ก๋ request header์ ํฌํจ๋ JWT์ ๋ํด ๊ฒ์ฆ ์์
์ ์ํํ๋ JwtVerificationFilter
public class JwtVerificationFilter extends OncePerRequestFilter { //request ๋น ํ ๋ฒ๋ง ์คํ๋๋ Security Filter๋ฅผ ๊ตฌํํ ์ ์๋ค.
private final JwtTokenizer jwtTokenizer; //JWT๋ฅผ ๊ฒ์ฆํ๊ณ Claims๋ฅผ ์ป๋ ๋ฐ ์ฌ์ฉ๋จ.
private final CustomAuthorityUtils authorityUtils;//JWT ๊ฒ์ฆ์ ์ฑ๊ณตํ๋ฉด Authentication ๊ฐ์ฒด๋ฅผ ์ฑ์จ ์ฌ์ฉ์์ ๊ถํ์ ์์ฑํ๋ ๋ฐ ์ฌ์ฉ๋จ.
public JwtVerificationFilter(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils) {
this.jwtTokenizer = jwtTokenizer;
this.authorityUtils = authorityUtils;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//์์ธ ์ฒ๋ฆฌ๊ฐ ์ถ๊ฐ๋จ. ์ถ๊ฐ๋ ์ ํธ๋ฆฌ๋ทฐํธ๋ AthenticationEntryPoint์์ ์ฌ์ฉํ ์ ์๋ค.
try {
Map<String, Object> claims = verifyJws(request);
setAuthenticationToContext(claims);
} catch (SignatureException se) {
request.setAttribute("exception", se);
} catch (ExpiredJwtException ee) {
request.setAttribute("exception", ee);
} catch (Exception e) {
request.setAttribute("exception", e);
}
//JWT์ ์๋ช
๊ฒ์ฆ์ ์ฑ๊ณตํ๊ณ , Security Context์ Authentication์ ์ ์ฅํ ๋ค์๋ ๋ค์ Security Filter๋ฅผ ํธ์ถํ๋ค.
filterChain.doFilter(request, response);
}
//ํน์ ์กฐ๊ฑด์ ๋ถํฉํ๋ฉด(true์ด๋ฉด) ํด๋น Filter์ ๋์์ ์ํํ์ง ์๊ณ ๋ค์ Filter๋ก ๊ฑด๋๋ฐ๋๋ก ํด์ค๋ค.
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
//JWT๊ฐ Authorization header์ ํฌํจ๋์ง ์์๋ค๋ฉด JWT ์๊ฒฉ ์ฆ๋ช
์ด ํ์ํ์ง ์์ ๋ฆฌ์์ค์ ๋ํ ์์ฒญ์ด๋ผ๊ณ ํ๋จํ๊ณ ๋ค์ Filter๋ก ์ฒ๋ฆฌ๋ฅผ ๋๊ธด๋ค.
String authorization = request.getHeader("Authorization");
return authorization == null || !authorization.startsWith("Bearer");
}
//JWT๋ฅผ ๊ฒ์ฆํ๋ ๋ฐ ์ฌ์ฉ๋๋ private ๋ฉ์๋
private Map<String, Object> verifyJws(HttpServletRequest request) {
//request์ header์์ JWT๋ฅผ ์ป๊ณ ์๋ค.
String jws = request.getHeader("Authorization").replace("Bearer ", "");
//JWT ์๋ช
์ ๊ฒ์ฆํ๊ธฐ ์ํ Secret Key๋ฅผ ์ป๋๋ค.
String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());
//JWT์์ Claims๋ฅผ ํ์ฑ -> ์ ์์ ์ผ๋ก ํ์ฑ๋๋ฉด ์๋ช
๊ฒ์ฆ ์ญ์ ์์ฐ์ค๋ฝ๊ฒ ์ฑ๊ณตํ ๊ฒ.
Map<String, Object> claims = jwtTokenizer.getClaims(jws, base64EncodedSecretKey).getBody();
return claims;
}
//Authentication ๊ฐ์ฒด๋ฅผ SecurityContext์ ์ ์ฅํ๊ธฐ ์ํ priavte ๋ฉ์๋
private void setAuthenticationToContext(Map<String, Object> claims) {
String username = (String) claims.get("username");
List<GrantedAuthority> authorities = authorityUtils.createAuthorities((List) claims.get("roles"));
Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
์์ธ ์ฒ๋ฆฌ ๋ก์ง์ ๊ฐ๋จํ๋ค.
try-catch๋ฌธ์ผ๋ก ํน์ ์์ธ ํ์ ์ Exception์ด catch ๋๋ฉด ํด๋น Exception์ catch ํ ํ์ Exception์ ๋ค์ throw ํ๋ค๋ ์ง ํ๋ ์ฒ๋ฆฌ๊ฐ ์๋, ๋จ์ํ request.setAttribute()๋ฅผ ์ค์ ํ๋ ์ผ๋ฐ์ ํ์ง ์๋๋ค.
์ด๋ฐ์์ผ๋ก ์์ธ ์ฒ๋ฆฌ๋ฅผ ํ๋ฉด, ์์ธ ๋ฐ์ ์ SecurityContext์ Authentication ๊ฐ์ฒด(ํด๋ผ์ด์ธํธ ์ ๋ณด)๊ฐ ์ ์ฅ๋์ง ์๋๋ค.
SecurityContext์ ํด๋ผ์ด์ธํธ์ ์ธ์ฆ ์ ๋ณด๊ฐ ์ ์ฅ๋์ง ์์ ์ํ๋ก ๋ค์ Security Filter ๋ก์ง์ ์ํํ๋ค๋ณด๋ฉด ๊ฒฐ๊ตญ์๋ Filter ๋ด๋ถ์์ AuthenticationException์ด ๋ฐ์ํ๊ฒ ๋๊ณ , ์ด AuthenticationException์ ๋ฐ๋ก ์๋์ ์ค๋ช ํ๋ AuthenticationEntryPoint๊ฐ ์ฒ๋ฆฌํ๊ฒ ๋๋ค.
5๏ธโฃ AuthenticationEntryPoint ๊ตฌํ
AuthenticationEntryPoint๋ SignatureException, ExpiredException ๋ฑ Exception ๋ฐ์์ผ๋ก ์ธํด SecurityContext์ Authentication์ด ์ ์ฅ๋์ง ์์ ๊ฒฝ์ฐ ๋ฑ AuthenticationException์ด ๋ฐ์ํ ๋ ํธ์ถ๋๋ ํธ๋ค๋ฌ ๊ฐ์ ์ญํ ์ ํ๋ค.
package com.codestates.todoapp.auth.handler;
import com.codestates.todoapp.auth.utils.ErrorResponder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//AuthenticationException์ด ๋ฐ์ํ ๋์ ํธ๋ค๋ฌ ์ญํ ์ ํ๋ค.
@Slf4j
@Component
public class MemberAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Exception exception = (Exception) request.getAttribute("exception");
ErrorResponder.sendErrorResponse(response, HttpStatus.UNAUTHORIZED);
logExceptionMessage(authException, exception);
}
//์์ธ ๋ฉ์์ง๋ฅผ ๊ธฐ๋กํ๋ ๋ด๋ถ ๋ฉ์๋
private void logExceptionMessage(AuthenticationException authException, Exception exception) {
String message = exception != null ? exception.getMessage() : authException.getMessage();
log.warn("Unauthorized error happend: {}", message);
}
}
6๏ธโฃ ErrorResponder(ErrorResponse๋ฅผ ํด๋ผ์ด์ธํธ์๊ฒ ์ ์กํ๊ธฐ ์ํ ํด๋์ค)
ErrorResponse๋ฅผ ์ถ๋ ฅ ์คํธ๋ฆผ์ผ๋ก ์์ฑํ๋ ์ญํ ์ ํ๋ค.
package com.codestates.todoapp.auth.utils;
import com.codestates.todoapp.response.ErrorResponse;
import com.google.gson.Gson;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//ErrorResponse๋ฅผ ์ถ๋ ฅ ์คํธ๋ฆผ์ผ๋ก ์์ฑํ๋ ์ญํ ์ ํ๋ ํด๋์ค.
public class ErrorResponder {
public static void sendErrorResponse(HttpServletResponse response, HttpStatus status) throws IOException {
Gson gson = new Gson();
ErrorResponse errorResponse = ErrorResponse.of(status);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(status.value());
response.getWriter().write(gson.toJson(errorResponse, ErrorResponse.class));
}
}
7๏ธโฃ AccessDeniedHandler ๊ตฌํ
์ธ์ฆ์๋ ์ฑ๊ณตํ์ผ๋ ํด๋น ๋ฆฌ์์ค์ ๋ํ ๊ถํ์ด ์์ผ๋ฉด ํธ์ถ๋๋ ํธ๋ค๋ฌ
package com.codestates.todoapp.auth.handler;
import com.codestates.todoapp.auth.utils.ErrorResponder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญํ ๋ฆฌ์์ค์ ๋ํ ์ ์ ํ ๊ถํ์ด ์์ ๊ฒฝ์ฐ ํธ์ถ๋๋ ํธ๋ค๋ฌ
@Slf4j
@Component
public class MemberAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ErrorResponder.sendErrorResponse(response, HttpStatus.FORBIDDEN);
log.warn("Forbidden error happend: {}", accessDeniedException.getMessage());
}
}
8๏ธโฃ SecurityCofiguration์ AuthenticationEntryPoint ๋ฐ AccessDeniedHandler ์ถ๊ฐ
package com.codestates.todoapp.config;
import com.codestates.todoapp.auth.filter.JwtAuthenticationFilter;
import com.codestates.todoapp.auth.filter.JwtVerificationFilter;
import com.codestates.todoapp.auth.handler.MemberAccessDeniedHandler;
import com.codestates.todoapp.auth.handler.MemberAuthenticationEntryPoint;
import com.codestates.todoapp.auth.handler.MemberAuthenticationFailureHandler;
import com.codestates.todoapp.auth.handler.MemberAuthenticationSuccessHandler;
import com.codestates.todoapp.auth.jwt.JwtTokenizer;
import com.codestates.todoapp.auth.utils.CustomAuthorityUtils;
import com.codestates.todoapp.member.entity.Member;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfiguration {
private final JwtTokenizer jwtTokenizer;
private final CustomAuthorityUtils authorityUtils;
public SecurityConfiguration(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils) {
this.jwtTokenizer = jwtTokenizer;
this.authorityUtils = authorityUtils;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.headers().frameOptions().sameOrigin()
.and()
.csrf().disable()
.cors(Customizer.withDefaults())
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //์ธ์
์ ์์ฑํ์ง ์๋๋ก ์ค์ ํ๋ค.
.and()
.formLogin().disable()
.httpBasic().disable()
.exceptionHandling()
.authenticationEntryPoint(new MemberAuthenticationEntryPoint()) //์์ธ ์ฒ๋ฆฌ ์ถ๊ฐ
.accessDeniedHandler(new MemberAccessDeniedHandler()) //์์ธ ์ฒ๋ฆฌ ์ถ๊ฐ
.and()
.apply(new CustomFilterConfigurer())//์ปค์คํฐ๋ง์ด์ง๋ Configuration ์ถ๊ฐ
.and()
.authorizeHttpRequests(authorize -> authorize
.antMatchers(HttpMethod.POST, "/members").permitAll()
.antMatchers(HttpMethod.PATCH, "/members/**").hasRole("USER")
.antMatchers(HttpMethod.GET, "/members").hasRole("ADMIN")
.antMatchers(HttpMethod.GET, "/members/**").hasAnyRole("USER", "ADMIN")
.antMatchers(HttpMethod.DELETE, "/members").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/members/**").hasAnyRole("USER", "ADMIN")
.anyRequest().permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder(){
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
//JwtAuthenticationFilter๋ฅผ ๋ฑ๋กํ๋ ์ญํ
public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity>{
@Override
public void configure(HttpSecurity builder) throws Exception{
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer);
jwtAuthenticationFilter.setFilterProcessesUrl("/auth/login");
jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler());
jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler());
JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, authorityUtils);
builder
.addFilter(jwtAuthenticationFilter) //JwtAuthenticationFilter๋ฅผ Spring Security Filter Chain์ ์ถ๊ฐํ๋ค.
.addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class); //JwtAuthenticationFilter์์ ๋ก๊ทธ์ธ ์ธ์ฆ์ ์ฑ๊ณตํ ํ ๋ฐ๊ธ๋ฐ์ JWT๊ฐ ํด๋ผ์ด์ธํธ์ request header์ ํฌํจ๋์ด ์์ ๊ฒฝ์ฐ์๋ง ๋์ํ๋ค.
}
}
}
์ด์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์คํํ ํ, ์ ํจํ์ง ์์ JWT ํ ํฐ ๋๋ ๋ง๋ฃ๋ JWT ๋ฅผ Authorization header์ ์ถ๊ฐํด์ request๋ฅผ ์ ์กํ๋ฉด ์๋์ response๋ฅผ ์ ์กํ๋ค.
๋ํ ๊ถํ์ด ์๋ ๋ฆฌ์์ค์ request๋ฅผ ์ ์ก ์, ์๋์ ๊ฐ์ response๋ฅผ ์ ์กํ๋ค.
์ด๊ฒ์ผ๋ก JWT์ ๋ํ ๊ธฐ๋ฅ์ด ์์ฑ๋์๋ค.
ใ ใ ใ ํ๋ฒ์ผ๋ก๋ ์ด๋ฆผ๋ ์์ง!! ์งํ ์ค์ธ ํ๋ก์ ํธ์ ๋ค์ ์ ์ฉํด ๋ณด๋ฉด์ ์ ์ฒด ํ๋ฆ์ ์ตํ์ผ๊ฒ ๋ค๐