티스토리 뷰

반응형

현대 웹서비스에서는 토큰을 사용하여 사용자들의 인증 작업을 처리하는 것이 가장 좋은 방법이다. 이번에는 토큰 기반의 인증 시스템에서 주로 사용하는 JWT(Json Web Token)에 대해 SpringBoot와 Spring Security 기반으로 직접 제작해보도록 하겠다.

1. Spring Security 처리 과정


Spring Security 아키텍쳐는 위와 같으며 각각의 처리 과정에 대해서 자세히 알아보도록 하자.(아래에서 설명하는 내용은 Json Web Token을 활용한 Spring Security의 구현 방식으로, Session과 Token 기반의 차이점에 대해서는 여기를 참고하시고, Form을 활용한 Session기반의 구현 방식이 궁금하시다면 여기를 참고해주세요!)

 

 

 

[ 0. 사전 세팅 ]

먼저 프로젝트에서 사용할 Dependency들을 build.gradle에 추가해준다.

dependencies {

    implementation 'io.jsonwebtoken:jjwt:0.9.1'
    
    implementation 'org.mariadb.jdbc:mariadb-java-client'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    
    implementation 'org.springframework.boot:spring-boot-starter-security'
    
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'org.springframework.security:spring-security-test'
}

 

그리고 정적 자원을 제공하는 클래스를 생성하여 아래와 같이 설정한다.

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/static/", "classpath:/public/", "classpath:/",
            "classpath:/resources/", "classpath:/META-INF/resources/", "classpath:/META-INF/resources/webjars/" };

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // /에 해당하는 url mapping을 /common/test로 forward한다.
        registry.addViewController( "/" ).setViewName( "forward:/index" );
        // 우선순위를 가장 높게 잡는다.
        registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);
    }

}

 

그리고 SpringSecurity에 대한 기본적인 설정들을 추가한다. SpringSecurity에 대한 설정 클래스에서는

  1. configure 메소드를 통해 정적 자원들에 대해서는 Security를 적용하지 않음을 추가한다.
  2. configure 메소드를 통해 어떤 요청에 대해서는 로그인을 요구하고, 어떤 요청에 대해서 로그인을 요구하지 않을지 설정한다.
  3. form 기반의 로그인을 비활성화 한다.
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 정적 자원에 대해서는 Security 설정을 적용하지 않음.
    @Override
    public void configure(WebSecurity web) {
        web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
                // 토큰을 활용하는 경우 모든 요청에 대해 접근이 가능하도록 함
                .anyRequest().permitAll()
                .and()
                // 토큰을 활용하면 세션이 필요 없으므로 STATELESS로 설정하여 Session을 사용하지 않는다.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // form 기반의 로그인에 대해 비활성화 한다.
            .formLogin()
                .disable();
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

 

 이번 예제에서는 토큰을 활용하고, 세션을 활용하지 않도록 설정해준다.

 

 

 

[ 1. 로그인 요청 ]

사용자는 로그인 하기 위해 아이디와 비밀번호를 입력해서 로그인 요청을 하게 된다. 이번에 작성하는 예제에서는 로그인 API를 호출하고, Json으로 사용자의 아이디와 비밀번호를 보내는 상황이다.

 

 

[ 2.  UserPasswordAuthenticationToken 발급 ]

전송이 오면 AuthenticationFilter로 요청이 먼저 오게 되고, 아이디와 비밀번호를 기반으로 UserPasswordAuthenticationToken을 발급해주어야 한다. 프론트 단에서 유효성 검사를 하겠지만, 안전을 위해서 다시 한번 아이디와 패스워드의 유효성 검사를 해주는 것이 좋지만 아래의 예제에서는 생략하도록 하겠다.(아이디나 비밀번호의 null 여부 등) 해당 Filter를 직접 구현하면 아래와 같다.

 

@Log4j2
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    public CustomAuthenticationFilter(final AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response) throws AuthenticationException{
        final UsernamePasswordAuthenticationToken authRequest;
        try{
            final User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
            authRequest = new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPw());
        } catch (IOException exception){
            throw new InputNotFoundException();
        }
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

 

위에서 사용되는 사용자 정보를 담는 User 객체는 아래와 같다.

@Entity
@Table(name = "USER")
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User extends Common implements Serializable {

    @Column(nullable = false, unique = true, length = 50)
    private String email;

    @Setter
    @Column(nullable = false)
    private String pw;

    @Setter
    @Column(nullable = false, length = 50)
    @Enumerated(EnumType.STRING)
    private UserRole role;

}

 

 

 

만약 아이디와 비밀번호가 제대로 전달되지 않았을 경우에는 예외 처리를 해주어야 하므로 InputNotFoundException 클래스를 생성하여 처리한다.

public class InputNotFoundException extends RuntimeException {

    public InputNotFoundException(){
        super();
    }

}

 

 

 

 

이렇게 직접 제작한 Filter를 이제 적용시켜야 하므로 UsernamePasswordAuthenticationFilter 필터 이전에 적용시켜야 한다. 그리고 해당 CustomAuthenticationFilter가 수행된 후에 처리될 Handler 역시 Bean으로 등록하고 CustomAuthenticationFilter의 핸들러로 추가해주어야 하는데, 해당 코드들은 WebSecurityConfig에 아래와 같이 추가해줄 수 있다.

@Log4j2
public class CustomLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response,
                                        final Authentication authentication) {
        final User user = ((MyUserDetails) authentication.getPrincipal()).getUser();
        final String token = TokenUtils.generateJwtToken(user);
        response.addHeader(AuthConstants.AUTH_HEADER, AuthConstants.TOKEN_TYPE + " " + token);
    }

}

