이전에 알아보았던 인증과 인가에서는 Session을 사용했습니다. Session은 Http Request의 Stateless한 특성을 보완하기 위해 유저의 정보를 서버에 저장하기 위해 사용되는 수단입니다.
🏷 원래 Http Request는 State를 저장하지 않기 때문에 이미 종료된 요청에 대해서 기억하지 못합니다.
이러한 특성을 무상태성(Staelessness)라고 합니다.
유저가 로그인을 한 이후에 생긴 session 정보를 서버에 저장해두고, 인증이 필요할 때마다 session 정보를 비교하여 인증상태를 유지하게 됩니다. Session은 서버를 기반으로 수행하는 인증이기 때문에 요청 회수가 증가할 수록 서버 부담 역시 증가하게 됩니다. 이런 서버의 부담을 줄여주기 위해 고안된 방식이 Token 인증 방식입니다.
JWT는 Token 인증 방식 중에 가장 범용적으로 사용되는 방식입니다. JSON Web Token의 약자로, JSON 형식을 암호화한 값을 Web Token으로써 인증 과정에 사용합니다.
JWT 생성과 사용 과정
어떻게 JWT가 생성되고 request - response 과정에서 어떻게 사용되는지 흐름을 한 번 확인해보겠습니다.
1. 먼저 POST로 Client가 Server에 로그인 요청을 보냅니다.
2. Server에서 유저의 정보를 확인한 후 JWT 생성합니다.
3. 그리고 JWT를 Client에게 응답과 함께 전달합니다.
4. Client는 이후에 전달받은 토큰을 저장합니다. 저장 위치는 Local Storage, Session Storage, Cookie등 다양합니다.
5. Clinet는 요청 시 항상 HTTP 헤더(Authorization 헤더)또는 쿠키에 토큰을 담아 보내어 자신이 인증된 유저이며 리소스에 접근이 가능함을 증명하는 통행증처럼 사용합니다.
5. 서버는 user가 가지고 온 토큰을 해독해서 유저 정보를 확인합니다.
6. 권한이 있는 유저인 것이 확인되면 요청한 리소스를 응답으로 전송해줍니다.
JWT의 구조
JWT는 xxxxx.yyyyy.zzzzz 와 같이 세 부분으로 나뉘어 있고, 세 부분은 '.'을 이용해 구분됩니다.
각 부분은 어떠한 내용을 담고 있는지 알아보겠습니다.
1. Header:
- typ: 토큰 종류
- alg: Sign에 사용될 알고리즘
- Base64로 endcoding
2. Payload:
- 서버에서 활용할 수 있는 유저 정보를 담는 부분
- 정보 접근 권한
- 유저 이름 등
- (민감한 정보는 담지 않는 것이 좋음)
- Base64로 endcoding
3. Signature
- Encoding된 Header와 Payload, 그리고 원하는 비밀키(+salt)를 Header에서 지정한 알고리즘을 사용하여 암호화 진행
- (base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret)을 지정한 알고리즘으로 암호화해서 만듬
JWT의 종류
JWT에는 Access Token과 Refresh Token 두 가지 종류의 Token이 있습니다.
1. Access Token
보호된 정보들(User의 email, 연락처, 사진 등)에 접근할 수 있는 권한 부여에 사용되는 토큰입니다.
Client가 처음 인증을 받게 될 때 (로그인 시) Access Token과 Refresh Token을 모두 받게 됩니다. 이 두가지 token 중에서 실제로 권한을 얻는데 이용되는 것은 Access Token입니다.
탈취되었을 때의 문제를 막기 위해 짧게 유효기간을 설정하는 것이 좋습니다.
2. Refresh Token
Access Token의 유효기간은 비교적 짧다고 하였는데, access token이 만료가 되었다고 해서 유저가 또 다시 로그인을 해야한다면 매우 번거로울 것입니다. 이 때 Refresh Token을 사용해서 다시 Access Token을 발급 받을 수 있습니다.
사실 Refresh Token을 탈취 당한다면 이 역시나 큰 문제가 될 수 있습니다. 그래서 정보 보호가 매우 중요한 웹사이트들은 Refresh Token을 사용하지 않는 경우도 있다고 합니다.
JWT 사용을 위한 Security 기본 설정
1. JWT를 생성하기 위해서는 먼저 dependency를 추가해주어야 합니다.
dependencies {
...
implementation 'com.auth0:java-jwt:3.19.2' //jwt 사용을 위한 dependency
...
}
2. SecurityConfig에 몇가지 설정을 추가해줍니다.
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity //@EnableWebSecurity 꼭 사용해야 되는지 더 알아보기..
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.headers().frameOptions().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //(1)
.and()
.formLogin().disable() //(2)
.httpBasic().disable() //(3)
.authorizeRequests() //(4)
.antMatchers("/api/v1/user/**") //(5)
.access("hasRole('ROLE_USER') or hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
.antMatchers("/api/v1/manager/**")
.access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
.antMatchers("/api/v1/admin/**")
.access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll();
return http.build();
}
}
(1) Session 이나 Cookie를 만들지 않고 Stateless하게 진행하겠다는 의미. JWT와 같은 토큰 방식을 사용할 때 설정.
🏷 SessionCreationPolicy 좀 더 알아보기
SessionCreationPolicy는 말그대로 session을 어떻게 생성할 것인지를 설정하는 것입니다.
.sessionCreationPolicy(SessionCreationPolicy.XXXX)
괄호 부분에 들어갈 정책 종류에는 몇 가지가 있습니다.
1) SessionCreationPolicy.ALWAYS: 스프링시큐리티가 항상 세션을 생성
2) SessionCreationPolicy.IF_REQUIRED: 스프링시큐리티가 필요시 생성(기본)
3) SessionCreationPolicy.NEVER: 스프링시큐리티가 생성하지않지만, 기존에 존재하면 사용
4) SessionCreationPolicy.STATELESS: 스프링시큐리티가 생성하지도않고 기존것을 사용하지도 않음 (JWT 같은 토큰 방식을 쓸 때 사용하는 설정)
출처:
https://fenderist.tistory.com/342
(2) .formLogin().disable(): form tag기반의 login을 사용하지 않겠다는 설정
🏷 form login은 username과 password를 이용한 로그인
(3) .httpBasic().disable(): 기본 로그인 폼을 사용하지 않겠다는 설정
(4) .authorizeRequests(): 특정 권한을 가진 사용자만 접근할 수 있도록 하는 메소드. HttpServletRequest를 이용한다는 것을 의미. antMatcher()를 사용한 builder pattern으로 권한 명시
(5) .antMatchers(): 특정한 경로를 지정
3. CORS 문제를 해결하기 위한 configuration을 해줍니다.
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CorsFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/api/**", config);
return new CorsFilter(source);
}
}
CORS Filter로는 preflight에서 요청하는 access control을 처리할 수 있도록 설정해주는 것입니다.
보면 accessControl과 관련된 항목들을 설정해주는 것을 알 수 있습니다.
Access-Control-Allow-Origin => .addAllowedOrigin()
Access-Control-Allow-Credentials => .setAllowCredentials()
Access-Control-Allow-Methods => .addAllowedMethod()
Access-Control-Allow-Headers => .addAllowedHeader()
...
설정한 CorsFilter는 SecurityFilterChain에 등록해줍니다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor // 추가
public class SecurityConfig {
private final CorsFilter corsFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
...
.and()
.addFilter(corsFilter) // 추가
...
}
}
🏷 CORS를 처리하기 위한 방법에는 몇 가지가 있는데 아래 블로그를 참조하면 좋을 것 같습니다!
https://wonit.tistory.com/572
JWT 생성을 위한 코드 작성
JWT를 사용하기 위한 기본적인 설정을 완료했으니
JWT를 생성하는 Filter도 별도로 작성해서 SecurityFilterChain에 추가해주어야 합니다.
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
//(1) attemptAuthentication() 로그인 처리
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("login 시도");
try {
ObjectMapper om = new ObjectMapper();
Member member = om.readValue(request.getInputStream(), Member.class);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(member.getUsername(), member.getPassword());
Authentication authentication = authenticationManager.authenticate(authenticationToken);
PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
return authentication;
} catch (IOException e) {
e.printStackTrace();;
}
return null;
}
//(2) successfulAuthentication() Jwt 생성 후 전달
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
System.out.println("successfulAuthentication");
PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();
String jwtToken = JWT.create()
.withSubject("cos jwt token")
.withExpiresAt(new Date(System.currentTimeMillis() + (60 * 1000 * 10)))
.withClaim("id", principalDetails.getMember().getId())
.withClaim("username", principalDetails.getMember().getUsername())
.sign(Algorithm.HMAC512("cos_jwt_token"));
response.addHeader("Authorization", "Bearer " + jwtToken);
}
}
1) attemptAuthentication()이 정상적으로 수행되면 JWT를 사용자에게 응답으로 돌려주게 됩니다.
2) successfulAuthentication()을 이용해서 Jwt를 전달해 줍니다.
SecurityFilterChain에 CustomDsl이라는 클래스를 만들어, CustomDsl을 이용해 JwtAuthenticationFilter를 등록해줍니다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private CorsFilter corsFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// http.addFilterBefore(new FirstFilter(), SecurityContextHolderFilter.class); // 주석 처리 or 제거
http.csrf().disable();
http.headers().frameOptions().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin().disable()
.httpBasic().disable()
.apply(new CustomDsl()) // 추가
.and()
.authorizeRequests()
.antMatchers("/api/v1/user/**")
.access("hasRole('ROLE_USER') or hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
.antMatchers("/api/v1/manager/**")
.access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
.antMatchers("/api/v1/admin/**")
.access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll();
return http.build();
}
public class CustomDsl extends AbstractHttpConfigurer<CustomDsl, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) throws Exception {
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
builder
.addFilter(corsFilter)
.addFilter(new JwtAuthenticationFilter(authenticationManager));
}
}
}
이 외에도 기본적인 코드들도 있어야 하지만,
제가 학습한 JWT와 관련된 설정 부분 코드들만 정리해보았습니다. 이것 저것 만져보면서 활용해 보고 추가적인 설명 지속적으로 덧붙이도록 하겠습니다.
질문이나 수정사항 알려주시면 더욱 감사합니다. :)
참고자료
https://velog.io/@jayjay28/2019-09-04-1109-%EC%9E%91%EC%84%B1%EB%90%A8
https://ttl-blog.tistory.com/104
'Java Spring > Security' 카테고리의 다른 글
RestController 테스트 시 JWT 생성하기 (2) | 2022.09.20 |
---|---|
SessionId 사용하여 로그인한 회원이 작성한 글 DB에 저장하기 (4) | 2022.08.15 |
Authorization(인가)-인가 내부 절차 & 권한 설정하기 (0) | 2022.08.01 |
Authentication(인증) (2) - 로그인 절차 & 코드구현 (0) | 2022.07.30 |
Authentication(인증) (1) - 사용자 정보 저장하기 & 비밀번호 암호화하여 저장하기(DelegatingPasswordEncoder는 무엇인가?) (1) | 2022.07.29 |