티스토리 뷰
[ 프로젝트 ]
사람이 인지하지 못한 화재에 대응하는 서비스입니다.
원리:화재 감지 센서와 AI 기술을 이용하여, 화재 발생시 자동으로 주변 소방서에 알림을 전송합니다.
<기대효과>
- 빠른 대응으로 더 큰 화재 피해 방지
- 사람이 인지하지 못한 화재 대응 가능
- 빈 집 화재 대응 가능
[ Spring ]
[ 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로 응답.
- Access-Contorl-Allow-Origin
-
++
- 클라이언트에서 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을 오버라이드 한 메소드에서 비교한다.
'🌱 프로젝트 > 소방알리미' 카테고리의 다른 글
Vue Framework에서 Build 하기 - 소방서 알림 #6 (0) | 2022.09.18 |
---|---|
로그인 오류 2 - 소방서 알림 #4 (0) | 2022.09.14 |
로그인 오류 1 - 소방서 알림 #3 (0) | 2022.09.12 |
시큐리티 설정 - 소방서 알림 #2 (0) | 2022.09.08 |
미니 프로젝트 - 소방서알림 #1 (0) | 2022.09.06 |