CustomLoginSuccessHandler는 AuthenticationProvider를 통해 인증이 성공될 경우 처리되는데, TokenUtils에 대해서는 아래에서 작성하도록 하겠다. 또한 인증과 관련해 자주 사용되는 상수는 아래의 AuthConstants 클래스에 정의해두었다.

 

 

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AuthConstants {

    public static final String AUTH_HEADER = "Authorization";
    public static final String TOKEN_TYPE = "BEARER";

}

 

 

 

로그인이 성공하면 TokenUtils를 통해 토큰을 생성하고, response에 이를 추가하여 반환한다.

 

 

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 정적 자원에 대해서는 Security 설정을 적용하지 않음.
    @Override
    public void configure(WebSecurity web) {
        web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
                // 토큰을 활용하는 경우 모든 요청에 대해 접근이 가능하도록 함
                .anyRequest().permitAll()
                .and()
                // 토큰을 활용하면 세션이 필요 없으므로 STATELESS로 설정하여 Session을 사용하지 않는다.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // form 기반의 로그인에 대해 비활성화 한다.
            .formLogin()
                .disable()
            .addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
	}
    
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
        CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManager());
        customAuthenticationFilter.setFilterProcessesUrl("/user/login");
        customAuthenticationFilter.setAuthenticationSuccessHandler(customLoginSuccessHandler());
        customAuthenticationFilter.afterPropertiesSet();
        return customAuthenticationFilter;
    }

    @Bean
    public CustomLoginSuccessHandler customLoginSuccessHandler() {
        return new CustomLoginSuccessHandler();
    }
    
}

 CustomAuthenticationFilter를 빈으로 등록하는 과정에서 UserName파라미터와 UserPassword파라미터를 설정할 수 있다. 이러한 과정을 거치면 UsernamePasswordToken이 발급되게 된다.

 

 

[ 3. UsernamePasswordToken을 Authentication Manager에게 전달 ]

AuthenticationFilter는 생성한 UsernamePasswordToken을 AuthenticationManager에게 전달한다. AuthenticationManager은 실제로 인증을 처리할 여러 개의 AuthenticationProvider를 가지고 있다.

 

[ 4. UsernamePasswordToken을 Authentication Provider에게 전달 ]

AuthenticationManager는 전달받은 UsernamePasswordToken을 순차적으로 AuthenticaionProvider들에게 전달하여 실제 인증의 과정을 수행해야 하며, 실제 인증에 대한 부분은 authenticate 함수에 작성을 해주어야 한다. SpringSecurity에서는 Username으로 DB에서 데이터를 조회한 다음에, 비밀번호의 일치 여부를 검사하는 방식으로 작동을 한다. 그렇기 때문에 먼저 UsernamePasswordToken 토큰으로부터 아이디를 조회해야 하고 그 코드는 아래와 같다.

@RequiredArgsConstructor
@Log4j2
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        final UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
        // AuthenticaionFilter에서 생성된 토큰으로부터 아이디와 비밀번호를 조회함
        final String email = token.getName();
        
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

}

 

 

[ 5. UserDetailsService로 조회할 아이디를 전달 ]

AuthenticationProvider에서 아이디를 조회하였으면, UserDetailsService로부터 아이디를 기반으로 데이터를 조회해야 한다. UserDetailsService는 인터페이스이기 때문에 이를 implements한 클래스를 작성해주어야 한다. 실제 반환값을 작성하는 부분은 7번에서 다룬다.

@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public MyUserDetails loadUserByUsername(String email) {
    
    }
    
}

 

또한 User와 관련된 SQL을 처리하는 JpaRepository를 구현한 UserRepository는 아래와 같다.

@Repository
public interface UserRepository extends JpaRepository <User, Long> {

    User findByEmailAndPw(String email, String pw);

    Optional<User> findByEmail(String email);
}

 

 

 

[ 6. 아이디를 기반으로 DB에서 데이터 조회 ]

전달받은 아이디를 기반으로 DB에서 조회하는 구현체는 우리가 직접 개발한 User일 것이고, UserDetailsService의 반환값은 UserDetails 인터페이스이기 때문에 이를 implements하여 구현한 MyUserDetails를 아래와 같이 작성할 수 있다.

@RequiredArgsConstructor
@Getter
public class MyUserDetails implements UserDetails {

    @Delegate
    private final User user;
    private final Collection<? extends GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

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

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

    @Override
    public boolean isAccountNonExpired() {
        return user.getIsEnable();
    }

    @Override
    public boolean isAccountNonLocked() {
        return user.getIsEnable();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return user.getIsEnable();
    }

    @Override
    public boolean isEnabled() {
        return user.getIsEnable();
    }
}

 

 

[ 7. 아이디를 기반으로 조회한 결과를 반환 ]

아이디를 기반으로 조회한 결과를 반환하기 위해서는 위에서 작성하던 UserDetailsServiceImpl을 마무리해주어야 하는데, 그 전에 사용자의 아이디를 기반으로 데이터가 조회하지 않았을 경우 처리해주기 위한 Exception 클래스를 추가해주어야 한다.

public class UserNotFoundException extends RuntimeException{

    public UserNotFoundException(String email){
        super(email + " NotFoundException");
    }

}

 

그리고 조회한 결과를 CustomAuthenticaionProvider로 반환하는 UserDetailsServceImpl을 마무리해주면 아래와 같다.

@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public MyUserDetails loadUserByUsername(String email) {
        return userRepository.findByEmail(email).map(u -> new MyUserDetails(u, Collections.singleton(new SimpleGrantedAuthority(u.getRole().getValue())))).orElseThrow(() -> new UserNotFoundException(email));
    }
    
}

위의 예제에서는 UserRepository로부터 조회한 결과를 Optional로 반환하고 있기 때문에 map 함수를 이용해서 새로운 UserDetails 객체로 생성하여 반환하고 있다. (만약 Optional에 대해 잘 모르신다면 여기를 참고해주세요!)

 

 

[ 8. 인증 처리 후 인증된 토큰을 AuthenticationManager에게 반환 ]

이제 CustomAuthenticationProvider에서 UserDetailsService를 통해 조회한 정보와 입력받은 비밀번호가 일치하는지 확인하여, 일치한다면 인증된 토큰을 생성하여 반환해주어야 한다. DB에 저장된 사용자 비밀번호는 암호화가 되어있기 때문에, 입력으로부터 들어온 비밀번호를 PasswordEncoder를 통해 암호화하여 DB에서 조회한 사용자의 비밀번호화 매칭되는지 확인해주어야 한다. 만약 비밀번호가 매칭되지 않는 경우에는 BadCredentialsException을 발생시켜 처리해준다.

@RequiredArgsConstructor
@Log4j2
public class CustomAuthenticationProvider implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;
    private final BCryptPasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(final Authentication authentication) throws AuthenticationException {
        final UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
        // AuthenticaionFilter에서 생성된 토큰으로부터 아이디와 비밀번호를 조회함
        final String userEmail = token.getName();
        final String userPw = (String) token.getCredentials();
        // UserDetailsService를 통해 DB에서 아이디로 사용자 조회
        final MyUserDetails userDetails = (MyUserDetails) userDetailsService.loadUserByUsername(userEmail);
        if (!passwordEncoder.matches(userPw, userDetails.getPassword())) {
            throw new BadCredentialsException(userDetails.getUsername() + "Invalid password");
        }

        return new UsernamePasswordAuthenticationToken(userDetails, userPw, userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

}

 

위와 같이 완성된 CustomAuthenticaionProvider를 이제 Bean으로 등록해주어야 하는데, 이것을 WebSecurityConfig에 작성하면 아래와 같다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserDetailsService userDetailsService;
    
    // 정적 자원에 대해서는 Security 설정을 적용하지 않음.
    @Override
    public void configure(WebSecurity web) {
        web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
                // 토큰을 활용하는 경우 모든 요청에 대해 접근이 가능하도록 함
                .anyRequest().permitAll()
                .and()
                // 토큰을 활용하면 세션이 필요 없으므로 STATELESS로 설정하여 Session을 사용하지 않는다.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // form 기반의 로그인에 대해 비활성화 한다.
            .formLogin()
                .disable()
            .addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);    
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
        CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManager());
        customAuthenticationFilter.setFilterProcessesUrl("/user/login");
        customAuthenticationFilter.setAuthenticationSuccessHandler(customLoginSuccessHandler());
        customAuthenticationFilter.afterPropertiesSet();
        return customAuthenticationFilter;
    }

    @Bean
    public CustomLoginSuccessHandler customLoginSuccessHandler() {
        return new CustomLoginSuccessHandler();
    }

    @Bean
    public CustomAuthenticationProvider customAuthenticationProvider() {
        return new CustomAuthenticationProvider(userDetailsService, bCryptPasswordEncoder());
    }

    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) {
        authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider());
    }

}

 

 

