티스토리 뷰

[ 프로젝트 ]

사람이 인지하지 못한 화재에 대응하는 서비스입니다.

원리:화재 감지 센서와 AI 기술을 이용하여, 화재 발생시 자동으로 주변 소방서에 알림을 전송합니다.


<기대효과>

  • 빠른 대응으로 더 큰 화재 피해 방지
  • 사람이 인지하지 못한 화재 대응 가능
  • 빈 집 화재 대응 가능

 

 

[ Spring ]

Spring MVC framework 구조도

[ Controller ]

 

1. MemberController

@Slf4j
@Controller
public class MemberController {
    @Autowired
    private FirestationService firestationService;

    @GetMapping({"", "/"})
    public String index () {
        return "index.html";
    }

    @GetMapping("/user/register")
    public String userRegister() {
        return "index.html";
    }

    @GetMapping("/admin/register")
    public String adminRegister() {
        return "admin.html";
    }

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

    @GetMapping("/admin/memberList")
    public String getList(Model model, HttpSession session) {
        model.addAttribute("memberList", firestationService.userList());

        // log.info(session.getId());
        return "memberList";
    }
}

 

[Method]

  • index : http://localhost:7777(공백이나 /) 입력시 index 화면을 띄우도록 한다.
  • userRegister : 사용자로 하여금 회원가입할 수 있도록 회원가입 화면을 제공한다.
  • adminRegister :  관리자로 하여금 회원가입 할 수 있도록 회원가입 화면을 제공한다.
  • getList : 관리자로 하여금 관리하는 영역의 사용자 정보를 Model 에 넣어 View 영역에 제공한다.

 

2. MemberApiController

@RestController
public class MemberApiController {

    @Autowired
    private MemberService memberService;

    @Autowired
    private FirestationService firestationService;


    @PostMapping("/auth/joinProc")
    public ResponseDto<Integer> userJoin(@RequestBody Member member) {
        memberService.memberJoin(member);

        return new ResponseDto<Integer>(HttpStatus.OK.value(), 1);
    }

    @PostMapping("/auth/FirejoinProc")
    public ResponseDto<Integer> firestationJoin(@RequestBody Firestation firestation) {
        firestationService.firestationJoin(firestation);
        return new ResponseDto<Integer>(HttpStatus.OK.value(), 1);
    }
}

 

[Method]

  • userJoin : 프론트영역에서 입력받은 값을 PostMapping 을 통해 Json 데이터로 받아오고 RequestBody를 통해Member 객체에 저장한다.
  • firestationJoin : 위 내용과 동일하다.

 

 

[ Model ]

 

사용자, 관리자로 총 2가지의 Model 이 존재한다

 

1. Member

@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy =  GenerationType.IDENTITY)
    private int id;

    @Column(nullable = false,length = 30)
    private String apartname;

    @Column(nullable = false, length = 30)
    private String building; // 동

    @Column(nullable = false, length = 30)
    private String unit; // 호

    @Column(nullable = false, length = 15)
    private String phonenumber;

    @Column(nullable = false, length = 30)
    private String nearestStation;

    @Enumerated(EnumType.STRING)
    private RoleType roleType;

    @ManyToOne
    @JoinColumn(name = "firestationname")
    private Firestation firestation;
}

 

사용자로부터 받을 정보는 아파트이름, 동, 호수, 전화번호, 가장 가까운 소방서, 롤 타입이 존재한다.

 

 2. Firestation

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
@Entity
public class Firestation {

    @Id
    @GeneratedValue(strategy =  GenerationType.IDENTITY)
    private int id;

    @Column(nullable = false, length = 30, unique = true)
    private String firestationname;

    @Column(nullable = false, length = 100)
    private String firestationPw;

    @Enumerated(EnumType.STRING)
    private RoleType roleType;

    @OneToMany(mappedBy = "firestation")
    private List<Member> members = new ArrayList<>();

}

 

소방서에서 사용할 정보는 소방서 이름과 소방서 비밀번호, 롤 타입, 유저정보를 볼 수 있는 리스트 타입을 선언하였다.

 

3. RoleType

public enum RoleType {
    ADMIN, USER
}

 

Enum 클래스를 만들어 롤 타입의 변수명을 확정시켰다.

( Enum 타입을 사용하는 이유는 혹시나 개발자가 ADMIN을 ADMINN 으로 입력하는 경우 등 실수 하는 경우를 막기위함이라고.. 들었다..)

 

4. ResponseDto

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ResponseDto<T> {
    int status;
    T data;
}

 

응답에 대하여 ResponseDto 클래스를 만들어 200 응답이나 404 응답을 받을 수 있도록 구현하였다.

 

 

