티스토리 뷰

728x90

1. 이 포스트는 Spirng Security를 사용하지만, Security filters 로그인을 맡기는 것이 아닌 직접 코딩하는 내용이다.

  1-1 보통 WebSecurity Config 클래스의 AuthenticationMangerBuilder를 통하여 접근방법을 등록한다.

    1-1-1 UserDetailsService를 구현하여 이 객체를 AuthenticationProvider에 제공하고

    1-1-2 이것을 AuthenticationManager가 사용한다.

 

2. 작업 순서는

  2-0 의존성 추가

  2-1 데이터베이스 생성 및 연결

  2-2 User, Role Entity 생성 및 연결

  2-3 UserRepository, RoleRepository 생성

 

  2-4 서비스 코드 생성

    2-4-1 UserDetails 구현하는 UserDetailsImpl

    2-4-2 로그인 로직을 가지는 SecurityService

    2-4-3 컨트롤러가 사용하는 UserService

 

  2-5 SecurityConfig 작성하기 

  2-6 UserController 생성

 

3. 의존성 추가

  3-1 전체 소스를 다 넣을 건 아니기 때문에 의미가 없지만 데이터베이스, 보안 부분는 곡 필요하다.

 

<!-- 데이터베이스 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>    

<!-- 보안 -->
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
      <scope>compile</scope>
    </dependency>
    
<!-- web -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-jasper</artifactId>
      <version>9.0.34</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>

 

4. 데이터베이스 생성

  4-1 가장 간단한 구조이다.

  4-2 user와 role이 서로 many to many의 구조를 갖는다.

 

 

 

5. Entity 설정

  5-0 AbstractEntity 클래스 - @MappedSuperclass로 내부적으로 자식 클래스가 단순히 복사해서 Entity를 구성한다.

  5-1 @MappedSuperclass에 대한 내용은 아래 포스트를 참고한다.

 

 

Hibernate Advanced : 상속 매핑 - Mapped Superclass Strategy

0. 이 포스트는 상속 매핑의 Mapped Superclass 정책에 대해서 작성한다. 1. Mapped Superclass 정책 1-1 자식 클래스만 @Entity로 수식되고 테이블로 전환된다. 1-2 자식 클래스는 상속받은 field 까지 테이블에.

kogle.tistory.com

 

  5-2 내용은 단순히 id 설정을 모든 entity에서 하기 싫어서 그냥 추출한 것 뿐이다.  

 

package pe.pilseong.flightreservation.entity;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

import lombok.Data;

@MappedSuperclass
@Data
public class AbstractEntity {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
}

 

  5-1 User Entity

    5-1-1 ManyToMany 매핑만 신경쓰면 된다.

 

package pe.pilseong.flightreservation.entity;

import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;

import lombok.Data;
import lombok.EqualsAndHashCode;

@Entity
@Table(name = "user")
@Data
@EqualsAndHashCode(callSuper=false)
public class User extends AbstractEntity{
  
  @Column(name = "first_name")
  private String firstName;

  @Column(name = "last_name")
  private String lastName;

  @Column(name = "email")
  private String email;

  @Column(name = "password")
  private String password;

  @ManyToMany
  @JoinTable(
    name = "user_role",
    joinColumns = @JoinColumn(name = "user_id"),
    inverseJoinColumns = @JoinColumn(name = "role_id")
  )
  private Set<Role> roles;
}

 

  5-2 Role Entity

    5-2-1 이 Entity는 GrantedAuthority 인터페이스를 를 상속받고 있다. getAuthority 메소드만 가지고 있다.

    5-2-2 GrantedAuthority는 보안 모듈이 내부적으로 getAuthrity를 통해 Role정보를 가지고 온다.

    5-2-3 이렇게 하지 않아도 되는데 나중에 UserDetails를 구현하는 User를 만들 때 지저분한 코드를 써야 한다.

    5-2-4 GrantedAuthority를 구현한 경우는

 

    return new org.springframework.security.core.userdetails.User(
      user.getEmail(), user.getPassword(), user.getRoles()
    );

 

    5-2-5 구현하지 않은 경우는 아래처럼 작업하면 된다.

 

    return new org.springframework.security.core.userdetails.User(
      user.getEmail(), user.getPassword(),
      user.getRoles().stream().map(role -> new SimpleGrantedAuthority(role.getName()))
        .collect(Collectors.toList());
    );

 

    5-2-6 Role Entity 코드

 

package pe.pilseong.flightreservation.entity;

import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;

import org.springframework.security.core.GrantedAuthority;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.JoinColumn;