[ 9. 인증된 토큰을 AuthenticationFilter에게 전달 ]

AuthenticaitonProvider에서 인증이 완료된 UsernamePasswordAuthenticationToken을 AuthenticationFilter로 반환하고, AuthenticationFilter에서는 LoginSuccessHandler로 전달한다.

 

[ 10. 인증된 토큰을 기반으로 JWT 발급 ]

LoginSuccessHandler로 넘어온 요청은 /user/loginSuccess로 redirect된다. 전달받은 Authentication 정보를 활용해 Json Web Token을 생성해주어야 하는데, 토큰과 관련된 요청을 처리하는 TokenUtils를 아래와 같이 만들어줄 수 있다.

@Log4j2
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class TokenUtils {

    private static final String secretKey = "ThisIsA_SecretKeyForJwtExample";

    public static String generateJwtToken(User user) {
        JwtBuilder builder = Jwts.builder()
                .setSubject(user.getEmail())
                .setHeader(createHeader())
                .setClaims(createClaims(user))
                .setExpiration(createExpireDateForOneYear())
                .signWith(SignatureAlgorithm.HS256, createSigningKey());

        return builder.compact();
    }

    public static boolean isValidToken(String token) {
        try {
            Claims claims = getClaimsFormToken(token);
            log.info("expireTime :" + claims.getExpiration());
            log.info("email :" + claims.get("email"));
            log.info("role :" + claims.get("role"));
            return true;

        } catch (ExpiredJwtException exception) {
            log.error("Token Expired");
            return false;
        } catch (JwtException exception) {
            log.error("Token Tampered");
            return false;
        } catch (NullPointerException exception) {
            log.error("Token is null");
            return false;
        }
    }

    public static String getTokenFromHeader(String header) {
        return header.split(" ")[1];
    }

    private static Date createExpireDateForOneYear() {
        // 토큰 만료시간은 30일으로 설정
        Calendar c = Calendar.getInstance();
        c.add(Calendar.DATE, 30);
        return c.getTime();
    }

    private static Map<String, Object> createHeader() {
        Map<String, Object> header = new HashMap<>();

        header.put("typ", "JWT");
        header.put("alg", "HS256");
        header.put("regDate", System.currentTimeMillis());

        return header;
    }

    private static Map<String, Object> createClaims(User user) {
        // 공개 클레임에 사용자의 이름과 이메일을 설정하여 정보를 조회할 수 있다.
        Map<String, Object> claims = new HashMap<>();

        claims.put("email", user.getEmail());
        claims.put("role", user.getRole());

        return claims;
    }

    private static Key createSigningKey() {
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(secretKey);
        return new SecretKeySpec(apiKeySecretBytes, SignatureAlgorithm.HS256.getJcaName());
    }

    private static Claims getClaimsFormToken(String token) {
        return Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(secretKey))
                .parseClaimsJws(token).getBody();
    }

    private static String getUserEmailFromToken(String token) {
        Claims claims = getClaimsFormToken(token);
        return (String) claims.get("email");
    }

    private static UserRole getRoleFromToken(String token) {
        Claims claims = getClaimsFormToken(token);
        return (UserRole) claims.get("role");
    }
}

 

인증이 성공되고 나면 CustomLoginSuccessHandler에서 Token이 생성되게 되고, 생성된 토큰을 반환하게 된다.

 

 

[ 10. 인증된 토큰을 기반으로 JWT 발급 ]

LoginSuccessHandler로 넘어온 요청은 /user/loginSuccess로 redirect된다. 전달받은 Authentication 정보를 활용해 Json Web T

 

 

2. Spring Security 처리 과정


이제 토큰을 생성해 주는 부분까지는 마무리를 하였고, 토큰을 발급받은 사용자만 원하는 로직을 처리할 수 있도록 해주어야 한다. 아래의 내용에서는 Interceptor를 활용해 유효한 토큰을 가진 사용자만 접근할 수 있도록 접근 제어를 해주고 있다.

 

 

[ 1. 유효한 토큰 검증을 위한 인터셉터 추가 ]

이 클래스는 토큰을 검증하도록 설정한 API에 대해 요청을 intercept하여 토큰의 유효성 검사를 진행한다. 유효성 검사에 실패하면 예외 API로 redirect를 시키고 있다.

