JWT란?
- Json Web Token의 약자
- Json 객체를 사용해 가볍고 자가수용적인(self-contained) 방식으로 정보를 안전성 있게 전달해주기 위한 토큰
JWT의 구조
- HEADER: 사용한 해쉬 알고리즘 -> HS256
- PAYLOAD: 담을 내용
- SIGNATURE: 서명 (ID+PASSWORD)
기존 로그인 방식 = 세션
문제점
- 세션과 쿠키는 모바일 애플리케이션에서 유효하게 사용할 수 없음(공유 불가)
- 렌더링된 HTML 페이지가 반환되지만, 모바일 애플리케이션에서는 JSON(or XML)과 같은 포멧 필요
- 다른 언어(예) React native로 구현된 모바일 어플리케이션 와 Java로 구현된 웹페이지)간에 세션 공유가 되지 않음
JWT 사용과정
- 브라우저의 로그인과정에서 회원정보를 입력
- 서버에게 authenticate를 요청
- 서버는 JWT를 사용해 Bearer Token을 발급
- 클라이언트는 토큰을 받아 토큰을 사용해 Authorization: Bearer JWT를 반환 해 서버에서 인증 후 클라이언트에게 다시 반환
JWT 장점
- 클라이언트 독립적인 서비스(stateless)
- CDN (Content Delivery Network) (중간에 캐시 서버하고도 인증처리가 가능)
- No Cookie-Session(No CSRF, 사이트간 요청 위조)
- 지속적인 토큰 저장
- 부하분산(로드밸런싱) 시스템이 적용되어 있는 서버에서는 DB에 저장해 다른 서버이더라도 활용이 가능
Spring Boot(+Spring Cloud) 에서 JWT를 사용해 로그인 구현
필수 의존성 주입(build.gradle)
- 필터 설정을 위한 AuthenticationFilter를 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
package com.example.userservice.security;
import com.example.userservice.dto.UserDto;
import com.example.userservice.service.UserService;
import com.example.userservice.vo.RequestLogin;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
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.ArrayList;
import java.util.Date;
@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final UserService userService;
private final Environment env;
public AuthenticationFilter(AuthenticationManager authenticationManager,
UserService userService,
Environment env) {
super.setAuthenticationManager(authenticationManager);
this.userService = userService;
this.env = env;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException { try {
// 로그인 시 첫번째 로 호출됨
RequestLogin creds = new ObjectMapper().readValue(request.getInputStream(),
RequestLogin.class); // UsernamePasswordAuthenticationToken() -> 토큰으로 변경
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(creds.getEmail(),
creds.getPassword(), new ArrayList<>()) );
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult)
throws IOException, ServletException { String userName = ((User) authResult.getPrincipal()).getUsername();
UserDto userDetails = userService.getUserDetailsByEmail(userName);
String token = Jwts.builder()
.setSubject(userDetails.getUserId())
.setExpiration(new Date(System.currentTimeMillis() +
Long.parseLong(env.getProperty("token.expiration_time")))) .signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret"))
.compact();
response.addHeader("token", token);
response.addHeader("userId", userDetails.getUserId());
}
}
|
cs |
|
|
- 필터를 사용해 WebSecurity 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
package com.example.userservice.security;
import com.example.userservice.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.config.annotation.authentication.
builders.AuthenticationManagerBuilder; 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.configuration.
WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurity extends WebSecurityConfigurerAdapter {
private final UserService userService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
// jwt 만들어 질때 다양한 정보는 application.yml 에서 가져오기 위해 Environment 가져옴
private final Environment env;
// 권한에 대한 설정
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
// http.authorizeRequests().antMatchers("/users/**").permitAll(); 전체에 대해 허가
http.authorizeRequests().antMatchers("/**") // 모든 사용자 제한
.hasIpAddress("192.168.219.104") // IP를 제한적으로 받음
.and()
.addFilter(getAuthenticationFilter()); // 필터를 통과한 데이터에 한에서 권한을 부여
// h2-console 사용가능
http.headers().frameOptions().disable();
}
// 필터 추가
private AuthenticationFilter getAuthenticationFilter() throws Exception {
// AuthenticationFilter 인스턴스 생성
AuthenticationFilter authenticationFilter =
new AuthenticationFilter(authenticationManager(), userService, env);
// AuthenticationManger 는 ProviderManager 를 구현한 클래스
// 인자로 전달받은 유저에 대한 인증 정보를 담고 있으며, 해당 인증정보가 유효할 경우
// UserDetailsService 에서 적절한 principal 을 가지고 있는 Authentication 객체를
// 반환해 주는 역할을 하는 인증 공급자(Provider)
// authenticationFilter.setAuthenticationManager(authenticationManager());
return authenticationFilter;
}
// 인증에 대한 설정
// select pwd from users where email=?
// db_pwd(encrypted) == input_pwd(must be encrypted)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
}
}
|
cs |
- application.yml에 token 정보 저장
- UserService(Interface)에 UserDetailsService를 상속
- 구현체인 UserServiceImpl에 loadUserByUsername 오버라이드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 두번째로 호출됨
UserEntity userEntity = userRepository.findByEmail(username);
if (userEntity == null) {
throw new UsernameNotFoundException(username);
}
// org.springframework.security.core.userdetails
return new User(
userEntity.getEmail(),
userEntity.getEncryptedPwd(),
true, true,
true, true,
new ArrayList<>());
}
|
cs |
순서
- HttpSecurity를 파라미터로 갖는 configure 오버라이딩 후 설정 정보 저장
- AuthenticationFilter를 반환하는 getAuthenticationFilter() 생성 후 UsernamePasswordAuthenticationFilter를 상속받은 AuthenticationFilter 인스턴스 생성 후 반환
- AuthenticationFilter는 Autehntication을 반환하는 atteptAuthentication을 오버라이딩 후 로그인 정보를 받아 토큰으로 변경 시켜 반환
- User의 비지니스 로직을 담당할 Service 인터페이스에 UserDetailsService를 상속 후 구현체에 loadUserByUserName 을 오버라이딩
- 로그인 완료후 JWT토큰을 생성할 successfulAuthentication을 AuthenticationFilter 클래스에 오버라이딩
- 토큰 생성 후 response header에 토큰 정보 저장
별도로 login Controller를 생성하지않아도 xxxxx:port/login 을 하면 로그인서비스와 맵핑 되어있음
Spring Cloud Gateway Filter 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
|
package com.example.gatewayservice.filter;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHeaders;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
private final Environment env;
public AuthorizationHeaderFilter(Environment env) {
super(Config.class);
this.env = env;
}
public static class Config {}
@Override
public GatewayFilter apply(Config config) {
return ((exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
return onError(exchange, "no authorization header", HttpStatus.UNAUTHORIZED);
}
String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
log.info(authorizationHeader);
String jwt = authorizationHeader.replace("Bearer", "");
if (!isJwtValid(jwt)) {
return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
}
return chain.filter(exchange);
});
}
// Mono, Flux -> Spring 5.0 에서 추가된 WebFlux = 클라이언트 요청이 들어왔을때 반환하는값 단일, 다중값을 비동기로 처리함
private Mono<Void> onError(ServerWebExchange exchange,
String err,
HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
log.error(err);
return response.setComplete();
}
private boolean isJwtValid(String jwt) {
boolean returnValue = true;
String subject = null;
try {
subject = Jwts.parser().setSigningKey(env.getProperty("token.secret"))
.parseClaimsJws(jwt).getBody()
.getSubject();
} catch (Exception exception) {
returnValue = false;
}
if (subject == null || subject.isEmpty()) {
returnValue = false;
}
return returnValue;
}
}
|
cs |
'FrameWork > Spring' 카테고리의 다른 글
Spring + JWT (0) | 2021.08.31 |
---|---|
Spring 웹 계층의 역할 (0) | 2021.08.26 |
기존 테스트에 Security 적용 (0) | 2021.08.21 |
네이버 로그인 추가하기 (0) | 2021.08.21 |
Session 저장소로 데이터베이스 사용하기 (0) | 2021.08.21 |