0. 의존 라이브러리 추가
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
1. web.xml 작성
1. <context-param> 수정
<context-param>의 <param-value>에 시큐리티에 관련한 설정을 작성할 security-context.xml 에 대한 경로를 추가한다.
<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>
2. DelegatingFilterProxy 추가
<filter>를 사용하여 DelegatingFilterProxy 클래스를 사용한 필터를 추가한다. <filter-name>은 반드시 'springSecurityFilterChain'으로 설정한다. 매핑은 전체 경로('/*')로 지정하면 된다.
<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>
2. root-context.xml 작성
시큐리티와 데이터베이스의 연동에 사용할 dataSource 빈을 생성한다. 내 경우에는 MyBatis와의 연동에 사용하는 BasicDataSource 클래스를 사용했다.
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="${driver}" />
<property name="url" value="${url}" />
<property name="username" value="${user}" />
<property name="password" value="${pass}" />
</bean>
3. security-context.xml 작성
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/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
https://www.springframework.org/schema/security/spring-security.xsd">
<security:http>
<security:intercept-url pattern="/" access="hasRole('ROLE_MEMBER')" />
<security:form-login login-page="/member/login" />
<security:remember-me data-source-ref="dataSource"
token-validity-seconds="604800" />
<security:logout logout-url="/member/logout"
delete-cookies="remember-me, JSESSION-ID" />
</security:http>
<bean id="memberDetailsService"
class="com.java.web.member.service.MemberDetailsService" />
<bean id="noPasswordEncoder"
class="com.java.web.member.service.NoPasswordEncoder" />
<security:authentication-manager>
<security:authentication-provider user-service-ref="memberDetailsService">
<security:password-encoder ref="noPasswordEncoder" />
</security:authentication-provider>
</security:authentication-manager>
</beans>
스키마는 root-context.xml에 작성된 스키마를 복사하여 security와 관련한 내용을 추가하면 된다.
<?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/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
https://www.springframework.org/schema/security/spring-security.xsd">
...
</beans>
인증의 시작(로그인)과 끝(로그아웃), 인증 필요 경로의 설정 등은 <security:http>에 작성한다.
<security:http>
<security:intercept-url pattern="/" access="hasRole('ROLE_MEMBER')" />
<security:form-login login-page="/member/login" />
<security:remember-me data-source-ref="dataSource"
token-validity-seconds="604800" />
<security:logout logout-url="/member/logout"
delete-cookies="remember-me, JSESSION-ID" />
</security:http>
* 자동 로그인(<security:remember-me>) 기능을 사용하기 위해서는 다음과 같이 데이터베이스에 테이블을 생성해야한다.
CREATE TABLE PERSISTENT_LOGINS(
USERNAME VARCHAR2(50),
SERIES VARCHAR2(200),
TOKEN VARCHAR2(200),
LAST_USED TIMESTAMP,
CONSTRAINT PK_PL PRIMARY KEY (SERIES)
);
인증 작업 수행의 설정은 <security:authentication-manager>에 작성한다.
<bean id="memberDetailService"
class="com.java.web.member.service.MemberDetailsService" />
<bean id="noPasswordEncoder"
class="com.java.web.member.service.NoPasswordEncoder" />
<security:authentication-manager>
<security:authentication-provider user-service-ref="memberDetailService">
<security:password-encoder ref="noPasswordEncoder" />
</security:authentication-provider>
</security:authentication-manager>
필요에 따라 인증 성공/실패 등을 처리할 클래스를 지정할 수 있다. 대부분 태그명이나 속성명과 동일한 이름의 인터페이스를 가지고 있으며 해당 인터페이스를 구현하여 사용자 정의 처리자로 지정하면 된다.
<bean id="memberAccessDeniedHandler"
class="com.java.web.member.service.MemberAccessDeniedHandler" />
<bean id="memberAuthSuccessHandler"
class="com.java.web.member.service.MemberAuthSuccessHandler" />
<security:http>
<security:form-login login-processing-url="/member/login"
login-page="/member/login"
authentication-success-handler-ref="memberAuthSuccessHandler" />
<security:access-denied-handler ref="memberAccessDeniedHandler" />
...
</security:http>
* MemberAccessDeniedHandler
package com.java.web.member.service;
import java.io.IOException;
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;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MemberAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.info("접근 거부됨 : {}", request.getRemoteAddr());
}
}
* MemberAuthSuccessHandler
package com.java.web.member.service;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MemberAuthSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
log.info("접근 허가됨 : {}", request.getRemoteAddr());
}
}
4. VO 클래스와 SQL 작성
사용자 정보와 인증 정보를 담을 VO 클래스와 해당 VO 클래스를 DB에 연결해 처리할 SQL을 작성한다.
회원 정보를 담는 테이블과 인증 정보를 담는 테이블은 1 대 N 관계이다. 한 명의 회원은 여러 개의 인증 정보를 가질 수 있기 때문이다.
1. MemberVO
MemberVO 클래스에서는 여러 개의 인증 정보(AuthVO)를 담을 수 있도록 멤버변수로 List를 선언해준다.
package com.java.web.vo;
import java.util.Date;
import java.util.List;
import org.springframework.format.annotation.DateTimeFormat;
import lombok.Data;
@Data
public class MemberVO {
private String memId;
private String memPass;
private String memName;
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date memBir;
private String memGen;
private String memStatus;
private List<AuthVO> authList;
}
2. AuthVO
package com.java.web.vo;
import lombok.Data;
@Data
public class AuthVO {
private String memId;
private String auth;
}
3. SQL 작성
회원 정보 테이블과 인증 정보 테이블은 1 대 N 관계이므로 resultMap을 사용해 MemberVO의 List<AuthVO>에 여러 개의 AuthVO가 들어갈 수 있도록 한다.
<resultMap id="memMap" type="memVO">
<result property="memId" column="MEM_ID" />
<result property="memPass" column="MEM_PASS" />
<result property="memName" column="MEM_NAME" />
<result property="memBir" column="MEM_BIR" />
<result property="memGen" column="MEM_GEN" />
<result property="memStatus" column="MEM_STATUS" />
<collection property="authList" resultMap="authMap" />
</resultMap>
<resultMap id="authMap" type="authVO">
<result property="memId" column="MEM_ID" />
<result property="auth" column="AUTH" />
</resultMap>
<select id="login" parameterType="memVO" resultMap="memMap">
SELECT A.MEM_ID, A.MEM_PASS, A.MEM_NAME, A.MEM_BIR, A.MEM_GEN, A.MEM_STATUS,
B.MEM_ID, B.AUTH
FROM MEMBER A LEFT OUTER JOIN AUTH B ON (A.MEM_ID = B.MEM_ID)
WHERE A.MEM_ID = #{memId}
</select>
5. UserDetailsService 인터페이스의 구현 클래스 생성
스프링 시큐리티는 UserDetailsService 인터페이스를 사용해 사용자 인증과 권한을 확인한다.
package com.java.web.member.service;
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 com.java.web.mapper.MemberMapper;
import com.java.web.vo.MemberVO;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MemberDetailsService implements UserDetailsService {
@Autowired
private MemberMapper memberMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("username : {}", username);
MemberVO memVO = new MemberVO();
memVO.setMemId(username);
memVO = memberMapper.login(memVO);
log.info("memVO : {}", memVO);
return memVO == null ? null : new AuthUser(memVO);
}
}
loadUserByUsername의 반환 타입은 UserDetails 인터페이스이다. UserDetails의 구현 클래스인 User 클래스를 상속한 클래스를 사용해 처리할 수 있도록 한다.
package com.java.web.member.service;
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 com.java.web.vo.MemberVO;
public class AuthUser extends User {
private static final long serialVersionUID = 1L;
private MemberVO memVO;
public AuthUser(String username, String password,
Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public AuthUser(MemberVO memVO) {
super(memVO.getMemId(), memVO.getMemPass(),
memVO.getAuthList().stream()
.map(auth -> new SimpleGrantedAuthority(auth.getAuth()))
.collect(Collectors.toList()));
this.memVO = memVO;
}
public MemberVO getMemVO() {
return memVO;
}
public void setMemVO(MemberVO memVO) {
this.memVO = memVO;
}
public static long getSerialversionuid() {
return serialVersionUID;
}
}
6. 로그인 페이지 작성
JSP를 사용해 로그인 페이지를 작성한다.
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
...
<form action="/member/login" method="post">
<label for="username" class="form-label">아이디</label>
<input type="text" id="username" name="username" class="form-control" />
<label for="password" class="form-label">비밀번호</label>
<input type="password" id="password" name="password" class="form-control" />
<button type="submit" class="btn btn-primary">로그인</button>
<sec:csrfInput />
</form>
스프링 시큐리티 태그 라이브러리를 설정해야한다.
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
로그인 처리를 할 <form> 태그를 작성한다.
<form action="/member/login" method="post">
<label for="username" class="form-label">아이디</label>
<input type="text" id="username" name="username" class="form-control" />
<label for="password" class="form-label">비밀번호</label>
<input type="password" id="password" name="password" class="form-control" />
<label for="remember-me" class="form-check-label">자동 로그인</label>
<input type="checkbox" id="remember-me" name="remember-me" class="form-check-input" />
<button type="submit" class="btn btn-primary">로그인</button>
<sec:csrfInput />
</form>
action은 <security:form-login>의 login-processing-url 속성값과 일치시켜야한다. login-processing-url 속성을 생략했을 경우 기본 경로는 '/login' 이다. method는 로그인이므로 당연히 'post' 이다.
아이디와 비밀번호를 받는 <input> 태그의 name은 기본값이 각각 아이디는 'username', 비밀번호는 'password' 이다. 이는 <security:form-login>에서 username-parameter 속성과 password-parameter 속성을 사용해 변경할 수 있다.
자동 로그인 사용 유무를 설정하는 <input type="checkbox" /> 는 name 속성의 값을 'remember-me'로 설정하면 된다.
마지막으로 <form> 태그 안에 반드시 <sec:csrfInput /> 태그를 추가한다. 이 태그가 없으면 권한이 있는 사용자이더라도 403 접근 거부 오류가 발생한다.
* 시큐리티 설정 이후의 요청 데이터 처리
시큐리티 설정 이후에는 통신 방식이 POST일 경우, <form> 태그 안에 <sec:csrfInput /> 태그를 사용해야한다.
enctype이 multipart/form-data 인 경우에는 action 속성의 뒤에
'?${_csrf.parameterName}=${_csrf.token}'로 쿼리 스트링을 추가해야한다.
<form action="/create?${_csrf.parameterName}=${_csrf.token}"
method="post" enctype="multipart/form-data">
...
</form>
비동기 통신의 경우에도 POST 방식일 때는 헤더에 '${_csrf.headerName}' : '${_csrf.token}'를 추가해야한다.
$.ajax({
url : '/test/' + testNum,
type : 'post',
beforeSend : function(xhr){
xhr.setRequestHeader("${_csrf.headerName}", "${_csrf.token}");
},
success : function(res){
...
},
dataType : 'json'
});
fetch('/test/getNum', {
method : 'POST',
headers : {
'${_csrf.headerName}' : '${_csrf.token}'
}
})
.then((res) => res.text())
.then((num) => {
testNum.value = num;
});
'Java > Spring Framework' 카테고리의 다른 글
[Spring Framework] 스프링 시큐리티 적용 후 서버에서 회원 정보 확인하기 (0) | 2023.02.15 |
---|---|
[Spring Framework] 어노테이션을 사용한 시큐리티 지정 (0) | 2023.02.15 |
[Spring Framework] 스프링 시큐리티 자동 로그인 (0) | 2023.02.14 |
[Spring Framework] 스프링 시큐리티 태그 라이브러리 (0) | 2023.02.14 |
[Spring Framework] 스프링 시큐리티와 데이터베이스 연동 (0) | 2023.02.14 |