JWT(JSON Web Token)
은 무연결성을 지키면서도 사용자의 인증 절차를 세션처럼 한번 수행하면 계속 인증된 정보를 사용하기 위해 사용합니다.
JWT는 세션과 다른 큰 차이점이 하나 존재하는데, 그건 서버에 인증 정보를 저장하는 것이 아니라, 클라이언트에 저장한다는 점입니다.
외부 공격으로 부터 안전하기 위하여 세션을 사용하지만, 클라이언트가 많아지면 서버의 과부화 문제, 여러 서버로 분산된 서비스의 경우 세션에 인증정보를 저장하기에 현실적으로 어려우므로 클라이언트에 암호화하여 저장하는 토큰을 사용합니다.
토큰은 엑세스 토큰(Access Token)
과 리프레시 토큰(Refresh Token)
두가지가 존재합니다.
클라이언트가 서버에서 인증을 하면, 서버는 클라이언트에 위의 두가지 토큰을 저장합니다.
엑세스 토큰(Access Token)
은 실제 권한이 필요한 리소스에 접근가능 한 권한을 부여하는데 사용합니다. 외부 공격자가 해당 토큰을 사용하여 클라이언트 인척 하는것을 방지하기 위해 짧은 유효기간
을 가집니다.
리프레시 토큰
은 유효기간이 짧은 엑세스 토큰
이 유효기간이 만료된다면 엑세스 토큰
을 다시 발급받는데 사용합니다.(다시 로그인할 필요x)
리프레시 토큰
도 탈취당하면 큰 문제가 될 수 있으므로 리프레시 토큰
을 사용하지 않는 서버도 많이 존재합니다.
JWT토큰은 3부분으로 구성됩니다.
{
"alg": "알고리즘",
"type": "JWT"
}
{
"sub": "접근권한",
"name" : "사용자명",
"ist": 123456
}
HMACSHA256(base64UrlEncode(header) + '-' + base64UrlEncode(payload), secret);
장점 | 단점 |
---|---|
stateless, scalability 특징을 가짐 | payload는 해독 쉬움 |
안전함 | 토큰이 길어지면 네트워크 과부화 |
어디서나 생성 가능 | 토큰은 자동으로 삭제 안됨 |
권한 부여에 용이 | 토큰은 어딘가에 저장되야 함 |
JWT를 사용하기 위해선 build.gradle에 외부 의존 라이브러리를 추가하여야 합니다.
dependencies{
implementation 'com.auth0:java-jwt:3.19.2'
...
}
@Data
@Entity
public class 엔티티{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String username;
private String password;
private String roles;
public List<String> getRoleList(){
if(this.roles.length() > 0){
return Arrays.asList(this.roles.split(",");
}
return new ArrayList<>();
}
}
이전에 사용하던 엔티티에서 roles
필드가 취가되고 getRoleList()
메서드가 추가되었습니다. roles 필드엔 권한들이 권한1, 권한2, 권한3, ...
문자열로 저장되고 getRoleList()
는 문자열을 ,
로 나누어 문자열 리스트로 반환합니다.
@Configuration
@EnableWebSecurity
public class 설정클래스{
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http.csrf().disable();
http.headers().frameOptions().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin().disable()
.httpBasic().disable()
.authorizeRequests()
.antMatcher("/경로1/경로1-1/user/**")
.access("hasRole('ROLE_USER') or hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
...
.anyRequest().permitAll();
return http.build();
}
}
http.csrf().disable()
: form태그 외 다른 요청은 무시합니다.headers().frameOptions().disable()
: h2 연결할때 필요합니다..sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
: 로그인 인증시 세션을 만들지 않고 토큰을 발행합니다. 로그인 인증시 세션을 생성하지 않을 뿐, 다른 작업에선 그대로 세션을 생성합니다.formLogin().disable()
: form Login을 사용하지 않습니다.httpBasic().disable()
: http통신을 사용하지 않고 https통신을 사용합니다. https사용시 username과 password가 head의 Authorization에 포함되어 전달됩니다.@Configuration
public class Cors설정{
@Bean
public CorsFilter corsFilter(){
UrlBasedCorsConfigurationSource 소스 = new UrlBasedCorsConfigurationSource();
CorsConfiguration 설정 = new CorsConfiguration();
설정.setAllowCredentials(true);
설정.addAllowedOrigin("*");
설정.addAllowedHeader("*");
설정.addAllowedMehtod("*");
소스.registerCorsConfiguration("/경로/**", 설정);
return new CorsFilter(소스);
}
}
.setAllowCredentials(true)
: CORS
접근 요청을 허용합니다..setAllowedOrigin("*")
: 모든 IP에 대한 응답을 허용합니다..setAllowedHeader("*")
: 모든 header에 응답을 허용합니다..setAllowedMehtod("*")
: GET,POST,PATCH,UPDATE,DELETE등 모든 http메서드 요청을 허용합니다..registerCorsConfiguration("/경로/**",설정);
: 해당 경로의 하위경로들에 해당 설정을 적용합니다.CorsFilter
는 필터로써 빈 객체로 등록하여 SpringContainer가 자동으로 생성은 시켜주지만 Filter를 SpringSecurity에 적용하기 위해서는 설정클래스의 FilterChain 빈
에 등록을 해야만 합니다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class 설정클래스{
private final CorsFilter corsFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
...
.and()
.addFilter(corsFilter)
}
}
.addFilter(corsFilter)
: SecurityFilterChain에 해당 필터를 추가합니다.
세션, 쿠키은 사용자를 확인 한 다음 Session ID
를 발급하여 클라이언트에 저장합니다. 이후 Session ID
에 해당하는 사용자이름, 권한등의 정보를 이용하여 사용자 인증을 생략합니다.
하지만 TOKEN방식은 로그인시 클라이언트에 발급을 하고 단순히 토큰에 권한정보를 담고 있어 서버에 따로 정보를 저장할 필요가 없습니다.
Bearer
는 JWT
또는 OAuth
에 대한 토큰을 사용합니다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class 설정클래스{
...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http.addFilterBefore(new 필터(), SecurityFilterChain내필터.class);
//http.addFilterAfter(new 필터(), SecurityFilterChain내필터.class);
...
}
}
.addFilterBefore()
.addFilterAfter()
는 필터 체인에 필터를 추가합니다. 이때 매개변수 인자로 추가할 필터, 기준이될 필터(존재 해야함)를 지정합니다. 메서드명 에서 알수 있듯이 Before는 바로앞에, After는 바로뒤에 필터를 추가합니다.
public class 필터 implements Filter{
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOExceptoin, SevletException{
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResopnse) response;
res.setCharacterEncoding("UTF-8");
if(req.getMethod().equals("HTTP메서드")){
String headerAuth = req.getHeader("Authorization");
if(headerAuth.equals("Authorization값")){
chain.doFilter(req,res);
}else{
PrintWriter writer = res.getWriter();
writer.println("인증 실패");
}
}
}
}
HttpServletRequest
, HttpServletResponse
는 ServletRequest와 ServletResponse를 상속받아 다시 다운캐스팅이 가능합니다.
해당하는 HTTP메서드
가 일치하고 header의 Authroization
필드의 값이 동일하면 다음 필터를 실행하고 다를시 에러를 발생시킵니다.
JWT는 이전에 해본적이 없지만, 많이 언급되어 이미 현업에서 많이 사용되고 있겠구나 예상은 했었습니다. 쿠키,세션과 더불어 오늘은 토큰에 대하여 학습하였고 클라우드 컴퓨팅의 발전, 분산 시스템의 도입 확대 등으로 앞으로 더욱더 많이 사용될 방식이겠구나 싶습니다.
https://github.com/ds02168/CodeStates_Spring/tree/main/section4-week1-WED