ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Delivery Together - JWT 인증방식의 로그인(3)
    Projects/Problem & Solution 2021. 12. 25. 21:59

    개요

    지금까지는 JWT의 사용 이유와 JWT의 특징을 알아보았고, 이어서 Spring Security 내에서 어떠한 절차로 인증 과정이 처리되는지 알아보았다. 이번에는 사용자가 입력한 로그인 정보를 통해 JWT를 생성해서 발급하는 과정까지 코드로 구현해보겠다.


    사전 설정

    1. 의존성 추가
    implementation 'io.jsonwebtoken:jjwt:0.9.1'

    JWT 생성을 위한 외부 라이브러리인 jjwt에 대한 의존성을 build.gradle에 추가한다.

     

    2. JwtTokenHelper 클래스 생성
    @Component
    public class JwtTokenHelper {
    
        private SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
    
        @Value("${jwt.auth.app_name}")
        private String appName;
    
        @Value("${jwt.auth.secret_key}")
        private String secretKey;
    
        @Value("${jwt.auth.expires_in}")
        private int expiresIn;
    
        /* JWT를 생성하는 메서드 */
        public String generateToken(String username) throws InvalidKeySpecException, NoSuchAlgorithmException {
    
            return Jwts.builder()
                    .setIssuer(appName)
                    .setSubject(username)
                    .setIssuedAt(new Date())
                    .setExpiration(generateExpirationDate())
                    .signWith(SIGNATURE_ALGORITHM, secretKey)
                    .compact();
        }
    
        /* JWT가 가지는 모든 클레임을 반환하는 메서드 */
        private Claims getAllClaimsFromToken(String token) {
            Claims claims;
    
            try {
                claims = Jwts.parser()
                        .setSigningKey(secretKey)
                        .parseClaimsJws(token)
                        .getBody();
            } catch (Exception e) {
                claims = null;
            }
    
            return claims;
        }
    
        /* JWT가 가지는 username을 반환하는 메서드 */
        public String getUsernameFromToken(String token) {
            String username;
    
            try {
                final Claims claims = this.getAllClaimsFromToken(token);
                username = claims.getSubject();
            } catch (Exception e) {
                username = null;
            }
    
            return username;
        }
    
        /* JWT의 만료시간을 설정하는 메서드 */
        private Date generateExpirationDate() {
            return new Date(new Date().getTime() + expiresIn * 1000);
        }
    
        /* 특정 JWT가 유효한지 판단하는 메서드 */
        public Boolean isTokenValidate(String token, UserDetails userDetails) {
            final String username = getUsernameFromToken(token);
    
            return (
                    username != null
                            && username.equals(userDetails.getUsername())
                            && !isTokenExpired(token)
            );
        }
    
        /* JWT의 만료시간이 지났는지 판단하는 메서드 */
        public boolean isTokenExpired(String token) {
            Date expireDate = getExpirationDateFromToken(token);
    
            return expireDate.before(new Date());
        }
    
        /* JWT의 만료시간을 반환하는 메서드 */
        private Date getExpirationDateFromToken(String token) {
            Date expireDate;
    
            try {
                final Claims claims = this.getAllClaimsFromToken(token);
    
                expireDate = claims.getExpiration();
            } catch (Exception e) {
                expireDate = null;
            }
    
            return expireDate;
        }
    
        /* JWT의 발급시간을 반환하는 메서드 */
        public Date getIssuedDateFromToken(String token) {
            Date issuedAt;
    
            try {
                final Claims claims = this.getAllClaimsFromToken(token);
    
                issuedAt = claims.getIssuedAt();
            } catch (Exception e) {
                issuedAt = null;
            }
    
            return issuedAt;
        }
    
        /* 클라이언트 요청의 토큰 정보를 반환하는 메서드 */
        public String getToken(HttpServletRequest request) {
            String authHeader = getAuthHeaderFromHeader(request);
    
            if (authHeader != null && authHeader.startsWith("Bearer "))
                return authHeader.substring(7);
    
            return null;
        }
    
        /* 클라이언트 요청의 헤더 정보를 반환하는 메서드 */
        public String getAuthHeaderFromHeader(HttpServletRequest request) {
            return request.getHeader("Authorization");
        }
    
    }

    JWT와 관련한 여러 가지 메서드를 정의한 클래스이다.

    외부에서 사용할 수 있도록, "@Component" 애너테이션을 활용하여 Bean으로 등록한다.

     

    3. SecurityConfig 설정
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    외부에서 AuthenticationManager를 사용하기 위해 SecurityConfig 클래스 내 위 코드를 추가한다.

    원하는 시점에 로그인을 수행하기 위해 AuthenticationManager를 사용한다.


    로그인 처리

    1. 로그인 요청 객체 생성
    @Data
    public class LoginRequest {
    
        private String username;
    
        private String password;
    }

    사용자는 username과 password를 통해서 로그인을 요청하기 때문에, username과 password를 포함한 객체인 LoginRequest 객체를 생성한다.

     

    2. JWT 객체 생성
    @Data
    public class LoginResponse {
    
        private String token;
    }

    로그인을 성공적으로 수행한 사용자에게 전달할 JWT에 대한 객체를 정의한 클래스이다.

     

    3. 로그인 Controller 추가
    @Autowired
    private AuthenticationManager authenticationManager;

    AuthenticationManager는 Authentication 객체를 만들고 인증을 처리하는 인터페이스이다.

     

    @PostMapping("/auth/login")
    public ResponseEntity<?> loginUser(@RequestBody LoginRequest loginRequest) throws InvalidKeySpecException, NoSuchAlgorithmException {
        final Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
    
        SecurityContextHolder.getContext().setAuthentication(authentication);
    
        User user = (User) authentication.getPrincipal();
    
        String jwtToken = jwtTokenHelper.generateToken(user.getUsername());
    
        LoginResponse response = new LoginResponse();
        response.setToken(jwtToken);
    
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    사용자로부터 전달받은 LoginRequest 객체를 처리하는 메서드이다.

    사용자로부터 전달받은 username과 password를 조합해서 UsernamePasswordAuthenticationToken 인스턴스를 만든다.

    이 토큰은 검증을 위해 AuthenticationManager에게 전달이 되고, 인증에 성공하면 Authentication 인스턴스를 반환한다.

    인증이 완료된 Authentication 인스턴스를 SecurityContextHolder.getContext().setAuthentication() 메서드를 통해서 SecurityContextHolder에 설정해준다.

    이 과정까지가 사용자로부터 받은 정보를 이용해서 로그인을 수행한 것이다.

    authentication.getPrincipal() 메서드를 사용해서 UserDetails 인터페이스를 구현한 User 객체에 대한 정보를 가져온다.

    사용자의 정보를 이용하여 JWT를 생성한 후 반환해줌으로써, 로그인 과정이 마무리된다.


    테스트

    1. 로그인 요청

     

    사용자가 로그인을 수행하기 위해 username과 password를 서버 측으로 요청한다.

     

    2. JWT 발급

     

    로그인이 성공적으로 완료가 되면, 서버 측에서 사용자 정보를 통해 만들어진 JWT를 발급한다.

     

    728x90

    댓글

Designed by Tistory.