Bläddra i källkod

2024-02-14 update

junggilpark 1 år sedan
förälder
incheckning
9b86fb33cd

+ 7 - 0
src/main/java/com/its/web/controller/view/ViewController.java

@@ -144,4 +144,11 @@ public class ViewController {
         model.addAttribute("notice", this.noticeService.findNotice(boardNo));
         return "notice/view";
     }
+
+    @ApiOperation(value = "06.운영자 - 01.로그인")
+    @GetMapping("/phits")
+    public String login(Model model, @Nullable @Param("LoginFail") String loginFail) {
+        model.addAttribute("loginFail", loginFail);
+        return "admin/login";
+    }
 }

+ 52 - 0
src/main/java/com/its/web/dto/admin/PrincipalDetail.java

@@ -0,0 +1,52 @@
+package com.its.web.dto.admin;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class PrincipalDetail implements UserDetails {
+    private TbWwwMemberDto user;
+
+    public PrincipalDetail(TbWwwMemberDto user) {
+        this.user = user;
+    }
+
+    @Override
+    public Collection<? extends GrantedAuthority> getAuthorities() {
+        Collection<GrantedAuthority> collectors = new ArrayList<>();
+        collectors.add(()->{return "ROLE_ADMIN";}); //add에 들어올 파라미터는 GrantedAuthority밖에 없으니
+        return collectors;
+    }
+
+    @Override
+    public String getPassword() {
+        return this.user.getPwd();
+    }
+
+    @Override
+    public String getUsername() {
+        return this.user.getEmail();
+    }
+
+    @Override
+    public boolean isAccountNonExpired() {
+        return true;
+    }
+
+    @Override
+    public boolean isAccountNonLocked() {
+        return true;
+    }
+
+    @Override
+    public boolean isCredentialsNonExpired() {
+        return true;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return true;
+    }
+}

+ 64 - 0
src/main/java/com/its/web/dto/admin/TbWwwMemberDto.java

@@ -0,0 +1,64 @@
+package com.its.web.dto.admin;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@ApiModel("TB_WWW_MEMBER DTO(관리자 정보 DTO)")
+public class TbWwwMemberDto{
+
+    @ApiModelProperty("이메일주소")
+    @JsonProperty("email")
+    private String email;   //VARCHAR(50)	N
+
+    @ApiModelProperty("웹 사용자 이름")
+    @JsonProperty("member_nm")
+    private String memberNm;   //VARCHAR(30)	Y
+
+    @ApiModelProperty("비밀번호")
+    @JsonProperty("pwd")
+    private String pwd;     //VARCHAR(100)	Y
+
+    @ApiModelProperty("연락처")
+    @JsonProperty("contact_num")
+    private String contactNum; //VARCHAR(30)	Y
+
+    @ApiModelProperty("권한등급")
+    @JsonProperty("user_auth")
+    private String userAuth;   //VARCHAR(3)	Y
+
+    @ApiModelProperty("비밀번호힌트질문('HNT')")
+    @JsonProperty("hint_ques")
+    private String hintQues;   //VARCHAR(7)	Y
+
+    @ApiModelProperty("비밀번호힌트질문답")
+    @JsonProperty("hint_ans")
+    private String hintAns;	//VARCHAR(100)
+
+    @ApiModelProperty("로그인 실패 횟수")
+    @JsonProperty("login_fail_count")
+    private String loginFailCount;	//NUMBER(2)
+
+    @ApiModelProperty("계정잠금여부(Y:계정잠김)")
+    @JsonProperty("is_account_lock")
+    private String isAccountLock;	//CHAR(1)
+
+    @ApiModelProperty("관리자계정접속가능아이피(*:모두가능)")
+    @JsonProperty("ip_address")
+    private String ipAddress;	//VARCHAR(200)
+
+    @ApiModelProperty("등록일시")
+    @JsonProperty("reg_dt")
+    private String regDt;	//VARCHAR(14)
+
+    @ApiModelProperty("삭제 여부")
+    @JsonProperty("del_yn")
+    private String delYn;	//CHAR(1)
+
+}

+ 0 - 15
src/main/java/com/its/web/interceptor/AdminInterceptor.java

@@ -1,15 +0,0 @@
-package com.its.web.interceptor;
-
-import org.springframework.context.annotation.Configuration;
-import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-
-@Configuration
-@EnableWebSecurity
-public class AdminInterceptor {
-
-//    @Bean
-//    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
-//        http.authorizeRequests((requests) -> requests.regexMatchers("/", "/api/**/*", "/"))
-//    }
-
-}

+ 9 - 0
src/main/java/com/its/web/mapper/its/admin/AdminMapper.java

@@ -0,0 +1,9 @@
+package com.its.web.mapper.its.admin;
+
+import com.its.web.dto.admin.TbWwwMemberDto;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface AdminMapper {
+    TbWwwMemberDto findMemberWidthId(String userId);
+}