@Entity
@Table(name = "role")
@Setter
@Getter
public class Role extends AbstractEntity implements GrantedAuthority {

  private static final long serialVersionUID = 1L;

  @Column(name = "name")
  private String name;

  @ManyToMany
  @JoinTable(
    name = "user_role",
    joinColumns = @JoinColumn(name = "role_id"),
    inverseJoinColumns = @JoinColumn(name = "user_id")
  )
  private Set<User> users;

  @Override
  public String getAuthority() {
    return this.name;
  }
}

 

6. Repository 생성

  6-1 data-jpa를 사용하고 있기 때문에 JpaRepository를 상속한다.

  6-2 UserRepository

    6-2-1 아래 소스에서는 username으로 email을 사용하고 있기 때문에 별도의 메소드를 만들어 주었다.

 

package pe.pilseong.flightreservation.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import pe.pilseong.flightreservation.entity.User;

public interface UserRepository extends JpaRepository<User, Long> {

  @Query("from User where email=:email")
	User findUserByEmail(@Param("email") String email);
}

 

  6-3 RoleRepository

 

package pe.pilseong.flightreservation.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import pe.pilseong.flightreservation.entity.Role;

public interface RoleRepository extends JpaRepository<Role, Long> {
  
}

 

7. Service 생성

  7-0 UserDetailsServiceImpl - UserDetailsService를 구현한 클래스

    7-0-1 UserService에 포함할 수도 있지만 보기 편하게 별도로 추출하였다.

    7-0-2 위에서 설명한 것 처럼 User의 세번 째 인자가 Collection<GrantedAuthority>이므로 코드가 간단하다.

 

package pe.pilseong.flightreservation.service;

import org.springframework.beans.factory.annotation.Autowired;
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 pe.pilseong.flightreservation.entity.User;
import pe.pilseong.flightreservation.repository.UserRepository;

@Service
public class UserDetailServiceImpl implements UserDetailsService {

  @Autowired
  private UserRepository userRepository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = this.userRepository.findUserByEmail(username);
    if (user == null) {
      throw new UsernameNotFoundException("User not found :: " + username);
    }
    return new org.springframework.security.core.userdetails.User(
      user.getEmail(), user.getPassword(), user.getRoles()
    );
  }
}

 

  7-1 로그인 로직을 담는 SecurityService 인터페이스 - login 정보를 제공하고 결과만 알려주면 된다.

    7-1-1 이 인터페이스를 사용하면 간단하게 로그인 처리가 가능하다.

 

package pe.pilseong.flightreservation.service;

public interface SecurityService {
  boolean login(String username, String password);
}

 

  7-2 SecurityServiceImple 코드

    7-2-1 여기에 핵심로직이 다 들어 있다.

    7-2-2 위에서 작성한 UserDetailsService를 주입받아서 사용하고 있다. 데이터베이스 정보를 가지고 온다.

    7-2-3 로그인 처리는 인증관리자에서 하기 때문에 주입받아야 한다. 

      7-2-3-1 스프링 2.x 에서는 인증관리자가 기본적으로 외부로 노출되지 않는다. 별도의 Bean 생성이 필요하다.

      7-2-3-2 이 작업은 다음 항에서 설명한다.

    7-2-4 주어진 계정, 비밀번호와 데이터베이스의 정보와 일치하는지 인증관리자를 통해 처리한다.

    7-2-5 인증 정보가 일치하는 경우 보안 컨텍스트에 인증 정보를 저장하게 된다. 인증이 필요시마다 체크한다.

    7-2-6 이 인증정보는 세션과 일치하므로 원할 때 마다 접근하여 인증 정보를 받아올 수 있다.    

 

package pe.pilseong.flightreservation.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

@Service
public class SecurityServiceImpl implements SecurityService {

  @Autowired
  private AuthenticationManager authenticationManager;

  @Autowired
  private UserDetailsService userDetailsServce;

  @Override
  public boolean login(String username, String password) {

    UserDetails userDetails = this.userDetailsServce.loadUserByUsername(username);

    UsernamePasswordAuthenticationToken token = 
      new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());

    authenticationManager.authenticate(token);

    boolean result = token.isAuthenticated();

    if (result) {
      SecurityContextHolder.getContext().setAuthentication(token);
    }

    return result;
  }
}

 

  7-2 UserService 인터페이스

    7-2-1 사용자 저장과 로그인 하는 메소드가 있다.

 

package pe.pilseong.flightreservation.service;


import pe.pilseong.flightreservation.dto.UserDTO;

public interface UserService {

  void saveUser(UserDTO userDTO);

