스프링 시큐리티

Back-End/Spring 2019. 7. 13. 12:28

스프링 시큐리티의 개요


1) 웹 보안의 3요소


- 인증 (Authentication) : 애플리케이션의 작업을 수행할 수 있는 주체 (사용자). 현재 접속중인 사용자가 누구인지 확인하는 과정


- 권한 인가 (Authorization) : 인증된 주체가 애플리케이션의 동작을 수행 할 수 있도록 허락되어 있는지 증명하는 과정.

현재 사용자가 특정 url에 접속할 권한이 있는지 검사하는 과정


- UI 처리 : 권한이 없는 사용자가 접근할 경우의 에러 화면 등을 보여주는 과정



2) 스프링 시큐리티


개발자가 직접 처리하던 보안 처리 과정을 스프링 프레임워크에서 제공하는 스프링 시큐리티를 사용하여 사용권한 관리,


비밀번호 암호화, 회원가입 처리, 로그인, 로그아웃 등의 웹 보안 관련 기능 개발을 쉽게 처리할 수 있음.



-실습소스-


1) 회원 정보 테이블 생성

1
2
3
4
5
6
7
8
create table users(
    userid varchar2(255not null--id
    passwd varchar2(255not null--비밀번호, 암호화 할 예정
    name varchar2(255not null--이름
    enabled number(1default 1--사용가능 여부, 값이 1이면 사용가능 , 0이면 사용할 수 없는 id라는걸 표시하는 속성
    authority varchar2(20default 'ROLE_USER',  --권한설정 속성, ROLE_USER와 admin으로 구성되어 있다. / 일반사용자/관리자
    primary key(userid) --기본키를 id로 설정함
);
cs



스프링 레거시 프로젝트로 작성.


pom.xml에 버전을 일부 수정하고, 오라클 라이브러리를 추가, spring security 라이브러리를 추가한다. 나머지는 샘플 pom.xml에 있는것과 동일


pom.xml 중 일부

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
    <properties>
        <java-version>1.8</java-version>
        <org.springframework-version>5.1.4.RELEASE</org.springframework-version>
        <org.aspectj-version>1.9.2</org.aspectj-version>
        <org.slf4j-version>1.7.25</org.slf4j-version>
        <spring.security.version>5.1.3.RELEASE</spring.security.version>
    </properties>



      <!-- 오라클 라이브러리 -->
<!--오라클도 상용 라이브러리이기 때문에 maven 저장소에는 오픈소스가 올라가져 있고 버전도 맞지 않는다.
 이 저장소를 추가해서 오라클 라이브러리를 추가 --> <repositories> <repository> <id>codelds</id> <url>https://code.lds.org/nexus/content/groups/main-repo</url> </repository> </repositories>
 
 
============================================================================================
 
 
        <!-- Spring Security -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-core</artifactId>
            <version>${spring.security.version}</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
 
 
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>${spring.security.version}</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
 
 
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>${spring.security.version}</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
 
 
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-taglibs</artifactId>
            <version>${spring.security.version}</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>

cs




web.xml을 열고, 스프링 시큐리티 관련 코드를 추가

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" 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 http://java.sun.com/xml/ns/javaee/web-app_2_5.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/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
            </param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
 
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>

//"/"라는것은 디폴트 서블릿 서블릿으로 요청이 들어오면 dispatcherservlet로 처리를 하고, dispatcherservlet에서는
// servlet-context를 읽어들여서 맵핑을 처리한다는 의미

    </servlet-mapping>
 

<!-- 스프링 시큐리티에서 사용하는 세션 이벤트 처리 관련 리스터 세션 만료 체크, 동시 로그인 제한 등의 기능 제공 -->
    <listener>
        <listener-class>
org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
    </listener>
    
    
    <!-- 애플리케이션의 모든 요청을 스프링 시큐리티에서 처리하도록 하는 필터 -->
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class> org.springframework.web.filter.DelegatingFilterProxy </filter-class>
    </filter>                                <!-- DelegatingFilterProxy서버가 돌아가면서 스프링시큐리티에서 처리를 하게 함 -->
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
// "/*" 는 모든 요청이라는 의미이고, 모든 요청시에는 DelegatingFilterProxy라는 스프링 시큐리티필터를 한번 거쳐서
// 가게 되어 있다.

    </filter-mapping>
    
    
    
    <!-- 한글 처리를 위한 인코딩 필터 -->
    <filter>
        <filter-name>encoding</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>
    </filter>
    <filter-mapping>
        <filter-name>encoding</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- 세션 타임아웃 설정 -->
    <session-config>
        <session-timeout>20</session-timeout>
    </session-config>
 
</web-app>
cs



servlet-context (서블릿 관련 설정 파일)

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?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:tx="http://www.springframework.org/schema/tx"
    xmlns:security="http://www.springframework.org/schema/security"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.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 -->
        
    <!-- view 밑에 include 디렉토리를 추가할 예정 -->
    <resources mapping="/resources/**" location="/resources/" />
    <resources location="/WEB-INF/views/include/" mapping="/include/**" />
    
    
    <!-- Resolves views selected for rendering by @Controllers to .jsp resources 
        in the /WEB-INF/views directory -->
        
        <!-- view resolver 추가 접두사와 접미사 추가. -->
    <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>
    
    <!-- com.example.security 하위에 있는 파일들을 다 빈으로 등록하고 찾아서 맵핑을 시킨다는 의미 -->
    <context:component-scan base-package="com.example.security" />
    
    <!-- @Secured 어노테이션 활성화, 사용 권한 제한 -->
    <security:global-method-security
        secured-annotations="enabled" />
        
</beans:beans>
cs



프로젝트의 spring폴더 하위에 security 폴더를 생성하고 폴더안에 security-context.xml 파일을 생성

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans 
    xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p" 
    xmlns:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation=
    "http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
 
 
    <!-- 정적 리소스 파일들에는 보안 설정을 하지 않음 -->
    <!-- include폴더 안에는 css같은 소스파일만 들어감 -->
    <http pattern="/include/**" security="none" />
    <http auto-config="true" use-expressions="true" 
        create-session="never">
        
        <!-- 관리자 영역 설정 , 관리자만 이 url에 접속할 수 있도록 한다.-->
        <intercept-url pattern="/admin/**" 
            access="hasRole('ROLE_ADMIN')" />
            
        <!-- 권한에 관계없이 접속 가능한 영역(guest도 접속 가능) -->
        <intercept-url pattern="/user/**" access="permitAll" />
        
        
        <!-- 로그인한 사용자 영역 -->
        <!-- 일발유저,테스터, 관리자, 손님까지 다 들어올수 있도록 한다는 의미 -->
        <intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_TEST','ROLE_ADMIN','ROLE_GUEST')" />
 
 
 
        <!-- 로그인폼 -->
        <!-- 로그인페이지, login_check는 로그인을 체크 
        userLoginSuccessHandler는 로그인을 성공했을때 처리할 코드
        userLoginFailureHandler는 로그인을 실패했을때 처리할 코드 -->
        <form-login login-page="/user/login.do" 
            login-processing-url="/user/login_check.do"
            authentication-success-handler-ref=
                "userLoginSuccessHandler"
            authentication-failure-handler-ref=
                "userLoginFailureHandler"
            username-parameter="userid" 
                password-parameter="passwd" /<!--userid는 유저아이디, passwd는 비밀번호-->
        <session-management>
        
        
        
    <!-- max-sessions="1" 동시접속 막기 (만약 2라고 설정하면 두 사람까지 접속 가능) error-if-maximum-exceeded="true" 로그인 세션 
        초과시 에러 옵션 expired-url="/user/login" 세션 만료시 이동할 주소 -->
    
        <concurrency-control max-sessions="1"
                expired-url="/user/login.do" 
                error-if-maximum-exceeded="true" />
                <!-- 만약 1명보다 더 많은 사람이 접속하려고 하면 에러메시지를 출력하게끔 설정 -->
                
        </session-management>
        
        
        
        <!-- 로그아웃 관련 처리 -->
        <logout delete-cookies=
        "JSESSIONID,SPRING_SECURITY_REMEMBER_ME_COOKIE"
            logout-success-url="/user/login.do" 
            logout-url="/user/logout.do"
            invalidate-session="true" />

        <!-- 접근권한이 없는 경우의 코드 (ex 관리자가 아니면 접속할 수 없습니다.)-->
        <access-denied-handler ref="userDeniedHandler" />

        <!-- ref는 bean의 id이다. -->
        <!-- 자동 로그인 관련 쿠키 저장, 세션이 종료된 후에도 자동 로그인할 수 있는 기능 86400 1일, 604800 7일 -->
        <remember-me key="userKey" 
        token-validity-seconds="86400" />
    </http>
    <beans:bean id="userDeniedHandler"
        class="com.example.security.service.UserDeniedHandler" />


    <beans:bean id="userLoginSuccessHandler"
        class= "com.example.security.service.UserLoginSuccessHandler" />
<!-- UserLoginSuccessHandler에는 로그인이 성공했을때 처리할 코드 작성 -->
    
<beans:bean id="userLoginFailureHandler" class= "com.example.security.service.UserLoginFailureHandler" />
<!-- UserLoginFailHandler에는 로그인이 실패 했을때 처리할 코드 작성 -->
 
    <!-- 로그인 인증을 처리하는 빈 -->
    <beans:bean id="userService" class= "com.example.security.service.UserAuthenticationService">
<!--UserAuthenticationService에는 로그인을 실제로 처리할 로직이 들어간다.  -->
 
        <beans:constructor-arg name = "sqlSession" ref = "sqlSession" />
    </beans:bean>


    <!-- 사용자가 입력한 비밀번호를 암호화된 비밀번호와 체크하는 로직이 포함됨 -->
    <authentication-manager>
        <authentication-provider user-service-ref = "userService" >
            <password-encoder ref="passwordEncoder">
                <salt-source user-property="username" />
            </password-encoder>
        </authentication-provider>
    </authentication-manager>
    
    
    <!-- 비밀번호 암호화 빈 -->
    <beans:bean id= "passwordEncoder" class= "org.springframework.security.authentication.encoding.ShaPasswordEncoder">
       <beans:constructor-arg name="strength" value="256" />
    </beans:bean>
</beans:beans>
cs



root-context.xml 작성 (트랜잭션 처리를 위해 소스탭에서 tx를 체크하기)

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?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:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
    
<!-- Root Context: defines shared resources visible to all other web components -->
    <!-- 오라클 연결 -->
    <bean id= "dataSource"
        class= "org.springframework.jdbc.datasource.DriverManagerDataSource">
        <!-- 드라이버 클래스 이름이 변경됨 -->
        <property name= "driverClassName" value= "net.sf.log4jdbc.sql.jdbcapi.DriverSpy"></property>
        <!--지금은 sql코드 디버깅을 위해서 driverspy를 쓰고 있음  -->
        
        
        
        <!-- 연결문자열에 log4jdbc가 추가됨 -->
        <property name="url" value=
        "jdbc:log4jdbc:oracle:thin:@localhost:1521/xe" />
        <!-- log4jdbc는 쿼리가 실행될때 로그로 전부다 출력하게 해주는 코드 -->
        <property name="username" value="spring" /<!-- 스프링 계정 아이디 -->
        <property name="password" value="1234" /<!-- 스프링 계정 비밀번호 -->
    </bean>
    
    
    <!-- SqlSessionFactory 객체 주입 -->
    <!-- mapper과 mybatis에 대한 정의 -->
    <bean id="sqlSessionFactory" class=
    "org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" 
        value="classpath:/mybatis-config.xml"></property>
        <property name="mapperLocations" 
        value="classpath:mappers/**/*Mapper.xml"></property>
    </bean>
    
    
    <!-- SqlSession 객체 주입 -->
    <bean id="sqlSession" class"org.mybatis.spring.SqlSessionTemplate" destroy-method="clearCache">
        <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory">
        
        </constructor-arg>
    </bean>
    
    <!-- 트랜잭션 처리를 할때 transactionManager라는 태그를 만들 예정-->
    <!-- 트랜잭션 어노테이션을 처리해주는 코드 -->
    <tx:annotation-driven transaction-manager="transactionManager" />
    
    
    <!-- transactionManager는 DataSourceTransactionManager는 클래스를 지정-->
    <bean id"transactionManager"
        class"org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
</beans>
cs





log4j.xml 은 프로젝트 생성당시 만들어진것을 그대로 사용.



mybatis-config.xml 은 xml파일을 새로 생성해서 사용. (생성만하고 그대로 놔둠)



log4jdbc.log4j2.properties 파일을 추가

1
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator //sql 명령어를 로깅 처리할 수 있는 코드
cs



logback.xml 은 샘플 프로젝트에 있는 것을 그대로 사용

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- log4jdbc-log4j2 -->
    <logger name="jdbc.sqlonly"        level="DEBUG"/>
    <logger name="jdbc.sqltiming"      level="INFO"/>
    <logger name="jdbc.audit"          level="WARN"/>
    <logger name="jdbc.resultset"      level="ERROR"/>
    <logger name="jdbc.resultsettable" level="ERROR"/>
    <logger name="jdbc.connection"     level="INFO"/>
    
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-4level [%logger.%method:%line]-
                %msg%n</pattern>
        </layout>
    </appender>
 
    <appender name="LOGFILE"
        class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/WEB-INF/logback.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logback.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 30일 지난 파일은 삭제한다. -->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-4level [%logger.%method:%line]
                - %msg %n</pattern>
        </encoder>
    </appender>
 
    <!-- 로그의 레벨( 지정된 로그 레벨 이상만 수집 ) : DEBUG < INFO < WARN < ERROR < FATAL -->
    <logger name="myweb" additivity="false">
        <level value="INFO" />
        <appender-ref ref="LOGFILE" />
        <appender-ref ref="CONSOLE" />
    </logger>
 
    <root>
        <level value="INFO" />
        <appender-ref ref="CONSOLE" />
    </root>
 
</configuration>
 
cs




회원가입, 로그인, 로그아웃만 작성할 예정


UserDTO.java 클래스를 생성

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
32
33
34
35
36
37
38
39
package com.example.security.model.dto;
 
import java.util.Collection;
 
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
 
public class UserDTO extends User {
    
    private String userid;
    
    public UserDTO(String username, String password, 
            boolean enabled, boolean accountNonExpired,
            boolean credentialsNonExpired, 
            boolean accountNonLocked, 
            Collection<extends GrantedAuthority> authorities,
            String userid) {
        
    
        super(username, password, enabled, accountNonExpired
                , credentialsNonExpired, accountNonLocked, authorities);
        //super()안에 들어가있는 필드들은 user클래스에 보내는 코드
        this.userid = userid;
    }
 
    
    public String getUserid() {
        return userid;
    }
 
    public void setUserid(String userid) {
        this.userid = userid;
    }
 
    @Override
    public String toString() {
        return "UserDTO [userid=" + userid + "]";
    }
}
cs



DAO를 생성


UserDAO.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.security.model.dao;
 
import java.util.Map;
 
public interface UserDAO {
    
    //회원가입 처리
    public int insertUser(Map<String,String> map);
    
    //로그인 처리
    public Map<String,Object> selectUser(String userid);
    
}
 
cs



UserDAOImpl.java

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
package com.example.security.model.dao;
 
import java.util.Map;
 
import javax.inject.Inject;
 
import org.apache.ibatis.session.SqlSession;
import org.springframework.stereotype.Repository;
 
@Repository //dao 빈으로 등록
public class UserDAOImpl implements UserDAO {
    
    @Inject     //mapper을 사용하기 위해 의존성을 주입
    SqlSession sqlSession;
    
    @Override
    public int insertUser(Map<StringString> map) {
        return sqlSession.insert("user.insertUser", map);
        //users테이블에 map에 담아놓은 레코드를 추가하는 메소드
    }
 
    @Override
    public Map<String, Object> selectUser(String userid) {
        return sqlSession.selectOne("user.selectUser", userid);
        //로그인을 확인하는 메소드
    }
 
}
 
cs



src/main/resources 폴더 하위에 mappers 폴더를 생성하고, 샘플 프로젝트에서 샘플 mapper을 복사하고 이름을 userMapper.xml로 바꾼다.



userMapper.xml

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
<?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와 중복되지 않도록 네임스페이스 기재 -->
<mapper namespace="user">
 
    <insert id="insertUser">
        insert into users
        values (#{userid}, #{passwd}, #{name}, 1, #{authority})
        <!-- 회원가입 쿼리, id, 비밀번호, 이름 , 신규가입이므로 1이 들어감,  그리고 관리자계정인지 일반계정인지 확인후 들어감-->
    </insert>
    
    <select id="selectUser" resultType="java.util.Map">
    
        select 
        userid as username, <!-- 스프링에서 체크하는 필드명과 맞춘 것 -->
        passwd as password, <!-- 스프링에서 체크하는 필드명과 맞춘 것 -->
            name, 
            enabled, 
            authority
        from users <!-- users테이블로부터 검색 -->
        where userid=#{userid} <!-- userid가 내가 입력한 userid와 같은 경우 -->
        <!-- 여기서 검색한 쿼리는 hashmap로 저장이 되어서 dao로 보낸다.-->
    </select>
</mapper>
cs




UserAuthentivationService.java

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.example.security.service;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
 
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
 
import com.example.security.model.dto.UserDTO;
 
//로그인 처리 클래스
public class UserAuthenticationService implements UserDetailsService {
//UserDetailsService는 스프링 프레임 워크에 내장되어있는 클래스
    
    private SqlSessionTemplate sqlSession;
    public UserAuthenticationService() {     }
    public UserAuthenticationService(
        SqlSessionTemplate sqlSession) {
        this.sqlSession=sqlSession;
    }
    
    
    //로그인 인증을 처리하는 코드
    //파라미터로 입력된 아이디값에 해당하는 테이블의 레코드를 읽어옴
    //정보가 없으면 예외를 발생시킴
    //정보가 있으면 해당 정보가 map(dto)로 리턴된다.
    
        @Override
        public UserDetails loadUserByUsername(String userid) 
            throws UsernameNotFoundException {
        
        //사용자 아이디 확인 (mapper에서 map에 담아 전달된 사용자의 아이디)
        Map<String,Object> user=sqlSession.selectOne("user.selectUser", userid);
        
        //아이디가 없으면 예외 발생
        if(user==nullthrow new UsernameNotFoundException(userid);
        
        //사용권한 목록
        List<GrantedAuthority> authority=new ArrayList<>();
        // 오라클에서는 필드명을 대문자로 적어야 함
        // 오라클에서는 BigInteger 관련 오류가 발생할 수 있으므로 아래와 같이 처리 
        // 속성이름을 대문자로 작성해야 에러가 발생하지 않는다.
        //(Integer)Integer.valueOf(user.get("ENABLED").toString()) == 1
        
    authority.add(
new SimpleGrantedAuthority(user.get("AUTHORITY").toString())); 
 
        //필드명은 대문자로
        return new UserDTO(user.get("USERNAME").toString(),
                user.get("PASSWORD").toString(),
                
                //1과 같으면 사용가능한 계정이고, 0이면 사용불가능한 계정이다.
                
                //ENABLED는 1이라는 의미
                (Integer)Integer.valueOf(user.get("ENABLED").toString())==1,
                true,true,true,authority,
                user.get("USERNAME").toString());
    }
 
}
cs




UserDeniedHandler.java


(관리자 페이지에 일반 사용자가 접근하거나 허가받지 않은 사용자가 접근할때 에러 메시지를 출력하는 페이지)

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
32
33
34
package com.example.security.service;
 
import java.io.IOException;
 
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
 
public class UserDeniedHandler 
    implements AccessDeniedHandler {
 
    //사용권한이 없을 때 지정한 페이지로 이동
    //관리자기능의 일반 사용자가 접근할때 에러를 출력하는 코드
    //security-context에 access-denied-handler로 설정되어있기 때문에 자동적으로 호출이 된다.
    @Override
    public void handle(HttpServletRequest req
            , HttpServletResponse res, AccessDeniedException ade)
            throws IOException, ServletException { 
        req.setAttribute("errMsg"
                "관리자만 사용할 수 있는 기능입니다.");
        
        //위의 경고메시지를 출력하고 denied.jsp로 강제로 이동하게 한다.
        String url="/WEB-INF/views/user/denied.jsp";
        RequestDispatcher rd=req.getRequestDispatcher(url);
        rd.forward(req, res);
//        req.getRequestDispatcher(
//                "/WEB-INF/views/user/denied.jsp").forward(req, res);
    }
 
}
cs



UserLoginFailureHandler.java


(로그인을 실패했을 때 실행되는 페이지)


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
32
33
34
package com.example.security.service;
 
import java.io.IOException;
 
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 
public class UserLoginFailureHandler 
    implements AuthenticationFailureHandler {
 
        //로그인이 실패했을 때의 처리
        @Override
        public void onAuthenticationFailure(
                HttpServletRequest request, 
                HttpServletResponse response,
                AuthenticationException exception) 
                throws IOException, ServletException {
        
        //request 영역에 변수 저장
        request.setAttribute("errMsg",
                "아이디 또는 비밀번호가 일치하지 않습니다.");
        
        //forward
        request.getRequestDispatcher(
                "/WEB-INF/views/user/login.jsp")
                    .forward(request, response);
        
    }
    
}
cs



HomeController.java 에 있는 home 메소드에 RequestMapping "/" 가 UseController.java의 맵핑 url과 동일하기


때문에 home 메소드를 주석처리 한다.



UserController.java 생성

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.example.security.controller;
 
import java.util.HashMap;
import java.util.Map;
 
import javax.inject.Inject;
 
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
 
import com.example.security.model.dao.UserDAO;
import com.example.security.service.ShaEncoder;
 
@Controller //컨트롤러 빈
public class UserController {
 
    @Inject
    ShaEncoder shaEncoder; //암호화 빈 
    
    @Inject
    UserDAO userDao; 
    
    @RequestMapping("/"//시작 페이지
    public String home(Model model) {
        return "home"//home.jsp로 이동
    }
    
    //로그인 페이지로 이동
    @RequestMapping("/user/login.do")
    public String login() {
        return "user/login";
    }

    //회원가입 페이지로 이동
    @RequestMapping("/user/join.do")
    public String join() {
        return "user/join";
    }
    
    //회원가입 처리     
    @RequestMapping("/user/insertUser.do")
    public String insertUser(@RequestParam String userid, 
            @RequestParam String passwd,
            @RequestParam String name, 
            @RequestParam String authority) {
        
        //비밀번호 암호화
        String dbpw=shaEncoder.saltEncoding(passwd, userid);
        
        Map<String,String> map=new HashMap<>();
        map.put("userid", userid);
        map.put("passwd", dbpw);
        map.put("name", name);
        map.put("authority", authority);
        // affected rows, 영향을 받은 행의 수가 리턴됨
        int result=userDao.insertUser(map);
        return "user/login"// login.jsp로 이동
    }
    
    //관리자 영역 페이지    
    @RequestMapping("/admin/")
    public String admin() {
        return "admin/main";
    }
}
cs



views 폴더 하위에 include 폴더를 만들고 css폴더 하위에 main.css 추가


include 폴더 하위에 header.jsp 추가



main.css

1
2
3
4
5
.@charset "UTF-8";
a:link {text-decoration: none; color:blue; }
a:hover {text-decoration: underline; color: red;}
a:visited {text-decoration: none; }
a:active {text-decoration: none; color:yellow; }
cs



header.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
 
 
<!-- 태그 라이브러리 선언 -->
<%@ taglib prefix="c"
uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt"
uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="fn" 
uri="http://java.sun.com/jsp/jstl/functions" %>
 
 
<!-- 컨텍스트 패스 설정 -->
<c:set var="path" value="${pageContext.request.contextPath}"/>
 
 
<!-- js, css 연결 -->
<script src="http://code.jquery.com/jquery-3.2.1.min.js">
</script>
<link rel="stylesheet" href="${path}/include/css/main.css">
cs



view 폴더하위에 user폴더 하위에 login.jsp 파일 생성


로그인 화면을 출력할 (login.jsp)

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
32
33
34
35
36
37
38
39
40
41
42
43
.<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<%@ include file="../include/header.jsp" %>
 
<script type="text/javascript">
function join(){
    location.href="${path}/user/join.do";
}
</script>
 
</head>
<body>
<h2>로그인 페이지</h2>
<span style="color:red;">${errMsg}</span>
<!-- 컨트롤러에 보면 login_check.do가 없지만 security-context파일에 가서 확인해보면
check.do가 있는것을 확인할 수 있다. (login-processing-url에 지정을 해놓았다.)-->
<form action="${path}/user/login_check.do" method="post">
<table>
    <tr>
        <td>아이디</td>
        <td><input name="userid"></td>
    </tr>
    <tr>
        <td>비밀번호</td>
        <td><input type="password" name="passwd">
            <input name="_spring_security_remember_me"
                type="checkbox">자동 로그인</td>
    </tr>
    <tr>
        <td colspan="2" align="center">
            <input type="submit" value="로그인">
            <input type="button" value="회원가입" onclick="join()">
        </td>
    </tr>
</table>
</form>
</body>
</html>
cs



회원가입 페이지 (join.jsp)

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
.<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<%@ include file="../include/header.jsp" %>
</head>
<body>
<h2>회원가입</h2>
<form action="${path}/user/insertUser.do" method="post">
<table>
    <tr>
        <td>아이디</td>
        <td><input name="userid"></td>
    </tr>
    <tr>
        <td>비밀번호</td>
        <td><input type="password" name="passwd"></td>
    </tr>
    <tr>
        <td>이름</td>
        <td><input name="name"></td>
    </tr>
    <tr>
        <td>사용권한</td>
        <td>
            <select name="authority">
            <!-- 사용권한은 일반사용자와 관리자 2가지로 나눈다. -->
                <option value="ROLE_USER">일반사용자</option>
                <option value="ROLE_ADMIN">관리자</option>
            </select>
        </td>
    </tr>
    <tr>
        <td colspan="2" align="center">    
            <input type="submit" value="회원가입">
            <!-- 회원가입 버튼을 누르면 컨트롤러에 insertUser.do로 넘어간다.-->
        </td>
    </tr>
</table>
</form>
</body>
</html>
cs



데이터베이스에서 users 테이블의 내용을 검색해보면 패스워드가 암호화되어서 들어가있는 것을 확인할 수 있다.


web.xml을 보면 한글처리를 하는 코드 밑에 스프링 시큐리티에서 처리하도록 하는 필터가 있는데, 


이렇게 하게되면 한글처리가 잘 되기때문에 한글이 깨진다면 이렇게 하면 된다.



  비밀번호 암호화


  join.jsp에서 입력된 비밀번호 값이 컨트롤러에 맵핑되서 암호화가 된 후에 테이블에 저장.




home.jsp 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<%@ include file="include/header.jsp" %>
<title>Home</title>
</head>
<body>
    <h2>Welcome!</h2>
    <h2>${msg}</h2>
    <a href="${path}/admin/">관리자 페이지</a><br>
    <a href="${path}/user/logout.do">로그아웃</a>
</body>
</html>
cs



view폴더 하위에 admin디렉토리를 만들고 main.jsp 파일 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<%@ include file="../include/header.jsp" %>
 
<!-- 권한이 없는 사용자가 권한이 필요한 페이지에 들어갔을때 출력되는 페이지 -->
<!-- 에러메시지가 출력되고 3초후에 메인페이지로 이동하게 된다. -->
 
<!-- 3초후 메인페이지로 이동 -->
<meta http-equiv="refresh" content="3,${path}">
<title>Insert title here</title>
 
</head>
<body>
<p>${errMsg}</p>
</body>
</html>
cs


user폴더 하위에 denied.jsp 파일 생성


(권한이 없는 사용자가 권한이 필요한 페이지에 들어갔을때 출력되는 페이지)

(에러메시지가 출력되고 3초후에 메인페이지로 이동하게 된다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<%@ include file="../include/header.jsp" %>
 
<!-- 권한이 없는 사용자가 권한이 필요한 페이지에 들어갔을때 출력되는 페이지 -->
<!-- 에러메시지가 출력되고 3초후에 메인페이지로 이동하게 된다. -->
 
<!-- 3초후 메인페이지로 이동 -->
<!--refresh쓰고, content="숫자 , ${path}"를 쓰면 "숫자"초 후에 path경로로 이동된다는 의미-->
<meta http-equiv="refresh" content="3,${path}">
<title>Insert title here</title>
 
</head>
<body>
<p>${errMsg}</p>
</body>
</html>
cs



시큐리티를 사용하면 url로 직접 쳐서 관리자페이지로 들어가는 것을 막을 수 있고 (보안때문에)


그리고, 뒤로가기를 눌렀을때 사용자 정보가 보여지는 것을 막을 수 있다.




  -로그인 처리 순서-


  login.jsp 페이지에서 로그인을 하려고 하면 login_check.do로 이동해야하는데 컨트롤러에는 이 url이 없다.


  이 url은 security-context.xml에 태그에 담겨져 있기 때문에 이 태그를 거쳐서 UserAuthenticationService.java로 오게된다.


  이 페이지에서 loadUserByUsername() 메소드를 호출해서 기존 테이블에 저장된 값하고, 사용자가 입력한 값을 비교해서


  틀리면 에러를 내고, 맞았으면 success를 낸다.



: