본문 바로가기
FrameWork/Spring

JWT 로그인 인증방식(Spring Cloud)

by 태윤2 2021. 8. 28.

 

JWT란?

  • Json Web Token의 약자
  • Json 객체를 사용해 가볍고 자가수용적인(self-contained) 방식으로 정보를 안전성 있게 전달해주기 위한 토큰

 

JWT의 구조

 

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

  1. HEADER: 사용한 해쉬 알고리즘 -> HS256
  2. PAYLOAD: 담을 내용
  3. SIGNATURE: 서명 (ID+PASSWORD)

 

기존 로그인 방식 = 세션

문제점

  • 세션과 쿠키는 모바일 애플리케이션에서 유효하게 사용할 수 없음(공유 불가)
  • 렌더링된 HTML 페이지가 반환되지만, 모바일 애플리케이션에서는 JSON(or XML)과 같은 포멧 필요
  • 다른 언어(예) React native로 구현된 모바일 어플리케이션 와 Java로 구현된 웹페이지)간에 세션 공유가 되지 않음

JWT 사용과정

  1. 브라우저의 로그인과정에서 회원정보를 입력
  2. 서버에게 authenticate를 요청
  3. 서버는 JWT를 사용해 Bearer Token을 발급
  4. 클라이언트는 토큰을 받아 토큰을 사용해 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(),
                truetrue,
                truetrue,
        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