Spring Security(1), (2)에서는 아이디, 비밀번호, 권한 설정을 security 설정 파일에 직접 작성했다.
그러나 이런 설정들이 많아진다면 관리가 힘들어질 수 있다.
그래서 데이터 베이스에 아이디, 비밀번호, 권한 설정들을 담아서 관리하고자 한다.
before - 직접 작성하여 관리
<s:authentication-manager>
<s:authentication-provider>
<s:user-service>
<s:user name="codelove" password="1365" authorities="ROLE_USER"/>
<s:user name="kimcoder" password="1366" authorities="ROLE_USER, ROLE_ADMIN"/>
</s:user-service>
</s:authentication-provider>
</s:authentication-manager>
after - DB연결 클래스로 관리
<s:authentication-manager>
<s:authentication-provider user-service-ref="loginService"/>
</s:authentication-manager>
이 loginService라는 Bean객체는 데이터 베이스에 있는 아이디, 비밀번호, 권한 데이터를 전달할 것이다.
이제부터 이 loginService를 구현해보는 시간을 가질 것이다.
먼저 예제 프로젝트의 구성은 이렇다.
1. Dependency 추가
Mybatis로 연동할 것이기 때문에 Mybatis 관련 라이브러리들도 pom.xml에 추가하기로 한다.
<depencencies>
...
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.2</version>
</dependency>
</dependencies>
그 외에는 이전 포스팅들에서 이미 추가한 라이브러리들이지만, 아직 추가하지 않은 라이브러리가 있다면 위와 같이 dependency들을 모두 추가해주자.
2. context 설정 파일
<web.xml>
context-param의 param-value에 /WEB-INF/spring/appServlet 경로에 있는 모든 context파일을 추가도록 설정한다.
정확하게 말하자면 "-context.xml"로 끝나는 파일을 말한다.
그 외에는 Spring Security(1) 포스팅과 동일하다.
<?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 https://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/appServlet/*-context.xml
</param-value>
</context-param>
<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>
<!-- 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>*.html</url-pattern>
</servlet-mapping>
<filter>
<filter-name>encodingFilter</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>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
<servlet-context.xml>
DataSource bean과 Mybatis를 이용하기 위한 bean을 추가해줘야 한다.
자세한 설명은 mybatis 포스팅을 참고하면 될 것이다.
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.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>
<beans:bean name="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<beans:property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
<beans:property name="url" value="jdbc:oracle:thin:@localhost:1521:xe"/>
<beans:property name="username" value="DB아이디"/>
<beans:property name="password" value="DB비밀번호"/>
</beans:bean>
<beans:bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<beans:property name="dataSource" ref="dataSource" />
<beans:property name="mapperLocations" value="classpath:com/example/demo/mapper/*.xml"/>
</beans:bean>
<beans:bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<beans:constructor-arg index="0" ref="sqlSessionFactory"/>
</beans:bean>
<context:component-scan base-package="com.example.demo" />
</beans:beans>
<security-context.xml>
앞에서 언급했듯이 loginService bean객체는 데이터 베이스에 있는 아이디, 비밀번호, 권한 데이터를 전달할 것이기 때문에, 이 객체를 authentication-provider의 user-service-ref속성으로 지정한다. 그 외 코드는 이전 포스팅 Spring Security(2)과 동일하다.
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:s="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.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-3.1.xsd">
<s:http auto-config="true" use-expressions="true">
<s:form-login
username-parameter="user_id"
password-parameter="user_pw"
login-processing-url="/login_check"
login-page="/login.html"
authentication-failure-url="/login.html?ng"
/>
<s:logout
logout-url="/security_logout"
logout-success-url="/"
invalidate-session="true"
delete-cookies="true"
/>
<s:intercept-url pattern="/login.html*" access="isAnonymous()"/>
<s:intercept-url pattern="/vip.html*" access="hasRole('ROLE_USER')"/>
<s:intercept-url pattern="/admin.html*" access="hasRole('ROLE_ADMIN')"/>
<s:access-denied-handler error-page="/"/>
</s:http>
<s:authentication-manager>
<s:authentication-provider user-service-ref="loginService"/>
</s:authentication-manager>
</beans:beans>
3. UserDetailService(Interface)
<LoginService.java>
loginService bean객체는 LoginService 클래스의 bean 객체이다. @Service 어노테이션을 이용하면 context 설정 파일에서 bean을 직접 추가하지 않아도 자동으로 해당 클래스의 bean을 생성시켜준다. @Service 어노테이션의 parameter을 "loginService"로 지정하여 bean id가 loginService인 bean을 생성하게 된 것이다. 기존 방식대로 받는 쪽은 @Autowired 어노테이션을 이용하면 된다.
설정 파일에서 authentication-provider의 user-service-ref속성으로 지정한 객체는 UserDetailService 인터페이스를 선언하여 loadUserByUsername 메소드를 override해야 하며, 이 메소드에서 완전한 UserDetails 객체를 리턴하면 Spring Security와 데이터베이스와의 연동이 정상적으로 이루어지게 된다. UserDetails 객체는 잠시 후 다루기로 한다.
먼저, loadUserByUsername 메소드의 기본 파라미터인 username에 대해 설명하고자 한다.
여러분들이 Security 로그인 페이지에서 아이디와 비밀번호를 입력할 때, 이 때 입력한 아이디가 username으로 전달된다. 이 username은 id이기 때문에 데이터베이스에서 id필드는 기본키 혹은 유니크키로 설정되어야 하며, 데이터 베이스에서 User를 찾을 때 반드시 필요한 고유값이다.
그리고 SqlSession에 대해서는 위에 첨부한 mybatis 포스팅에 설명했기 때문에 별도의 설명은 생략한다.
Dao 객체의 selectUser메소드를 이용하여 UserDetails 객체에 데이터를 채워넣고,
User를 찾지 못했다면(userDetailsDto==null) 예외를 발생시키고,
User를 찾았다면 Dao 객체의 getAuthList메소드를 이용하여 해당 User의 권한 목록을 가져와서 UserDetails 객체로 데이터를 넘겨주면 완전한 UserDetails 객체가 완성된다.
package com.example.demo;
import java.util.ArrayList;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
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;
@Service("loginService")
public class LoginService implements UserDetailsService{
@Autowired
private SqlSessionTemplate sqlSession;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
ArrayList<String> authList = new ArrayList<String>();
Dao dao = sqlSession.getMapper(Dao.class);
UserDetailsDto userDetailsDto = dao.selectUser(username);
authList = dao.getAuthList(username);
if (userDetailsDto == null) { //User을 찾지 못했을 경우
throw new UsernameNotFoundException(username);
}
else {
userDetailsDto.setAuthority(authList);
}
return userDetailsDto; //완전한 UserDetails 객체
}
}
4. UserDetails(Interface)
3번에서 설명한 UserDetails에 대한 부분이다.
<UserDetailsDto.java>
이 DTO에 UserDetails 인터페이스를 선언하여, 7개의 메소드를 필수적으로 override하였다.
- getAuthorities() : 권한들을 반환
- getPassword() : password를 반환
- getUsername() : id를 반환
- isAccountNonExpired() : 계정 만료 여부 반환
- isAccountNonLocked() : 계정 잠김 여부 반환
- isCredentialsNonExpired() : 비밀번호 만료 여부 반환
- isEnabled() : 계정 사용 가능 여부 반환
여기서 밑줄 친 4개의 여부 반환 메소드에서 모두 true를 반환하면 정상적인 계정으로 판단한다.
이 예제에서는 isEnabled만 건드리고 나머지는 true로 고정시켰다.
setAuthority 메소드는 authority 리스트에 권한들을 저장해주는 역할을 하며, SimpleGrantedAuthority객체는 권한을 String형으로 편리하게 다루는 객체이다.
package com.example.demo;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class UserDetailsDto implements UserDetails{
private String ID;
private String PASSWORD;
private boolean ENABLED;
private ArrayList<GrantedAuthority> authority;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authority;
}
public void setAuthority(ArrayList<String> authList) {
ArrayList<GrantedAuthority> auth = new ArrayList<GrantedAuthority>();
for(int i=0;i<authList.size();i++) {
auth.add(new SimpleGrantedAuthority(authList.get(i)));
}
this.authority=auth;
}
@Override
public String getPassword() {
// TODO Auto-generated method stub
return PASSWORD;
}
@Override
public String getUsername() {
// TODO Auto-generated method stub
return ID;
}
@Override
public boolean isAccountNonExpired() {
// TODO Auto-generated method stub
return true;
}
@Override
public boolean isAccountNonLocked() {
// TODO Auto-generated method stub
return true;
}
@Override
public boolean isCredentialsNonExpired() {
// TODO Auto-generated method stub
return true;
}
@Override
public boolean isEnabled() {
// TODO Auto-generated method stub
return ENABLED;
}
}
5. Controller
<HomeController.java>
Spring Security(2) 포스팅과 동일함.
6. DAO(Mybatis 연동)
Mybatis에 대한 내용은 위에 첨부한 Mybatis 포스팅에서 설명하였기 때문에 간단히만 설명하고 넘어갈 것이다.
<Dao.java> (com.example.demo)
- selectUser 메소드 : 데이터베이스에서 ID가 username인 User를 찾아 UserDetailDto객체형으로 반환
- getAuthList 메소드 : 데이터베이스에서 ID가 username인 User의 모든 AUTHORITY를 리스트로 반환
package com.example.demo;
import java.util.ArrayList;
public interface Dao {
public UserDetailsDto selectUser(String username);
public ArrayList<String> getAuthList(String username);
}
<Dao.xml> (com.example.demo.mapper)
각 메소드에 대한 쿼리 설정 파일
※ 반환형이 리스트일 경우, resultType에는 리스트의 요소에 해당하는 자료형으로 지정
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.1//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.Dao">
<select id="selectUser" resultType="com.example.demo.UserDetailsDto">
SELECT * FROM USERACC WHERE ID=#{param1}
</select>
<select id="getAuthList" resultType="String">
SELECT AUTHORITY FROM USERAUTH WHERE ID=#{param1}
</select>
</mapper>
7. Database 테이블 생성
<USERACC 테이블>
User의 계정 정보 테이블(ID, PASSWORD, ENABLED)
기본키 - ID
SQL> CREATE TABLE USERACC(
2 ID VARCHAR2(20) NOT NULL,
3 PASSWORD VARCHAR2(300) NOT NULL,
4 ENABLED CHAR(1) CHECK(ENABLED='0' OR ENABLED='1'),
5 PRIMARY KEY (ID));
<USERAUTH 테이블>
User의 권한 정보 테이블(ID, AUTHORITY)
외래키 - ID(USERACC 테이블의 ID 필드와 연결)
SQL> CREATE TABLE USERAUTH(
2 ID VARCHAR2(20) NOT NULL,
3 AUTHORITY VARCHAR2(500) DEFAULT 'ROLE_USER' NOT NULL,
4 FOREIGN KEY (ID) REFERENCES USERACC (ID));
두 테이블을 성공적으로 생성했다면 여러분들은 각자 두 테이블에 데이터를 채워넣고 COMMIT(COMMIT;)까지 완료하면 된다.
8. VIEW
<admin.jsp>, <login.jsp>, <vip.jsp>, <home.jsp>
Spring Security(2)와 모두 동일
9. 실행
/demo/vip.html로 요청 -> 로그인화면
정상적으로 로그인 완료
ENABLED가 FALSE인 계정으로 로그인 했을 경우
"User is disabled" 문구가 출력되며 로그인 실패
다음 포스팅에서는 로그인 실패 로직을 커스터마이징하는 방법을 알아볼 것이다.
로그인 실패 핸들러를 커스터마이징하면 오류 문구도 개발자가 직접 지정할 수 있게 된다.
'Spring Series > Spring Framework' 카테고리의 다른 글
[Spring] Spring Security(5) - 로그인 성공 커스터마이징 (0) | 2021.02.04 |
---|---|
[Spring] Spring Security(4) - 로그인 실패 커스터마이징 (5) | 2021.02.02 |
[Spring] Mybatis (0) | 2021.01.26 |
[Spring] Spring Security(2) - 로그인 페이지 구현 (0) | 2021.01.20 |
[Spring] Spring Security(1) - 기본 사용법 (0) | 2021.01.19 |
댓글