Json Web Token

2024. 1. 2. 20:19Spring Boot

우선 JWT는

토큰을 관리하는 provider class를 만들어 관리한다

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import com.example.demo.principal.PrincipalDetailsService;

import jakarta.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.Date;
import java.util.stream.Collectors;

import java.util.Map;
import java.util.HashMap;

@RequiredArgsConstructor
@Component
@PropertySource("classpath:jwt.properties")
public class jwtProvider {

    @Value("${secret}")
    private String key;
    
    private static final String AUTHORITIES_KEY = "auth";

    @Value("${seconds}")
    private String seconds;

    private Key secretKey;

    // 만료시간 : 1Hour
    private long exp;

    @Autowired
    private PrincipalDetailsService principalDetailsService;

    @PostConstruct
    protected void init() {
        byte[] keyBytes = Decoders.BASE64.decode(key);
        this.secretKey = Keys.hmacShaKeyFor(keyBytes);
        this.exp = Long.parseLong(seconds);
    }

    // 토큰 생성
    public String createToken(String name) {
        Map<String, Object> payloads = new HashMap<String, Object>();
        Map<String, Object> headers = new HashMap<>();

        headers.put("alg", "HS256");
        headers.put("typ", "JWT");

        payloads.put("name", name);
        // 토큰의 expire 시간을 설정
        long now = (new Date()).getTime();
        Date validity = new Date(now + this.exp);

        return Jwts.builder()
                .setHeader(headers)
                .setClaims(payloads)
                .signWith(secretKey, SignatureAlgorithm.HS256) // 사용할 암호화 알고리즘과 , signature 에 들어갈 secret값 세팅
                .setExpiration(validity) // set Expire Time 해당 옵션 안넣으면 expire안함
                .compact();
    }

    // 권한정보 획득
    // Spring Security 인증과정에서 권한확인을 위한 기능
    public Authentication getAuthentication(String token) {
        System.out.printf("get authentication: %s\n",this.getAccount(token));
        UserDetails userDetails = principalDetailsService.loadUserByUsername(this.getAccount(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    // 토큰에 담겨있는 유저 account 획득
    public String getAccount(String token) {
        // 만료된 토큰에 대해 parseClaimsJws를 수행하면 io.jsonwebtoken.ExpiredJwtException이 발생한다.
        try {
            System.out.println(Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody().get("name"));
        } catch (ExpiredJwtException e) {
            e.printStackTrace();
            return e.getClaims().getSubject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return (String)Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody().get("name");
    }


    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            // Bearer 검증
            if (!token.substring(0, "BEARER ".length()).equalsIgnoreCase("BEARER ")) {
                return false;
            } else {
                token = token.split(" ")[1].trim();
            }
            Jws<Claims> claims = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
            // 만료되었을 시 false
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }
}

 

 

Authentication은

특정 주소에 올바른 data를 form으로 넘겨주면 cookie에 JWT를 넘겨주도록 짰다

    @RequestMapping(path = "/login/jwt", method = RequestMethod.POST)
    public String login_jwt(HttpServletRequest req, HttpServletResponse res) {
        String token = jwtProvider.createToken(req.getParameter("name"));
        Cookie cookie = new Cookie("jwt", token);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(60*60*24*7);
        res.addCookie(cookie);
        return "redirect:/";
    }

 

 

 

Authorization은

filter를 활용했다

import java.io.IOException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import com.example.demo.jwt.jwtProvider;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class jwtAuthorizationFilter extends BasicAuthenticationFilter{

    private jwtProvider jwtProvider;
    public jwtAuthorizationFilter(AuthenticationManager authenticationManager, jwtProvider jwtProvider) {
        super(authenticationManager);
        this.jwtProvider = jwtProvider;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
        System.out.println("인증 필요 요청 됨");
        req.setAttribute("name", null);
        Cookie[] cookies = req.getCookies();
        String token = null;
        for(Cookie cookie: cookies) {
            if(cookie.getName().equals("jwt"))
                token = cookie.getValue();
        }
        System.out.printf("authorization jwt: %s\n",token);
        if(token == null || jwtProvider.validateToken(token)) {
            chain.doFilter(req, res);
            return;
        }
        req.setAttribute("name", jwtProvider.getAccount(token));
        chain.doFilter(req, res);
    }
}

 

보통 로그인 된 정보를 임시로 security session에 저장하고 session을 stateless로 관리하지만

session 로그인도 동시에 구현하기 위해서

일부로 servlet request에 따로 attribute를 만들어 저장하였다

 

 

 

로그인 과정에서 security filter인 BasicAuthenticationFilter를 상속받아 이용했음으로 아래와 같이 설정하여 주었다

package com.example.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.example.demo.jwt.jwtProvider;
import com.example.demo.user.user_repository;

import lombok.RequiredArgsConstructor;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    @Autowired
    private CorsConfig corsConfig;

    private final jwtProvider tokenProvider;

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
          
      http.httpBasic((http_basic)-> http_basic.disable());

      http.apply(new jwtDsl());

        http.sessionManagement((session) ->
            session
                .sessionCreationPolicy(SessionCreationPolicy.ALWAYS));

        return http.build();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    public class jwtDsl extends AbstractHttpConfigurer<jwtDsl, HttpSecurity> {
		@Override
		public void configure(HttpSecurity http) throws Exception {
			AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
			http
            .addFilter(new jwtAuthorizationFilter(authenticationManager, tokenProvider));
		}
	}


}

 

 

'Spring Boot' 카테고리의 다른 글

Https  (0) 2024.01.12
MySQL  (0) 2024.01.12
CustomFilter  (0) 2024.01.02
SecurityFilterChain  (0) 2024.01.02
MongoDB  (0) 2023.12.28