유저 서비스에 스프링 시큐리티 넣기 - nhnacademy-be10-WannaB/wannab-wiki GitHub Wiki
- 지금 방식도 틀린건 아님
- 그런데 어차피 OAuth 붙여야하잖아
- 그러면 그냥 로그인은 이렇게 디비 찔러서 전통적인 방식으로 하고 OAuth는 따로 처리하게?
- 근데 사실 그래도 되긴해
- 그치만 하나로 통합하면 깔끔하지 않을까?
@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 인자를 추가
- 타입추론을 막기 위함
@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
- PasswordEncoderConfig 추가
- 비밀번호 암호화
@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 사용하지 않으므로 비활성화
public interface UserRepository extends JpaRepository<User, Long> {
Boolean existsByUsername(String username);
User findByUsername(String username);
}
- User findByUsername(String username);
- 이거 왜 Optional 없음?
- User 못찾으면 어떡할건데?
@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 이넘 타입 저장할때 전략지정해주어야함
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일때만 유저 로그인 허용
@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);
}
}
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로 주는걸로 하긴했는데
- 헤더로 응답해도 ㄱㅊ을거같은데
- 딱히 뭐 장단점은 없음
[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/)