[ Repository ]

 

1. FirestationRepository

public interface FirestationRepository extends JpaRepository<Firestation, Integer> {
    Optional<Firestation> findByFirestationname(String firestationname);
}

 

JpaRepository를 상속하여 DB와의 CRUD을 편이하게 사용하려고 하였다.

Jpa에서 제공하는 쿼리네이밍 규칙을 사용하여 method를 만들었다.

- findByFirestationname : … where x.firestationname = ?1

 

2. MemberRepository

public interface MemberRepository extends JpaRepository<Member, Integer> {
}

 

FirestationRepository와 마찬가지로 DB와의 CRUD를 편리하게 사용하기 위해 JpaRepository를 상속하였다.

 

[ Service ]

 

1. MemberService

@Service
public class MemberService {

    @Autowired
    private MemberRepository memberRepository;

    @Transactional
    public void memberJoin(Member member) {
        member.setRoleType(RoleType.USER);
        memberRepository.save(member);
    }

//    @Transactional(readOnly = true)
//    public Page<Member> 회원목록(Pageable pageable) {
//        return memberRepository.findAll(pageable);
//    }
}

 

[Method]

  • memberJoin : 컨트롤러에서 PostMapping을 통해 Member 객체에 저장한 값에 Roletype을 USER로 설정하고 memberRepository에 save 한다. (save 는 JpaRepository 를 상속함으로서 사용가능했다.)

 

2. FirestationService

@Service
@RequiredArgsConstructor
public class FirestationService {

    @Autowired
    private FirestationRepository firestationRepository;

    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private BCryptPasswordEncoder encoder;

    private final JwtTokenProvider jwtTokenProvider;


    @Transactional
    public void firestationJoin(Firestation firestation) {

        String rawPw = firestation.getFirestationPw();
        String encPw = encoder.encode(rawPw);
        firestation.setRoleType(RoleType.ADMIN);
        firestation.setFirestationPw(encPw);

        firestationRepository.save(firestation);
    }

    @Transactional(readOnly = true)
    public List<Member> userList() {

        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        UserDetails userDetails = (UserDetails)principal;

        String firestation = ((UserDetails) principal).getUsername();

        return memberRepository.findAll().stream().filter(f -> firestation.equals(f.getNearestStation()))
                .collect(Collectors.toList());

    }

//    public String login(LoginRequestDto loginRequestDto) {
//        Firestation firestation = firestationRepository.findByFirestationname(loginRequestDto.getFirestationname())
//                .orElseThrow(() -> new IllegalArgumentException(loginRequestDto.getFirestationname()));

//        return jwtTokenProvider.createToken(firestation.getFirestationname(), firestation.getRoleType());
//    }
}

 

[Method]

  • firestationJoin : 회원정보를 받아오고 롤 타입을 ADMIN 으로 설정 후 pw의 경우 BcryptPasswordEncoder를 통해 인코딩 한 후에 FirestationRepository에 save한다.
  • userList : principal 변수에 로그인한 세션을 담고 firestation 변수에 세션의 아이디를 담는다. 그리고 Repository 인터페이스의 findByAll (Jpa Repository 내장 함수)를 이용해 다 찾고 필터링을 통해 Member의 nearestStation 과  세션의 아이디가 같은 Member의 정보만 List로 뽑아온다.

 

[ Config ]

 

1. SecurityConfig

( 트래픽이 증가될일이 없어서 서버를 하나만 사용해도되겠다고 생각하여 jwt 토큰을 사용하지는 않았다.)

 

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

    @Autowired
    private MemberDetailService memberDetailService;

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(memberDetailService).passwordEncoder(encoderPWD());
    }

//    private final JwtTokenProvider jwtTokenProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .httpBasic().disable()
                .csrf().disable()
                .cors().configurationSource(corsConfigurationSource())
                .and()
//                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .authorizeRequests()
                    .antMatchers("/", "/auth/**", "/css/**", "/js/**").permitAll()
                    .antMatchers("/admin/**").hasAnyRole("ADMIN")
                    .anyRequest().authenticated()
                .and()
                    .formLogin()
                    .loginPage("/auth/loginForm") // 로그인 페이지
                    .loginProcessingUrl("/auth/loginProc")
                    .defaultSuccessUrl("/")
                    .failureUrl("/auth/loginForm");
//                .and()
//                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
//                        UsernamePasswordAuthenticationFilter.class);

    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();

        configuration.addAllowedOriginPattern("*");
        configuration.addAllowedHeader("*");
        configuration.addAllowedMethod("*");
        configuration.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST"));
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }

}

 

