Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- 오라클
- 정수형타입
- 한국건설관리시스템
- 참조형변수
- EnhancedFor
- 컬렉션 타입
- 어윈 사용법
- NestedFor
- 예외미루기
- 자동차수리시스템
- 환경설정
- oracle
- abstract
- 추상메서드
- 사용자예외클래스생성
- 자바
- 메소드오버로딩
- 제네릭
- exception
- 다형성
- Java
- 인터페이스
- 객체 비교
- 컬렉션프레임워크
- 예외처리
- 대덕인재개발원
- 집합_SET
- cursor문
- GRANT VIEW
- 생성자오버로드
Archives
- Today
- Total
거니의 velog
231212_SPRING 2 (15-2) 본문
package kr.or.ddit.controller;
public class SecurityController {
/*
* [ 18장 : 스프링 시큐리티 ]
*
* 1. 스프링 시큐리티 소개
*
* - 애플리케이션에서 보안 기능을 구현하는데 사용되는 프레임워크이다.
* - 스프링 시큐리티는 필터 기반으로 동작하기 때문에 스프링 MVC와 분리되어 동작한다.
*
* # 기본 보안 기능
* - 인증 (Authentocation)
* > 애플리케이션 사용자의 정당성을 확인한다.
*
* - 인가 (Authorization)
* > 애플리케이션의 리소스나 처리에 대한 접근을 제어한다.
*
* # 시큐리티 제공 기능
* - 세션 관리
* - 로그인 처리
* - CSRF 토큰 처리
* - 암호화 처리
* - 자동 로그인
*
* *** CSRF 용어 설명
* - 크로스 사이트 요청 위조는 웹 사이트 취약점 공격의 하나로, 사용자가 자신의 의지와는 무관하기 공격자가
* 의도한 행위(수정, 삭제, 등록 등)를 특정 웹 사이트에 요청하게 하는 공격을 말한다.
*
* > CSRF 공격을 대비하기 위해서는 스프링 시큐리티 CSRF Token을 이용하여 인증을 진행한다.
*
* # 시큐리티 인증 구조
*
* 클라이언트에서 타겟으로 들어가기 위해서 요청을 진행한다. 이 때 타겟에 설정되어 있는 요청 접근 권한이 '사용자'
* 등급일 때로 설정되어 있다고 가정하자. 타겟으로 접근하기 위한 요청을 날렸고 요청 안에 사용자 등급에 해당하는 인가
* 정보가 포함되어 있지 않으면 스프링 시큐리티는 인증을 진행할 수 있도록 인증 페이지(로그인 페이지)를 제공하여
* 사용자에게 인증을 요청한다. 사용자는 아이디, 비밀번호를 입력 후 인증을 요청한다.
* 클라이언트에서 서버로 요청한 HttpServletRequest의 요청 객체를 AuthenticationFilter가 요청을
* 인터셉터 하는데, UsernamePasswordAuthenticationToken을 통해 인증을 진행할 토큰을 만들어
* AuthenticationManager에게 위임한다. 넘겨 받은 id, pw를 이용해 인증을 진행하는 데 성공시,
* Authentication 객체 생성과 성공을 전달하고, 그렇지 않으면 Exception 에러를 발생시킨다.
* 인증에 성공 후, 만들어진 Authentication 객체를 AuthenticationProvider에게 전달하고
* UserDetailService에게 넘겨받은 Authentication 객체 정보를 이용해서 Database에 일치하는 회원 정보
* 를 조회하여 꺼낸다. 꺼낸 정보를 UserDetails로 만들고 최종 User 객체에 회원 정보를 등록한다.
* 등록이 되면서 User Session 정보가 생성된다. 이후 스프링 시큐리티 내 SecurityContextHolder
* (시큐리티 인메모리)에 UserDetail 정보를 저장한다. 그리고 최종적으로 JSESSIONID가 유효한지를 검증 후
* 인증을 완료 후 타겟 정보로 넘어가도록 돕는다.
*
* 2. 스프링 시큐리티 설정
*
* # 환경 설정
* - 의존 라이브러리 설정(pom.xml 설정)
* > spring-security-web
* > spring-security-config
* > spring-security-core
* > spring-security-taglibs
*
* # 웹 컨테이너 설정(web.xml)
* - 스프링 시큐리티가 제공하는 서블릿 필터 클래스를 서블릿 컨테이너에 등록하나.
* - 스프링 시큐리티는 필터 기반이므로 시큐리티 필터체인을 등록한다.
* > context-param의 param-value 추가
* (추가 파라미터: /WEB-INF/spring/security-context.xml)
* > SpringSecurityFilterChain 추가
*
* # 스프링 시큐리티 설정
*
* - 스프링 시큐리티 컴포넌트를 빈으로 정의한다.
* - spring/security-context.xml 설정
*
* # 웹 화면 접근 정책
*
* - 웹 화면 접근 정책을 정한다. (테스트를 위한 각 화면 당 접근 정책을 설정한다.)
*
* 대상 | 화면 | 접근 정책
* ------------------------------------------------------------
* 일반 게시판 | 목록 화면 | 모두가 접근 가능하다.
* | 등록 화면 | 로그인한 회원만 접근 가능하다.
* ------------------------------------------------------------
* 공지사항 게시판 | 목록 화면 | 모두가 접근 가능하다
* | 등록 화면 | 로그인한 관리자만 접근 가능하다.
* ------------------------------------------------------------
*
* # 화면 설명
* - 컨트롤러
* > controller/SecurityBoardController
* > controller/SecurityNoticeController
*
* - 화면
* > board/list, register
* > notice/list, register
*
* 3. 접근 제한 설정
*
* - 시큐리티 설정을 통해서 특정 URI에 접근을 제한할 수 있다.
*
* # 환경 설정
*
* - 스프링 시큐리티 설정
* > URI 패턴으로 접근 제한을 설정한다.
* > security-context.xml 설정
* <security:intercept-uri pattern="/board/list" access="permitAll" />
* <security:intercept-uri pattern="/board/register" access="hasRole('ROLE_MEMBER')" />
* > notice도 마찬가지로 설정...
*
* # 화면 설명
* - 일반 게시판 목록 화면(모두 접근 가능하도록 되어 있음 : permitAll)
* - 일반 게시판 등록 화면(회원 권한을 가진 사용자만 접근 가능 : hasRole('ROLE_MEMBER'))
* > 접근 제한에 걸려 스프링 시큐리티가 기본적으로 제공하는 로그인 페이지로 이동한다.
* - 공지사항 게시판 목록 화면(모두 접근 가능하도록 되어 있음 : permitAll)
* - 공지사항 게시판 등록 화면(관리자 권한을 가진 사용자만 접근 가능 : hasRole('ROLE_ADMIN'))
* > 접근 제한에 걸려 스프링 시큐리티가 기본적으로 제공하는 로그인 페이지로 이동한다.
*
* 4. 로그인 처리
*
* - 메모리 상에 아이디와 패스워드를 지정하고 로그인을 처리한다.
* - 스프링 시큐리티 5버전부터는 패스워드 암호화 처리기를 반드시 이용하도록 변경 되었다.
* - 암호화 처리기를 사용하지 않도록 "{noop}" 문자열을 비밀번호 앞에 사용한다.
*
* # 환경 설정
*
* - 스프링 시큐리티 설정
* > security-context.xml 설정
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="member" password="{noop}1234" authorities="ROLE_MEMBER" />
<security:user name="admin" password="{noop}1234" authorities="ROLE_ADMIN" />
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
*
* # 화면 설명
*
* - 일반 게시판 등록 화면
* > 접근 제한에 걸려 스프링 시큐리티가 기본적으로 제공하는 로그인 페이지가 연결되고,
* 일반 회원 등급인 ROLE_MEMBER 권한을 가진 member 계정으로 로그인 후 해당 페이지로 접근 가능.
* - 공지사항 게시판 등록 화면
* > 접근 제한에 걸려 스프링 시큐리티가 기본적으로 제공하는 로그인 페이지가 연결되고,
* 관리자 등급인 ROLE_ADMIN 권한을 가진 admin 계정으로 로그인 후 해당 페이지로 접근 가능.
*
* 5. 접근 거부 처리
*
* - 접근 거부가 발생한 상황을 처리하는 접근 거부 처리자의 URI를 지정할 수 있다.
*
* # 환경 설정
*
* - 스프링 웹 설정(servlet-context.xml 설정)
* > <context:component-scan base-package="kr.or.ddit.security..." />
* ** 필요에 의한 패키지 라인을 bean으로 등록하여 사용해야 할 때 스프링 웹 설정에서 base-package를 설정할 수 있다.
*
* - 스프링 시큐리티 설정(security-context.xml 설정)
* > 접근 거부 처리자의 URI를 지정
* > <security:access-denied-handler error-page="/accessError/" />
*
* # 접근 거부 처리
*
* - 접근 거부 처리 컨트롤러 작성
* > security/CommonController
*
* # 화면 설명
* - 일반 게시판 등록 페이지
* > 접근 제한에 걸려 스프링 시큐리티가 제공하는 로그인 페이지가 나타나고, 회원 권한을 가진 계정으로
* 접근 시 접근 가능
* - 공지사항 게시판 등록 페이지
* > 접근 제한에 걸려 스프링 시큐리티가 제공하는 로그인 페이지가 나타나고, 회원 권한을 가진 계정으로
* 접근 시에 공지사항 게시판 등록 페이지는 관리자 권한으로만 접근 가능하므로 접근이 거부된다.
* 이 때, access-denied-handler로 설정되어 있는 URI로 이동하고 해당 페이지에서 접근이
* 거부 되었을 떄 보여질 페이지의 정보가 나타난다.
*
* 6. 사용자 정의 접근 거부 처리자
*
* - 접근 거부가 발생한 상황에 단순 메시지 처리 이상의 다양한 처리를 하고 싶다면 AccessDeniedHandler를 직접 구현한다.
*
* # 환경 설정
* - 스프링 시큐리티 설정(security-context.xml 설정)
* > id가 'customAccessDenied'를 가진 빈을 등록한다.
* > <security:access-denied-handler ref="customAccessDenied" />
*
* # 접근 거부 처리자 클래스 정의
* - CustomAccessDeniedHandler 클래스 정의
* > AccessDeniedHandler 인터페이스를 참조받아서 handle 메소드를 재정의하여 사용한다.
* 우리는 접근이 거부되었을 때 빈으로 등록해 둔 CustomAccessDeniedHandler 클래스가 발동해 해당 메소드가
* 실행되고 response 내장 객체를 활용하여 /accessError URL로 이동하여 접근 거부시 보여질 페이지로 이동하지만
* 이곳에서 더 많은 로직을 처리할 수도 있다. (request, response 내장 객체를 이용하여 다양한 처리 가능)
*
* # 화면 설명
* - 일반 게시판 등록 페이지
* > 접근 제한에 걸려 스프링 시큐리티가 제공하는 로그인 페이지가 나타나고, 회원 권한을 가진 계정으로 접근 시 접근 가능
* - 공지사항 게시판 등록 페이지
* > 접근 제한에 걸려 스프링 시큐리티가 제공하는 로그인 페이지가 나타나고, 회원 권한을 가진 계정으로 접근 시에 공지사항
* 게시판 등록 페이지는 관리자 권한만 접근 가능하므로 접근이 거부된다. 이 때, access-denied-handler로 설정
* 되어 있는 ref 속성에 부여된 클래스 메소드로 이동하고 해당 페이지에서 접근이 거부되었을 때 페이지의 정보가 나타난다.
*
* 7. 사용자 정의 로그인 페이지
*
* - 기본 로그인 페이지가 아닌 사용자가 직접 정의한 로그인 페이지를 사용한다.
*
* # 환경 설정
* - 스프링 시큐리티 설정(security-context.xml 설정)
*
* <security:form-login login-page="/login" /> 설정
* : 사용자가 직접 만든 로그인 페이지로 이동할 '/login' URL을 가지고 있는 컨트롤러 메소드를 정의
*
* # 로그인 페이지 정의
*
* - 사용자가 정의한 로그인 컨트롤러
* > controller 패키지 안에 LoginController 생성
* - 사용자가 정의한 로그인 뷰
* > views/loginForm.jsp
*
* ** 시큐리티에서 제공하는 기본 로그인 페이지로 이동하지 않고, 사용자가 정의한 로그인 페이지의 URI를 요청하여
* 해당 페이지에서 권한을 체크하도록 한다. 인증이 완료되면 최초의 요청된 target URI로 이동한다.
* 그렇지 않은 경우 사용자가 만들어놓은 접근 거부 페이지로 이동한다.
*
* 8. 로그인 성공 처리
*
* - 로그인을 성공한 후에 로그인 이력 로그를 기록하는 등의 동적을 하고 싶은 경우가 있다.
* 이런 경우 AuthenticationSuccessHandler라는 인터페이스를 구현해서 로그인 성공 처리자로 지정할 수 있다.
*
* # 환경 설정
* - customLoginSuccess Bean 등록
* <security:form-login login-page="/login" authentication-success-handler-ref="customLoginSuccess" />
*
* # 로그인 성공 처리자 클래스 정의
* - 로그인 성공 처리자
* > SavedRequestAwareAuthenticationSuccessHandler는 AuthenticationSuccessHandler의 구현 클래스이다.
* 인증 전에 접근을 시도한 URL로 리다이렉트하는 기능을 가지고 있으며 스프링 시큐리티에서 기본적으로
* 사용되는 구현 클래스이다.
* - 로그인 성공 처리자2
* > AuthenticationSuccessHandler 인터페이스를 직접 구현하여 인증 전에 접근을 시도한 URL로
* 리다이렉트하는 기능을 구현한다.
*
* # 화면 설명
* - 일반 게시판 등록 화면
* > 사용자가 정의한 로그인 페이지에서 회원 권한에 해당하는 계정으로 로그인 시, 성공했다면 성공 처리자인
* CustomLoginSuccess 클래스로 넘어가 넘겨받은 파라미터들 중 authentication 안에
* principal로 User 정보를 받아서 username과 password를 출력한다.
* (출력 정보는 로그인 성공 시 인증된 회원 정보이다.)
*
* 9. 로그아웃 처리
*
* - 로그아웃을 위한 URI를 지정하고, 로그아웃 처리 후에 별도의 작업을 하기 위해서 사용자가 직접 구현한 처리자를
* 등록할 수 있다.
*
* # 환경 설정
*
* - 스프링 시큐리티 설정(security-context.xml 설정)
* > <security:logout logout-url="/logout" invalidate-session="true" />
*
* ** logout 경로를 스프링에서 제공하는 /logout 경로로 설정한다.
* logout 처리 페이지에서도 action 경로를 /logout으로 설정한다.
*
* 10. JDBC를 이용한 인증/인가 처리
*
* - 지정한 형식으로 테이블을 생성하면 JDBC를 이용해서 인증/인가를 처리할 수 있다.
* - 생성할 테이블은 사용자를 관리하는 테이블(users)과 권한을 관리하는 테이블이다.
*
* # 데이터베이스 테이블 준비
*
* - users, authorities 테이블 준비
*
* # 환경설정
*
* - 의존 라이브러리 설정
* > 데이터베이스 관련 라이브러리를 추가한다.
* > 기존 데이터베이스 연결을 위한 라이브러리를 가져와 등록(pom.xml)
*
* # 스프링 설정(root-context.xml 설정)
*
* - 데이터 소스 설정(기존 설정)
*
* # 스프링 시큐리티 설정(security-context.xml 설정)
*
* - customPasswordEncoder 빈 등록 진행
* <security:authentication-manage> 태그 설정
*
* # 비밀번호 암호화 처리기 클래스 정의
* - 비밀번호 암호화 처리기
* 스프링 시큐리티 5부터는 기본적으로 PasswordEncoder를 지정해야 하는데, 제대로 하려면 생성된 사용자 테이블(users)
* 에 비밀번호를 암호화하여 저장해야 한다. 테스트를 위해서 생성한 데이터는 암호화를 처리하지 않으므로 로그인 하면
* 당연히 로그인 에러가 발생할 것이다. (암호화된 비밀번호가 날아가는게 아니라서)
* 그래서 암호화를 하지 않는 PasswordNoOpPasswordEncoder를 직접 구현하여 지정하면 로그인 시 암호화를 고려하지
* 않으므로 로그인이 정상적으로 이루어지는 걸 확인할 수 있다.
* ** PasswordEncoder를 참조받아서 우리가 원하는 로직으로 변경해서 사용(암호화를 사용하지 않는 루트로)
*
* 11. 사용자 테이블 이용한 인증/인가 처리
*
* - 스프링 시큐리티가 기본적으로 이용하는 테이블 구조를 그대로 생성해서 사용해도 되지만, 기존에 구축된 회원 테이블이 있다면
* 약간의 작업으로 기존 테이블을 활용할 수 있다.
*
* # 데이터베이스 테이블 준비
* - member, member_auth 테이블 준비
*
* # 환경 설정
* - 스프링 시큐리티 설정(security-context.xml 설정)
* > bcryptPasswordEncoder 빈 등록 진행
* > <security:jdbc-user-service> 태그 설정
* > <security:password-encoder> 태그 설정
*
* # 쿼리 정의
* - 인증할 때 필요한 쿼리
* > select user_id, user_pw, enabled from member where user_id = ?
*
* - 권한을 확인할 때 필요한 쿼리
* > select m.user_id, ma.auth from member m, member_auth ma where ma.user_no = m.user_no and m.user_id = ?
*
* # BCryptPasswordEncoder 클래스를 이용하여 직접 encode된 비밀번호를 찾아 데이터베이스에 셋팅한다.
* - 12번 UserDetails 정보를 등록하면서 같이 진행
*
* - BCryptPasswordEncoder 클래스를 활용한 단방향 비밀번호 암호화
* > encode() 메소드를 통해서 SHA-2 방식의 8바이트 Hash 암호를 매번 랜덤하게 생성한다.
* > 똑같은 비밀번호를 입력하더라도 암호화되는 문자열은 매번 다른 문자열을 반환한다.
*
* 비밀번호를 입력하면 암호화된 비밀번호로 인코딩하는데, 암호화된 비밀번호와 DB 테이블에 있는 암호화된 비밀번호가
* 일치한지를 파악 후 일치하면 로그인 성공으로 다음 스탭을 진행한다.
* > BCryptPasswordEncoder 클래스의 encode() 메소드를 통해 만들어지는 암호화된 해쉬 다이제스트들은 입력한
* 비밀번호 문자에 해당하는 수십억개의 다이제스트들 중에서 일치하는 다이제스트가 존재할 경우 비밀번호의 일치로 보고
* 인증을 성공시켜준다.
*
* 12. UserDetailsService 재정의
*
* - 스프링 시큐리티의 UserDetailsService를 구현하여 사용자 상세 정보를 얻어오는 메서드를 재정의한다.
*
* # 환경 설정
* - 의존 라이브러리 설정(pom.xml) 설정
* > 데이터베이스 관련 라이브러리
* - 스프링 시큐리티 설정(security-context.xml 설정)
* > customUserDetailsService 빈 등록
* > security:authentication-provider 태그 설정
*
* # 클래스 재정의
* - UserDetailsService 재정의
* > security/CustomUserDetailsService 클래스 생성
* > 기존 사용중인 read를 기반으로 한 readById 재정의
* > CustomUserDetailsService 클래스 내 loadUserByUsername 메소드에서 인코딩 된 비밀번호 확인 후
* 데이터베이스 비밀번호 수정
* > Member 테이블 내 특정 계정들 비밀번호를 암호화된 비밀번호로 수정한다.
*
* 13. 스프링 시큐리티 표현식
*
* - 스프링 시큐리티를 이용하면 인증 및 권한 정보에 따라 화면을 동적으로 구성할 수 있고,
* 로그인 한 사용자 정보를 보여줄 수도 있다.
*
* # 공동 표현식
*
* - hasRole([role])
* > 해당 롤이 있으면 true
* - hasAnyRole([role1, role2])
* > 여러 롤들 중에서 하나라도 해당하는 롤이 있으면 true
* - principal
* > 인증된 사용자의 사용자 정보(UserDetails 인터페이스를 구현한 클래스의 객체)를 의미
* - authentication
* > 인증된 사용자의 인증 정보(Authentication 인터페이스를 구현한 클래스의 객체)fmf dmlal
* - permitAll
* > 모든 사용자에게 허용
* - denyAll
* > 모든 사용자에게 거부
* - isAnonymous()
* > 익명의 사용자의 경우(로그인을 하지 않은 경우도 해당)
* - isAuthenticated()
* > 인증된 사용자면 true를 반환
* - isFullyAuthenticated()
* > Remember-me로 인증된 것이 아닌 일반적인 방법으로 인증된 사용자인 경우 true
*
* # 표현식 사용
*
* - 표현식을 이용하여 동적 화면 구성
* > home.jsp 수정
* : 표현식을 이용한 내용 추가
*
* - 로그인한 사용자 정보 보여주기
* > views/board/register.jsp 수정
* > views/notice/register.jsp 수정
*
* 14. 자동 로그인
*
* - 로그인하면 특정 시간 동안 다시 로그인 할 필요가 없는 기능이다.
* - 스프링 시큐리티는 메모리나 데이터베이스를 사용하여 처리한다.
* - 기능을 구현하기 위해 <security:remember-me> 태그를 이용하여 시큐리티 설정 파일을 수정한다.
*
* # 데이터베이스 테이블
* - persistent_logins 테이블 준비
*
* # 환경 설정
* - 스프링 시큐리티 설정(security-context.xml 설정)
* <security:remember-me data-source-ref="dataSource" token-validaty="604800" /> 태그 설정
* <security:logout logout-url="/logout" invalidate-session="true"
* delete-cookies="remember-me, JSESSION_ID" /> 태그 설정
*
* # 자동 로그인
* - 로그인 상태 유지 체크박스 추가
* > loginForm.jsp 수정 (자동 로그인 체크 박스 만들기)
*
* # 자동 로그인 시, 만들어지는 쿠키 정보들
* - JSESSIONID와 remember-me 쿠키가 만들어진다.
* - JSESSIONID를 삭제 후, 다시 로그인을 진행하더라도 로그인 후 진행될 페이지가 정상적으로 나타나는 것을 확인할 수 있음
* > 자동 로그인이 remember-me 쿠키에 의해서 데이터베이스에 저장되는 각 hash token 값에 의해 재설정됨을 확인]
*
* 15. 스프링 시큐리티 어노테이션
*
* - 스프링 시큐리티는 어노테이션을 사용하여 필요한 설정을 추가할 수 있다.
*
* # 사용 어노테이션
*
* - @Secured
* > 스프링 시큐리티 모듈을 지원하기 위한 어노테이션으로 초기부터 사용되었다.
* - @PreAuthorize
* > 메서드가 실행되기 전에 적용할 접근 정책을 지정할 때 사용한다.
* - @PostAuthorize
* > 메서드가 실행한 후에 적용할 접근 정책을 지정할 때 사용한다.
*/
}
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<context:component-scan base-package="kr.or.ddit" />
<!--
pre-post-annotations 속성을 'enabled'로 설정하면 @PreAuthorize, @PostAuthorize를 사용할 수 있고
secured-annotations 속성을 'enabled'로 설정하면 @Secured를 사용할 수 있다.
-->
<security:global-method-security pre-post-annotations="enabled" secured-annotations="enabled" />
</beans:beans>
[security-context.xml]
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 6. 사용자 정의 접근 거부 처리자 Bean 추가 -->
<bean id="customAccessDenied" class="kr.or.ddit.security.CustomAccessDeniedHandler"></bean>
<bean id="customLoginSuccess" class="kr.or.ddit.security.CustomLoginSuccessHandler"></bean>
<!-- <bean id="customPasswordEncoder" class="kr.or.ddit.security.CustomNoOpPasswordEncoder"></bean> -->
<bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>
<bean id="customUserDetailsService" class="kr.or.ddit.security.CustomUserDetailsService"></bean>
<security:http>
<!-- 3. 접근 제한 설정 : URL 패턴으로 접근 제한을 설정한다. -->
<!-- <security:intercept-url pattern="/board/list" access="permitAll" />
<security:intercept-url pattern="/board/register" access="hasRole('ROLE_MEMBER')" />
<security:intercept-url pattern="/notice/list" access="permitAll" />
<security:intercept-url pattern="/notice/register" access="hasRole('ROLE_ADMIN')" /> -->
<!-- 3. 접근 제한 설정 끝 -->
<!--
폼 기반 인증 기능을 사용한다. : 사용자 정의 로그인 페이지 추가(login-page),
로그인 성공 처리 추가 : 로그인 성공 후 처리를 담당하는 처리자로 지정(customLoginSuccess)
-->
<security:form-login login-page="/login" authentication-success-handler-ref="customLoginSuccess" />
<!-- 5. 접근 거부 처리자 : 접근 거부 처리자의 URI를 지정 -->
<!-- <security:access-denied-handler error-page="/accessError" /> -->
<!-- 6. 사용자 정의 접근 거부 처리자 추가 : customAccessDenied를 접근 거부 처리자로 등록 -->
<security:access-denied-handler ref="customAccessDenied" />
<!--
14. 자동 로그인 적용
- 데이터 소스를 지정하고 테이블을 이용해서 기존 로그인 정보를 기록
- 쿠키의 유효 시간을 지정한다. (604800 : 7일)
-->
<security:remember-me data-source-ref="dataSource" token-validity-seconds="604800" />
<!-- 9. 로그아웃 처리 : 로그아웃 처리를 위한 URI를 지정하고, 로그아웃한 후에 세션을 무효화한다. -->
<security:logout logout-url="/logout" invalidate-session="true"
delete-cookies="remember-me, JSESSION_ID" />
</security:http>
<security:authentication-manager>
<security:authentication-provider user-service-ref="customUserDetailsService">
<!-- <security:user-service>
<security:user name="member" password="{noop}1234" authorities="ROLE_MEMBER" />
<security:user name="admin" password="{noop}1234" authorities="ROLE_ADMIN" />
</security:user-service> -->
<!-- 사용자 정의 테이블을 이용한 인증/인가 처리 -->
<!-- <security:jdbc-user-service data-source-ref="dataSource"/>
<security:password-encoder ref="customPasswordEncoder" /> -->
<!-- 사용자 정의 테이블을 이용한 인증/인가 처리 끝 -->
<!--
UserDetailsService를 설정하면서 데이터베이스 연동 후 사용자가 정의한 테이블로 mapper를 통한 데이터 바인딩으로
인증/인가 진행 시 주석
-->
<!-- <security:jdbc-user-service data-source-ref="dataSource"
users-by-username-query="select user_id, user_pw, enabled from member where user_id = ?"
authorities-by-username-query="select m.user_id, ma.auth from member m, member_auth ma where ma.user_no = m.user_no and m.user_id = ?" />
<security:password-encoder ref="bcryptPasswordEncoder" /> -->
<security:password-encoder ref="bcryptPasswordEncoder" />
</security:authentication-provider>
</security:authentication-manager>
</beans>
package kr.or.ddit.controller;
import java.security.Principal;
import java.util.Iterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import kr.or.ddit.vo.CrudMember;
import kr.or.ddit.vo.CustomUser;
@Controller
@RequestMapping("/board")
public class BoardController {
private static final Logger log = LoggerFactory.getLogger(BoardController.class);
@RequestMapping(value = "/list", method = RequestMethod.GET)
public String list() {
log.info("list : access to all");
return "board/list";
}
// 회원 권한을 가진 사용자만 접근이 가능하다.
@PreAuthorize("hasAnyRole('ROLE_MEMBER', 'ROLE_ADMIN')")
@RequestMapping(value = "/register", method = RequestMethod.GET)
public String register(Principal principal) {
log.info("register : access to member");
// 사용자 정보 가져오기
// [방법 1] principal 객체를 받아와서 인증된 정보 가져오기
log.info("name : " + principal.getName());
// [방법 2] User 객체 정보를 얻어와서 사용
CustomUser user = (CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
log.info("username : " + user.getUsername());
log.info("password : " + user.getPassword());
CrudMember member = user.getMember();
log.info("userId : " + member.getUserId());
log.info("userName : " + member.getUserName());
Iterator<GrantedAuthority> ite = user.getAuthorities().iterator();
while(ite.hasNext()) {
log.info("auth : " + ite.next().getAuthority());
}
return "board/register";
}
}
package kr.or.ddit.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
@RequestMapping("/notice")
public class NoticeController {
private static final Logger log = LoggerFactory.getLogger(NoticeController.class);
@RequestMapping(value = "/list", method = RequestMethod.GET)
public String list() {
log.info("list : access to all");
return "notice/list";
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@RequestMapping(value = "/register", method = RequestMethod.GET)
public String register() {
log.info("register : access to admin");
return "notice/register";
}
}
- http://localhost/notice/login.do
[pom.xml]
<!-- 스프링 시큐리티 라이브러리 의존 관계 정의 시작 -->
<!-- 스프링 시큐리티를 웹에서 동작하도록 해줌 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<!-- 스프링 시큐리티 설정을 도와줌 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<!-- 스프링 시큐리티 일반 기능 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<!-- 스프링 시큐리티 태그 라이브러리를 연결해 줌 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<!-- 스프링 시큐리티 라이브러리 의존 관계 정의 끝 -->
[web.xml]
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_3_1.xsd">
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/root-context.xml
/WEB-INF/spring/security-context.xml
</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processes application requests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/appServlet/servlet-context.xml
/WEB-INF/spring/security-context.xml
</param-value>
</init-param>
<init-param>
<param-name>throwExceptionIfNoHandlerFound</param-name>
<param-value>true</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<!--
web.xml의 설정은 WAS(Tomcat) 자체 설정일 뿐이다.
multipart-config : 메모리 사이즈, 업로드 파일 저장 위치, 최대 크기 설정
- location : 저장될 디렉토리(필수)
- max-file-size : 업로드 파일 최대 크기(기본값 : -1L, 제한이 없다)
- max-request-size : 한번 요청 시 업로드 파일 최대 크기
- file-size-threshold : 설정 크기가 넘는 경우 임시 디렉토리에 저장(기본값 0, 설정하지 않는 한 무조건 저장)
web.xml에서 설정하지 않을 때는 @MultipartConfig 어노테이션으로 설정이 가능하다.
- 요청을 받는 컨트롤러에 설정이 가능하다. (메소드 라인이 아니라 컨트롤러인 클래스 라인에 설정한다)
- @MultipartConfig(
location = "D:/upload",
maxFileSize = "24683394",
maxRequestSize = "478212294",
fileSizeThreshold = "154985741"
)
임시 파일이 저장되는 경로는 다음과 같다.
- C:\Users\개인PC이름\AppData\Local\Temp=
-->
<!-- <multipart-config>
<location>C:\\upload</location>
<max-file-size>24683394</max-file-size>
<max-request-size>478212294</max-request-size>
<file-size-threshold>154985741</file-size-threshold>
</multipart-config> -->
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--
한글 처리를 위한 UTF-8 필터 등록
JSP나 서블릿 처리할 때마다 넘겨받은 request를 setCharacterEncoding으로 UTF-8 설정했던 부분을 필터로 대체함
-->
<filter>
<filter-name>encordingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encordingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--
MultipartFilter 적용 : 파일 업로드를 위한 필터 적용
- MultipartFilter의 역할은 들어온 요청이 multipart/form-data 유형의 요청인지를 확인하여 multipart 형태의 요청이면
MultipartResolver를 통해 multipart 요청을 확인한다.
그리고 해당 요청이 적절한 요청이면 MultipartHttpServletRequest로 랩핑한다.
- MultipartFilter의 기본 빈 이름은 'filterMultipartResolver'이다.
-->
<filter>
<filter-name>MultipartFilter</filter-name>
<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>MultipartFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 서블릿 필터 클래스를 서블릿 컨테이너에 등록함 -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 3. 상태 코드를 사용한 이동 대상 페이지 설정 시작 -->
<!-- <error-page>
<error-code>400</error-code>
<location>/WEB-INF/views/error/errorCommon400.jsp</location>
</error-page>
<error-page>
<error-code>404</error-code>
<location>/WEB-INF/views/error/errorCommon404.jsp</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/WEB-INF/views/error/errorCommon500.jsp</location>
</error-page> -->
<!-- 3. 상태 코드를 사용한 이동 대상 페이지 설정 끝 -->
<!-- 4. 예외 타입을 사용한 이동 대상 페이지 설정 시작 -->
<!-- <error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/WEB-INF/views/error/errorCommon.jsp</location>
</error-page> -->
<!-- 4. 예외 타입을 사용한 이동 대상 페이지 설정 끝 -->
<!-- 5. 기본 에러 페이지 설정 시작 -->
<!-- <error-page>
<location>/WEB-INF/views/error/errorCommon.jsp</location>
</error-page> -->
<!-- 5. 기본 에러 페이지 설정 끝 -->
</web-app>
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
<beans:property name="order" value="2" />
</beans:bean>
<!-- Tiles 설정을 위한 Bean 등록 시작 -->
<beans:bean id="tilesViewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<beans:property name="viewClass" value="org.springframework.web.servlet.view.tiles3.TilesView" />
<beans:property name="order" value="1" />
</beans:bean>
<beans:bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
<beans:property name="definitions">
<beans:list>
<beans:value>/WEB-INF/spring/tiles-config.xml</beans:value>
</beans:list>
</beans:property>
</beans:bean>
<!-- Tiles 설정을 위한 Bean 등록 끝 -->
<context:component-scan base-package="kr.or.ddit.controller" />
<!--
서블릿 표준용 MultipartResolver 를 스프링 빈으로 정의
- StandardServletMultipartResolver 사용 시 설정
> Servlet 3.0의 Part를 이용한 MultipartFile 데이터 처리
-->
<!-- <beans:bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
</beans:bean> -->
<!--
pre-post-annotations 속성을 'enabled'로 설정하면 @PreAuthorize, @PostAuthorize를 사용할 수 있고
secured-annotations 속성을 'enabled'로 설정하면 @Secured를 사용할 수 있다.
-->
<context:component-scan base-package="kr.or.ddit.security" />
<security:global-method-security pre-post-annotations="enabled" secured-annotations="enabled" />
<!--
인터셉터 설정
- loginInterceptor 클래스를 빈으로 정의한다.
> 설정한 클래스는 해당 위치에 존재해야 함(설정에 맞는 위치에 있어야 한다)
-->
<!-- <beans:bean id="loginInterceptor" class="kr.or.ddit.controller.intercept.LoginInterceptor" />
<interceptors>
<interceptor>
<mapping path="/login1" />
<beans:ref bean="loginInterceptor" />
</interceptor>
</interceptors>
<beans:bean id="accessLoggingInterceptor" class="kr.or.ddit.controller.intercept.AccessLoggingInterceptor" />
<interceptors>
<interceptor>
<mapping path="/**" />
<exclude-mapping path="/resources/**"/>
<beans:ref bean="accessLoggingInterceptor" />
</interceptor>
</interceptors> -->
</beans:beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 6. 사용자 정의 접근 거부 처리자 Bean 추가 -->
<bean id="customAccessDenied" class="kr.or.ddit.security.CustomAccessDeniedHandler"></bean>
<bean id="customLoginSuccess" class="kr.or.ddit.security.CustomLoginSuccessHandler"></bean>
<!-- <bean id="customPasswordEncoder" class="kr.or.ddit.security.CustomNoOpPasswordEncoder"></bean> -->
<bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>
<bean id="customUserDetailsService" class="kr.or.ddit.security.CustomUserDetailsService"></bean>
<security:http>
<!--
폼 기반 인증 기능을 사용한다. : 사용자 정의 로그인 페이지 추가(login-page),
로그인 성공 처리 추가 : 로그인 성공 후 처리를 담당하는 처리자로 지정(customLoginSuccess)
-->
<security:form-login login-page="/login" authentication-success-handler-ref="customLoginSuccess" />
<!-- 6. 사용자 정의 접근 거부 처리자 추가 : customAccessDenied를 접근 거부 처리자로 등록 -->
<security:access-denied-handler ref="customAccessDenied" />
<!--
14. 자동 로그인 적용
- 데이터 소스를 지정하고 테이블을 이용해서 기존 로그인 정보를 기록
- 쿠키의 유효 시간을 지정한다. (604800 : 7일)
-->
<security:remember-me data-source-ref="dataSource" token-validity-seconds="604800" />
<!-- 9. 로그아웃 처리 : 로그아웃 처리를 위한 URI를 지정하고, 로그아웃한 후에 세션을 무효화한다. -->
<security:logout logout-url="/logout" invalidate-session="true"
delete-cookies="remember-me, JSESSION_ID" />
</security:http>
<security:authentication-manager>
<security:authentication-provider user-service-ref="customUserDetailsService">
<security:password-encoder ref="bcryptPasswordEncoder" />
</security:authentication-provider>
</security:authentication-manager>
</beans>
package kr.or.ddit.mapper;
import java.util.Map;
import kr.or.ddit.vo.crud.NoticeMemberVO;
public interface ILoginMapper {
public NoticeMemberVO loginCheck(NoticeMemberVO member);
public NoticeMemberVO idCheck(String memId);
public int signup(NoticeMemberVO memberVO);
public NoticeMemberVO idFind(Map<String, String> map);
public NoticeMemberVO pwFind(Map<String, String> map);
public NoticeMemberVO readByUserId(String username);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.or.ddit.mapper.ILoginMapper">
<resultMap type="noticeMemberVO" id="noticeMemberMap">
<id property="memNo" column="mem_no" />
<result property="memNo" column="mem_no" />
<result property="memId" column="mem_id" />
<result property="memPw" column="mem_pw" />
<result property="memName" column="mem_name" />
<result property="memGender" column="mem_gender" />
<result property="memEmail" column="mem_email" />
<result property="memPhone" column="mem_phone" />
<result property="memPostCode" column="mem_postcode" />
<result property="memAddress1" column="mem_address1" />
<result property="memAddress2" column="mem_address2" />
<result property="memProfileImg" column="mem_profileimg" />
<result property="memRegDate" column="mem_regdate" />
<result property="enabled" column="enabled" />
<collection property="authList" resultMap="noticeAuthMap" />
</resultMap>
<resultMap type="noticeMemberAuthVO" id="noticeAuthMap">
<result property="memNo" column="mem_no" />
<result property="auth" column="auth" />
</resultMap>
<!-- ctrl + shift + y : 소문자 변환 -->
<select id="loginCheck" parameterType="noticeMemberVO" resultType="noticeMemberVO">
select
mem_no,
mem_id,
mem_pw,
mem_name,
mem_gender,
mem_email,
mem_phone,
mem_postcode,
mem_address1,
mem_address2,
mem_agree,
mem_profileimg,
mem_regdate
from noticemember
where mem_id = #{memId}
and mem_pw = #{memPw}
</select>
<select id="idCheck" parameterType="string" resultType="noticeMemberVO">
select
mem_no,
mem_id,
mem_pw,
mem_name,
mem_gender,
mem_email,
mem_phone,
mem_postcode,
mem_address1,
mem_address2,
mem_agree,
mem_profileimg,
mem_regdate
from noticemember
where mem_id = #{memId}
</select>
<insert id="signup" parameterType="noticeMemberVO">
insert into noticemember (
mem_no,
mem_id,
mem_pw,
mem_name,
mem_gender,
mem_email,
mem_phone,
mem_postcode,
mem_address1,
mem_address2,
mem_agree,
mem_profileimg,
mem_regdate,
enabled
) values (
seq_noticemember.nextval,
#{memId},
#{memPw},
#{memName},
#{memGender},
#{memEmail},
#{memPhone},
#{memPostCode},
#{memAddress1},
#{memAddress2},
#{memAgree},
#{memProfileImg},
sysdate,
1
)
</insert>
<select id="idFind" parameterType="java.util.HashMap" resultType="noticeMemberVO">
select
mem_no,
mem_id,
mem_pw,
mem_name,
mem_gender,
mem_email,
mem_phone,
mem_postcode,
mem_address1,
mem_address2,
mem_agree,
mem_profileimg,
mem_regdate
from noticemember
where 1=1
and mem_email = #{memEmail}
and mem_name = #{memName}
</select>
<select id="pwFind" parameterType="java.util.HashMap" resultType="noticeMemberVO">
select
mem_no,
mem_id,
mem_pw,
mem_name,
mem_gender,
mem_email,
mem_phone,
mem_postcode,
mem_address1,
mem_address2,
mem_agree,
mem_profileimg,
mem_regdate
from noticemember
where 1=1
and mem_id = #{memId}
and mem_email = #{memEmail}
and mem_name = #{memName}
</select>
<select id="readByUserId" parameterType="string" resultMap="noticeMemberMap">
select
m.mem_no, mem_id, mem_pw, mem_name, mem_gender, mem_email, mem_phone,
mem_postcode, mem_address1, mem_address2, mem_agree, mem_profileimg, mem_regdate, enabled,
a.auth
from noticemember m left outer join noticemember_auth a on(m.mem_no = a.mem_no)
where m.mem_id = #{memId}
</select>
</mapper>
package kr.or.ddit.vo;
import java.util.Collection;
import java.util.stream.Collectors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import kr.or.ddit.vo.crud.NoticeMemberVO;
public class CustomUser extends User {
private NoticeMemberVO member;
public CustomUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public CustomUser(NoticeMemberVO member) {
// Java 스트림을 사용한 경우(람다 표현식)
// - 자바 버전 8부터 추가된 기능
// map : 컬렉션(List, Map, Set 등), 배열 등의 설정되어 있는 각 타입의 값들을 하나씩 참조하여 람다식으로 반복 처리할 수 있게 해준다.
// collect : Stream()을 돌려 발생되는 데이터를 가공 처리 하고 원하는 형태의 자료형으로 변환을 돕는다.
// 회원 정보 안에 들어 있는 역할명 들을 컬렉션 형태의 스트림으로 만들어서 보내준다.
// *** 람다 표현식은 복잡한 메서드 라인을 간단한 표현식으로 출력할 수 있다는 장점이 있는 대신 디버깅을 할 수 없다는 점이 단점이다.
super(member.getMemId(), member.getMemPw(), member.getAuthList().stream().map(auth -> new SimpleGrantedAuthority(auth.getAuth())).collect(Collectors.toList()));
this.member = member;
}
public NoticeMemberVO getMember() {
return member;
}
public void setMember(NoticeMemberVO member) {
this.member = member;
}
}
package kr.or.ddit.security;
import javax.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import kr.or.ddit.mapper.ILoginMapper;
import kr.or.ddit.vo.CustomUser;
import kr.or.ddit.vo.crud.NoticeMemberVO;
public class CustomUserDetailsService implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(CustomUserDetailsService.class);
@Inject
private BCryptPasswordEncoder bpe;
@Inject
private ILoginMapper loginMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("loadUserByUsername() 실행...!");
log.info("Load User By Username : " + username);
// UserDetailService를 등록하는 과정에서 우리가 할 목표는 User 객체의 정보와
// 인증되어 실제로 사용될 내 id에 해당하는 회원정보를 CrudMember에 담고 그 녀석을 UserDetails 정보 안에서
// 가용할 수 있도록 만든다.
NoticeMemberVO member;
try {
member = loginMapper.readByUserId(username);
log.info("queried by member mapper : " + member);
return member == null ? null : new CustomUser(member);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
package kr.or.ddit.controller.crud.notice;
import java.util.List;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import kr.or.ddit.service.INoticeService;
import kr.or.ddit.vo.crud.NoticeVO;
import kr.or.ddit.vo.crud.PaginationInfoVO;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
@RequestMapping("/notice")
public class NoticeRetrieveController {
@Inject
private INoticeService noticeService;
@PreAuthorize("hasAnyRole('ROLE_MEMBER', 'ROLE_ADMIN')")
@RequestMapping(value = "/list.do")
public String noticeList(
@RequestParam(name = "page", required = false, defaultValue = "1") int currentPage,
@RequestParam(required = false, defaultValue = "title") String searchType,
@RequestParam(required = false) String searchWord,
Model model
) {
PaginationInfoVO<NoticeVO> pagingVO = new PaginationInfoVO<NoticeVO>();
// 검색 기능 추가
// 검색을 했을 때의 조건은 키워드(searchWord)가 넘어왔을 때 정확하게 검색을 진행한 거니까
// 이 때, 검색을 진행하기 위한 타입과 키워드를 PaginationInfoVO에 셋팅하고 목록을 조회하기 위한 조건으로
// 쿼리를 조회할 수 있도록 보내준다.
if(StringUtils.isNotBlank(searchWord)) {
pagingVO.setSearchType(searchType);
pagingVO.setSearchWord(searchWord);
model.addAttribute("searchType", searchType);
model.addAttribute("searchWord", searchWord);
}
// 현재 페이지 전달 후, start/endRow와 start/endPage 설정
pagingVO.setCurrentPage(currentPage);
int totalRecord = noticeService.selectNoticeCount(pagingVO); // 총 게시글 수 가져오기
pagingVO.setTotalRecord(totalRecord);
List<NoticeVO> dataList = noticeService.selectNoticeList(pagingVO);
pagingVO.setDataList(dataList);
model.addAttribute("pagingVO", pagingVO);
return "notice/list";
}
@RequestMapping(value = "/detail.do", method = RequestMethod.GET)
public String noticeDetail(int boNo, Model model) {
NoticeVO noticeVO = noticeService.selectNotice(boNo);
model.addAttribute("notice", noticeVO);
return "notice/detail";
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 6. 사용자 정의 접근 거부 처리자 Bean 추가 -->
<bean id="customAccessDenied" class="kr.or.ddit.security.CustomAccessDeniedHandler"></bean>
<bean id="customLoginSuccess" class="kr.or.ddit.security.CustomLoginSuccessHandler"></bean>
<!-- <bean id="customPasswordEncoder" class="kr.or.ddit.security.CustomNoOpPasswordEncoder"></bean> -->
<bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>
<bean id="customUserDetailsService" class="kr.or.ddit.security.CustomUserDetailsService"></bean>
<security:http>
<!--
폼 기반 인증 기능을 사용한다. : 사용자 정의 로그인 페이지 추가(login-page),
로그인 성공 처리 추가 : 로그인 성공 후 처리를 담당하는 처리자로 지정(customLoginSuccess)
-->
<security:form-login login-page="/notice/login.do" authentication-success-handler-ref="customLoginSuccess" />
<!-- 6. 사용자 정의 접근 거부 처리자 추가 : customAccessDenied를 접근 거부 처리자로 등록 -->
<security:access-denied-handler ref="customAccessDenied" />
<!--
14. 자동 로그인 적용
- 데이터 소스를 지정하고 테이블을 이용해서 기존 로그인 정보를 기록
- 쿠키의 유효 시간을 지정한다. (604800 : 7일)
-->
<security:remember-me data-source-ref="dataSource" token-validity-seconds="604800" />
<!-- 9. 로그아웃 처리 : 로그아웃 처리를 위한 URI를 지정하고, 로그아웃한 후에 세션을 무효화한다. -->
<security:logout logout-url="/logout" invalidate-session="true"
delete-cookies="remember-me, JSESSION_ID" />
</security:http>
<security:authentication-manager>
<security:authentication-provider user-service-ref="customUserDetailsService">
<security:password-encoder ref="bcryptPasswordEncoder" />
</security:authentication-provider>
</security:authentication-manager>
</beans>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<div class="login-box">
<div class="card">
<div class="card-body login-card-body">
<h2 class="login-box-msg"><b>DDIT</b> BOARD</h2>
<form action="/login" method="post" id="signForm" name="signForm">
<div class="input-group mb-3">
<input type="text" id="memId" name="username" class="form-control" placeholder="아이디를 입력해주세요">
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-envelope"></span>
</div>
</div>
<span class="error invalid-feedback" style="display: block;">${errors.memId }</span>
</div>
<div class="input-group mb-3">
<input type="password" id="memPw" name="password" class="form-control" placeholder="비밀번호를 입력해주세요">
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-lock"></span>
</div>
</div>
<span class="error invalid-feedback" style="display: block;">${errors.memPw }</span>
</div>
<div class="row">
<div class="col-8">
<div class="icheck-primary">
<input type="checkbox" id="remember" name="remember-me">
<label for="remember">
Remember Me
</label>
</div>
</div>
<div class="col-4">
<button type="button" class="btn btn-dark btn-block" id="loginBtn">로그인</button>
</div>
</div>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
<sec:csrfInput />
</form>
<script type="text/javascript">
$(function() {
// 로그인 페이지를 요청 시, background에 이미지 삽입
$("body").css("background-image", "url('${pageContext.request.contextPath}/resources/dist/img/background04.jpg')").css("background-size", "cover");
var idFindBtn = $("#idFindBtn");
var pwFindBtn = $("#pwFindBtn");
var loginBtn = $("#loginBtn");
var idFindForm = $("#idFindForm");
var pwFindForm = $("#pwFindForm");
idFindBtn.on("click", function(){
var memEmail = $("#memEmail").val();
var memName = $("#memName").val();
if(!memEmail) {
alert("이메일을 입력해 주세요.");
$("#memEmail").focus();
return false;
}
if(!memName) {
alert("이름을 입력해 주세요.");
$("#memName").focus();
return false;
}
var data = {
memEmail : memEmail,
memName : memName
};
$.ajax({
type: "post",
url: "/notice/idFind.do",
data: JSON.stringify(data),
contentType: "application/json;charset=utf-8",
/* beforeSend: function(xhr) { // 데이터 전송 전 헤더에 csrf 값 설정
xhr.setRequestHeader("${_csrf.headerName}", "${_csrf.token}"");
}, */
beforeSend: function(xhr) { // 데이터 전송 전 헤더에 csrf 값 설정
xhr.setRequestHeader(header, token);
},
success: function(res) {
console.log("결과 : " + res);
// 회원님의 아이디는 [<font id="id" color="red" class="h2"></font>] 입니다.
if(!res) { // 리턴받은 결과가 없음, 즉 아이디 없음
$(".idText").html("회원님의 아이디를 찾을 수 없습니다.");
}else { // 아이디 있음
var id = res.memId;
$(".idText").html("회원님의 아이디는 [<font id='id' class='h2' style='color:red'>"+id+"</font>] 입니다.");
}
}
});
});
pwFindBtn.on("click", function(){
var memId = $("#memId").val();
var memEmail = $("#memEmail2").val();
var memName = $("#memName2").val();
if(!memId) {
alert("아이디를 입력해 주세요.");
$("#memId").focus();
return false;
}
if(!memEmail) {
alert("이메일을 입력해 주세요.");
$("#memEmail2").focus();
return false;
}
if(!memName) {
alert("이름을 입력해 주세요.");
$("#memName2").focus();
return false;
}
var data = {
memId : memId,
memEmail : memEmail,
memName : memName
};
$.ajax({
type: "post",
url: "/notice/pwFind.do",
data: JSON.stringify(data),
contentType: "application/json;charset=utf-8",
beforeSend: function(xhr) { // 데이터 전송 전 헤더에 csrf 값 설정
xhr.setRequestHeader(header, token);
},
success: function(res) {
console.log("결과 : " + res);
// 회원님의 비밀번호는 [<font color="red" class="h2" id="password"></font>] 입니다.
if(!res) { // 리턴받은 결과가 없음, 즉 비밀번호 없음
$(".pwText").html("회원님의 비밀번호를 찾을 수 없습니다.");
}else { // 비밀번호 있음
var pw = res.memPw;
$(".pwText").html("회원님의 비밀번호는 [<font id='password' class='h2' style='color:red'>"+pw+"</font>] 입니다.");
}
}
});
});
loginBtn.on("click", function(){
location.href = "/notice/login.do";
});
});
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- csrf meta 정보를 등록 -->
<meta id="_csrf" name="_csrf" content="${_csrf.token }" />
<meta id="_csrf_header" name="_csrf_header" content="${_csrf.headerName }" />
<title>AdminLTE 3 | Log in</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
<link rel="stylesheet" href="${pageContext.request.contextPath }/resources/plugins/fontawesome-free/css/all.min.css">
<link rel="stylesheet" href="${pageContext.request.contextPath }/resources/plugins/icheck-bootstrap/icheck-bootstrap.min.css">
<link rel="stylesheet" href="${pageContext.request.contextPath }/resources/dist/css/adminlte.min.css">
<script src="${pageContext.request.contextPath }/resources/plugins/jquery/jquery.min.js"></script>
<script type="text/javascript">
var token = "";
var header = "";
$(function(){
token = $("meta[name='_csrf']").attr("content");
header = $("meta[name='_csrf_header']").attr("content");
});
</script>
</head>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- csrf meta 정보를 등록 -->
<meta id="_csrf" name="_csrf" content="${_csrf.token }" />
<meta id="_csrf_header" name="_csrf_header" content="${_csrf.headerName }" />
<title>AdminLTE 3 | Simple Tables</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
<link rel="stylesheet" href="${pageContext.request.contextPath }/resources/plugins/fontawesome-free/css/all.min.css">
<link rel="stylesheet" href="${pageContext.request.contextPath }/resources/dist/css/adminlte.min.css">
<script src="${pageContext.request.contextPath }/resources/plugins/jquery/jquery.min.js"></script>
<script type="text/javascript" src="${pageContext.request.contextPath }/resources/ckeditor/ckeditor.js"></script>
<script type="text/javascript">
var token = "";
var header = "";
$(function(){
token = $("meta[name='_csrf']").attr("content");
header = $("meta[name='_csrf_header']").attr("content");
});
</script>
</head>
- http://localhost/notice/forget.do
- http://localhost/notice/list.do
package kr.or.ddit.controller.exception;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.NoHandlerFoundException;
// @ControllerAdvice 어노테이션은 스프링 컨트롤러에서 발생하는 예외를 처리하는 핸들러 클래스임을 명시한다.
@ControllerAdvice
public class CommonExceptionHandler {
// // @ExceptionHandler 어노테이션은 괄호 안에 설정한 예외 타입을 해당 메소드가 처리한다는 것을 의미한다.
// @ExceptionHandler(Exception.class)
// public String Handle(Exception e, Model model) {
// model.addAttribute("exception", e);
// return "error/errorCommon";
// }
//
// @ExceptionHandler(NoHandlerFoundException.class)
// @ResponseStatus(value = HttpStatus.NOT_FOUND)
// public String handle404(Exception e) {
// return "error/custom404";
// }
}
- http://localhost/notice/login.do
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
<sec:csrfInput/>
</form>
idCheckBtn.on("click", function(){
var id = $("#memId").val();
if(!id) {
alert("아이디를 입력해 주세요!");
return false;
}
var data = {
memId : id
};
$.ajax({
type: "post",
url: "/notice/idCheck.do",
data: JSON.stringify(data),
contentType: "application/json;charset=utf-8",
beforeSend: function(xhr) { // 데이터 전송 전 헤더에 csrf 값 설정
xhr.setRequestHeader(header, token);
},
success: function(res) {
console.log("중복확인 후 넘겨받은 결과 : " + res);
if(res == "NOTEXIST") { // 아이디 사용 가능
alert("사용 가능한 아이디 입니다.");
idCheckFlag = true; // 중복 확인 했다는 flag 설정
}else { // 아이디 중복
alert("이미 사용중인 아이디 입니다.");
//idCheckFlag = false; // 중복 확인 했다는 flag 설정
}
}
});
});
@RequestMapping(value = "/signup.do", method = RequestMethod.POST)
public String signup(HttpServletRequest req, NoticeMemberVO memberVO, Model model, RedirectAttributes ra) {
String goPage = "";
Map<String, String> errors = new HashMap<String, String>();
if(StringUtils.isBlank(memberVO.getMemId())) {
errors.put("memId", "아이디를 입력해 주세요.");
}
if(StringUtils.isBlank(memberVO.getMemPw())) {
errors.put("memId", "비밀번호를 입력해 주세요.");
}
if(StringUtils.isBlank(memberVO.getMemName())) {
errors.put("memId", "이름을 입력해 주세요.");
}
if(errors.size() > 0) { // 넘겨받은 데이터의 에러가 존재
model.addAttribute("errors", errors);
model.addAttribute("member", memberVO);
model.addAttribute("bodyText", "register-page");
goPage = "conn/register";
}else { // 정상적인 데이터를 받았을 때
ServiceResult result = noticeService.signup(req, memberVO);
if(result.equals(ServiceResult.OK)) { // 가입 성공
ra.addFlashAttribute("message", "회원가입을 완료하였습니다!");
goPage = "redirect:/notice/login.do";
}else { // 가입 실패
model.addAttribute("message", "서버에러, 다시 시도해 주세요!");
model.addAttribute("member", memberVO);
model.addAttribute("bodyText", "register-page");
goPage = "conn/register";
}
}
return goPage;
}
@Service
public class NoticeServiceImpl implements INoticeService {
@Inject
private INoticeMapper noticeMapper;
@Inject
private ILoginMapper loginMapper;
@Inject
private IProfileMapper profileMapper;
private TelegramSendController tst = new TelegramSendController();
// 스프링 시큐리티를 활용한 비밀번호 암호화를 처리할 PasswordEncoder 의존성 주입
@Inject
private PasswordEncoder pe;
@Override
public ServiceResult signup(HttpServletRequest req, NoticeMemberVO memberVO) {
ServiceResult result = null;
// 회원가입 시, 프로필 이미지로 파일을 업로드 하는데 이때 업로드 할 서버 경로
String uploadPath = req.getServletContext().getRealPath("/resources/profile");
File file = new File(uploadPath);
if (!file.exists()) {
file.mkdirs();
}
String proFileImg = ""; // 회원정보에 추가될 프로필 이미지 경로
try {
// 넘겨받은 회원정보에서 파일 데이터 가져오기
MultipartFile proFileImgFile = memberVO.getImgFile();
// 넘겨받은 파일 데이터가 존재할 때
if (proFileImgFile.getOriginalFilename() != null && !proFileImgFile.getOriginalFilename().equals("")) {
String fileName = UUID.randomUUID().toString(); // UUID 파일명 생성
fileName += "_" + proFileImgFile.getOriginalFilename(); // UUID_원본파일명으로 파일명 생성
uploadPath += "/" + fileName; // /resources/profile/uuid_원본파일명
proFileImgFile.transferTo(new File(uploadPath)); // 해당 위치에 파일 복사
proFileImg = "/resources/profile/" + fileName; // 파일 복사가 일어난 파일의 위치로 접근하기 위한 URI 설정
}
memberVO.setMemProfileImg(proFileImg);
// 비밀번호 암호화(스프링 시큐리티 적용 후)
memberVO.setMemPw(pe.encode(memberVO.getMemPw()));
} catch (Exception e) {
e.printStackTrace();
}
int status = loginMapper.signup(memberVO);
if (status > 0) { // 등록 성공
// 한명의 회원이 등록될 때 하나의 권한을 무조건 가질 수 있도록 권한 등록(스프링 시큐리티 적용 이후)
loginMapper.signupAuth(memberVO.getMemNo());
result = ServiceResult.OK;
} else {
result = ServiceResult.FAILED;
}
return result;
}
package kr.or.ddit.mapper;
import java.util.Map;
import kr.or.ddit.vo.crud.NoticeMemberVO;
public interface ILoginMapper {
public NoticeMemberVO loginCheck(NoticeMemberVO member);
public NoticeMemberVO idCheck(String memId);
public int signup(NoticeMemberVO memberVO);
public NoticeMemberVO idFind(Map<String, String> map);
public NoticeMemberVO pwFind(Map<String, String> map);
public NoticeMemberVO readByUserId(String username);
public void signupAuth(int memNo);
}
<insert id="signup" parameterType="noticeMemberVO" useGeneratedKeys="true">
<selectKey keyProperty="memNo" order="BEFORE" resultType="int">
select seq_noticemember.nextval from dual
</selectKey>
insert into noticemember (
mem_no,
mem_id,
mem_pw,
mem_name,
mem_gender,
mem_email,
mem_phone,
mem_postcode,
mem_address1,
mem_address2,
mem_agree,
mem_profileimg,
mem_regdate,
enabled
) values (
#{memNo},
#{memId},
#{memPw},
#{memName},
#{memGender},
#{memEmail},
#{memPhone},
#{memPostCode},
#{memAddress1},
#{memAddress2},
#{memAgree},
#{memProfileImg},
sysdate,
1
)
</insert>
<insert id="signupAuth" parameterType="int">
insert into
noticemember_auth
values(#{memNo}, 'ROLE_MEMBER')
</insert>
- http://localhost/notice/signup.do
'대덕인재개발원 > 대덕인재개발원_웹기반 애플리케이션' 카테고리의 다른 글
231216_SPRING CRUD 보강 1 (0) | 2023.12.15 |
---|---|
231213_SPRING 2 (16-1) (0) | 2023.12.12 |
231212_SPRING 2 (15-1) (0) | 2023.12.11 |
231211_SPRING 2 (14-1) (0) | 2023.12.11 |
231208_SPRING 2 (13-2) (0) | 2023.12.08 |