[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개의 댓글

관련 채용 정보