유저 서비스에 스프링 시큐리티 넣기 - nhnacademy-be10-WannaB/wannab-wiki GitHub Wiki

왜 스프링 시큐리티를 넣어야하는가?

  • 지금 방식도 틀린건 아님
    • 그런데 어차피 OAuth 붙여야하잖아
    • 그러면 그냥 로그인은 이렇게 디비 찔러서 전통적인 방식으로 하고 OAuth는 따로 처리하게?
      • 근데 사실 그래도 되긴해
      • 그치만 하나로 통합하면 깔끔하지 않을까?

1. RedisConfig

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory, ObjectMapper objectMapper) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper));
        return template;
    }
}
  • ObjectMapper 인자를 추가
  • 타입추론을 막기 위함

2. PasswordEncoderConfig

@Configuration
public class PasswordEncoderConfig {

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

}
  • PasswordEncoderConfig 추가
  • 비밀번호 암호화

3. SecurityConfig

@Configuration
public class SecurityConfig{

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authManager, ObjectMapper mapper,
                                           RedisTemplate<String, Object> redisTemplate) throws Exception {
        JwtLoginFilter jwtLoginFilter = new JwtLoginFilter(authManager, mapper, redisTemplate);

        http
                .csrf(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterAt(jwtLoginFilter, UsernamePasswordAuthenticationFilter.class)
        ;

        http.authorizeHttpRequests(auth -> auth
                .anyRequest().permitAll()
        );

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}
  • 우리는 json으로 로그인함
    • form login 비활성화
    • http basic 비활성화
    • session 사용하지 않으므로 비활성화

4. UserRepsitory

public interface UserRepository extends JpaRepository<User, Long> {
   Boolean existsByUsername(String username);

   User findByUsername(String username);
}
  • User findByUsername(String username);
    • 이거 왜 Optional 없음?
    • User 못찾으면 어떡할건데?

5. UserEntity

    @Builder.Default
    @Column(name = "user_role")
    @Enumerated(EnumType.STRING)
    private Role role = Role.USER;
    
    @Builder.Default
    @Column(name = "user_state")
    @Enumerated(EnumType.STRING)
    private State state = State.ACTIVATE;
  • 각 컬럼 줄 한칸씩 띄움
  • Role 이넘 타입 저장할때 전략지정해주어야함

6. CustomUserDetails

public class CustomUserDetails implements UserDetails {

    @Getter
    private final Long id;
    private final String username;
    private final String password;
    private final List<GrantedAuthority> authorities;
    private final State state;

    public CustomUserDetails(User user) {
        this.id = user.getUserId();
        this.username = user.getUsername();
        this.password = user.getPassword();
        this.state = user.getState();
        this.authorities = List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole()));
    }

    @Override
    public boolean isEnabled() {
        return this.state == State.ACTIVATE;
    }

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

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }
}
    @Override
    public boolean isEnabled() {
        return this.state == State.ACTIVATE;
    }
  • User의 상태가 active일때만 유저 로그인 허용

7. CustomUserDetailsService

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다"));

        return new CustomUserDetails(user);
    }
}

8. JwtLoginFilter

public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;
    private final ObjectMapper mapper;
    private final RedisTemplate<String, Object> redisTemplate;

    public JwtLoginFilter(AuthenticationManager authenticationManager, ObjectMapper mapper,
                          RedisTemplate<String, Object> redisTemplate) {
        this.authenticationManager = authenticationManager;
        this.mapper = mapper;
        this.redisTemplate = redisTemplate;
        setFilterProcessesUrl("/api/auth/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {

        LoginRequest loginRequest = parseRequest(request);

        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                loginRequest.username(),
                loginRequest.password()
        );

        return this.authenticationManager.authenticate(token);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        CustomUserDetails principal = (CustomUserDetails) authResult.getPrincipal();
        String role = principal.getAuthorities()
                .stream()
                .findFirst()
                .map(GrantedAuthority::getAuthority)
                .orElseThrow(() -> new RuntimeException("권한 없음"));

        String accessToken = JwtUtil.createAccessToken(principal.getId(), role);
        String refreshToken = JwtUtil.createRefreshToken(principal.getId(), role);

        redisTemplate.opsForHash().put(REFRESH_KEY, String.valueOf(principal.getId()), refreshToken);

        // Header 로 바꿀까?
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        LoginResponse loginResponse = new LoginResponse(accessToken, refreshToken);

        mapper.writeValue(response.getWriter(), loginResponse);
    }

    private LoginRequest parseRequest(HttpServletRequest request){
        try {
            return mapper.readValue(request.getInputStream(), LoginRequest.class);
        } catch (IOException e) {
            throw new RuntimeException("로그인 요청이 올바르지 않습니다", e);
        }
    }
}
  • 일단은 응답 dto로 주는걸로 하긴했는데
  • 헤더로 응답해도 ㄱㅊ을거같은데
    • 딱히 뭐 장단점은 없음

Ref

[Spring Security using filter and not Controller](https://stackoverflow.com/questions/63929468/spring-security-using-filter-and-not-controller)

[Spring Security Configuration with Flow Diagrams](https://www.infoq.com/articles/spring-security-flow-diagrams/)

회원가입

image

폼 기반 로그인

image

토큰 리프레쉬

image
⚠️ **GitHub.com Fallback** ⚠️