@Log4j2
public class JwtTokenInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws IOException {
        final String header = request.getHeader(AuthConstants.AUTH_HEADER);

        if (header != null) {
            final String token = TokenUtils.getTokenFromHeader(header);
            if (TokenUtils.isValidToken(token)) {
                return true;
            }
        }
        response.sendRedirect("/error/unauthorized");
        return false;
    }

}

 

 

[ 2. 예외 처리 컨트롤러 추가 ]

토큰의 유효성 검증에 실패한 경우 아래의 /error/unauthorized API로 redirect된다.

@RestController
@RequestMapping(value = "/error")
public class ErrorController {

    @GetMapping(value = "/unauthorized")
    public ResponseEntity<Void> unauthorized() {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    }

}

 

 

[ 3. 인터셉터 추가 및 패턴 적용 ]

작성한 인터셉터 클래스를 설정에 추가하고, 토큰의 유효성을 검증할 API의 Path 패턴을 적용한다.

이번 예제에서는 전체 사용자를 조회하는 /user/findAll 에 대해 유효한 토큰을 헤더에 포함시켜 요청한 경우만 API를 호출가능하도록 설정하였다.

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    	// 작성한 인터셉터를 추가한다.
        registry.addInterceptor(jwtTokenInterceptor())
        // 예제의 경우 전체 사용자를 조회하는 /user/findAll 에 대해 토큰 검사를 진행한다.
                .addPathPatterns("/user/findAll");
    }

    @Bean
    public FilterRegistrationBean<HeaderFilter> getFilterRegistrationBean() {
        FilterRegistrationBean<HeaderFilter> registrationBean = new FilterRegistrationBean<>(createHeaderFilter());
        registrationBean.setOrder(Integer.MIN_VALUE);
        registrationBean.addUrlPatterns("/*");
        return registrationBean;
    }

    @Bean
    public HeaderFilter createHeaderFilter() {
        return new HeaderFilter();
    }

    @Bean
    public JwtTokenInterceptor jwtTokenInterceptor() {
        return new JwtTokenInterceptor();
    }

}

 

 

[ 4. 사용자 API 추가 ]

모든 사용자가 호출 가능한 회원가입 API와 위에서 적용한 PathPattern으로 유효한 토큰을 전송한 사용자만 호출가능한 전체 사용자 목록 조회 API를 추가하자.

@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/user")
public class UserController {

    private final BCryptPasswordEncoder passwordEncoder;
    private final UserService userService;

    @PostMapping(value = "/signUp")
    public ResponseEntity<String> signUp(@RequestBody final SignUpDTO signUpDTO) {
        return userService.findByEmail(signUpDTO.getEmail()).isPresent()
                ? ResponseEntity.badRequest().build()
                : ResponseEntity.ok(TokenUtils.generateJwtToken(userService.signUp(signUpDTO)));
    }

    @GetMapping(value = "/list")
    public ResponseEntity<UserListResponseDTO> findAll() {
        final UserListResponseDTO userListResponseDTO = UserListResponseDTO.builder()
                .userList(userService.findAll()).build();

        return ResponseEntity.ok(userListResponseDTO);
    }

}

 

위의 로직을 구현하기 위해 다음과 같은 DTO 클래스를 추가하였다.

@Getter
public class SignUpDTO {

    private String email;
    private String pw;

}
@Getter
@Builder
public class UserListResponseDTO {

    private final List<User> userList;

}

 

그리고 UserService에서는 다음과 같은 로직들로 위의 API를 처리하고 있다.

@RequiredArgsConstructor
@Service
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder passwordEncoder;

    @Override
    public User signUp(final SignUpDTO signUpDTO) {
        final User user = User.builder()
                .email(signUpDTO.getEmail())
                .pw(passwordEncoder.encode(signUpDTO.getPw()))
                .role(UserRole.ROLE_USER)
                .build();

        return userRepository.save(user);
    }

    @Override
    public Optional<User> findByEmail(final String email) {
        return userRepository.findByEmail(email);
    }

    @Override
    public List<User> findAll() {
        return userRepository.findAll();
    }
}

 

 

 

3. Spring Security 처리 과정 요약


[ Spring Security 처리 과정 요약 ]

  1. 사용자가 아이디 비밀번호로 로그인을 요청함
  2. AuthenticationFilter에서 UsernamePasswordAuthenticationToken을 생성하여 AuthenticaionManager에게 전달
  3. AuthenticaionManager는 등록된 AuthenticaionProvider(들)을 조회하여 인증을 요구함
  4. AuthenticaionProvider는 UserDetailsService를 통해 입력받은 아이디에 대한 사용자 정보를 DB에서 조회함
  5. 입력받은 비밀번호를 암호화하여 DB의 비밀번호화 매칭되는 경우 인증이 성공된 UsernameAuthenticationToken을 생성하여 AuthenticaionManager로 반환함
  6. AuthenticaionManager는 UsernameAuthenticaionToken을 AuthenticaionFilter로 전달함
  7. AuthenticationFilter는 전달받은 UsernameAuthenticationToken을 LoginSuccessHandler로 전송하고, 토큰을 response의 헤더에 추가하여 반환함

 

 

