본문 바로가기
Develop/Spring

[Spring] JWT 를 활용한 인증 구현

by 코딩의성지 2020. 3. 28.

하이 ~~~ 여러분들

 

이전 포스팅에서 제가 Spring 에서 CRUD를 구현했었다. 이렇게 서비스를 제공하는 것도 중요하지만, 이러한 웹서비스에서는 인증을 어떻게 구현하느냐도 중요하다.

 

스프링에서는 Spring Security라는 아주 강력한 보안관련 프레임워크를 제공한다. 우리는 오늘 Spring Security중에서 JWT라는 놈을 이용해서 인증서비스를 구현해볼 것이다.

 

 

<영상으로 공부하기 !!>

youtu.be/TlWzEr4cXfc

오늘 글이 ... 도움되신 분들은 !! 구독과 좋아요도 많이들 부탁드린다 :)

세션 방식과 토큰 방식

 

과거에는 우린 서버 (세션) 기반의 인증시스템을 활용해왔다. (지금도 이러한 기반의 시스템은 아직 많다 !)

 

<Session 생성>

 

세션 기반의 인증 시스템은 유저가 인증할 때 이 기록을 서버에 저장하고 어떤 서비스를 요청 받았을 때 그 세션 값을 읽어서 유저를 인증해주는 구조이다. 그런데 서버가 하나만 있으면 세션을 사용해도 괜찮다.

 

하지만 대부분의 엔터프라이즈 환경의 서비스는 여러대의 서버를 운영한다. 보통 어떤 요청이 들어오면 L4 같은 걸로 로드밸런싱해서 각각의 서버로 요청이 가게되는데, 이때 각각의 서버는 우리가 저장해둔 세션의 정보가 공유되어야한다. 이런 세션 클러스터링 문제도 세션 시스템에서는 신경을 써줘야하니 굉장히 복잡하다. 또한 세션을 공유하다보니 동시 접속하는 유저의 수가 많아지면 많아질 수록 서버나 DB에 부하를 줄 수도 있다. 

 

그리고 세션이랑 쿠키는 거의 함께 많이 쓰는데 CORS( Cross-Origin Resource Sharing) 문제도 해결해줘야하니 얼마나 복잡한가 .. ㅎㅎ

 

이러한 문제점을 한방에 해결할 수 있는게 바로 토큰이다. 우리 Spring 특징 중에 stateless한 서비스라고 했던 것 기억하는가? 바로 토큰을 이용한 인증방식이 그렇다.

 

<토큰 생성 및 저장>

 

<토큰 검증>

만약 유저가 아이디와 비밀번호로 로그인을 시도한다고 생각해보면, 서버측에서 해당 정보를 검증한 뒤에 서명된 토큰을 발급해준다. 그 토큰을 웹브라우저 자체의 스토리지에 저장하고 , 이제 어떤 서비스를 요청할 때 그 토큰을 서버 전달해준다.서버는 해당 토큰을 검증한 뒤에 해당 유저의 요청이 맞으면 요청에 대한 응답을 제공해준다.

 

이 토큰은 위조가 될 수 없다. 만약 위조된 토큰이 날라오면 검증단계에서 걸러지게 된다.  또한 토큰 만료기간 설정을 잘 해주면 보안적으로 아주 우수하게 관리할 수 있다.

 

물론 완벽하진 않다. 쿠키를 사용하지 않아 보안이 우수하긴 하지만 토큰은 HTTP 헤더에 포함시켜서 던지다보니 도중에 스피닝 될 우려도 있다. 물론 https로 암호화 해서 전송하긴 하지만 ... 그래도 취약점이 있다는 것은 잘 알고 사용하자. (그래도 쿠키보다 낫다.)

 

또한 CORS 한 문제를 완전히 해결할 수 있다. 아무 도메인에서나 토큰 값만 유효하면 정상적으로 요청이 처리되기 때문이다.

 

 

 

JWT

 

자 이제 본격적으로 우리가 사용할 토큰 !! JWT에 대해 알아보자.

 

JWT는 JSON Web Token의 줄인말이다. 

 

JWT는 웹표준으로써 다양한 프로그래밍 언어에서 지원된다.

 

JWT는 . 을 구분자로 해서 3가지 값으로 구성된다.

 

보통 헤더(header)는 두가지 정보를 포함한다. 토큰의 타입을 지정할 수있는 typ 와 RSA, SHA256 등 해싱 알고리즘 정보를 나타내는 alg 이다.

 

그리고 내용(payload)에는 토큰에 담을 정보가 포함되는데 이러한 정보를 클레임이라고한다.

 

