자바/스프링(Spring)

Spring security : JWT 토큰 인증 / 인가 적용하기

류창 2023. 4. 11. 18:03
반응형

 

 

스프링 시큐리티의 필터체인이다.

 

 

이것이 스프링 시큐리티의 동작 원리를 한 그림으로 나타낸것입니다.

처음보면 참 어지럽다..

 

여기서 주목해야하는 부분은  Authentication(인증) / Authorization (인가)

 

 

https://github.com/codingspecialist/Springboot-Security-JWT-Easy

 

GitHub - codingspecialist/Springboot-Security-JWT-Easy

Contribute to codingspecialist/Springboot-Security-JWT-Easy development by creating an account on GitHub.

github.com

예제 코드는 여기서 따왔다.

 

 

 

 Security Config 

 

하나씩 살펴보겠다.  

 

addFilter ( ) :  cors설정 => 모든 cors 통과

csrf().disable()  =>  csrf 토큰 비활성화  

 

이 두 옵션은 이 포스트에 정리를 해놨다. 모르는 지식이면  보고오면 좋다.

https://taehoung0102.tistory.com/227

 

XSS , CORS, CSRF 의 차이점

1. XSS 란? 이 공격을 한마디로 표현하자면, 스크립트를 사용한 공격이다. XSS의 목적은 쿠키 및 세션탈취 , 광고 등등 주요 타겟이 클라이언트다. 혹시 이런 경험 해본적 없는가? 사이트 또는 게시

taehoung0102.tistory.com

 

session 설정 :  sessionCreationPolicy.stateless => 세션을 안쓰겠다는 말

 

.formLogin.diable : 로그인 폼 비활성화

=> 해당 옵션은 시큐리티가 자동으로 지원하는 로그인폼을 안쓰겠다는말 

=> 실제로 사용하면, 로그인 폼 경로, 로그인 처리 경로 등등 잘 써먹을수있다. 

 

.httpBasic().disable: JWT 방식을 사용할것이니 비활성화 (Bearer) 방식

 

 

.addFilter (JWT 인증필터)  :추후 설명

.addFilter(JWT 인가 필터)  : 추후 설명

 

 

.authorizeRequests() : 이곳에서 경로에 따라 권한을 체크할수있다.

 

EX)  User만 접근가능  경로, Manage만 접근가능 경로, Admin만 접근가능 경로 ..

 

 

 

JWT 인증 필터

JWT 인증필터이다. 

 

우선 extends를 보면 UsernamePasswordAuthenticationFilter를 상속받는다.

 

이 필터는 맨 처음 스프링 시큐리티의 필터중 하나이다.

이 3번째 필터

 

이 Username... 필터는  스프링 시큐리티의 /login 요청을 타고 온다.

 

이 필터가 실행이되면 ,  attemptAuth..  인증 시도 메소드가 실행이된다.

 

코드를 내려읽어보면,  ObjectMapper를 사용하여,LoginRequestDto.class와 request의 username과 password를Key : value로 파싱해서 입력해서 반환해주었다.

 

설명을 매우 잘해놓으셧다.

 

여기서부터 JWT 인증 시도를 시작한다.

 

우선,  UsernamePasswordAuthenticationToken을 생성한다.

이 토큰은 JWT 토큰이 아니다.

 

JWT를 시도하기전에,   요청으로준  유저정보  == DB에 담긴 유저정보 가 같은지를 판별하기위한 토큰이다.

 

UsernamePasswordAuthenticationToken은 요청으로 준 유저정보 측에 해당한다.

 

그 후 , AuthenticationManager.authenticate(토큰)으로

요청으로준  유저정보  == DB에 담긴 유저정보 가 같은지를 판별한다.

 

여기서 같지않으면 UnAuthorized 401 메소드가 발생한다!

이렇게 말이다

 

그 다음 AttempAuth가 성공적으로 200코드를 뱉으면, 

 

다음과같은  successfulAuthent코드가 일어난다.

 

여기서 진짜 JWT 코드를 생성한다.

 

JWT.create() 메소드를통해, 생성할수있다.

 

withSubject : 말그대로 주체가 될만한 값을 넣어야한다. 즉, 객체를 분별할수있는 고유한 ID가 적합하다.

withExpiresAt: 토큰의 유효기간이다.  보통 현재시각+ 토큰의 기간을 넣는다.

withClaim: 토큰 안에 내가 넣고싶은 요소들을 마음껏 넣을수있다.  

sign :  토큰을 만들기위해 일종의 싸인을 해야한다.  서버만 아는 시크릿코드를넣어  암호화 알고리즘을 넣는다.

 

그 후, JWT의 헤더에  (HEADER_STRING) Authorization 을 넣고 ,  (TOKEN_PREFIX) Bearer + 토큰을 보낸다. 

 

 

-----------------------------------------------------------------------------------------------------------------

여기까지가 인증을 통한 JWT 발송 절차다.

 

JWT를 발송했으면 나중에 JWT를 받아서 그것이 유효한 토큰인지 확인하는  "인가" 작업이 필요하다.

 

 

 

 

JWT 인가 필터

 

JWT인가필터는   BasicAuthenticathionFileter를 extends 받는다. 

 

이 필터 역시, 시큐리티 체인에 속하는 필터다.

 

 

5번째에 존재한다.

 

 

해당 필터는 권한이 필요한 경로를 접근할때만, 적용이된다. 즉, 권한이 없는 경로는 적용이 안된다.

 

 

이 필터를 거치면 다음과 같은 메소드가 발생한다.

 

 

doFilterInternal 메소드가 발생한다. 

 

해당 메소드에서,  요청으로 들어온 Authorization 토큰과 ,  DB에 있는 정보가 일치한지 검증한다.

 

이것이 일치하면, 시큐리티 세션에 접근하여 값 저장,  일치하지않는다면 그냥 리턴시킨다.

 

 

-------------------------------------------------------

 

부록: JwtTokenProvider

 

JWT 인가, 인증을할때 필요한 기능들을 JwtTokenProvider에 집약시킨 것이므로,

사용하면  인가. 인증필터코드가 매우 짧아지니  구현해서 쓰는걸 추천한다.

 

EX)  JwtTokenProvider

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
@Component
public class JwtTokenProvider {
 
    @Value("${jwt.secret}")
    private String secretKey;
 
    @Value("${jwt.expire}")
    private long validityInMilliseconds;
 
    public String createToken(String username, List<String> roles) {
        Claims claims = Jwts.claims().setSubject(username);
        claims.put("roles", roles);
 
        Date now = new Date();
        Date validity = new Date(now.getTime() + validityInMilliseconds);
 
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(validity)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }
 
    public Authentication getAuthentication(String token) {
        String username = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
        List<String> roles = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().get("roles", List.class);
 
        List<GrantedAuthority> authorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        User principal = new User(username, "", authorities);
 
        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }
 
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}
 
cs

 

EX)  Jwt인가 필터

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
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
 
    private final JwtTokenProvider jwtTokenProvider;
 
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtTokenProvider jwtTokenProvider) {
        super(authenticationManager);
        this.jwtTokenProvider = jwtTokenProvider;
    }
 
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String header = request.getHeader("Authorization");
        if (header == null || !header.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }
        String token = header.replace("Bearer """);
        if (jwtTokenProvider.validateToken(token)) {
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }
}
 
 
cs

 

반응형