4. Spring Security 샘플 코드 및 실행


[ Spring Security 예제 실행 방법 ]

  1. https://github.com/MangKyu/SpringSecurity-Example으로부터 소스를 클론받는다.
  2. CREATE DATABASE security DEFAULT CHARSET UTF8; 으로 데이터베이스를 생성한다.
  3. application.properties에서 DB username과 password를 개인에 맞게 변경해준다.
  4. back 폴더로 가서 서버를 실행시킨다.
  5. frontend 폴더로 가서 클라이언트를 실행시킨다.
    npm install
    npm run dev

  6. SignUp을 통해 먼저 회원가입을 한다.
  7. 아직 로그인을 하지 않았기 때문에 Get All Users를 클릭하면 에러가 발생한다.
  8. 회원가입한 계정으로 Sign In을 한다.
  9. 로그인이 성공한 후에는 Get All Users를 클릭하면 사용자 목록을 얻을 수 있다.

 

 

만약 로그인을 했을때 위의 그림과 같이 Authorization: BEAREAR {token} 이 보인다면 토큰이 정상적으로 생성된 것이다.

 

관련 포스팅

  1. 토큰 기반 인증 시스템과 서버 기반 인증 시스템의 차이 (1/3)
  2. JWT(Json Web Token) 토큰이란? (2/3)
  3. SpringBoot로 Spring Security 기반의 JWT 토큰 구현하기(3/3)

 

 

 

참고 자료

