[Spring Security] DaoAuthenticationManager와 UserDetailsService

WOOK JONG KIM·2022년 12월 1일
0

패캠_java&Spring

목록 보기
78/103
post-thumbnail

이제까지 User 정보는 모두 InMemory 상태에서 처리를 하였음

하지만, 실제 개발 상황에서 이렇게 쓰지 않음
-> 대부분 Mysql이나 Oracle 과 같은 RDBMS를 쓰거나, MongoDB 와 같은 기타 데이터베이스를 사용해 사용자를 관리

스프링 시큐리티를 써서 서비스를 만들라고 하면, 대부분의 개발자들은 UserDetails 를 구현한 User 객체UserDetailsService 부터 구현
-> 왜냐하면, UserDetailsService와 UserDetails 구현체만 구현하면 스프링 시큐리티가 나머지는 쉽게 쓸 수 있도록 도움을 준다

User Details Service가 빈으로 등록되어있으면 DaoAuthenticationProvider가 Bean에 UsernamePasswordAuthenticationToken을 넘겨서 User Details라는 Principal 객체로 받게 되어있다

Principal 객체를 가지고 Token에 의해 인증이 된 사용자라면 넣어서 return 하도록 구조화

web : 웹 컨트롤러나 리소스를 여러 서버에서 공통으로 사용하는 경우 web 컴포넌트로 관리

comp : 컴포넌트가 되는 모듈을 넣는다(여기에 User와 Authority 정의)


Comp-User-Admin

build.gralde에서 jpa 의존성 추가

SpAuthority.java

@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;

}

SpUser.java(UserDetails 구현)

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name="sp_user_table")
public class SpUser implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;

    // 유저와 라이프 사이클이 같기에 ALL로 지정
    @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;
    }

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

Repository

public interface SpUserRepository extends JpaRepository<SpUser, Long> {

    Optional<SpUser> findSpUserByEmail(String email);
}

UserService

@Service
@Transactional
public class SpUserService implements UserDetailsService {

    private final SpUserRepository spUserRepository;

    public SpUserService(SpUserRepository spUserRepository) {
        this.spUserRepository = spUserRepository;
    }


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return spUserRepository.findSpUserByEmail(username)
                .orElseThrow(() -> new UsernameNotFoundException(username));
    }

    public Optional<SpUser> findUser(String email){
        return spUserRepository.findSpUserByEmail(email);
    }

    public SpUser save(SpUser user){
        return spUserRepository.save(user);
    }

    public void addAuthority(Long userId, String authority){
        spUserRepository.findById(userId).ifPresent(user -> {
            SpAuthority newRole = new SpAuthority(user.getUserId(), authority);
            if(user.getAuthorities() == null){
                HashSet<SpAuthority> authorities = new HashSet<>();
                authorities.add(newRole);
                user.setAuthorities(authorities);
                save(user);
            } else if(!user.getAuthorities().contains(newRole)){
                HashSet<SpAuthority> authorities = new HashSet<>();
                authorities.addAll(user.getAuthorities());
                authorities.add(newRole);
                user.setAuthorities(authorities);
                save(user);
            }
        });
    }

    public void removeAuthority(Long userId, String authority){
        spUserRepository.findById(userId).ifPresent(user -> {
            if(user.getAuthorities()==null) return;
            SpAuthority targetRole = new SpAuthority(user.getUserId(), authority);
            if(user.getAuthorities().contains(targetRole)){
                user.setAuthorities(
                        user.getAuthorities().stream().filter(auth -> !auth.equals(targetRole))
                                .collect(Collectors.toSet())
                );
                save(user);
            }
        });
    }
}

서버 실행 시 실행되는 쿼리

Hibernate: 
    
    create table sp_user_authority (
       user_id bigint not null,
        authority varchar(255) not null,
        primary key (user_id, authority)
    )
Hibernate: 
    
    create table sp_user_table (
       user_id bigint generated by default as identity,
        email varchar(255),
        enabled boolean not null,
        password varchar(255),
        primary key (user_id)
    )
Hibernate: 
    
    alter table sp_user_authority 
       add constraint user_id 
       foreign key (user_id) 
       references sp_user_table

Server-login-userDetails

Security Config

@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final SpUserService userService;

    public SecurityConfig(SpUserService userService) {
        this.userService = userService;
    }
	
    // 기존에 User Details Service를 구현해놓은 객체 인자로 사용
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    // DB에서 사용자를 편의상 넣었다 뺐다 하기 위해(테스트 시에만 사용하자)
    @Bean
    PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    @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(),
                        // path: /h2-console 옵션을 사용하기 위해 Path를 열어야함
                        PathRequest.toH2Console()
                )
        ;
    }

}

application.yml

server:
  port: 9055

spring:
  devtools:
    livereload:
      enabled: true
    restart:
      enabled: true

  #   thymleaf을 모듈에서 발견하기 위해
  thymeleaf:
    prefix: classpath:/templates/
    cache : false
    check-template-location: true

  h2:
    console:
      enabled: true
      path: /h2-console

  datasource:
    url: jdbc:h2:mem:userdetails-test;
    driverClassName: org.h2.Driver
    username: sa
    password:

build.gradle

apply from: "/Applications/dev/works/fastcampus/lec9/sp-fastcampus-spring-sec/server/web-common.gradle"

dependencies {

    implementation("$boot:spring-boot-starter-data-jpa")

    runtime("com.h2database:h2")

    compile project(":comp-user-admin")
    compile project(":web-user-admin")
}

comp에 구현해 놓은 User 객체를 서버에서 사용할 수 있도록 하였음

UserDetailsTestApplication.java

@SpringBootApplication(scanBasePackages = {
        "com.sp.fc.user",
        "com.sp.fc.web"
})
@EntityScan(basePackages = {
        "com.sp.fc.user.domain"
})
@EnableJpaRepositories(basePackages = {
        "com.sp.fc.user.repository"
})
public class UserDetailsTestApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserDetailsTestApplication.class, args);
    }

}

UserService 의존성 주입을 위한 스캔 범위를 Main Application에서 지정


Web-User-Admin

기본적인 컨트롤러 구현

@Controller
public class HomeController {

    @GetMapping("/")
    public String main(){
        return "index";
    }

    @GetMapping("/login")
    public String login(){
        return "loginForm";
    }

    @GetMapping("/login-error")
    public String loginError(Model model){
        model.addAttribute("loginError", true);
        return "loginForm";
    }

    @GetMapping("/access-denied")
    public String accessDenied(){
        return "AccessDenied";
    }

    @PreAuthorize("hasAnyAuthority('ROLE_USER')")
    @GetMapping("/user-page")
    public String userPage(){
        return "UserPage";
    }

    @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
    @GetMapping("/admin-page")
    public String adminPage(){
        return "AdminPage";
    }


    @ResponseBody
    @GetMapping("/auth")
    public Authentication auth(){
        return SecurityContextHolder.getContext().getAuthentication();
    }

}

H2 Console 예시

insert 쿼리문을 통해 사용자 계정을 생성하고, 권한을 지정해주는 것이 가능

위처럼 권한 부여시 관리자페이지, 유저 페이지 둘다 접속 가능

profile
Journey for Backend Developer

0개의 댓글