본문 바로가기
개발 이론/Spring Security

[Spring Security] UserDetails, UserDetailsService, PasswordEncoder, AuthenticationProvider로 간단 예제

by dal_been 2023. 11. 8.
728x90

간단하게 앞에 배웠던 내용을 정리해보자

 

 

AuthenticationFilter는 요청을 가로채서 인증 책임을 AuthennticationManager에 위임하여

AuthenticationManager는 AuthenticationProvider를 이용해 요청을 인증한다

여기서 AuthenticationProvider는 UserDetailService와 PasswordEncdoer를 호출해 암호를 검증하는 인증논리를 정의한다

 

이후 반환된 성공한 인증 호출의 세부정보는 AuthenticationFilter에 의해 SecurityContext에 저장된다

 

 


 

간단한 예제에서는 bcryt 및 scrypt로 해시된 암호를 검사하고 formLogin 인증방법으로 구성할 것이다

 

@Entity
public class User extends UserDetails{
	@Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    private String password;
    
    @Enumerated(EnumType.STRING)
    private EncryptionAlgorithm algorithm;
    
    @OneToMany(mappedBy="user",fetch=FetchType.EAGER)
    private List<Authority> roles;
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return user.getAuthorities().stream()
                   .map(a -> new SimpleGrantedAuthority(a.getName()))
                   .collect(Collectors.toList());
    }

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

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

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

 

public Enum EncryptionAlgorithm{
	BCRYPT,SCRYPT
}

 

@Entity
public class Authority {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    @JoinColumn(name = "user")
    @ManyToOne
    private User user;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }
}

 

 

 

 

지금까지 UserDetails와 Authority 관련 테이블 생성을 완료하였다. 이제 Service 를 만들어보자

Repository는 JpaRepository를 구현한 인터페이스가 있다고 가정하자

 

 

@Service
public class JpaUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public User loadUserByUsername(String username) {
        Supplier<UsernameNotFoundException> s =
                () -> new UsernameNotFoundException("Problem during authentication!");

        User u = userRepository.findUserByUsername(username).orElseThrow(s);

        return u;
    }
}

 

원래는 도메인을 컨트롤러단에 던지는 것보다는 dto로 만들어서 보내주는게 좋지만 간단한 예제이기때문에 dto생성은 생략한다

 

 

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationProviderService authenticationProvider;

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

    @Bean
    public SCryptPasswordEncoder sCryptPasswordEncoder() {
        return new SCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(authenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
            .defaultSuccessUrl("/main", true);
        http.authorizeRequests().anyRequest().authenticated();
    }
}

 

@Service
public class AuthenticationProviderService implements AuthenticationProvider {

    @Autowired
    private JpaUserDetailsService userDetailsService;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    private SCryptPasswordEncoder sCryptPasswordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        CustomUserDetails user = userDetailsService.loadUserByUsername(username);

        switch (user.getUser().getAlgorithm()) {
            case BCRYPT:
                return checkPassword(user, password, bCryptPasswordEncoder);
            case SCRYPT:
                return checkPassword(user, password, sCryptPasswordEncoder);
        }

        throw new  BadCredentialsException("Bad credentials");
    }

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

    private Authentication checkPassword(CustomUserDetails user, String rawPassword, PasswordEncoder encoder) {
        if (encoder.matches(rawPassword, user.getPassword())) {
            return new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities());
        } else {
            throw new BadCredentialsException("Bad credentials");
        }
    }
}

 

AuthenticationProvider를 구현함으로써 인증 논리를 작성하였다. 인증 논리를 위해서는 종속성으로 UserDetailsService구현과 두개의 암호 인코더가 필요하고 이외에도 authenticat()와 support() 메서드를 재정의해야한다.

 

 

 

 

그래서 결과적으로 실행해보면 첫번재는 시큐리티의 로그인 화면이 나올것이다. 그래서 로그인하면 아마 에러 페이지가 나올것이다. 그 이이유는 어떠한 url도 설정한 것이 없고, 뷰단도 설정한 것이 없기 때문이다.

일단 로그인화면과 로그인후 에러 페이지가 나오고 "/logout"하면 로그아웃 페이지가 나온다면 성공한 것이다.