반응형
댓글
댓글쓰기 폼
  • 녹턴 위의 글대로 서버실행 후 postman에서 json으로 request하면은
    2020-06-28 22:38:39.403 ERROR 33844 --- [0.1-8081-exec-1] c.m.e.s.c.s.CustomAuthenticationProvider : ##############
    2020-06-28 22:38:39.494 ERROR 33844 --- [0.1-8081-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception

    com.mang.example.security.exception.UserNotFoundException: NotFoundException
    at com.mang.example.security.app.user.service.UserDetailsServiceImpl.lambda$loadUserByUsername$1(UserDetailsServiceImpl.java:21) ~[main/:na]
    at java.base/java.util.Optional.orElseThrow(Optional.java:408) ~[na:na]
    at com.mang.example.security.app.user.service.UserDetailsServiceImpl.loadUserByUsername(UserDetailsServiceImpl.java:21) ~[main/:na]
    at com.mang.example.security.app.user.service.UserDetailsServiceImpl.loadUserByUsername(UserDetailsServiceImpl.java:13) ~[main/:na]
    at com.mang.example.security.config.security.CustomAuthenticationProvider.authenticate(CustomAuthenticationProvider.java:34) ~[main/:na]
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:199) ~[spring-security-core-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at com.mang.example.security.config.security.CustomAuthenticationFilter.attemptAuthentication(CustomAuthenticationFilter.java:24) ~[main/:na]
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:92) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:77) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:358) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:271) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]


    Process finished with exit code -1

    라고 뜨는데 도무지 해결방법을 못차겠네요. 소스는 깃에서 그대로 실행하였습니다.
    몇일째 삽질중인데 도움을 요청합니다.
    2020.06.28 22:47
  • 망나니개발자 지금 UserNotFoundException이 발생하고 있는 것을 보니, DB에 사용자 데이터가 존재하지 않는 것 같습니다!
    http://localhost:8081/user/init 로 GET 요청을 보내서 DB에 데이터를 추가해주시고, DB에 데이터가 잘 들어갔나 확인해보신다음에 다시 한번 요청 보내보세요! 지금 문제는 요청을 보낸 userEmail이 존재하지 않아서 발생하는 상황입니다!
    2020.06.28 23:05 신고
  • 녹턴 user테이블도 db에서 별도로 만들어주는게맞죠?? 2020.06.29 08:38
  • 망나니개발자 CREATE DATABASE security DEFAULT CHARSET UTF8;
    로 DB만 생성해주면 UserVO 클래스에 의해 user라는 DB가 자동으로 생성됩니다
    2020.06.29 11:25 신고
  • 녹턴 테이블조차 생성되기전에 에러가 납니다(security DB에 테이블이 0개임, 데이터x)
    /user/init으로 get요청을 보내면 초기화가 된다고 하셧는데 왜 에러메시지에서 데이터 중복이나는지 우선 모르겠습니다.
    소스를 깃허브에서 그대로 받고 위의 설명대로(user/init 후 user/login) 그대로 진행했지만 잘안되는것 같습니다.
    2020.06.29 23:56
  • 망나니개발자 제가 지금 확인해보니 일부 커밋 내용들이 GitHub에 업뎃이 안된 것 같아서 지금 올렸는데, 다시 master에 있는 것을 받아서 테스트 해보시겠어요!? 지금 다시 해봤는데 잘 되긴 합니다 2020.06.30 00:08 신고
  • 녹턴 우선 다시해보니 토큰을 받아오는것은 잘되는데 /user/init get요청은 아래의 에러가납니다.
    ((conn=359) Duplicate entry 'admin@naver.com' for key 'user.UK_j09k2v8lxofv2vecxu2hde9so')
    그리고 궁금한게 USER라는 테이블이 생성된것이 마리아디비에서 확인이안되며 json body에 user/login post요청시 email에 user@naver.com으로 해도 토큰값이 리턴되는데 왜 그런지 궁금합니다.
    2020.07.05 21:24
  • 망나니개발자 연결된 DB가 접속한 DB랑 동일한지 확인을 해보셔야할 것 같습니다. 요청이 간다는 것은 DB의 테이블과 데이터가 존재한다는 것인데, 안보인다는 것은 모순이 되는 것 같습니다 2020.07.06 12:42 신고
  • 익명 자세한 설명, 정리 감사합니다. 2020.07.21 12:23
  • 망나니개발자 도움이 되었다니 기분이 좋네요ㅎㅎ 감사합니다! 2020.07.21 13:21 신고
  • 웹린이 좋은 정보 감사드립니다~

    그런데 이상하게 계속 아래의 에러가 나네요 ㅜㅜ

    com.mang.example.security.exception.InputNotFoundException: null
    at com.mang.example.security.config.security.CustomAuthenticationFilter.attemptAuthentication(CustomAuthenticationFilter.java:32) ~[main/:na]
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:92) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:77) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]

    db에 admin@naver.com 유저 정보가 저장되는것까지 확인하였습니다.

    그런데 /user/loginView 에서 admin@naver.com // test로 로그인하면 계속 위 에러가 나네요 ㅜㅜ
    2020.07.31 15:32
  • 망나니개발자 지금 보고계신 내용은 JWT 토큰 관련된 예제이기 때문에 /user/loginView를 통해서 Form 방식으로 아이디/비밀번호를 전송하면 안되고, JSON의 형태로 POST 방식으로 아이디/비밀번호를 전송하셔야 합니다! 실행 방법을 수정하였으니, 사진 참고해서 요청 보내보세욥!! 2020.07.31 15:50 신고
  • 튜브 안녕하세요 !

    덕분에 열심히 따라가고 있습니다 근데 UserVO 클래스와 UserRepository 클래스는 올리지 않으신건가요 ㅠㅠ?
    2020.10.19 17:15
  • 망나니개발자 제가 위에 설명에는 안적어두고, 깃에 넣어뒀는데 수정해드렸습니다:)
    아래에 깃에도 코드가 있으니 참고해주세요!!
    https://github.com/MangKyu/SpringSecurity-Example/tree/master/back/src/main/java/com/mang/example/security/app/user
    2020.10.19 18:57 신고
  • 튜브 오우 감사해요!!!
    근데 혹시 데이터베이스 테이블 생성할때 security로 정해야하는 이유가 있나요 ??

    코드에 security database를 명시해둔 곳은 없어서요! User테이블은 알겠습니다만 ㅠㅠ
    2020.10.20 14:03
  • 망나니개발자 SpringBoot에서는 properties에서 설정을 관리할 수 있습니다. 저 같은 경우에는 기본적인 DB의 이름을 properties에 security로 잡아두었기 때문입니다!
    https://github.com/MangKyu/SpringSecurity-Example/blob/master/back/src/main/resources/application.properties
    2020.10.20 14:15 신고
  • woong 안녕하세여
    보다보니 없는게 보여서요
    authconstants 는 뭐죠
    직접 정의한 클래스 아닌가요? 안보여서요
    2020.12.10 11:16
  • 망나니개발자 넵 직접 정의하였는데, 말씀해주셔서 내용에 추가하였습니다! 확인 부탁드려요! 2020.12.10 22:01 신고
  • 초보 안녕하세요. spring boot를 공부하고 있는 초보 개발자 입니다.
    flow에 대해서 이해하려고 하고 있는데,
    로그인 정보를 입력한 뒤 로그인api uri를 호출하게 되면 필터가 가로채서 인증 과정을 하게 되는 것이 맞는지 궁금합니다.
    그리고 정상적인 로그인을 통해 인증이 완료되면 인증된 토큰을 클라이언트쪽으로 반환을 해줘야 하는데, loginsuccesshandler를 통해 restcontroller 없이 반환이 되는 건지 궁금합니다.
    2021.03.09 02:22
  • 망나니개발자 넵! 필터가 가로채서 인증 과정을 하게 되는 것이 맞고, 인증이 완료되면 handler에서 처리하게 됩니다. 2021.03.09 10:02 신고
  • 초보 답변 감사합니다.
    필터가 dispatcher servlet 앞단에서 실행이 되게 되는 것으로 알고있는데, 그럼 필터에서 모든과정을 거친 뒤에 dispathcer servlet쪽으로 가지 않고 필터에서 바로 client쪽으로 반환이 되는 건가요? ㅠㅠ 검색을 해도 잘 나오지 않아 이렇게 계속 질문드리네요 ㅠㅠ
    2021.03.09 11:17
  • 망나니개발자 넵 Spring Context로 가지 않고 Filter에 의해 처리될 겁니다! 2021.03.09 18:41 신고
  • 자바이러스 url(리소스)에 대한 접근권한을 세분화시키고 싶으면 interceptor에서 jwt token값으로부터 Admin, User 여부를 가져와서 검증하는 로직을 추가하면 될까요?? 2021.04.27 22:58 신고
  • 망나니개발자 넵! 그렇게 하실 수 있습니다! 2021.04.27 23:29 신고
  • 익명 안녕하세요. 좋은 코드 공유 감사드립니다.

    logout 할 경우 login 시 만든 토큰을 제거할 수 있나요?
    2021.07.28 16:58
  • 망나니개발자 토큰은 클라이언트에게 넘겨지고, 클라이언트가 요청할 때마다 헤더에 얹어서 보내게 됩니다. 그렇기 때문에 제거는 불가능하고, 대신 블랙리스트 관리 등을 해야할 것 같습니다:) 2021.07.28 21:05 신고
  • 구독자 제가 node를 잘 몰라서 그런데 최신 node.js를 설치해서 하니깐 아래와 같은 에러가 납니다. npm audit fix 해도 안됩니다.

    >npm install
    npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
    npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

    added 1797 packages from 1891 contributors and audited 1873 packages in 111.591s

    11 packages are looking for funding
    run `npm fund` for details

    found 1092 vulnerabilities (635 low, 125 moderate, 328 high, 4 critical)
    run `npm audit fix` to fix them, or `npm audit` for details

    >npm run dev
    > frontend@1.0.0 dev C:\workspace\SpringSecurity-Example\master\frontend
    > webpack-dev-server --inline --progress --config build/webpack.dev.conf.js

    10% building modules 1/1 modules 0 activeevents.js:377
    throw er; // Unhandled 'error' event
    ^

    Error: getaddrinfo ENOTFOUND skoh:8083
    at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:69:26)
    Emitted 'error' event on Server instance at:
    at GetAddrInfoReqWrap.doListen [as callback] (net.js:1502:12)
    at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:69:17) {
    errno: -3008,
    code: 'ENOTFOUND',
    syscall: 'getaddrinfo',
    hostname: 'skoh:8083'
    }
    npm ERR! code ELIFECYCLE
    npm ERR! errno 1
    npm ERR! frontend@1.0.0 dev: `webpack-dev-server --inline --progress --config build/webpack.dev.conf.js`
    npm ERR! Exit status 1
    npm ERR!
    npm ERR! Failed at the frontend@1.0.0 dev script.
    npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

    npm ERR! A complete log of this run can be found in:
    npm ERR! C:\Users\skoh8\AppData\Roaming\npm-cache\_logs\2021-08-19T05_50_52_513Z-debug.log
    2021.08.19 15:00
  • 망나니개발자 혹시 사용중이신 노드 버전이 어떻게 될까요!? 2021.08.19 22:10 신고
  • 구독자 Windows Installer
    node-v14.17.5-x86.msi
    입니다.
    2021.08.20 02:19
  • 망나니개발자 로그를 확인해보니 작성된 소스코드쪽 문제는 아닌 것 같은데, 한번 이 내용 참고해보시겠어요!?
    https://stackoverflow.com/questions/23259697/error-getaddrinfo-enotfound-in-nodejs-for-get-call
    2021.08.20 11:32 신고
  • 구독자 감사합니다!
    윈도우 환경변수에 이상한 값들이 들어가있었네요.
    2021.08.20 19:18
  • 망나니개발자 잘 해결하셨나보네요!! 다행입니닷:) 2021.08.20 20:29 신고
반응형
공지사항
Total
1,479,682
Today
3,003
Yesterday
4,488
TAG more
«   2021/09   »
      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    
글 보관함