Spring Security 메모 - JWT

timothy jeong·2022년 7월 17일
1

Spring Security

목록 보기
8/8

Security JWT

[image:B311F605-850C-4E96-8DA1-081A2BB8B670-37344-00001564992BB504/스크린샷 2022-07-14 오후 8.20.39.png]

TOKEN

A Token can be a plain string of format universally unique identifier (UUID) or it can be of type JSON Web Token (JWT) usually that get generated when the user authenticated for the first time during login

이러한 토큰을 공유함으로써 Credential 을 직접적으로 네트워크에서 공유하는 행위를 줄일 수 있다.

  • Token helps us not to share the credentials for every request which is a security risk to make credentials send over the network frequently.

  • Tokens can be invalidated during any suspicious activities without invalidating the user credentials.

  • Tokens can be created with a short life span.

  • Tokens can be used to store the user related information like roles/authorities etc.

  • Reusability - We can have many separate servers, running on multiple platforms and domains, reusing the same token for authenticating the user.

  • Security - Since we are not using cookies, we don’t have to protect against cross-site request forgery (CSRF) attacks.

  • Stateless, easier to scale. The token contains all the information to identify the user, eliminating the need for the session state. If we use a load balancer, we can pass the user to any server, instead of being bound to the same server we logged in on. 51

JWT

JWT 는 3파트로 나뉜다.
‘.’ 으로 나뉜다.

  • In the header, we store metadata/info related to the token. If I chose to sign the token, the header contains the name of
    the algorithm that generates the signature.

[image:5283AA6C-4236-4497-B30C-B2E20ED5DCB6-37344-00001569DCC60F82/스크린샷 2022-07-14 오후 8.36.22.png]

[payload]

  • Payload 부분에는 인증, 인가에 사용되는 유저 디테일, 롤에 대한 정보가 담긴다. 그외에 여러 정보도 담을 수 있다.

[image:8E8C36CE-E2CA-414F-9FA6-EBA2E29C69A3-37344-0000156A1F058C3D/스크린샷 2022-07-14 오후 8.37.08.png]

[Signature]

Signature 는 optional part 이다. 하지만 중요하다.
우리 서버로 들어온 JWT 토큰이 과연 우리가 만든 토큰인지 아닌지 검증하는 기능을 한다.

[image:0D3B5C75-D109-41AF-BF55-D09BFE21AA9B-37344-0000156B5A3BBC4E/스크린샷 2022-07-14 오후 8.40.48.png]

종합적으로 보면

[image:C9CDD45E-756F-4614-9CBE-6066F840FE91-37344-0000156BBA7D436D/스크린샷 2022-07-14 오후 8.41.55.png]

왜 signature 부분만 암호화해서 보낼까? Header 와 payload 부분도 encoding 해서 보내면 되지 않을까?
암호화하는 알고리즘은 서버의 컴퓨팅 자원을 많이 쓰기 때문에 주의해야한다.

Using JWT

이 의존성을 추가한다.

implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'

config 수정

그리고 csrf 를 더이상 사용하지 않는다.
JWT 자체가 토큰이기 때문이다.

그리고 기본적으로 JSESSION 이 발급되는것도 막아준다.

그리고 이 api 서버에 접속한 모든 프론트에 Authorization 헤더가 노출되게끔 설정을 cors 에서 해준다.

@Override
protected void configure(HttpSecurity http) throws Exception {

    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and().cors().configurationSource(request -> {
                CorsConfiguration config = new CorsConfiguration();
                config.setAllowedOrigins(Collections.singletonList("*"));
                config.setAllowedMethods(Collections.singletonList("*"));
                config.setAllowedHeaders(Collections.singletonList("*"));
                config.setMaxAge(3600L);
                config.setAllowCredentials(true);
                config.setExposedHeaders(List.of("Authorization"));
                return config;
            })
            .and().csrf().disable()
            .authorizeRequests()
            .mvcMatchers(HttpMethod.GET, "/user/check/nickname").permitAll()
            .mvcMatchers(HttpMethod.POST, "/user/signup", "/user/signin").permitAll()
            .and().httpBasic();
}

filter 만들기

public class JWTTokenGeneratorFilter extends OncePerRequestFilter {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (null != authentication) {
            SecretKey key = Keys.hmacShaKeyFor(SecurityConstants.JWT_KEY.getBytes(StandardCharsets.UTF_8));
            String jwt = Jwts.builder().setIssuer("homes").setSubject("JWT Token")
                    .claim("username", authentication.getName()) // claim 은 payload 부분에 들어갈 데이터다.
                    .claim("authorities", populateAuthorities(authentication.getAuthorities()))
                    .setIssuedAt(new Date())
                    .setExpiration(new Date((new Date()).getTime() + 3000000)) // 30분뒤 expired
                    .signWith(key).compact();
            response.setHeader(SecurityConstants.JWT_HEADER, jwt);
        }

        chain.doFilter(request, response);
    }

//    로그인 api 가 아니면 동작하지 않도록 만듦.
//    회원 가입시에도 토큰이 발급되지 않음.
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        return !request.getServletPath().equals("/user/signin");
    }

    private String populateAuthorities(Collection<? extends GrantedAuthority> collection) {
        Set<String> authoritiesSet = new HashSet<>();
        for (GrantedAuthority authority : collection) {
            authoritiesSet.add(authority.getAuthority());
        }
        return String.join(",", authoritiesSet);
    }
}

public class JWTTokenValidatorFilter extends OncePerRequestFilter {


    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String jwt = request.getHeader(SecurityConstants.JWT_HEADER);
        if (null != jwt) {
            try {
                SecretKey key = Keys.hmacShaKeyFor(
                        SecurityConstants.JWT_KEY.getBytes(StandardCharsets.UTF_8));

                Claims claims = Jwts.parserBuilder()
                        .setSigningKey(key)
                        .build()
                        .parseClaimsJws(jwt)
                        .getBody();
                String username = String.valueOf(claims.get("username"));
                String authorities = (String) claims.get("authorities");
                Authentication auth = new UsernamePasswordAuthenticationToken(username,null,
                        AuthorityUtils.commaSeparatedStringToAuthorityList(authorities));
                SecurityContextHolder.getContext().setAuthentication(auth);
            }catch (Exception e) {
                throw new BadCredentialsException("Invalid Token received!");
            }
        }
        chain.doFilter(request, response);
    }

    // 로그인, 회원가입인 경우에는 필터가 돌아가지 않도록 한다.
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        return request.getServletPath().equals("/user/signin") ||
                request.getServletPath().equals("/user/signup") ||
                request.getServletPath().equals("/user/signup/admin");
    }
}
profile
개발자

0개의 댓글