스프링 보안 새 인증 서버(0.3.1) - 2부

17680 단어 oauth2javasecurity
이전 부분에서 메모리 사용자 및 클라이언트와 함께 최소 인증 서버를 설정했습니다. 이 부분에서는 구현을 약간 변경하고 사용자를 메모리가 아닌 데이터베이스에 저장하도록 합니다.

엔터티부터 시작하겠습니다.

@Entity
@Getter
@Setter
@NoArgsConstructor
public class AppUser implements UserDetails {

  @Id
  private String id;
  private String username;
  private String password;
  @ManyToMany(fetch = FetchType.EAGER)
  @JoinTable(name = "user_authority",
      joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
      inverseJoinColumns = @JoinColumn(name = "authority_id", referencedColumnName = "id"))
  private Collection<Authority> authorities;
  private Boolean isAccountExpired = false;
  private Boolean isAccountLocked = false;
  private Boolean isCredentialsExpired = false;
  private Boolean isEnabled = true;

  public AppUser(String username, String password,
      Collection<Authority> authorities) {
    this.id = UUID.randomUUID().toString();
    this.username = username;
    this.password = password;
    this.authorities = authorities;
  }

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

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

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

  @Override
  public boolean isAccountNonExpired() {
    return !this.isAccountExpired;
  }

  @Override
  public boolean isAccountNonLocked() {
    return !this.isAccountLocked;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return !this.isCredentialsExpired;
  }

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


Spring 보안에서 ProviderManager와 함께 DaoAuthenticationProvider를 사용할 것이므로 엔터티에서 UserDetails를 구현합니다.

권한에 대한 @ManyToMany 관계도 확인할 수 있습니다.Authority 엔터티를 살펴보겠습니다.

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Authority implements GrantedAuthority {

  @Id
  private String id;
  private String name;

  public Authority(String name) {
    this.id = UUID.randomUUID().toString();
    this.name = name;
  }


  @Override
  public String getAuthority() {
    return this.name;
  }
}


여기에는 많지 않으며 권한에는 이름과 ID만 있습니다. 우리는 GrantedAuthority를 구현해야 했습니다. 왜냐하면 UserDetails 권한이 Collection<? extends GrantedAuthority>이기 때문입니다. Hibernate는 또한 AppUser 엔티티의 @JoinTable 주석을 기반으로 조인 테이블을 생성합니다.

기본 보안 구성에 대한 구성은 이전 부분에서 변경해야 합니다. 이제 어떻게 보이는지 살펴보겠습니다.

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

  private final UserService userService;

  @Bean
  public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
      throws Exception {

    http
        .authorizeRequests()
        .antMatchers("/users/**").permitAll()
        .anyRequest().authenticated()
        .and()
        .csrf().ignoringAntMatchers("/users/**")
        .and()
        .formLogin(Customizer.withDefaults());

    return http.build();
  }

  @Bean
  public AuthenticationManager authenticationManagerBean() throws Exception {
    var provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(userService);
    provider.setPasswordEncoder(NoOpPasswordEncoder.getInstance()); //temporary
    return new ProviderManager(provider);
  }

  @Bean
  public UserDetailsService userDetailsService() {
    return this.userService;
  }

}


먼저 사용자를 생성할 방법이 필요하기 때문에/users/** 패턴에 대한 모든 요청을 허용했습니다. 새 사용자를 생성하기 위한 엔드포인트만 있지만 실제 시나리오에서는 이와 같은 모든 요청을 허용하지 않을 것입니다. 그런 다음 csrf도 비활성화해야 합니다. 왜냐하면 스파에서 이 엔드포인트를 호출하기를 원할 것이기 때문입니다. 로그인 페이지와 같은 스프링 앱의 일부로 등록 페이지를 생성하려면 csrf를 떠나야 합니다.

다음으로 새로운 빈AuthenticationManager을 반환하고 공급자로 ProviderManager만 전달하는 빈DaoAuthenticationProvider을 노출합니다. 그 전에 우리는 이전에 주입한 DaoAuthenticationProvider 의 구현을 사용하도록 UserDetailsService 지시합니다. 구현은 다음과 같습니다.

@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {

  private final AppUserRepository appUserRepository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    return this.appUserRepository.findByUsername(username)
        .orElseThrow(() -> new UsernameNotFoundException("User not found"));
  }

  public UserResponse save(CreateUserRequest request) {
    var user = new AppUser(request.getUsername(), request.getPassword(), List.of());
    this.appUserRepository.save(user);
    return new UserResponse(user);
  }
}


또한 NoOpPasswordEncoder 를 사용하지 않을 수도 있지만 단순성을 위해 여기에서 사용했습니다. 거기에서 다른 것PasswordEncoder을 간단히 사용할 수 있으며 빈으로 노출하고 사용자를 생성할 때 저장하기 전에 암호를 인코딩할 수도 있습니다.

이제 변경 사항을 테스트해 보겠습니다. 이전 부분과 동일하게 다음 링크를 엽니다.
http://localhost:8080/oauth2/authorize?response_type=code&client_id=client&scope=openid&redirect_uri=http://spring.io&code_challenge=YHRCg0i58poWtvPg_xiSHFBqCahjxCqTyUbhYTAk5ME&code_challenge_method=S256

링크를 열기 전에 고유한 code_challenge 및 검증자를 생성하고 교체해야 합니다.

/login 페이지로 리디렉션되지만 지금은 데이터베이스에 사용자가 없기 때문에 아직 로그인할 수 없습니다. 먼저 사용자를 생성해 보겠습니다.



이와 같은 응답에 암호를 반환하면 안 됩니다. 이는 데모용일 뿐입니다.

이제 사용자 이름과 암호를 사용하여 로그인하면 'spring.io?code=...'로 리디렉션되고 코드를 복사하고 토큰 끝점을 호출해야 합니다.



다시 말하지만, 자신의 code_verifier와 이전 리디렉션에서 얻은 코드를 입력했는지 확인하세요.

이제 데이터베이스에 사용자가 있지만 클라이언트는 여전히 메모리에 저장되어 있습니다. 다음 부분에서는 클라이언트를 위해 동일한 작업을 수행합니다.

좋은 웹페이지 즐겨찾기