[Method]

  • configure : 아래는 WebSecurityAdapter 추상 클래스의 선언되어있는 함수이다.
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		this.disableLocalConfigureAuthenticationBldr = true;
}

 

소방서의 정보를 등록할 때 비밀번호를 인코딩 후 저장하였기 때문에 확인을 할 때 역시 BcryptPasswordEncoder를 통해 인코딩한 값을 받아 비교를 한다.

 

  • configure : 아래는 마찬가지로 WebSecurityAdapter 추상 클래스의 선언되어있는 함수이다.
protected void configure(HttpSecurity http) throws Exception {
		this.logger.debug("Using default configure(HttpSecurity). "
				+ "If subclassed this will potentially override subclass configure(HttpSecurity).");
		http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
		http.formLogin();
		http.httpBasic();
}

 

선언정보는 전 게시글에 기록해 두었다.

 

  • CorsConfigureSource : 현재 프로젝트를 Vue.js 프레임워크를 통해 Front 영역을 구현하고 있는데 브라우저에서는 두 가지의 포트번호를 사용할 수 없기에 Cors 에러가 발생한다고 한다.
      • Access-Contorl-Allow-Origin
        • CORS 요청을 허용할 사이트
      • Access-Contorl-Allow-Method
        • CORS 요청을 허용할 Http Method들 (e.g. GET,PUT,POST)
      • Access-Contorl-Allow-Headers
        • 특정 헤더를 가진 경우에만 CORS 요청을 허용할 경우
      • Access-Contorl-Allow-Credencial
        • 자격증명과 함께 요청을 할 수 있는지 여부.
        • 해당 서버에서 Authorization로 사용자 인증도 서비스할 것이라면 true로 응답.

 

 

++

 

'스프링 시큐리티 구조, 인증과정

 

  • 클라이언트에서 Login Request
  • AuthenticationFilter가 요청을 가로채 UsernamePasswordAuthenticationToken 생성하고 AuthenticationManger, AuthenticationProviders, UserDetailsService를 따라 들어가 UserDetails를 통해 DB에 클라이언트에서 요청한 정보가 DB에 존재하는지 확인하고 ProviderManager에게 Token정보를 넘긴다.
  • DB에 존재한다면 다시 UserDetails, UserDetailsService, AuthenticationProvider을 통해 최종적으로 AuthenticationFilter로 권한 등을 설정하여 Authentication 객체를 넘긴다.
  • AuthenticationFilter는 전달받은 AuthenticationFilter 객체를 SecurityContext에 저장한다.

 

2. MemberDetail

public class MemberDetail implements UserDetails {

    private Firestation firestation;

    public MemberDetail(Firestation firestation) {
        this.firestation = firestation;
    }

    @Override
    public String getPassword() {
        return firestation.getFirestationPw();
    }

    @Override
    public String getUsername() {
        return firestation.getFirestationname();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collectors = new ArrayList<>();

        collectors.add(()-> {
            return "ROLE_"+firestation.getRoleType();
        });
        return collectors;
    }
}

 

firestation 인스턴스 변수로 firestation 클래스를 사용한다 ( 컴포지션 )

 

[Method]

  • getPassWord : 패스워드를 받아온다
  • getUsername : 유저네임을 받아온다
  • isAccountNonExpired : 계정이 만료되었는지 리턴한다 ( false = 만료되었다. )
  • isAccountNonLocked : 계정이 잠겨있는지 리턴한다 ( false = 잠겨있다. )
  • isCredentialsNonExpired : 비밀번호가 만료되었는지 리턴한다 ( false = 만료되었다 )
  • isEnabled : 계정이 활성화되었는지 리턴한다 ( false = 활성화 되지 않았다 )
  • getAuthorities : 계정이 가지고 있는 권한 목록을 리턴한다.

 

2. MemberDetailService

@RequiredArgsConstructor
@Service
public class MemberDetailService implements UserDetailsService {

    @Autowired
    private FirestationRepository firestationRepository;

    @Override
    public UserDetails loadUserByUsername(String firestationname) throws UsernameNotFoundException {
        Firestation principal = firestationRepository.findByFirestationname(firestationname)
                .orElseThrow(()->{
                    return new UsernameNotFoundException("존재하지 않는 회원입니다.");
                });
        return new MemberDetail(principal);
    }
}

 

[Method]

  • loadUserByUsername : 전달받은 값과 DB에 있는 값과 JpaRepository 인터페이스에서 선언한  findByFirestationname  메소드를 통해 비교한 값을 리턴한다. 따로 비밀번호는 SecurityConfig에서 configure을 오버라이드 한 메소드에서 비교한다.

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함