클레임은 세 종류로 나눠지는데 iss,sub,aud,exp,nbf,sat,jti 와 같은 이미 정의되어 있는 registered claim,

충돌 방지 이름이 필요할 때 url 형식으로 네이밍하는 public claim,

클라이언트와 서버 간의 협의 하에 사용되는 클레임인 private claim

 

요렇게 세가지로 나눠진다.

 

그리고 마지막 서명(signature) 는 헤더의 인코딩값과 정보의 인코딩값을 합쳐서 주어진 비밀키로 해싱하여 생성되는 서명값이다. 보통 유저의 비밀번호를 비밀키로 사용하곤 한다.

 

 

구현

 

자 이론적인 설명은 끝났으니 실제로 구현해보자!

1
2
3
4
5
6
7
8
9
10
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

일단 pom.xml 에다가 위의 두가지 내용의 디펜던시를 설정해주자.

 

gradle 사용자 라면 build.gradle 내부에

dependencies {
   ...  
   implementation 'javax.xml.bind:jaxb-api'
   implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
   runtime 'io.jsonwebtoken:jjwt-impl:0.11.2'
   runtime 'io.jsonwebtoken:jjwt-jackson:0.11.2'
   ...
}

을 넣어주자.

 

그리고 토큰 생성과 검증을 해줄 로직이 들어갈 SecurityService 와 SecurityServiceImpl 을 구현하자.

1
2
3
4
5
6
7
8
import org.springframework.stereotype.Service;
 
@Service
public interface SecurityService {
    String createToken(String subject, long ttlMillis);
 
    String getSubject(String token);
}
 
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
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Service;
 
 
@Service
public class SecurityServiceImpl implements SecurityService {
    private static final String SECRET_KEY = "aasjjkjaskjdl1k2naskjkdakj34c8sa";
 
    @Override
    public String createToken(String subject, long ttlMillis) {
        if (ttlMillis <= 0) {
            throw new RuntimeException("Expiry time must be greater than Zero : ["+ttlMillis+"] ");
        }
        // 토큰을 서명하기 위해 사용해야할 알고리즘 선택
        SignatureAlgorithm  signatureAlgorithm= SignatureAlgorithm.HS256;
 
        byte[] secretKeyBytes = DatatypeConverter.parseBase64Binary(SECRET_KEY);
        Key signingKey = new SecretKeySpec(secretKeyBytes, signatureAlgorithm.getJcaName());
        return Jwts.builder()
.setSubject(subject)
.signWith(signingKey, signatureAlgorithm)
.setExpiration(new Date(System.currentTimeMillis()+ttlMillis))
.compact();
    }
 
    @Override
    public String getSubject(String token) {
        Claims claims = Jwts.parserBuilder()
.setSigningKey(DatatypeConverter.parseBase64Binary(SECRET_KEY))
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
    }
}
 

 createToken 메서드는 서버에서 토큰을 만들어서 발행하는 역할한다.

그리고 getSubject 메서드에서는 비밀키로 토큰을 풀어서 값을 가져오는 역할을 한다. 

 

아래는 해당 서비스의 메서드를 호출하는 컨트롤러 코드이다.

 

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
 
 
@RestController
@RequestMapping("/security")
public class SecurityController {
    @Autowired
    private SecurityService securityService;
 
    @GetMapping("/gen/token")
    public Map<String, Object> genToken(@RequestParam(value="subject"String subject) {
        String token = securityService.createToken(subject, (2 * 1000 * 60));
        Map<String, Object> map = new LinkedHashMap<String, Object>();
        map.put("result", token);
        return map;
    }
 
    @ResponseBody
    @GetMapping("/get/subject")
    public Map<String, Object> getSubject(@RequestParam("token"String token) {
        String subject = securityService.getSubject(token);
        Map<String, Object> map = new LinkedHashMap<String, Object>();
        map.put("result", subject);
        return map;
    }
}
 
 

 

 

 

결과

 

포스트맨으로 확인해보자.

 

일단 토큰 발행을 위한 콜을 해보자.

 

오...! 잘된다 ㅎㅎㅎ

 

 

그리고 발행된 토큰은 http://jwt.io  에서 확인할 수 있다.

 

 

JWT.IO

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

jwt.io

오오오 ...! 잘나온다 ㅎㅎㅎ

 

이제 발급된 토큰을 가지고 검증을 해보자.

 

짜자잔 !! 토큰을 통해 검증을해서 내가 입력한 subject인 kang이 나오는걸 확인할 수 있다.

 

자~~ 오늘은 여기까지 하도록하겠다 다들 잘따라 구현해볼수 있도록하자. 그럼 오늘도 열공하자 !! 

반응형

댓글