Spring/Spring 문법

Spring Security 적용

열심히 해 2024. 11. 14. 21:03



로그인 처리 과정

 

 

스프링 시큐리티를 적용하면서 크게 6개의 클래스를 사용했습니다.

 

  • WebSecurityConfig
  • UserDetailsImpl
  • UserDetailsServiceImpl
  • JwtUtil
  • JwtAuthenticationFilter
  • JwtAuthorizationFilter

 

코드를 올리고, 해당 클래스가 어떤 역할을 맡았는지 이야기해보겠습니다. 코드를 읽으며 아래 설명을 차례대로 보시면 이해하기 편하실 겁니다.

 

 

- WebSecurityConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;
    private final AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
        filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
        return filter;
    }

    @Bean
    public JwtAuthorizationFilter jwtAuthorizationFilter() {
        return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf((csrf) -> csrf.disable());

        http.sessionManagement((sm) -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        http.authorizeHttpRequests((request) ->
                request
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                        .requestMatchers("/auth/**").permitAll()
                        .anyRequest().authenticated()
        );

        return http.build();
    }
}

 

 

  • @EnableWebSecurity: Spring Security 지원을 가능하게 합니다.
  • AuthenticationManager 를 바로 Bean 으로 등록할 수 없어서 AuthenticationConfiguration 을 통해 가져와서 등록합니다.
  • JwtAuthenticationFilter 를 Bean 으로 등록합니다. 이 필터에서 인증 처리를 할 것이기에 AuthenticationManager 을 set합니다.
  • JwtAuthorizationFilter 를 Bean 으로 등록합니다. 이 필터는 인가 처리를 합니다. 
  • SecurityFilterChain 을 Bean 으로 등록합니다. 여기서는 CSRF 설정, 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정, 요청의 URL 패턴에 따라 접근 권한 부여 등을 설정합니다.

 

인증 관리자 (AuthenticationManager) : 
  UserDetailsService 에게 username 을 전달하고 회원 상세 정보를 요청

SecurityFilterChain 을 Bean 으로 등록하는 이유:
  필터 체인을 빈으로 등록해야 스프링 컨테이너가 이를 관리하면서 HTTP 요청이 들어올 때마다 등록된 필터 체인을 사용해 요청을 처리하게 됩니다.

 

 

 

- UserDetailsImpl

@RequiredArgsConstructor
public class UserDetailsImpl implements UserDetails {
    private final User user;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        UserRole role = user.getUserRole();
        String authority = role.toString();

        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(simpleGrantedAuthority);

        return authorities;
    }

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

    // 사용자는 email 을 id로 하여 로그인 합니다.
    @Override
    public String getUsername() {
        return user.getEmail();
    }

    public User getUser() {
        return user;
    }
}

 

  • 회원 정보를 담기 위한 객체를 생성합니다.

 

 

- UserDetailsServiceImpl

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

    // 사용자는 email 을 id로 하여 로그인 합니다.
    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email).orElseThrow(() ->
                new UsernameNotFoundException(email + "을 찾지 못했습니다."));
        return new UserDetailsImpl(user);
    }
}

 

  • 회원 DB 에서 회원 조회합니다.
  • 조회된 회원 정보(user) 를 UserDetails 로 변환합니다.

 

 

 

- JwtUtil: Jwt 방식을 채택했기 때문에 필요한 클래스

@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {

    private static final String BEARER_PREFIX = "Bearer ";
    private static final long TOKEN_TIME = 60 * 60 * 1000L; // 60분

    @Value("${jwt.secret.key}")
    private String secretKey;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    public String createToken(Long userId, String email, String nickname, UserRole userRole) {
        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(String.valueOf(userId))
                        .claim("email", email)
                        .claim("nickname", nickname)
                        .claim("userRole", userRole)
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                        .setIssuedAt(date) // 발급일
                        .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                        .compact();
    }

    public String substringToken(String tokenValue) {
        if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
            return tokenValue.substring(7);
        }
        throw new ServerException("Not Found Token");
    }

    public Claims extractClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.error("Invalid JWT signature, error :{}", e.getMessage());
        } catch (ExpiredJwtException e) {
            log.error("Expired JWT token, error :{}", e.getMessage());
        } catch (UnsupportedJwtException e) {
            log.error("Unsupported JWT token, error :{}", e.getMessage());
        } catch (IllegalArgumentException e) {
            log.error("JWT claims is empty, error :{}", e.getMessage());
        }
        return false;
    }
}

 

  • Jwt 와 관련된 기능을 담어놓은 클래스입니다.
  • 토큰 생성, 토큰 앞 접두사 탈착, 토큰으로부터 사용자 정보 획득, 토큰 검증 등의 기능이 있습니다.

 

 