+ 51 - 0
src/main/java/com/its/web/security/WebLoginFailureHandler.java

@@ -0,0 +1,51 @@
+package com.its.web.security;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.authentication.*;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+
+@Slf4j
+@Service
+public class WebLoginFailureHandler implements AuthenticationFailureHandler {
+
+    @Override
+    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
+        String key = "LoginFail";
+        String message = "계정을 찾을 수 없습니다.";
+        if (exception instanceof AuthenticationServiceException) {
+            message ="시스템에 오류가 발생했습니다.";
+        }
+        else if (exception instanceof UsernameNotFoundException) {
+            message = "아이디를 찾을 수 없습니다.";
+        }
+        else if (exception instanceof BadCredentialsException) {
+            message = "아이디 또는 비밀번호가 일치하지 않습니다.";
+        }
+        else if (exception instanceof DisabledException) {
+            message = "현재 사용할 수 없는 계정입니다.";
+        }
+        else if (exception instanceof LockedException) {
+            message = "현재 잠긴 계정입니다.";
+        }
+        else if (exception instanceof AccountExpiredException) {
+            message = "이미 만료된 계정입니다.";
+        }
+        else if (exception instanceof CredentialsExpiredException) {
+            message = "비밀번호가 만료된 계정입니다.";
+        }
+        log.error("{}: {}, {}", key, message, request.getParameter("username"));
+        request.setAttribute(key, message);
+
+        message = URLEncoder.encode(message, "UTF-8");
+        response.sendRedirect("/phits?LoginFail=" + message);
+    }
+}

+ 100 - 0
src/main/java/com/its/web/security/WebLoginSuccessHandler.java

@@ -0,0 +1,100 @@
+package com.its.web.security;
+
+import com.its.web.dto.admin.TbWwwMemberDto;
+import com.its.web.mapper.its.admin.AdminMapper;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.web.WebAttributes;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.WebAuthenticationDetails;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@AllArgsConstructor
+@Service
+public class WebLoginSuccessHandler implements AuthenticationSuccessHandler {
+
+//    private final TbUserCnncHsRepository cnncHsRepo;
+    private final AdminMapper mapper;
+
+    @Override
+    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
+
+//        String currSysTime = ItsUtils.getSysTime();
+        // IP, 세션 ID
+        WebAuthenticationDetails details = (WebAuthenticationDetails)authentication.getDetails();
+
+        // 인증 ID
+        String userId = authentication.getName();
+        log.info("login Remote-IP/Session-ID/User-ID: {}/{}/{}", details.getRemoteAddress(), details.getSessionId(), userId);
+
+        // 권한 리스트
+        List<GrantedAuthority> authList = new ArrayList<>(authentication.getAuthorities());
+        for (GrantedAuthority auth : authList) {
+            log.info("login Roll: {}", auth.getAuthority());
+        }
+
+        TbWwwMemberDto userInfr = (TbWwwMemberDto)authentication.getPrincipal();
+//        String remoteIp = ItsUtils.getHttpServletRemoteIP(request);
+//        TbUserCnncHs cnncHs = TbUserCnncHs.builder()
+//                .operSystId(userInfr.getOperSystId())
+//                .userId(userInfr.getUserId())
+//                .loginHms(ItsUtils.getSysTime())
+//                .logoutHms("")
+//                .build();
+//        this.cnncHsRepo.insertData(cnncHs.getOperSystId(), cnncHs.getLoginHms(), cnncHs.getUserId(), cnncHs.getLogoutHms());
+//        log.info("Login History: {}, {}", cnncHs, remoteIp);
+//
+//        userInfr.setLoginHms(cnncHs.getLoginHms());
+//        userInfr.setLogoutHms("");
+//
+//        String uri = WebConstants.DEFAULT_URI;
+//        String domain = "/";
+//
+//        HttpSession session = request.getSession(false); // 세션을 생성 하지 않음
+//        if (session != null) {
+//            log.info("Session[{}}] [{}], Login Authentication: User: {}, {}, {}, Login History: {}, {}",
+//                    session.getId(), currSysTime, userInfr.getUserId(), userInfr.getOperSystId(), userInfr.getLoginHms(), remoteIp, cnncHs.getLoginHms());
+//        } else {
+//            session = request.getSession(true); // 새로운 세션을 생성
+//            session.setMaxInactiveInterval(WebConstants.MAX_INACTIVE_SESSION_TIMEOUT);
+//            log.info("Session[{}] [{}], Login Authentication, Session Create: User: {}, {}, {}, {}, Login History: {}, {}",
+//                    session.getId(), currSysTime, userInfr.getUserId(), userInfr.getOperSystId(), userInfr.getLoginHms(), session.getMaxInactiveInterval(), remoteIp, cnncHs.getLoginHms());
+//        }
+//        session.setAttribute(WebConstants.USER_UUID, WebMvcConfig.encUserId(cnncHs.getUserId()));
+//        session.setAttribute(WebConstants.USER_TIME, cnncHs.getLoginHms());
+//        session.setAttribute(WebConstants.LOGIN_USER, userInfr);
+//
+//        CookieUtils.setCookie(response, WebConstants.USER_UUID, WebMvcConfig.encUserId(cnncHs.getUserId()), 60*60, domain);
+//        CookieUtils.setCookie(response, WebConstants.USER_TIME, cnncHs.getLoginHms(), 60*60, domain);
+//        CookieUtils.setCookie(response, WebConstants.USER_OPER_SYST_ID, userInfr.getOperSystId(), 60*60, domain);
+
+//        clearAuthenticationAttributes(request);
+//        response.sendRedirect(uri);
+    }
+
+    /**
+     * 로그인 성공시 로그인 실패 시 작성한 에러 세션 지우기
+     * @param request
+     */
+    protected void clearAuthenticationAttributes(HttpServletRequest request) {
+        HttpSession session = request.getSession(false);
+        //log.error("clearAuthenticationAttributes: {}", session);
+        if (session == null) {
+            log.warn("session already cleared.");
+            return;
+        }
+
+        session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
+    }
+}

