[서버개발캠프] Spring Security + Refresh JWT DB접근없이 인증과 파싱하기

Sieun Sim·2020년 1월 20일
6

서버개발캠프

목록 보기
6/21

보통의 예제들은 UserDetailsService에서 회원 DB로 직접 loadByUsername같은 메소드를 이용해 access하여 확인하는 작업을 매 request마다 수행한다. 나는 이 작업이 stateless라는 JWT의 기본 컨셉과 맞지 않는다고 생각했는데, 매번 토큰에 담겨있는 정보에 대해 의심해 DB에 접근할거면 차라리 다른 방법을 쓰는게 나을 것 같다.

https://daddyprogrammer.org/post/636/springboot2-springsecurity-authentication-authorization/

글쓴이의 댓글에 비슷한 내용이 있었다.

사실 SecurityContextHolder.getContext에 Authentication값을 세팅시 유저의 모든 정보를 세팅할 필요는 없고 권한 정보만 세팅해도 됩니다. 따라서 수정한 코드는 JWT토큰에 저장된msrl, roles 정보만으로만 유저정보를 구성하여 SecurityContextHolder정보를 세팅하였습니다.

그래서 함수를 새로 만들었다. 저 글쓴이분께서 쓰신 것도 있는데 버전이 좀 다른지 오류가 나서 참고해서 좀 바꿨다.

Token 관리하는 클래스에 parsing 함수를, RequestFilter 클래스에 그 파싱한 유저 정보를 가지고 권한과 username만 포함해 UserDetails(Principal 객체)를 새로 만들었다.

public Map<String, Object> getUserParseInfo(String token) {
        Claims parseInfo = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        Map<String, Object> result = new HashMap<>();
        result.put("username", parseInfo.getSubject());
        result.put("role", parseInfo.get("role", List.class));
        return result;
    }

GrantedAuthority 객체때문에 생기는 귀찮은 일들

userDetails.getAuthorities() 로 받아오는 객체는 이렇다.

Collection<? extends GrantedAuthority> getAuthorities()


Returns the authorities granted to the user. Cannot return null.

Returns:the authorities, sorted by natural key (never null)

일반적인 list나 collection으로 받아오려해도 계속 자료형이 안맞는다는 오류가 나길래 직접 role을 2개이상 부여한 유저를 만들어서 로그인토큰을 디코드해 확인해보았다. jwt에 claim으로 그대로 보내면 저 Collection대로 예쁘게 들어가는게 아니라 role 하나당 {"authority": "역할"} 형태로 들어가게 된다. [{"authority": "역할1"}, {"authority": "역할2"}] 이런식으로.. 그래서 parsing할 때 따로 처리를 해주어야 한다. 여러가지 방법이 있겠지만 token에 저런 형태로 들어가있는건 보기 흉한것 같아서 토큰에 넣기 전 GrantedAuthority.getAuthority() 메소드를 이용해 String List로 변환했다. 근데 꺼내서 쓸때 또 따로 처리해줘야해서 아주 귀찮고 더러워졌다 ^^..

public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        List<String> li = new ArrayList<>();
        for (GrantedAuthority a: userDetails.getAuthorities()) {
            li.add(a.getAuthority());
        }
        claims.put("role",li);
        return Jwts.builder().setClaims(claims).setSubject(userDetails.getUsername()).setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
                .signWith(SignatureAlgorithm.HS512, secret).compact();
    }

public interface GrantedAuthority extends java.io.Serializable

반환타입: java.lang.String

getAuthority()

If the GrantedAuthority can be represented as a String and that String is sufficient in precision to be relied upon for an access control decision by an AccessDecisionManager (or delegate), this method should return such a String.

Custom User build하기