- JwtAuthenticationFilter

@Slf4j(topic = "인증 및 Jwt 발급")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final JwtUtil jwtUtil;

    public JwtAuthenticationFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
        setFilterProcessesUrl("/auth/signin");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            SigninRequest signinRequest = new ObjectMapper().readValue(request.getInputStream(), SigninRequest.class);
            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(signinRequest.getEmail(), signinRequest.getPassword(), null)
            );
        } catch (IOException e) {
            log.error("로그인 에러:{}", e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        log.info("로그인 성공, Jwt 발급");
        Long userId = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getId();
        String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
        String nickname = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getNickname();
        UserRole role =  ((UserDetailsImpl) authResult.getPrincipal()).getUser().getUserRole();

        String jwt = jwtUtil.createToken(userId, username, nickname, role);
        response.addHeader("Authorization", jwt);
    }
}

 

  • 인증 필터입니다.
  • 로그인(인증) 요청 URL 을 설정합니다.
  •  AuthenticationManager 에 의해 인증이 진행됩니다. 로그인 - 인증 처리는 사용자가 입력한 아이디와 비밀번호가 UsernamePasswordAuthenticationToken 에 담깁니다. 이것과 위에서 UserDetails 에 담은 아이디와 비밀번호가 매칭되며 이루어 집니다.
  • 인증에 성공하면 회원 정보를 담은 Jwt를 발급합니다.

 

-  JwtAuthorizationFilter

@Slf4j(topic = "인가")
@RequiredArgsConstructor
public class JwtAuthorizationFilter extends OncePerRequestFilter {
    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("Authorization");

        if (StringUtils.hasText(token)) {
            token = jwtUtil.substringToken(token);
            log.info(token);

            if (!jwtUtil.validateToken(token)) {
                log.error("Validation Failed");
                return;
            }

            Claims claims = jwtUtil.extractClaims(token);

            setAuthentication(claims.getSubject());
        }
        filterChain.doFilter(request, response);
    }

    private void setAuthentication(String userId) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = createAuthentication(userId);
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
    }

    private Authentication createAuthentication(String userId) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(userId);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

 

 

  • 인가 필터입니다.
  • HTTP 요청이 인가 필터를 탄다는 것은, 해당 요청이 Jwt를 가진 -로그인이 완료된- 사용자가 보낸 요청이라 할 수 있습니다.
  • jwtUtil.extractClaims(token): 토큰에 담긴 사용자 정보를 읽습니다.
  • setAuthentication(claims.getSubject()): 인증이 완료된 사용자의 상세 정보(Authentication)를 저장합니다.

 

SecurityContext:
  인증이 완료된 사용자의 상세 정보(Authentication)를 저장합니다.SecurityContext는 SecurityContextHolder 로 접근할 수 있습니다.

인증 객체 Authentication:
  현재 인증된 사용자를 나타내며 SecurityContext에서 가져올 수 있습니다. principal, credentials, authorities 로 구성되며, 일반적으로 principal 에 UserDetails 가 담기게 됩니다.

 

 

'Spring > Spring 문법' 카테고리의 다른 글

Transaction Propagation  (0) 2024.11.25
QueryDsl - Projections 의 4가지 방식  (0) 2024.11.21
QueryDSL이란 무엇일까??  (0) 2024.11.14
@RequestParam 에서 매개 변수에 null 넣기  (0) 2024.11.14
Java Spring properties  (1) 2024.10.26