// 에드센스

지난 포스팅에서 JWT가 무엇인지 알아보았다. 그럼 실제로 구현해보자.

https://llshl.tistory.com/26

 

[Web] JWT란?

1. JWT란? Json Web Token의 줄임말이다. 두 개체에서 JSON객체를 사용하여 가볍고 자가수용적인 방식으로 정보를 안정성 있게 전달해 주는 인증 방식이다. 2. 세션과의 차이점 세션 보통 로그인을 구현

llshl.tistory.com


1. JWT 테스트 시나리오

  1. 이메일과 비밀번호를 받아 회원 등록
  2. 로그인하면 JWT 토큰 반환
  3. JWT토큰을 Client 헤더에 등록하고, 요청 시 Spring Security에서 제한을 걸어놓은 리소스의 접근을 확인

 

전체 코드는 깃헙에 업로드 해놓았다.

참고로 이 코드는 

여기

블로그에서 참고했다. 설명이 아주 잘 돼 있으니 참고하면 좋다.

 

 

 

주요 코드만 살펴보자.

 

 

 


2. JWT를 사용해 로그인 기능을 구현해보자

 

회원가입 컨트롤러

    // 회원가입
    @PostMapping("/join")
    public Long join(@RequestBody Map<String, String> user) {
        return userRepository.save(User.builder()
                .email(user.get("email"))
                .password(passwordEncoder.encode(user.get("password")))
                .roles(Collections.singletonList("ROLE_ADMIN")) // 최초 가입시 USER 로 설정
                .build()).getId();
    }

그냥 별거없다. 이메일과 비밀번호를 입력받아서 Jpa의 save를 해준다. 참고로 데이터베이스는 H2를 썼다.

이렇게 포스트맨의 바디에 회원가입 정보를 담아서 요청을 보냈다.

 

 

 

 

로그인 컨트롤러

    // 로그인
    @PostMapping("/login")
    public String login(@RequestBody Map<String, String> user) {
        User member = userRepository.findByEmail(user.get("email"))
                .orElseThrow(() -> new IllegalArgumentException("가입되지 않은 E-MAIL 입니다."));
        if (!passwordEncoder.matches(user.get("password"), member.getPassword())) {
            throw new IllegalArgumentException("잘못된 비밀번호입니다.");
        }
        return jwtTokenProvider.createToken(member.getUsername(), member.getRoles());
    }

로그인 컨트롤러에서는 입력받은 이메일과 비밀번호가 데이터베이스에 있는 정보와 일치하는지 확인하고

일치한다면 JWT 토큰을 만들어서 반환해준다.

토큰에 사용자의 이메일(getUsername)과 권한 정보를 넣어줄 것이기에 파라미터로 넘겨준다.

회원가입한 정보 그대로 로그인 요청을 보낸다. 그러면 응답으로 JWT토큰이 돌아왔다!

 

 

 

 

JWT 토큰 생성

@RequiredArgsConstructor
@Component
public class JwtTokenProvider {
    private String secretKey = "llshlllshlllshlllshl";

    // 토큰 유효시간 30분
    private long tokenValidTime = 30 * 60 * 1000L;
    private final UserDetailsService userDetailsService;

    // 객체 초기화, secretKey를 Base64로 인코딩한다.
    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    // JWT 토큰 생성
    public String createToken(String userPk, List<String> roles) {
        Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위
        claims.put("roles", roles); // 정보는 key / value 쌍으로 저장된다.
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims) // 정보 저장
                .setIssuedAt(now) // 토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + tokenValidTime)) // set Expire Time
                .signWith(SignatureAlgorithm.HS256, secretKey)  // 사용할 암호화 알고리즘과
                // signature 에 들어갈 secret값 세팅
                .compact();
    }

    // JWT 토큰에서 인증 정보 조회
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    // 토큰에서 회원 정보 추출
    public String getUserPk(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }

    // Request의 Header에서 token 값을 가져옵니다. "X-AUTH-TOKEN" : "TOKEN값'
    public String resolveToken(HttpServletRequest request) {
        return request.getHeader("X-AUTH-TOKEN");
    }

    // 토큰의 유효성 + 만료일자 확인
    public boolean validateToken(String jwtToken) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }
}

 

 

이 클래스의 createToken 메소드를 통해 토큰을 생성한다.

이렇게 생성된 JWT 토큰은 이후 사용자의 요청마다 헤더에 담아져 함께 전달된다.

 

 

이 토큰을 디코딩해보면

우리가 토큰에 넣어주었던 사용자의 정보가 잘 나오는것을 확인 할 수 있다.

이건 https://jwt.io/ 여기서 할 수 있다.

 

 

 

권한이 있어야 접근 가능하도록

토큰이 권한을 가져다 주는지 확인하기 위해 Spring Security의 WebSecurityConfig 클래스를 설정해준다.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.headers().frameOptions().disable();
        http.authorizeRequests()
                .antMatchers("/h2-console/**").permitAll(); // 누구나 h2-console 접속 허용
        http
                .httpBasic().disable() // rest api 만을 고려하여 기본 설정은 해제하겠습니다.
                .csrf().disable() // csrf 보안 토큰 disable처리.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 기반 인증이므로 세션 역시 사용하지 않습니다.
                .and()
                .authorizeRequests() // 요청에 대한 사용권한 체크
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("USER")
                .anyRequest().permitAll() // 그외 나머지 요청은 누구나 접근 가능
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
                        UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests(); // 권한요청 처리 설정 메서드

        // JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 전에 넣는다
    }

 

configure 메소드에서 user와 admin으로 시작하는 요청에는

각각 USER와 ADMIN의 권한이 있어야만 접근 가능하도록 제한을 걸어두었다.

 

 

 

권한은 어떻게 확인?

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 헤더에서 JWT 를 받아옵니다.
        String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
        // 유효한 토큰인지 확인합니다.
        if (token != null && jwtTokenProvider.validateToken(token)) {
            // 토큰이 유효하면 토큰으로부터 유저 정보를 받아옵니다.
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            // SecurityContext 에 Authentication 객체를 저장합니다.
            SecurityContextHolder.getContext().setAuthentication(authentication);
            System.out.println("토큰 유효하다");
        }
        chain.doFilter(request, response);
    }
}

이 필터가 요청을 가로채어 JWT 토큰이 유효한지 판단한다. 토큰 검증 후 유효하다면 다시 요청을 진행시킨다.

 

 

 

확인해보자

    @PostMapping("/user/test")
    public Map userResponseTest() {
        Map<String, String> result = new HashMap<>();
        result.put("result","user ok");
        return result;
    }

    @PostMapping("/admin/test")
    public Map adminResponseTest() {
        Map<String, String> result = new HashMap<>();
        result.put("result","admin ok");
        return result;
    }

접근 제한을 걸어둔 이 두개의 리소스에 요청을 보내어 결과가 잘 받아지는지 확인해보자.

회원가입시 기본적으로 얻는 권한은 USER다.

 

 

 

 

Headers에 우리가 설정해둔 키인 X-AUTH-TOKEN에 대응되게 value로 JWT 토큰을 넣어서 요청을 보내주면 올바른 응답이 돌아옴을 확인할 수 있다. 즉 인증이 되었다는 것.

 

 

 

 

 

당연하게도 토큰을 빼고 요청을 보내면 응답을 받을 수 없다. 

 

 

 

 

또한 USER권한으로는 ADMIN에 접근이 안됨을 확인할 수 있다.

 

 

 

 

참고:

  1. 정지원 2022.06.05 15:02

    혹시 깃헙 주소가 어떻게 되시나요?/ 전체 코드 보고싶습니다!

+ Recent posts