본문 바로가기

backend/Spring

02. 스프링 시큐리티(spring-security)02 - DB와 연동(유저, 유저권한)

이전 글에서 말씀드린바와 같이 살표본 내용을 직접 구현해보려합니다. 스프링은 추상화된 클래스, 인터페이스를 제공하고 사용자가 이를 직접 구현하게 만듭니다. 이러한 특징은 security에서도 마찬가지입니다.

Refference
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#what-is-acegi-security https://docs.spring.io/spring-security/site/docs/current/apidocs/org/springframework/security/core/userdetails/UserDetails.html

개발환경

  • spring-boot
  • gradle
  • java8



1. 우선 security를 사용하기 위해서 gradle에 다음과 같이 spring-boot-starter-security를 추가합니다.

build.gradle


2. UserDetailsService 구현.

추상화 해놓은 UserDetailsService를 상속받아 loadUserByUsername()메소드를 구현해줍니다. loadUserByUsername의 역할은 메소드명의 의미 그대로 유저의 id를 통해서 유저에 대한 인증 정보를 가져오는 것 입니다. loadUserByUsername 내부에서는 아래 로직이 포함되어 있습니다.

1.유저 ID를 통해서 해당 유저를 찾는 로직.
2.유저의 권한들을 가져오는 로직.

JPA를 사용한다면 연관관계를 맺어주면 더 좋을 것 같습니다.

public class LoginService implements UserDetailsService{
    @Autowired
    UserRepository userRepository;
 
    @Autowired
    UserRoleRepository userRoleRepository;
 
    @Autowired
    LoginRepository loginRepository;
 
    @Autowired
    PasswordEncoder passwordEncoder;
 
    @Override
    public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {
        User user = loginRepository.findById(id);
        List<UserRole> userRoles = userRoleRepository.findAllById(id);
        if(user !=null && !userRoles.isEmpty()){
            return new CustomUserDetails(user, userRoles);
        }else{
            System.out.println("사용자한테 response를 보내야 한다.");
            return null;
        }
    }




3. UserDetails 구현

위에서 loadUserByUsername()가 CustomUserDetails를 return 하고 있는 걸 확보셨을 겁니다.
CustomUserDetails은 UserDetails:스프링시큐리티가 제공하는 interface입니다.
Cutsom객체(유저정보와 인증정보를 같이 담을 객체)에 UserDetails를 상속받아 @override를 구현하면 됩니다.

@override 정보는 다음과 같습니다.

getAuthorities()
사용자에게 부여 된 권한을 리턴합니다.
String getPassword()
사용자를 인증하는 데 사용되는 암호를 반환합니다.
String getUsername()
사용자를 인증하는 데 사용되는 사용자 이름을 반환합니다.
boolean isAccountNonExpired()
사용자 계정이 만료되었는지 여부를 나타냅니다.
boolean isAccountNonLocked()
사용자가 잠겨 있는지 여부를 나타냅니다.
boolean isCredentialsNonExpired()
사용자의 자격 증명 (암호)이 만료되었는지 여부를 나타냅니다.
boolean isEnabled()
사용자의 사용 가능 여부를 나타냅니다.

이렇게 만들어진 CustomUserDetails은 spring-security에서 유저인증에 사용 됩니다.

public class CustomUserDetails extends User implements UserDetails {
    private static final long serialVersionUID = 1L;
    List<UserRole> hasRoles;
 
    public CustomUserDetails(User user, List<UserRole> hasRoles) {
        super(user);
        this.hasRoles = hasRoles;
    }
 
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.hasRoles;
    }
 
    @Override
    public String getPassword() {
        return super.getPassword();
    }
 
    @Override
    public String getUsername() {
        return super.getId();
    }
 
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
 
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
 
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
 
    @Override
    public boolean isEnabled() {
        return true;
    }
}




4. GrantedAuthority를 구현하는 UserRole, User Domain

UserRole을 관리하는 DB테이블을 따로 가지기 위해서 해당하는 class를 작성하였고, 스프링 security가 제공하는 GrantedAuthority interface를 아래와 같이 구현하였습니다.

getAuthority()에서 userRole을 String으로 return 해줍니다.

