UserDetail과 UserDetailsServiceJdbcUserDetailsManager(UserDetailsService의 구현체)설정 방법재정의해주어야 하는 쿼리Group Based Access Control?
UserDetail Provides core user information. Implementations are not used directly by Spring Security for security purposes. They simply store user information which is later encapsulated into Authentication objects. This allows non-security related user information (such as email addresses, telephone numbers etc) to be stored in a convenient location. Concrete implementations must take particular care to ensure the non-null contract detailed for each method is enforced. See User for a reference implementation (which you might like to extend or use in your code).
UserDetailService Core interface which loads user-specific data. It is used throughout the framework as a user DAO and is the strategy used by the DaoAuthenticationProvider. The interface requires only one read-only method, which simplifies support for new data-access strategies.

UserDetail과 UserDetailsService
package com.sp.fc.user.domain; import org.springframework.security.core.userdetails.UserDetails; import java.util.Set; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import javax.persistence.*; @Data @AllArgsConstructor @NoArgsConstructor @Builder @Entity @Table(name="sp_user") public class SpUser implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long userId; @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JoinColumn(name = "user_id", foreignKey = @ForeignKey(name="user_id")) private Set<SpAuthority> authorities; private String email; private String password; private boolean enabled; @Override public String getUsername() { return email; } @Override public boolean isAccountNonExpired() { return enabled; } @Override public boolean isAccountNonLocked() { return enabled; } @Override public boolean isCredentialsNonExpired() { return enabled; } }
package com.sp.fc.user.domain; import org.springframework.security.core.GrantedAuthority; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import javax.persistence.*; @Data @AllArgsConstructor @NoArgsConstructor @Builder @Entity @Table(name="sp_user_authority") @IdClass(SpAuthority.class) public class SpAuthority implements GrantedAuthority { @Id @Column(name="user_id") private Long userId; @Id private String authority; }
package com.sp.fc.user.service; import com.sp.fc.user.domain.SpUser; import com.sp.fc.user.repository.SpUserRepository; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @Service @Transactional public class SpUserService implements UserDetailsService { private final SpUserRepository userRepository; public SpUserService(SpUserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return userRepository.findSpUserByEmail(username).orElseThrow(() -> new UsernameNotFoundException(username)); } public Optional<SpUser> findUser(String email){ return userRepository.findSpUserByEmail(email); } }
package com.sp.fc.user.web.config; import com.sp.fc.user.service.SpUserService; import org.springframework.boot.autoconfigure.security.servlet.PathRequest; import org.springframework.context.annotation.Bean; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @EnableWebSecurity(debug = true) @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { private final SpUserService userService; public SecurityConfig(SpUserService userService) { this.userService = userService; } @Bean PasswordEncoder passwordEncoder(){ return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); } @Bean RoleHierarchy roleHierarchy(){ RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER"); return roleHierarchy; } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests(request-> request.antMatchers("/").permitAll() .anyRequest().authenticated() ) .formLogin(login-> login.loginPage("/login") .loginProcessingUrl("/loginprocess") .permitAll() .defaultSuccessUrl("/", false) .failureUrl("/login-error") ) .logout(logout-> logout.logoutSuccessUrl("/")) .exceptionHandling(error-> error.accessDeniedPage("/access-denied") ) ; } @Override public void configure(WebSecurity web) throws Exception { web.ignoring() .requestMatchers( PathRequest.toStaticResources().atCommonLocations(), PathRequest.toH2Console() ) ; } }
JdbcUserDetailsManager(UserDetailsService의 구현체)

설정 방법
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication().dataSource(dataSource); }
- 이름 그대로 JDBC를 통해 데이터베이스에서 사용자 인증 정보를 가져옴
select username, password, enabled from users where username = ? -- 사용자 조회 select username, authority from authorities where username = ? -- 사용자의 권한 조회
- jdbcAuthentication 메소드는 UserDetailsService 인터페이스 구현체로 JdbcUserDetailsManager 객체를 등록함
- JdbcUserDetailsManager 클래스는 JdbcDaoImpl 클래스를 상속하며, 보다 풍부한 기능을 제공함
inMemoryAuthentication 메소드는 UserDetailsService 인터페이스 구현체로 InMemoryUserDetailsManager 객체를 등록했었다.
재정의해주어야 하는 쿼리

- JdbcDaoImpl 클래스는 수행 목적에 따라 3개의 SQL 쿼리를 정의하고 있는데 이를 위 테이블 구조에 맞게 재정의하여 활용해야 함
- usersByUsernameQuery — 사용자명과 일치하는 하나 이상의 사용자를 조회
- 조회하는 값들은 반드시 username: String, password: String, enabled: Boolean 컬럼 순서이어야함
select username, password, enabled from users where username = ?
- 조회하는 두 번째 값은 반드시 authority: String 컬럼이어야 함
select username, authority from authorities where username = ?
- 조회하는 세 번째 값은 반드시 authority: String 컬럼이어야 함
select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id
- usersByUsernameQuery, groupAuthoritiesByUsernameQuery SQL 쿼리 재정의
- enableGroups — Group-based Access Control 활용시 true 입력
- groupAuthoritiesByUsername 쿼리 정의시 자동으로 true 설정됨
- enableAuthorities — Group-based Access Control 활용시 false 입력
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication() .dataSource(dataSource) .usersByUsernameQuery( "SELECT " + "login_id, passwd, true " + "FROM " + "USERS " + "WHERE " + "login_id = ?" ) .groupAuthoritiesByUsername( "SELECT " + "u.login_id, g.name, p.name " + "FROM " + "users u JOIN groups g ON u.group_id = g.id " + "LEFT JOIN group_permission gp ON g.id = gp.group_id " + "JOIN permissions p ON p.id = gp.permission_id " + "WHERE " + "u.login_id = ?" ) .getUserDetailsService().setEnableAuthorities(false) ; }
Group Based Access Control?
- 대부분의 어플리케이션에서는
Users → Roles
로도 충분함
- Group을 사용하고 싶은 경우는, 롤이 세분화되어 있고 하나의 유저에 대해서 여러 롤을 적용하고 싶을 때 이용하는 것임