  boolean login(UserDTO userDTO);
}

 

  7-3 UserServiceImpl 클래스

    7-3-1 컨트롤러에서 이 서비스를 호출하여 인증을 처리하게 된다. login을 보면 SecurityService의 login을 사용한다.

    7-3-2 회원가입 시는 데이터베이스 저장 전에 암호화하는 것이 필요하다. 여기서는 BCrypt를 사용하였다.

 

package pe.pilseong.flightreservation.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import pe.pilseong.flightreservation.dto.UserDTO;
import pe.pilseong.flightreservation.entity.User;
import pe.pilseong.flightreservation.repository.UserRepository;

@Service
public class UserServiceImpl implements UserService {

  @Autowired
  private UserRepository userRepository;

  @Autowired
  private SecurityService securityService;

  @Autowired
  private BCryptPasswordEncoder passwordEncoder;

  @Override
  public void saveUser(UserDTO userDTO) {
    User user = new User();

    user.setFirstName(userDTO.getFirstName());
    user.setLastName(userDTO.getLastName());
    user.setEmail(userDTO.getEmail());
    user.setPassword(this.passwordEncoder.encode(userDTO.getPassword()));

    this.userRepository.save(user);
  }

  @Override
  public boolean login(UserDTO userDTO) {
    return this.securityService.login(userDTO.getEmail(), userDTO.getPassword());
  }
}

 

    7-3-3 UserDTO는 다음과 같다.

 

package pe.pilseong.flightreservation.dto;

import lombok.Data;

@Data
public class UserDTO {

  private Long id;
  
  private String firstName;

  private String lastName;

  private String email;

  private String password;

  private String confirmPassword;
}

 

8. 보안 설정을 작성한다.

  8-1 WebConfig

    8-1-1 가장 중요한 부분은 인증관리자를 생성하는 부분이다. 수동으로 해야 한다.

    8-1-2 WebSecurityConfigurerAdapter를 Override하는 메소드는 이름이 중요하다. authenticationManagerBean이다.

    8-1-3 암호화 클래스 생성도 별도로 해주어야 한다.

    8-1-4 인증없이 페이지로 접근하는 경우 인증화면으로 전환이 필요하다. 그것을 위해서 formLogin을 사용하였다.

 

package pe.pilseong.flightreservation.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
        .antMatchers("/showRegistration", "/", "/index.html", "/registerUser", 
          "/login", "/showLogin", "/login/*", "/reservations/*")
        .permitAll()
        .antMatchers("/admin/showAddFlight").hasAuthority("ADMIN")
        .anyRequest().authenticated()
      .and()
        .formLogin().loginPage("/showLogin").permitAll()
      .and()
        .csrf().disable();
  }

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

  @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }
}

 

9. 컨트롤러 작성

  9-1 UserController이다. 

    9-1-1 어떻게 사용하는지만 보면 된다. 

    9-1-2 UserService를 주입 받아서 login 메소드만 호출하면 된다.

 

package pe.pilseong.flightreservation.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;

import pe.pilseong.flightreservation.dto.FlightSearchDTO;
import pe.pilseong.flightreservation.dto.UserDTO;
import pe.pilseong.flightreservation.service.UserService;

import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class UserController {

  @Autowired
  private UserService userService;

  private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);


  @GetMapping("/showLogin")
  public String showLogin(Model model) {
    model.addAttribute("user", new UserDTO());

    return "/login/login";
  }

  @PostMapping("/login") 
  public String login(@ModelAttribute UserDTO userDTO, Model model) {
    LOGGER.info("Inside login " + userDTO.toString());
    if (this.userService.login(userDTO)) {
      model.addAttribute("flightSearch", new FlightSearchDTO());
      return "findFlights";
    } else {
      return "redirect:showLogin";
    }
  }

  @GetMapping("/showRegistration")
  public String showRegistrationPage(Model model) {
    LOGGER.info("Inside showRegistrationPage");

    model.addAttribute("user", new UserDTO());

    return "/login/registerUser";
  }

  @PostMapping(value = "/registerUser")
  public String registerUser(@ModelAttribute UserDTO userDTO, Model model) {
    LOGGER.info("Inside registerUser " + userDTO.toString() );

    if (userDTO.getPassword().equals(userDTO.getConfirmPassword())) {
      this.userService.saveUser(userDTO);
      return "redirect:showLogin";
    } else {
      userDTO.setPassword("");
      userDTO.setConfirmPassword("");
      model.addAttribute("user", userDTO);
      return "/login/registerUser";
    }
  }
}
728x90
댓글