User Domain에 대한 특별한 구현 내용은 없이 아래와 같이 클래스를 작성하였습니다.

public class UserRole implements GrantedAuthority, Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long idx;
    private String id;
 
    @Column(name = "user_role_enum", columnDefinition = "enum('USER','ADMIN','OWNER','HEAD_COACH')")
    @Enumerated(EnumType.STRING)
    private UserRoleEnum userRoleEnum = UserRoleEnum.USER;
 
    public UserRole(String id, UserRoleEnum userRoleEnum) {
        this.id = id;
        this.userRoleEnum = userRoleEnum;
    }
 
    @Override
    public String getAuthority() {
        return this.getUserRoleEnum().toString();
    }
}
 
public class User implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long idx;
    private String id;
    private String username;
    private String password;
    @Column(name = "team_name")
    private String teamName;
    @Transient
    private Team team;
    @Column(name = "user_status", columnDefinition = "enum('ACTIVE','INACTIVE')")
    @Enumerated(EnumType.STRING)
    private UserStatus userStatus = UserStatus.ACTIVE;
 
    public User(User user) {
        this.id = user.getId();
        this.username = user.getUsername();
        this.password = user.getPassword();
    }
}




5. @EnableWebSecurity를 추가하고, WebSecurityConfigurerAdapter를 상속

@EnableWebSecurity과 WebSecurityConfigurerAdapter를 사용하면 아래와 같은 기능을 사용할 수 있습니다.

  • 우리의 어플리케이션에 어떤 URL 로 들어오던간에 사용자에게 인증을 요구할수있음.
  • 사용자이름과 비밀번호 및 롤기반으로 사용자를 생성할수있다.
  • HTTP Basic 과 폼기반 인증을 가능하게함.
  • 스프링 시큐리티는 자동적으로 로그인페이지,인증실패 url 등을 제공한다.

출처: http://hamait.tistory.com/313 [HAMA 블로그]

WebSecurityConfigurerAdapter를 상속받으면 다양한 Override를 구현 할 수 있도록 제공합니다.
우리는 configure(HttpSecurity http), configure(AuthenticationManagerBuilder auth)를 구현할겁니다.

configure(HttpSecurity http) antMatchers를 통해서 url에 대해 access제한을 둘 수 있습니다. 또한, Login, Logout에 대한 url과 설정 처리도 아래와 같이 간단하게 처리 할 수 있습니다.

configure(AuthenticationManagerBuilder auth) 위에서 만든 LoginService(userDetailsService를 상속받아 구현했던 Service)를 넣고, 암호화 설정까지 넣어서 구현해 주면, 로그인 요청이 들어올때, spring security에서 LoginService를 사용하게 됩니다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Autowired
    private LoginService loginService;
 
    @Autowired
    PasswordEncoder passwordEncoder;
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();//사용안함 CSRF 토큰없이 개발 // HTTP Status 403 - Expected CSRF token not found. Has your session expired 
        http.headers().frameOptions().sameOrigin().disable();//iframe 에러 //samorigin X-Frame-Options: DENY 에러 발생 
        http.sessionManagement();
        http.addFilterAfter(new MyFilter(), LogoutFilter.class);
        http.authorizeRequests()
                .antMatchers(new String[]{"/css/**", "/js/**", "/img/**", "/html/**"}).permitAll()
                .antMatchers("/admin/**").access("hasAuthority('ADMIN')")
                .antMatchers("/**").permitAll()
                .and()
                .formLogin().loginPage("/forLogin")
                .loginProcessingUrl("/login")
                .failureUrl("/loginFail")
                .usernameParameter("id")
                .passwordParameter("password")
                .defaultSuccessUrl("/", true)
                .and()
                .logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/");
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.eraseCredentials(false).userDetailsService(loginService).passwordEncoder(passwordEncoder);
    }
 
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();//스프링에서 권장하는 hash 알고리즘 
    }




위에서 설명한 내용 이외에도 spring-security에서 제공하는 다양한 옵션과, 설정내용은 맨위에 남긴 spring-security document에 잘 정리되어 있으니 참고하시면 쉽게 적용하실 수 있을거 같습니다.

읽어주셔서 감사합니다^^