public Authentication getAuthentication(String token) {
        Map<String, Object> parseInfo = jtu.getUserParseInfo(token);
        System.out.println("parseinfo: " + parseInfo);
        List<String> rs =(List)parseInfo.get("role");
        Collection<GrantedAuthority> tmp= new ArrayList<>();
        for (String a: rs) {
            tmp.add(new SimpleGrantedAuthority(a));
        }
        UserDetails userDetails = User.builder().username(String.valueOf(parseInfo.get("username"))).authorities(tmp).password("asd").build();
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                userDetails, null, userDetails.getAuthorities());
        return usernamePasswordAuthenticationToken;
    }

최근 문서를 보면 User.builder()는 UserDetails를 반환한다. 옛날에는 User를 반환했던듯..?

https://docs.spring.io/spring-security/site/docs/4.2.13.RELEASE/apidocs/org/springframework/security/core/userdetails/User.UserBuilder.html#authorities-org.springframework.security.core.GrantedAuthority...-

공식 문서를 참고해 만들었다.

At minimum the username, password, and authorities should provided. The remaining attributes have reasonable defaults.

근데 이름, 비번, 권한을 싹 다 설정해야만 해서 password는 그냥 아무 값이나 줬다. 나는 db접근 안하고 jwt parsing만으로 이용하려고 새로 만든건데 오히려 더 귀찮고 복잡해진것같다ㅠ 그래도 일단 컨셉에 충실해보려고 한다. JWT 쓰면서 UserDetails 를 굳이 이렇게 억지로 연계해서 쓰는게 무슨 의미가 있나 점점 회의감이 들고있다...

doFilterInternal 함수

위의 custom 사항들을 반영한 실제 필터 함수는 이렇게 된다.

@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        final String requestTokenHeader = request.getHeader("Authorization");

        String username = null;
        String jwtToken = null;
        // JWT Token is in the form "Bearer token". Remove Bearer word and get
        // only the Token
        if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
            jwtToken = requestTokenHeader.substring(7);
            try {
                username = jwtTokenUtil.getUsernameFromToken(jwtToken);
            } catch (IllegalArgumentException e) {
                logger.warn("Unable to get JWT Token");
            }  catch (ExpiredJwtException e) {
                logger.warn("JWT Token has expired");
            }
        } else {
            logger.warn("JWT Token does not begin with Bearer String");
        }

        if (username != null && !jtu.isTokenExpired(jwtToken)) {
            //DB access 대신에 파싱한 정보로 유저 만들기!
            Authentication authen =  getAuthentication(jwtToken);
            //만든 authentication 객체로 매번 인증받기
            SecurityContextHolder.getContext().setAuthentication(authen);
        } else {
            logger.info("this Token is EXPIRED !");
        }
        logger.info("CONTEXT  :  " + SecurityContextHolder.getContext().getAuthentication());

            /*
            ValueOperations<String, Object> vop = redisTemplate.opsForValue();
            Token result = (Token)vop.get(username);
            String tokstr = result.getToken();앞
            */


        chain.doFilter(request, response);
    }

SecurityContextHolder.getContext().setAuthentication(authen) 이 부분이 실제로 인증을 해주는 부분인데 set 한 뒤 SecurityContextHolder.getContext().getAuthentication(authen) 를 호출해보면 온갖 정보가 담겨있는 객체를 눈으로 직접 확인해볼 수 있다. 그런데 아무래도 이건 매 요청마다 해주어야 하는 것 같다. 뭔가 WAS에서 가지고 유지해주는건줄 알았는데 session 설정을 키고 테스트해봐도 매번 null값으로 초기화된다. 나중에 더 알아봐야 할 듯

1개의 댓글

comment-user-thumbnail
2020년 3월 31일

동일한 컨셉으로 사용중인데 권한관련 정보가 많을 경우(이것을 JWT로 인코딩 사용)
토큰 길이가 길어지니
http header의 max size에 걸려서 에러가 발생하는 문제가 있더군요.
일단은 JWT 인코딩시 압축해서 해결하기는 했는데..
혹시 이부분은 어떻게 해결하셨는지 궁금합니다.

답글 달기