+ 50 - 0
src/main/java/com/its/web/security/WebSecurityConfig.java

@@ -0,0 +1,50 @@
+package com.its.web.security;
+
+import com.its.web.service.admin.LoginService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Slf4j
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class WebSecurityConfig {
+
+    private final LoginService loginService;
+    private final WebLoginSuccessHandler webLoginSuccessHandler;
+    private final WebLoginFailureHandler webLoginFailureHandler;
+
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        http.csrf().disable();
+        http.authorizeRequests()
+                .antMatchers("/swagger-ui.html", "/swagger/**", "/swagger-resources/**", "/webjars/**", "/v2/api-docs").permitAll()
+                .antMatchers("/", "/trafficMap/**", "/api/**", "/notice/**", "/center/**", "/statistics/**").permitAll()
+                .antMatchers("/images/**", "/css/**", "/js/**", "/font/**").permitAll()
+                .antMatchers("/phits").hasIpAddress("192.168.20.46")
+                .anyRequest().authenticated();
+        http.formLogin()
+                .loginPage("/phits")
+                .loginProcessingUrl("/phits/login")
+                .failureUrl("/")
+                .successHandler(this.webLoginSuccessHandler);
+//                .failureHandler(this.webLoginFailureHandler);
+        return http.build();
+    }
+
+    public DaoAuthenticationProvider daoAuthenticationProvider() {
+        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
+        authenticationProvider.setUserDetailsService(this.loginService);
+//        authenticationProvider.setPasswordEncoder(passwordEncoder());
+        // loadUserByUsername 의 UsernameNotFoundException 이 BadCredentialsException 로 발생함.
+        // Exception 을 catch 하기 위해서는 아래를 false 로 설정하면 됨.
+        authenticationProvider.setHideUserNotFoundExceptions(true);
+        return authenticationProvider;
+    }
+}

+ 48 - 0
src/main/java/com/its/web/service/admin/LoginService.java

@@ -0,0 +1,48 @@
+package com.its.web.service.admin;
+
+import com.its.web.dto.admin.PrincipalDetail;
+import com.its.web.dto.admin.TbWwwMemberDto;
+import com.its.web.mapper.its.admin.AdminMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+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;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class LoginService implements UserDetailsService {
+    private final AdminMapper adminMapper;
+
+    @Override
+    public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
+        TbWwwMemberDto dto = adminMapper.findMemberWidthId(userId);
+        if (dto != null) {
+            PrincipalDetail principal = new PrincipalDetail(dto);
+
+//            if (dto.getIpAddress() != null) {
+//                String[] ipArray = dto.getIpAddress().split(",");
+//                if (ipArray.length > 0) {
+//                    List<String> ipList = new ArrayList<>(Arrays.asList(ipArray));
+//                    try {
+//                        InetAddress ipAddress = InetAddress.getLocalHost();
+//                        if (ipList.contains("*") || ipList.contains(ipAddress.getHostAddress())) {
+//                            return principal;
+//                        }
+//                        else {
+//                             throw new UsernameNotFoundException("접속 가능 IP를 확인해주세요");
+//                        }
+//                    } catch (UnknownHostException e) {
+//                        log.error("IP 정보를 확인 할수 없습니다.");
+//                    }
+//                }
+//            }
+            return principal;
+        }
+        else {
+            throw new UsernameNotFoundException(userId + " 을(를) 찾을 수 없습니다.");
+        }
+    }
+}

