인증, 인가의 개념과 Spring security 사용 예시에 대해 알아보자.
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-taglibs -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<security:http> <!-- 기본값: use-expressions="true" -->
<security:form-login/>
</security:http>
<security:authentication-manager>
</security:authentication-manager>
<param-value>
/WEB-INF/spring/root-context.xml
/WEB-INF/spring/security-context.xml
</param-value>
<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>
기본 예시
접근 제한 설정
<http>
<intercept-url pattern="url패턴" access="권한 체크(권한명, 표현식)" />
</http>
<http use-expressions=false>
xsi:schemaLocation="
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-4.2.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
<!-- css, js, image는 접근 제어 대상이 아니기에 보안필터 체인을 적용하지 않는다. -->
<security:http pattern="/static/**" security="none"/>
<security:http pattern="/design/**" security="none"/>
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/js/**" security="none"/>
<security:http>
<!-- 접근 허용 정책 -->
<security:intercept-url pattern="/customer/noticeReg.htm" access="isAuthenticated()"/> <!-- 접근 권한을 설정하는 태그 -->
<security:intercept-url pattern="/customer/noticeDel.htm" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/**" access="permitAll"/>
<!-- 에러 메세지 화면으로 이동하게 하는 코드(접근 금지에 대한 특정 페이지로 이동하도록 지정) -->
<!-- <security:access-denied-handler error-page ="/common/accessError.htm"/> -->
<security:form-login/>
</security:http>
<!-- 인 메모리 방식으로 사용자 계정 + 역할(권한) 설정 -->
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="hong" authorities="ROLE_USER" password="{noop}1234" />
<security:user name="admin" authorities="ROLE_USER, ROLE_MANAGER, ROLE_ADMIN" password="{noop}11234"/>
<!-- 한 사람이 여러개의 역할을 맡을 수 있다. -->
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
500 오류: There is no PasswordEncoder mapped for the id "null"
- 스프링 시큐리티 버젼 5부터반드시 PasswordEncoder 지정이 필요하다. 만약 PasswordEncoder 지정없이 임시로 사용하고자 한다면 {noop} 문자열을 password앞에 추가해야 한다.
<security:user name="hong" authorities="ROLE_USER" password="{noop}1234" />
<!-- 에러 메세지 화면으로 이동하게 하는 코드(접근 금지에 대한 특정 페이지로 이동하도록 지정) -->
<security:access-denied-handler error-page ="/common/accessError.htm"/>
package org.doit.ik.controller;
@Controller
@RequestMapping("/common/*")
@Log4j
public class CommonController {
// /common/accessError.htm
@GetMapping("/accessError.htm")
public String accessDenied(Model model, Authentication auth) throws Exception{
log.info("> /common/accessError.htm... GET");
model.addAttribute("msg", "Access Denied");
return "/common/accessError";
}
} // class
<body>
<div>
<h1> My Access Denied Page </h1>
<h2><c:out value="${SPRING_SECURITY_403_EXCEPTION.getMessage()}"/></h2>
<h2><c:out value="${msg}"/></h2>
기타오류 설명 부분
</div>
</body>
@Component("customAccessDeniedHandler")
@Log4j
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.error("Access Denied Handler");
log.error("Redirect");
response.sendRedirect("/common/accessError.htm");
}
}
<security:access-denied-handler ref = "customAccessDeniedHandler"/>
<security:form-login login-page = "/joinus/login.htm"/>
@GetMapping("/login.htm")
public String login() throws Exception{
return "joinus.login";
}
(1) 반드시 form 태그 action은 /login, method는 post방식
<form action="/login" method="post">
(2) input 태그에서 아이디의 name 속성은 반드시 UserName
<input name="username" class="text" id="id" />
(3) input 태그에서 비밀번호의 name 속성은 반드시 Password
<input type="password" name="password" class="text" id="pwd"/>
(4) token 값 가지고 넘어가도록 CSRF 속성 준다.
<input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }">
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<li>
<sec:authorize access="isAnonymous()">
<a href="${pageContext.request.contextPath}/joinus/login.htm">로그인</a>
</sec:authorize>
<sec:authorize access="isAuthenticated()">
<form action="${ pageContext.request.contextPath }/joinus/logout.htm" method="post">
[<sec:authentication property="principal.username"/>] 님
<input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }">
<button>로그아웃</button>
</form>
</sec:authorize>
</li>
<security:logout logout-url="joinus/logout.htm"
logout-success-url="/" invalidate-session="true"/>
CSRF 토큰값을 넘기고 싶지 않은 경우!
- security-context.xml에 아래 코드를 추가하면 된다.
<!-- CSRF 토큰 사용하지 않겠다는 의미 --> <security:csrf disabled="true"/>
@Component("customLoginSuccessHandler")
@Log4j
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler{@Override
public void onAuthenticationSuccess(
HttpServletRequest request
, HttpServletResponse response,
Authentication authentication //인증받은 사용자의 정보
) throws IOException, ServletException {
log.warn("> Login Success...");
List<String> roleNames = new ArrayList<String>();
authentication.getAuthorities().forEach( auth -> {
roleNames.add(auth.getAuthority()); //권한 정보를 가진 집합 안에서 권한을 읽어와서 add
});
log.warn("> ROLE NAMES : " + roleNames );
//권한에 따라 로그인 시 redirect 되는 곳을 정해줌
if ( roleNames.contains("ROLE_ADMIN")) {
response.sendRedirect("/");
return;
} else if( roleNames.contains("ROLE_MANAGER")){
response.sendRedirect("/customer/notice.htm");
return;
} else if( roleNames.contains("ROLE_USER")){
response.sendRedirect("/customer/notice.htm");
return;
}
}
}
<security:form-login
login-page = "/joinus/login.htm"
authentication-success-handler-ref="customLoginSuccessHandler"
default-target-url="/index.htm"
/>
<!-- <security:form-login/> -->
<security:form-login
login-page = "/joinus/login.htm"
authentication-success-handler-ref="customLoginSuccessHandler"
default-target-url="/index.htm"
authentication-failure-url="/joinus/login.htm?error=true"
/>
<c:if test="${param.error eq 'true'}">
<div>
<strong style="color: red">아이디 또는 패스워드가 일치하지 않습니다.</strong><br />
<c:if test="${ SPRING_SECURITY_LAST_EXCEPTION != null}">
Message : <c:out value="${SPRING_SECURITY_LAST_EXCEPTION.message}" />
</c:if>
</div>
</c:if>
사용자 정보 및 사용자 권한 정보를 담은 테이블 2개가 있어야 한다.