로그인 - JWT

zunzero·2022년 8월 27일
0

스프링, JPA

목록 보기
8/23

쿠키, 세션, JWT

쿠키, 세션, JWT는 모두 stateless한 네트워크 서버의 특징을 연결성으로 사용하기 위한 방법이다.
쿠키와 세션은 서버의 저장소에 해당 값과 매칭되는 값을 가지고 있어야 하기 때문에, 서버 자원이 많이 사용된다는 단점이 있다.
이러한 단점을 해결하기 위해 JWT가 등장했다. JWT는 토큰 자체에 유저 정보를 담아서 암호화한 토큰이다.
암호화된 내용을 디코딩하여 요청 정보를 파악할 수 있다.

JWT 구조

위와 같이 JWT는 3개의 값으로 이루어져 있다.

  • Header: 토큰 타입과 해시 암호화 알고리즘 정보 2가지를 담고 있다.
    - typ: 토큰의 타입 지정 -> JWT
    • alg: 해싱 알고리즘 지정 -> 주로 HMAC SHA256 혹은 RSA 사용
  • Payload: 토큰의 정보를 담는 부분
    - 주로 User의 id 값이나 유효기간 등을 담는다.
  • Signature: 시크릿 키를 포함하여 암호화 되어있다.

jwt는 쉽게 인코딩 될 수 있어 중요한 값을 Payload에 포함하면 안된다.
jwt는 쉽게 인코딩 될 수 있지만, 시크릿 키를 아는 서버만이 완벽하게 디코딩하여 정보를 알아낼 수 있기 때문에 Signature 값을 통해 사용자를 식별할 수 있다.

JWT 동작 과정

JWT 발급 과정

사용자 정보를 바탕으로 시크릿 키를 활용해서 JWT를 발급한다.

JWT 유효성 검사 과정

사용자가 요청에 JWT를 보내면 서버에서 시크릿키를 사용해서 JWT의 토큰의 유효성을 체크한다.
유효한 토큰이라면 Payload 정보를 통해 사용자 인증을 하게 된다.
토큰의 유효기간이나 사용자의 권한 등을 최종적으로 체크한다.

JWT와 스프링 시큐리티

나는 JWT는 이용했지만 스프링 시큐리티는 이용하지 않았다.
내가 참여했던 프로젝트는 이미 스프링 인터셉터와 세션을 이용한 로그인을 구현하여 웹 애플리케이션의 로그인 과정을 구현해두었다.
뒤늦게 모바일 애플리케이션으로의 로그인 기능을 구현해야 했는데, 스프링 시큐리티는 필터를 거쳐 동작한다.
애초에 스프링 시큐리티에 대한 지식도 거의 없고, 이미 구현된 기능을 최대한 건드리고 싶지 않아서 스프링 시큐리티는 사용하지 않고 JWT만 만들어서 모바일 애플리케이션 로그인 기능을 구현했다.
왜 모바일 애플리케이션에 대해서는 JWT를 사용했느냐고 묻는다면, 모바일 애플리케이션에는 JWT를 사용해야 한다고 어렴풋이 들은 기억과, JWT에 대한 호기심으로 도전했다.
결과는 매우 엉성했지만...

나의 JWT

@Service
@Transactional
@Slf4j
@RequiredArgsConstructor
public class TokenServiceImpl implements TokenService {

    private final AgentRepository agentRepository;
    private final OfficialsRepository officialsRepository;
    
    @Override
    public String createToken(Long primaryKey, LoginResponse loginResponse, String type) {
        // 공통
        Date now = new Date();
        JwtBuilder jwtBuilder = Jwts.builder()
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
                .setIssuedAt(now)
                .claim("id", primaryKey)
                .claim("username", loginResponse.getU_name())
                .claim("role", loginResponse.getU_auth())
                .signWith(SignatureAlgorithm.HS256, "secret");

        if (Objects.equals(type, "access")) {
            return this.accessToken(type, jwtBuilder).compact();
        } else if (Objects.equals(type, "refresh")) {
            return this.refreshToken(type, jwtBuilder).compact();
        } return null;
    }

    // access token 생성 (1시간)
    private JwtBuilder accessToken(String access, JwtBuilder jwtBuilder) {
        Date now = new Date();
        return jwtBuilder.setExpiration(new Date(now.getTime() + Duration.ofMinutes(60).toMillis()))
                .setIssuer(access);
    }

    // refresh token 생성 (1주일)
    private JwtBuilder refreshToken(String refresh, JwtBuilder jwtBuilder) {
        Date now = new Date();
        return jwtBuilder.setExpiration(new Date(now.getTime() + Duration.ofDays(7).toMillis()))
                .setIssuer(refresh);
    }
    
	@Override
    public Claims parseJwtToken(String authorizationHeader) {
        if(authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
            throw new IllegalStateException("NoToken");
        }
        String token = authorizationHeader.substring("Bearer ".length());
        try {
            return Jwts.parser()
                    .setSigningKey("secret")
                    .parseClaimsJws(token)
                    .getBody();
        } catch (JwtException e) {
            throw new JwtException("ExpiredToken");
        }
    }
    
    @Override
    public boolean validateToken(String token) {
        token = token.substring("Bearer ".length());

        return !Jwts.parser()
                .setSigningKey("secret")
                .parseClaimsJws(token)
                .getBody().getExpiration().before(new Date());
    }
    
	@Override
    public Agent getAgentFromRequest(String authorization) {
        Claims token = parseJwtToken(authorization);
        Long agent_id = Long.valueOf(token.get("id").toString());
        return agentRepository.findById(agent_id);
    }

    @Override
    public Officials getOfficialFromRequest(String authorization) {
        Claims token = parseJwtToken(authorization);
        Long official_id = Long.valueOf(token.get("id").toString());
        return officialsRepository.findById(official_id);
    }
}
@RestController
@RequiredArgsConstructor
@Slf4j
public class AppLoginControllerImpl implements AppLoginController {
	private final AppLoginService appLoginService;
    private final TokenService tokenService;
    
    @Override
    @PostMapping("/app/login")
    public LoginResponse login(@RequestBody AppLoginRequest loginRequest) {
    	// 로그인 성공/실패 검증 로직
        ...
        
        // 토큰 생성
		String token = tokenService.createToken(primaryKey, loginResponse, "access");  // accessToken 생성 (유효시간 30분)
        String refreshToken = tokenService.createToken(primaryKey, loginResponse, "refresh");   // refreshToken 생성 (유효시간 7일)

        loginResponse.setToken(token);
        loginResponse.setRefreshToken(refreshToken);

        return loginResponse;

}
profile
나만 읽을 수 있는 블로그

0개의 댓글