+ 25 - 0
src/main/resources/mybatis/mapper/its/admin/AdminMapper.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="com.its.web.mapper.its.admin.AdminMapper">
+    <select id="findMemberWidthId" parameterType="java.lang.String" resultType="com.its.web.dto.admin.TbWwwMemberDto">
+        SELECT
+            email,
+            member_nm,
+            pwd,
+            contact_num,
+            user_auth,
+            hint_ques,
+            hint_ans,
+            login_fail_count,
+            is_account_lock,
+            ip_address,
+            reg_dt,
+            del_yn
+         FROM
+            TB_WWW_MEMBER
+        WHERE email = #{userId}
+          AND del_yn = 'N'
+    </select>
+
+</mapper>

+ 3 - 1
src/main/resources/static/css/center.css

@@ -70,7 +70,6 @@
     display: flex;
     justify-content: center;
     overflow: auto;
-    min-height: 500px;
 }
 
 .centerWrap .container {
@@ -80,6 +79,9 @@
     display: flex;
     flex-direction: column;
 }
+.centerWrap .container.way {
+    min-height: 600px;
+}
 
 .centerWrap .header {
     padding: 2rem 0;

+ 8 - 0
src/main/resources/static/css/login.css

@@ -0,0 +1,8 @@
+.loginWrap {
+    width: 100%;
+    height: calc(100% - 98px);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-direction: column;
+}

+ 23 - 0
src/main/resources/templates/admin/login.html

@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html lang="en" xmlns:th=http://www.thymeleaf.org>
+<head>
+    <meta charset="utf-8"/>
+    <meta name="viewport" content="width=device-width,initial-scale=1"/>
+    <meta name="theme-color" content="#000000"/>
+    <meta name="description" content="포항시 교통정보센터입니다"/>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
+    <title>포항시 교통정보센터</title>
+    <th:block th:include="/include/head.html"></th:block>
+    <link rel="stylesheet" th:href="@{/css/login.css}">
+</head>
+<body id="body">
+<div class="loginWrap">
+    <div></div>
+    <div>관리자</div>
+    <input name="username" placeholder="ID">
+    <input name="password" placeholder="비밀번호">
+    <div>로그인</div>
+</div>
+<th:block th:include="/include/footer.html"></th:block>
+</body>
+</html>

+ 38 - 3
src/main/resources/templates/center/way.html

@@ -8,12 +8,13 @@
     <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
     <title>포항시 교통정보센터</title>
     <th:block th:include="/include/head.html"></th:block>
+    <script th:src="@{${@environment.getProperty('kakao-url')}}"></script>
     <link rel="stylesheet" th:href="@{/css/center.css}">
 </head>
 <body id="body">
 <th:block th:include="/include/header.html"></th:block>
 <div class="centerWrap">
-    <div class="container">
+    <div class="container way">
         <h2 class="header">오시는 길</h2>
         <div class="way-content">
             <div>
@@ -27,7 +28,7 @@
                 </p>
                 <h2>버스노선 안내</h2>
                 <p>
-                    <a>포항시 버스정보시스템 <img src="/images/icon/direction.png"></a>
+                    <a href="javascript:openBusSite(0)">포항시 버스정보시스템 <img src="/images/icon/direction.png"></a>
                 </p>
             </div>
         </div>
@@ -35,4 +36,38 @@
 </div>
 <th:block th:include="/include/footer.html"></th:block>
 </body>
-</html>
+</html>
+
+<script th:inline="javascript">
+    const container = document.getElementById('map'); //지도를 담을 영역의 DOM 레퍼런스
+    // const position = new kakao.maps.LatLng(36.018993682733615, 129.34316175442146);
+    const position = new kakao.maps.LatLng(36.01899593392164, 129.34316182110044);
+    const options = { //지도를 생성할 때 필요한 기본 옵션
+        // center: new kakao.maps.LatLng(36.0191816, 129.3432983), //지도의 중심좌표.
+        center: position, //지도의 중심좌표.
+        level: 2,
+        maxLevel: 9,
+        minLevel: 1,
+        disableDoubleClickZoom: true
+    };
+    _Map = new kakao.maps.Map(container, options);
+    _Map.setCursor('default');
+    const marker = new kakao.maps.Marker({
+        position : position,
+        title : '포항시청 - 버스노선 안내 바로가기',
+        cursor : 'pointer',
+    })
+
+    marker.setMap(_Map);
+
+    new kakao.maps.event.addListener(marker, 'click', function(event) {
+        openBusSite(0);
+    });
+
+    const infoWindow = new kakao.maps.InfoWindow({
+        position : position,
+        content : '<div onclick="openBusSite(0)" title="포항시청 - 버스노선 안내 바로가기" style="cursor:pointer; display: flex; align-items: center; justify-content: center; width: 220px;">경상북도 포항시 남구 시청로 1</div>',
+    })
+
+    infoWindow.open(_Map, marker);
+</script>