먼저 jwt에 대해 설명하겠다.

jwt는 Header.Payload.Signature 구조로 이루어져 있다.

Header - JWT임을 명시 & 사용된 암호화 알고리즘

Payload(Claim) - 정보

Signature - 서명으로 토큰의 무결성을 검증하는데 사용된다. 구조는 (BASE64(Header)+BASE64(Payload) + 암호화 키)와 같다.

jwt는 내부 정보를 단순 BASE64 방식으로 인코딩하기 때문에 외부에서 쉽게 디코딩 할 수 있다. 따라서 외부에서 열람해도 되는 정보를 담아야한다.

jwt 암호화 종류

-양방향 : 대칭키 / 비대칭키

-단방향

 

로직

 

SecurityConfig.java

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final AuthenticationConfiguration authenticationConfiguration;
    private final JWTUtil jwtUtil;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
        //세션 사용 안하므로 csrf 꺼주면 된다.
                .csrf(AbstractHttpConfigurer::disable);
        //formLogin을 사용 안 하므로 UsernamePasswordAuthenticationFilter를 커스텀화해야 한다.
        //formLogin은 세션을 활용한 방식이므로 jwt 사용할 때는 필요없음
        http
                .formLogin(AbstractHttpConfigurer::disable);
        //jwt 인증 사용하므로 http basic 인증 방식 사용 X basic 인증 방식이란 헤더에 username과 password을 암호화하여 인증하는 것을 말한다.
        http
                .httpBasic(AbstractHttpConfigurer::disable);
        http
                .authorizeHttpRequests((auth)->auth
                        .requestMatchers("/login", "/", "/join").permitAll()
                        .requestMatchers("/admin").hasRole("ADMIN")
                        .anyRequest().authenticated());
        http       // UsernamePasswordAuthenticationFilter자리에 LoginFilter 넣음 / authenticationManager()함수로 인자 전달
                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration),jwtUtil), UsernamePasswordAuthenticationFilter.class);
        http
                .addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);
        //jwt는 세션을 stateless로 관리. stateless란 서버가 클라이언트의 상태를 보존하지 않는 것으로 서버 무한 확장이 가능해진다.
        http
                .sessionManagement((session)-> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

 

MainController.java

@Controller
@ResponseBody
public class MainController {

    @GetMapping("/")
    public String mainP(){

        String name = SecurityContextHolder.getContext().getAuthentication().getName();

        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
        Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
        GrantedAuthority grantedAuthority = iterator.next();
        String authority = grantedAuthority.getAuthority();

        return "Main Controller" + name + authority;
    }
}

 

CustomUserDetails.java

public class CustomUserDetails implements UserDetails {

    private final UserEntity userEntity;

    public CustomUserDetails(UserEntity userEntity) {
        this.userEntity = userEntity;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        //SimpleGrantedAuthority에 들어갈 ROLE의 형태는 ROLE_이라는 prefix가 있어야 한다.
        authorities.add(new SimpleGrantedAuthority(userEntity.getRole()));
        return authorities;
    }

    @Override
    public String getPassword() {
        return userEntity.getPassword();
    }

    @Override
    public String getUsername() {
        return userEntity.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

 

JWTFilter.java

public class JWTFilter extends OncePerRequestFilter {

    private final JWTUtil jwtUtil;

    public JWTFilter(JWTUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authorization = request.getHeader("Authorization");
        if (authorization == null || !authorization.startsWith("Bearer ")) {
            System.out.println("token null");
            // 다음 필터로 넘김
            filterChain.doFilter(request, response);
            return; // 리턴 필수
        }

        String token = authorization.substring("Bearer ".length());
        if(jwtUtil.isExpired(token)){
            System.out.println("token expired");
            filterChain.doFilter(request, response);
            return; // 리턴 필수
        }
        String username = jwtUtil.getUserName(token);
        String role = jwtUtil.getRole(token);

        UserEntity userEntity = new UserEntity();
        userEntity.setUsername(username);
        userEntity.setRole(role);
        //비번은 안 필요함

        CustomUserDetails customUserDetails = new CustomUserDetails(userEntity);

        //이미 로그인 되어있는 상태이므로 LoginFilter 안 거침 따라서 attemptAuthentication() 대신하여 Authentication 생성
        //Authentication 객체를 통해 권한 확인 / 이는 세션의 한 종류이지만 응답이 완료되면 바로 삭제됨
        Authentication authentication = new UsernamePasswordAuthenticationToken(customUserDetails,null, customUserDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request, response);
    }
}

 

JWTUtil.java

@Component
public class JWTUtil {
    private SecretKey secretKey;

    public JWTUtil(@Value("${spring.jwt.secret}") String secret){
        //properties에 있는 jwt secret 키를 HS256알고리즘으로 암호화
        this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
    }

    public String getUserName(String token){
        //verifyWith()함수로 token이 이 서버에서 만들었는지 확인
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username",String.class);
    }

    public String getRole(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
    }

    public Boolean isExpired(String token) {

                                                                                //현재 시간 기준으로 before이면 true 반환
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
    }

    public String createToken(String username, String role, Long expiredMs){
                //jwt token 만들어서 반환
        return Jwts.builder()
                .claim("username", username) // Payload에 값 저장하는 함수
                .claim("role",role)
                .issuedAt(new Date(System.currentTimeMillis())) // 토큰이 만들어진 시간
                .expiration(new Date(System.currentTimeMillis()+expiredMs)) // 토큰이 만료되는 시간
                .signWith(secretKey) // secretKey 이용해 signature 생성
                .compact();
    }
}

 

LoginFilter.java

public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    private final JWTUtil jwtUtil;
    private final AuthenticationManager authenticationManager;
    //security config에서 인자로 넘어오므로 @RequiredArgsConstructor 사용 X
    public LoginFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil) {
        this.authenticationManager = authenticationManager;
        this.jwtUtil = jwtUtil;
    }

    @Override
    //공부자료 05에 있는 Authentication 생성하는 함수
    //AbstractAuthenticationProcessingFilter에서 검증을 하기 위한 메소드 구현
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //클라이언트 요청에서 username, password 추출해주는 함수
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        System.out.println(username);

        //AuthenticationManager에서 검증을 하려면 token에 담아야 함
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password, null);
        //token을 AuthenticationManager에게 전달
        //반환값엔 token에 대한 정보와 UserDetail을 구현한 클래스의 정보가 들어있다.
        return authenticationManager.authenticate(authRequest);
    }

    //로그인 실패시 실행되는 메서드
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.setStatus(401);
    }

    //로그인 성공시 실행되는 메서드 여기서 jwt 발급하면 됨
    @Override                                                                                                           //UsernamePasswordAuthenticationFilter가 반환한 객체
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
                                                            //UserDetail 반환하는 함수
        CustomUserDetails customUserDetails = (CustomUserDetails) authResult.getPrincipal();
        String username = authResult.getName();
                                                        //UserDetail을 구현한 클래스의 함수 호출
        Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();
        Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
        GrantedAuthority grantedAuthority = iterator.next();

        String role = grantedAuthority.getAuthority();
        String token = jwtUtil.createToken(username, role, 60*60*10L);
        response.addHeader("Authorization", "Bearer " + token);
    }
}

 

CustomUserDetailsService.java

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userData = userRepository.findByUsername(username);
        if(userData != null) {
            return new CustomUserDetails(userData);
        }
        return null;
    }
}

 

 

+ Recent posts