본문 바로가기
개발입문/SPRING 게시판 만들기

[SPRING] 로그인 처리

by 양히◡̈ 2022. 10. 26.

로그인 처리는 Spring Security 로 처리하고 있다.

Reference ▶ https://docs.spring.io/spring-security/site/docs/current/reference/html5/

maven repository에서 pom.xml 에 의존성을 추가해야 한다.

아래 네 개를 각각 검색하여

추가하면 된다.

 

의존성 관련하여 아래 링크에서 참고해볼 수 있다.
Reference ▶ 
https://docs.spring.io/spring-security/site/docs/current/reference/html5/#modules

pom.xml 에 의존성을 추가한다.

<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-web -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>5.5.2</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-config -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.5.2</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-core -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>
    <version>5.5.2</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-taglibs -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.5.2</version>
</dependency>

 

 

 

security-context.xml 생성

이제 새로운 XML파일을 만들 것이다.

스프링 시큐리티는 단독으로 설정할 수 있기 때문에 기존의 root-context.xml이나 servlet-context.xml과는 별도로 sercurity-context.xml 파일을 작성하는 것이 좋다.

아래 보이는 경로(spring) 안에 new Spring Bean Configuration File 생성 > name: security-context.xml

xml의 네임스페이스는 자바의 import와 비슷한 역할을 하고, bean은 자바의 객체 생성과 비슷한 역할을 한다고 이해하면 된다.

방금 생성한 파일의 namespaces 에서 security도 추가로 체크한다.

다시 source 탭으로 돌아와서 코드를 추가한다.

<?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-5.5.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

	<security:http>
		<security:form-login login-page="/customLogin"/>
	</security:http>

	<security:authentication-manager>
	</security:authentication-manager>

</beans>

작성하고나면 아래 사진처럼 빨갛게 오류가 표시되는데, 버전 차이의 문제이다.

그러면 표시된 부분을 5.4 버전으로 변경해주면 된다.

 

 

 

 

 

web.xml 설정

스프링 시큐리티가 스프링 MVC에서 사용되기 위해서는 필터를 이용해 스프링 동작에 관여하도록 해야 한다.

web.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>

그리고 아래쪽에 새로운 코드를 추가한다.

<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>

* csrf : 사이트 간 요청 위조(또는 크로스 사이트 요청 위조)는 웹사이트 취약점 공격의 하나로, 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 하는 공격을 말한다.

웹은 요청과 응답의 1싸이클로 동작하는데, 어디의 요청인지 확인하지 않고 응답하면 위와 같은 문제가 발생할 수 있다.

요청시 확인코드를 전송하고, 응답이 확인코드를 비교하도록 만들면 이를 방지할 수 있다.

security를 설정했기 때문에, 이제 이전과 같이 글쓰기를 전송해보면 위와 같은 오류 페이지가 나온다,

 

 

 

로그인 컨트롤러

register.jsp 를 열고, <form> 여는태그 아래에 코드를 추가한다.

<form role="form" action="/board/register" method="post">
        <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

 

로그인과 로그아웃 관련 컨트롤러를 생성한다.

src/main/java > kr.icia.controller > new Class 생성 > name: CommonController

package kr.icia.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import lombok.extern.log4j.Log4j;

@Controller
@Log4j
public class CommonController {
	@GetMapping("/customLogin")
	public void loginInput(String error, String logout, Model model) {
		if (error != null)
			model.addAttribute("error", "계정을 확인해주세요.");
		if (logout != null)
			model.addAttribute("logout", "로그아웃");
	}
}

@GetMapping("/customLogout")
public void logoutGet() {
    log.info("custom logout");
}

 

 

 

로그인 페이지 만들기

이제 로그인페이지를 만들 것이다. 아래 경로에 new jsp file 생성 > name: customLogin.jsp

get.jsp에서 상단 부분을 복사하고, header와 footer를 포함한다는 부분을 추가로 작성해준 후, 내부 코드를 입력한다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!-- jstl core를 쓸 때 태그에 c로 표시 -->
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<!-- jstl fmt를 쓸 때 태그에 fmt로 표시 / fmt: formatter 형식 맞춰서 표시 -->

<%@ include file="includes/header.jsp"%>

<div class="row">
	<div class="col-lg-12">
		<div class="panel panel-default">
			<h1>로그인 처리</h1>
			<h2>${error}</h2>
			<h2>${logout}</h2>

			<form method="post" action="/login">
				<div class="form-group">
					<input type="text" name="username" palceholder="userid" class="form-control">
				</div>
				<div class="form-group">
					<input type="password" name="password" palceholder="password" class="form-control">
				</div>
				<div class="form-group">
					<input type="checkbox" name="remember-me"> 자동 로그인
				</div>
				<div class="form-group">
					<input type="submit" value="login">
				</div>
				
				<input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }"/>
			</form>
		</div>
	</div>
</div>

<%@ include file="includes/footer.jsp"%>

이번에는 같은 방법으로 로그아웃 페이지를 만든다. name: customLogout.jsp

상단 부분과 header, footer 포함 부분을 customLogin.jsp와 동일하게 구성한 뒤, 내부 코드를 만든다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!-- jstl core를 쓸 때 태그에 c로 표시 -->
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<!-- jstl fmt를 쓸 때 태그에 fmt로 표시 / fmt: formatter 형식 맞춰서 표시 -->

<%@ include file="includes/header.jsp"%>

<div class="row">
	<div class="col-lg-12">
		<div class="panel panel-default">
			<h1>로그아웃 처리</h1>
			<h2>${error}</h2>
			<h2>${logout}</h2>

			<div class="panel-body">
				<form role="form" method="post" action="/customLogout">
					<fieldset>
						<!-- 관련 요소를 묶는 역할 -->
						<a href="index.html" class="btn btn-lg btn-success btn-block">
							logout </a>
					</fieldset>

					<input type="hidden" name="${_csrf.parameterName }"
						value="${_csrf.token }"/>
				</form>
				<!-- get방식으로 로그아웃 페이지에 접근하고, 로그아웃 버튼을 누르면 post방식으로 처리 -->
			</div>
		</div>
	</div>
</div>

<script>
	$(".btn-success").on("click", function(e) {
		e.preventDefault();
		$("form").submit();
	});
</script>

<c:if test="${param.logout != null }">
	<script>
		$(document).ready(function() {
			alert("로그아웃");
		});
	</script>
	<!-- 로그아웃 파라미터 값이 있다면, 로그아웃 안내창 표시 -->
</c:if>

<%@ include file="includes/footer.jsp"%>

 

home.jsp 도 마찬가지로 필요한 부분을 복사해 넣은 뒤 <scipt>를 입력한다.

로그아웃 처리시 /로 이동하고, homeController에 의하여 home.jsp로 넘어온다면, 다시 /board/list 로 이동시키는 작업이다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!-- jstl core를 쓸 때 태그에 c로 표시 -->
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<!-- jstl fmt를 쓸 때 태그에 fmt로 표시 / fmt: formatter 형식 맞춰서 표시 -->

<%@ include file="includes/header.jsp"%>

<script>
	self.location = "/board/list";
</script>


<%@ include file="includes/footer.jsp"%>

 

 

 

로그인과 로그아웃 처리시 사용자명 보이기

header.jsp 에 language 설정 밑에 taglib을 추가한다.

<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>

Ctrl + F 로 "douglas" 를 검색한다.

검색해서 나오는 부분을 아래와 같이 구분해준 뒤 커서가 위치한 부분에 <sec:authorize> 코드를 추가하고, span 내부와 밑 부분도 추가 작성한다.

Douglas McGee는 주석처리한다.

<!-- Nav Item - User Information -->
<li class="nav-item dropdown no-arrow"><a
    class="nav-link dropdown-toggle" href="#" id="userDropdown"
    role="button" data-toggle="dropdown" aria-haspopup="true"
    aria-expanded="false"> 

    <!-- 정상로그인 -->
    <sec:authorize access="isAuthenticated()">
        <span
            class="mr-2 d-none d-lg-inline text-gray-600 small">
            <!-- Douglas McGee -->
            <sec:authentication property="principal.username"/>
            <!-- principal : 세션의 일종. 서버에서 생성. 웹브라우저 시작부터 종료까지 정보를 유지 -->
        </span>
    </sec:authorize>

    <sec:authorize access="isAnonymous()"><!-- 익명 -->
        <i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"> </i>
    </sec:authorize>


        <img class="img-profile rounded-circle"
        src="/resources/img/undraw_profile.svg">


</a> <!-- Dropdown - User Information -->

좀 더 밑으로 내려가서, 아래 사진 코드에서 빨간색 선이 위치한 부분에 코드를 추가한다. 그 아래 있는 <a>태그는 <sec:authorize> 안에 위치시키고, 추가로 하나 더 만든다.

<sec:authorize access="isAuthenticated()">
    <a class="dropdown-item" href="/customLogout"> 
        <i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
        Logout
    </a>
</sec:authorize>
<sec:authorize access="isAnonymous()">
    <a class="dropdown-item" href="/customLogin">
        <i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
        Login<!-- 익명상태라면 로그인표시 -->
    </a>
</sec:authorize>

 

 

 

 

JDBC를 이용하여 로그인처리하기

이제 로그인 계정을 테이블로 만들어 관리하기 위해서

sql developer 에서 쿼리문을 작성하고 실행한다.

create table tbl_member(
    userid varchar2(50) not null primary key,
    userpw varchar2(100) not null,
    username varchar2(100) not null,
    regdate date default sysdate,
    updatedate date default sysdate,
    enabled char(1) default '1');

create table tbl_member_auth (
    userid varchar2(50) not null,
    auth varchar2(50) not null,
    constraint fk_member_auth foreign key(userid) references tbl_member(userid)
);

commit;

STS로 돌아와서 로그인 관련 VO를 만든다.

src/main/java > kr.icia.domain > new Class 생성 > name: AuthVO

package kr.icia.domain;

import lombok.Data;

@Data
public class AuthVO {
	private String userid; //사용자 아이디
	private String auth; //권한
	//즉, 사용자별 권한 등록
}

src/main/java > kr.icia.domain > new Class 생성 > name: MemberVO

package kr.icia.domain;

import java.util.Date;
import java.util.List;

import lombok.Data;

@Data
public class MemberVO {
	private String userid;
	private String userpw;
	private String userName;
	private boolean enabled; //계정 정지 유무
	
	private Date regDate;
	private Date updateDate;
	private List<AuthVO> authList;
	//하나의 아이디는 여러개의 권한 소유 가능
}

인터페이스 생성

src/main/java > kr.icia.mapper > new Interface 생성 > name: MemberMapper

package kr.icia.mapper;

import kr.icia.domain.MemberVO;

public interface MemberMapper {
	public MemberVO read(String userid);
	//사용자가 아이디를 입력하면 그에 해당하는 계정 정보를 DB에서 추출
}

src/main/resources > kr > icia > mapper > new XML file 생성 > name: MemberMapper.xml

dtd는 다른 mapper xml파일에서 가져온 후 코드를 작성한다.

<?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 namespace="kr.icia.mapper.MemberMapper">
 
 	<!-- resiltType: 자동으로 설정된 리턴타입. resultMap: 수동으로 설정된 리턴타입 -->
 	<resultMap type="kr.icia.domain.AuthVO" id="authMap">
 		<result property="userid" column="userid"/>
 		<result property="auth" column="auth"/>
 	</resultMap>
 	
 	<resultMap type="kr.icia.domain.MemberVO" id="memberMap">
 		<id property="userid" column="userid"/>
 		<result property="userid" column="userid"/>
 		<result property="userpw" column="userpw"/>
 		<result property="userName" column="userName"/>
 		<result property="regDate" column="regDate"/>
 		<result property="updateDate" column="updateDate"/>
 		<collection property="authList" resultMap="authMap"/>
 	</resultMap>
 
 	<!-- 회원 정보 테이블과 회원 권한 테이블을 join하여 1개의 타입으로 회원 관련 정보를 리턴 -->
 	
 	<select id="read" resultMap="memberMap">
 		select
 		mem.userid, userpw, username, enabled, regdate, updatedate, auth
 		from
 		tbl_member mem left outer join
 		tbl_member_auth auth on mem.userid=auth.userid
 		where
 		mem.userid = #{userid}
 	</select>
 	<!-- left outer join : 좌 테이블, 우 테이블이 있을 때 좌 테이블 기준으로 레코드 추출.
 	좌 테이블은 모두 추출, 우 테이블은 일치하는 값들 추출.
 	로그인 창에서 입력한 사용자 계정을 넘겨받아서 일치하는 데이터 검색. -->
 	
 </mapper>

src/main/java > new Class 생성 > package: kr.icia.security.domain / name: CustomUser / Superclass: User

스프링프레임워크의 user를 상속받는 클래스를 생성한다.

패키지 생성 후 클래스명에 빨간줄이 뜨는데 여기에 커서를 놓고 2번 선택지를 선택한다.

그러면 의무적으로 구현해야하는 생성자가 자동으로 만들어진다.

VO를 이용하여 생성자를 호출할 수 있도록 새로운 생성자를 만든다.

package kr.icia.security.domain;

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.icia.domain.MemberVO;

public class CustomUser extends User {
	private static final long serialVersionUID = 1L;
	private MemberVO member; // DB에서 추출한 회원정보 초기화

	public CustomUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
		super(username, password, authorities);
		// 상속을 받으면서 의무적으로 구현한 생성자.
		// <? extends 클래스명> : 제너릭 타입의 상위 제한
		// <? super 클래스명> : 제너릭 타입의 하위 제한
		// <?> : 제너릭 타입 제한 없음
	}

	public CustomUser(MemberVO vo) {
		super(vo.getUserid(), vo.getUserpw(), vo.getAuthList().stream()
				.map(auth -> new SimpleGrantedAuthority(auth.getAuth())).collect(Collectors.toList()));
		this.member = vo;
		//사용자 아이디, 패스워드, 권한 목록으로 초기화
	}
	//사용자가 로그인 창에서 아이디와 패스워드를 입력하면, 해당 아이디를 가지고 일치하는 회원 정보를 찾기 (서비스 처리)
}

 

 

src/main/java > new Class 생성 > package: kr.icia.security / name: customUserDetailsService / Interface: userDetailsService

package kr.icia.security;

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 kr.icia.domain.MemberVO;
import kr.icia.mapper.MemberMapper;
import kr.icia.security.domain.CustomUser;
import lombok.Setter;
import lombok.extern.log4j.Log4j;

@Log4j
public class CustomUserDetailsService implements UserDetailsService {

	@Setter(onMethod_ = @Autowired)
	private MemberMapper memberMapper;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		log.warn("load user by userName : " + username);
		MemberVO vo = memberMapper.read(username);
		//전달된 id로 사용자 정보를 검색
		
		return vo == null ? null : new CustomUser(vo);
		//검색되지 않으면 null, 검색되면 해당 정보를 리턴
	}
}

 

 

 

 

BCryptPasswordEncoder 클래스를 이용한 패스워드 보호

암호화 처리는 스프링 시큐리티에서 제공하는 클래스를 이용할 것이다.

bcrypt 방식을 이용하는 PasswordEncoder는 이미 스프링 시큐리티에서 제공하므로 이를 빈으로 추가한다.

security-context.xml 의 <security:http> 코드 위에 bean을 추가하고, <security:http>와 <security:authentication-manager>도 수정한다.

<?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-5.4.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

	<!-- 암호화 처리, 사용자 계정 정보 처리 -->
	<bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
	<bean id="customUserDetailsService" class="kr.icia.security.CustomUserDetailsService"/>
	
	<security:http>
		<security:form-login login-page="/customLogin"/>
		<security:logout logout-url="/customLogout" invalidate-session="true"/>
	</security:http> 

	<security:authentication-manager>
		<security:authentication-provider user-service-ref="customUserDetailsService">
			<security:password-encoder ref="bcryptPasswordEncoder"/>
		</security:authentication-provider>
	</security:authentication-manager>

</beans>
​

 

 

 테스트 클래스를 이용한 계정 및 권한 추가

테스트 클래스를 만들어 임의의 계정 100개를 생성하도록 해볼 것이다.

src/test/java > new Class 생성 > package: kr.icia.security / name: MemberTests

package kr.icia.security;

import java.sql.Connection;
import java.sql.PreparedStatement;

import javax.sql.DataSource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import kr.icia.mapper.MemberMapper;
import lombok.Setter;
import lombok.extern.log4j.Log4j;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/spring/root-context.xml", "file:src/main/webapp/WEB-INF/spring/security-context.xml"})
@Log4j
public class MemberTests {
	@Setter(onMethod_ = @Autowired)
	private PasswordEncoder pwencoder; //암호화 객체
	
	@Setter(onMethod_ = @Autowired)
	private DataSource ds; //DB접근 객체
	
	@Setter(onMethod_ = @Autowired)
	private MemberMapper memberMapper; //회원정보 쿼리
	
	@Test
	public void testInsertMember() {
		String sql = "insert into tbl_member(userid, userpw ,username) values (?,?,?)";
		
		for (int i=0; i<100; i++) {
			Connection con = null;
			PreparedStatement pstmt = null;
			
			try {
				con = ds.getConnection();
				pstmt = con.prepareStatement(sql);
				
				pstmt.setString(2, pwencoder.encode("pw"+i));
				
				if(i<80) {
					pstmt.setString(1, "user"+i);
					pstmt.setString(3, "일반사용자"+i);
				} else if(i<90) {
					pstmt.setString(1, "manager"+i);
					pstmt.setString(3, "운영자"+i);
				} else {
					pstmt.setString(1, "admin"+i);
					pstmt.setString(3, "관리자"+i);
				}
				pstmt.executeUpdate();
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				if(pstmt != null) {
					try {
						pstmt.close();
					} catch (Exception e) {}
				}
				if(con != null) {
					try {
						con.close();
					} catch (Exception e) {}
				}
			}
		}
	}
}
 

 

root-context 에서는 DB정보와 스캔 등, security-context 에서는 로그인 관련 정보가 있으므로 둘 다 연동하도록 해야 한다.

* BCryptPasswordEncoder : 암호화 클래스

* invalidate-session :세션 무효화

* authentication-manager : 정상적으로 로그인되었는지 체크

* DataSource : DB와의 연동 소스

테스트를 구동해보면, 콘솔창에 위와 같이 100개의 계정이 임의로 생성된 것을 볼 수 있다.

이번에는 권한을 추가해볼 것이다.

이번에는 방금 전 테스트했던 코드는 주석처리하고, 각 계정별 권한을 부여하는 테스트 코드를 작성한다.

//각 계정별 권한 추가
@Test
public void testInsertAuth() {
    String sql = "insert into tbl_member_auth (userid,auth)" + "values (?,?)";

    for (int i=0; i<100; i++) {
        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = ds.getConnection();
            pstmt = con.prepareStatement(sql);

            if(i<80) {
                pstmt.setString(1, "user"+i);
                pstmt.setString(2, "ROLE_USER"+i);
            } else if(i<90) {
                pstmt.setString(1, "manager"+i);
                pstmt.setString(2, "ROLE_MEMBER"+i);
            } else {
                pstmt.setString(1, "admin"+i);
                pstmt.setString(2, "ROLE_ADMIN"+i);
            }
            pstmt.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (pstmt != null) {
                try {
                    pstmt.close();
                } catch (Exception e) {}
            }
            if(con != null) {
                try {
                    con.close();
                } catch (Exception e) {}
            }
        }
    }
}

 

로그인이 되었다면 Logout 창이 표시되도록 header.jsp 에서 logout 부분에 하이퍼링크를 수정한다.

그리고, "#logoutModal"을 검색해서 현재는 필요 없는 팝업이므로 data-terget="#logoutModal" 속성을 a태그에서 삭제한다.

 

 

 

서버구동 후 화면 우측 상단에 프로필 클릭하여 로그인 버튼을 누르면 로그인 페이지로 넘어간다.

ID: user0 / PW : pw0
입력 후 로그인을 누르면, 생성된 계정으로 로그인된다.

 

 

 

로그인 한 계정으로 글쓰기, 수정, 삭제 처리하기

sevlet-context.xml 의 Namespaces에서 security 체크 후, 코드 추가 작성

<security:global-method-security pre-post-annotations="enabled" secured-annotations="enabled"/>
<!-- 시큐리티와 관련된 어노테이션을 사용할 수 있도록 설정 -->

 

BoardController.java 에서 글쓰기, 수정, 삭제시 권한을 가진 사람만 가능하도록

"/register", "/modify", "/remove" 에 어노테이션을 추가한다.

@PreAuthorize("isAuthenticated()") //로그인한 사용자만 접근

 

 

그리고 "/remove"에는 writer 매개변수를 받아 처리할 수 있도록 밑에 선택된 부분처럼 수정한다.

register.jsp 에서도 작성자 계정을 이제 직접 작성하는 것이 아니라 계정 정보를 따와서 작성할 수 있도록 아래와 같이 추가로 value 값을 입력한다.

 

스크립트도 수정한다.
먼저 변수를 선언하고 값을 할당한다.

var csrfHeaderName = "${_csrf.headerName}";
var csrfTokenValue = "${_csrf.token}";
/* ajax처리시 csrf값을 함께 전송하기 위한 준비.
스프링 시큐리티는 데이터 post 전송시 csrf값을 꼭 확인하므로 */

첨부파일 처리 스크립트 부분에도 정보를 물려서 전달할 수 있도록 아래와 같이 코드를 추가로 입력한다.

전송되기 전에 먼저 계정을 확인하도록 하는 것이다.

deleteFile 부분도 추가한다.

여기까지 한 후 서버를 구동한 후 살펴보면,

일단 로그인하지 않은 상태에서는 글쓰기 버튼을 눌렀을 때 빈 화면만 나오고 있다. (콘솔 에러)

(추후 구현해야할 부분이다.)

로그인을 한 후에는 글쓰기 버튼을 눌렀을 때 글쓰기 화면으로 넘어가고, 작성자에 계정명이 뜨게 된다.

 

 

글읽기 페이지에서 로그인한 사용자만 수정 가능하도록 처리

get.jsp 에서 글읽기부분을 수정할 것이다.
수정 <button> 을 지우고 새로 작성한다.

 
<sec:authentication property="principal" var="pinfo"/>
<!-- principal 정보를 pinfo라는 이름으로 jsp에서 이용. -->
<sec:authorize access="isAuthenticated()">
<!-- 인증된 사용자만 허가 -->
    <c:if test="${pinfo.username eq board.writer }">
<!-- 인증되었으면서 작성자가 본인일 때 수정버튼 표시 -->
        <button data-oper="modify" class="btn btn-warning" id="boardModBtn">수정</button>
    </c:if>
</sec:authorize>

게시글의 작성자와 같다면 수정 버튼을 보이도록 하는 것이다.

 

 

 

 

로그인한 사용자만 댓글을 사용하도록 처리

댓글부분도 로그인한 사용자만 가능하도록 바꿀 것이다.

<sec:authorize access="isAuthenticated()">
    <button id="addReplyBtn" class="btn btn-primary btn-xs float-right">댓글</button>
</sec:authorize>

스크립트 부분도 수정할 것이다.

bnoValue를 선언한 바로 아래에 작성한다.

var replyer=null;
<sec:authorize access="isAuthenticated()">
//replyer='<sec:authentication property="principal.username"/>';
replyer='${pinfo.username}';
</sec:authorize>
 
 

댓글 작성자도 수정이 불가능해야 하므로, 스크립트에서 chat.on click 부분에서 replyer 부분에 readonly attribute를 추가한다.

스크립트에서 #addReplyBtn 을 클릭했을 때의 이벤트 부분을 수정할 것이다.

커서가 위치한 부분 사이에 추가로 입력한다.

modal.find("input[name='replyer']").val(replyer);
modal.find("input[name='replyer']").attr("readonly","readonly");

 

 

 

다시 스크립트 윗부분으로 올라와서 커서 부분에 추가 작성한다.

var csrfHeaderName="${_csrf.headerName}";
var csrfTokenValue="${_csrf.token}";

$(document).ajaxSend(function(e,xhr,options){
    xhr.setRequestHeader(csrfHeaderName, csrfTokenValue);
}); //csrf 값을 미리 설정해 두고, ajax처리시마다 이용

 

이제 댓글 수정 버튼을 눌렀을 때 이벤트 처리를 수정할 것이다. //댓글 수정 처리 (modalBtn.on("click") 부분 스크립트를 찾아서 다음과 같이 수정한다.

//댓글 수정 처리
modalModBtn.on("click", function(e) {
    var originalReplyer = modalInputReplyer.val();

    var reply = {
        rno : modal.data("rno"),
        reply : modalInputReply.val(),
        replyer : originalReplyer
    };

    if(!replyer) {
        alert("로그인 후 수정 가능");
        modal.modal("hide");
        return;
    }

    if(replyer != originalReplyer){
        alert("자신이 작성한 댓글만 수정 가능");
        modal.modal("hide");
        return;
    }

    replyService.update(reply, function(result) {
        alert(result);
        modal.modal("hide");
        showList(pageNum);
    });
});

 

로그인 정보가 없다면 로그인 후 수정이 가능하다는 메세지를, 댓글작성자와 다르다면 자신이 작성한 댓글만 수정이 가능하다는 메세지를 출력하도록 한다.

//댓글 삭제 처리(modalRemoveBtn.on("click") 부분도 수정한다.

//댓글 삭제 처리
modalRemoveBtn.on("click", function(e) {
    var rno = modal.data("rno");
    var originalReplyer = modalInputReplyer.val();

    if(!replyer) {
        alert("로그인 후 삭제 가능");
        modal.modal("hide");
        return;
    }

    if(replyer != originalReplyer){
        alert("자신이 작성한 댓글만 삭제 가능");
        modal.modal("hide");
        return;
    }

    replyService.remove(rno, originalReplyer, function(result) {
        alert(result);
        modal.modal("hide");
        showList(pageNum);
    });
});

 

수정 부분과 형태가 거의 비슷하나, replyService.remove 메소드에 originalReplyer도 추가했다.

reply.js 의 댓글 삭제 처리 부분을 수정한다.

변수에 replyer가 추가되고, url과 success 사이에 data와 contentType이 추가된다.

//댓글 삭제처리
function remove(rno, replyer, callback, error) {
    $.ajax({
        type : 'delete',
        url : '/replies/' + rno,

        data : JSON.stringify({rno:rno,replyer:replyer}),
        contentType : "application/json; charset=utf-8",

        success : function(deleteResult, status, xhr) {
            if (callback) {
                callback(deleteResult);
            }
        },
        error : function(xhr, status, er) {
            if (error) {
                error(er);
            }	
        }
    });
}

 

 

BoardController.java 에서 @GetMapping("/register") 부분에도 @PreAuthorize 어노테이션을 추가한다.

@GetMapping("/register") // 글쓰기버튼을 누르면 게시물 입력폼 보이기
@PreAuthorize("isAuthenticated()") //로그인한 사용자만 접근
public void register() {
    // 이동할 주소를 리턴하지 않는다면, 요청한 이름으로의 jsp파일을 찾음
}

이제 서버 구동 후 살펴보면 로그인을 했을 때만 댓글이 사용 가능해지고, 댓글 작성자 또한 로그인한 계정으로 들어간다.

수정과 삭제도 정상적으로 구동되는 것을 확인할 수 있다.

 

 

 

 

로그인 한 계정으로만 첨부파일 등록이 가능하도록 처리

UploadController.java 에서 public ResponseEntity<List<AttachFileDTO>> uploadAjaxPost()
public ResponseEntity<String> deleteFile() 두 가지 메소드에 어노테이션을 추가한다.

업로드와 삭제시에도 로그인한 계정으로만 가능하도록 하는 것이다.

@PreAuthorize("isAuthenticated()")

 

modify.jsp 에서 form태그 안에 input 히든을 추가한다.

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

 

작성자에 readonly 특성을 추가한다.

그리고 수정과 삭제 <button>태그들을 아래와같이 <c:if> 내에 위치시킨다. 목록 <button>은 그대로 밖에 있다.

<sec:authentication property="principal" var="pinfo" />
<sec:authorize access="isAuthenticated()">
    <c:if test="${pinfo.username eq board.writer}">
        <button type="submit" data-oper='modify' class="btn btn-success">수정</button>
        <button type="submit" data-oper='remove' class="btn btn-danger">삭제</button>
    </c:if>
</sec:authorize>

<button type="submit" data-oper="list" class="btn btn-info">목록</button>

 

스크립트에서 //첨부파일 목록 표시 끝 부분(익명즉시실행함수) 아래에 공간을 만들어 새로운 변수를 추가한다.

var csrfHeaderName = "${_csrf.headerName}";
var csrfTokenValue = "${_csrf.token}";

 

그리고 바로 아래에 [x]버튼을 처리하는 ajax 부분에서 이전과 동일하게  xhr을 같이 처리할 수 있도록 하는 beforeSend 부분을 추가한다.

//첨부파일 등록과 표시 시작 부분의 ajax에도 똑같이 beforeSend를 추가한다.

$.ajax({
    url : '/deleteFile',
    data : {
        fileName : targetFile,
        type : type
    },
    beforeSend : function(xhr) {
        xhr.setRequestHeader(csrfHeaderName, csrfTokenValue);
    },
    dataType : 'text',
    type : 'POST',
    success : function(result) {
        alert(result);
        targetLi.remove();
    }
});

서버 구동 후 로그인하여 글 수정을 해보면 첨부파일 부분도 정상적으로 수정된다.

 

 

 

 

 

 

 

자동로그인 기능 구현

sql developer 에서 자동로그인 처리용 테이블을 만든다.

--자동로그인 처리용 테이블
create table persistent_logins (
username varchar2(64) not null,
series varchar2(64) primary key,
token varchar2(64) not null,
last_used timestamp not null);
commit;

 

STS로 돌아와서, security-context.xml 에서 "/customLogin" 바로 아래에 코드를 추가한다.

<security:remember-me data-source-ref="dataSource" token-validity-seconds="604800"/>
<!-- 7일간 로그인 유지 -->

서버구동 후 확인해본다.

로그인 시 자동로그인을 체크해야 한다.
서버는 구동중인 상태에서, 웹 브라우저를 종료한 후 다시 접속해도 로그인 상태가 유지된다.

또한, sql developer에 persistent_logins 테이블에도 데이터가 추가된다.

 

 

 

로그인하지 않은 상태에서 댓글 수정/삭제가 되는 오류 제거하기

로그아웃한 상태로 게시글을 읽었을 때, 댓글 버튼을 누르면 댓글 수정 모달창이 뜨게 된다. 그리고 수정 버튼을 눌렀을 때 수정이 완료되어 버린다.

이러한 오류를 제거하기 위한 작업을 해볼 것이다.

ReplyController.java 각 메소드에 어노테이션을 추가한다.

// 글읽기 부분

@PreAuthorize("isAuthenticated()")
@PostMapping(value = "/new", consumes = "application/json", produces = { MediaType.TEXT_PLAIN_VALUE })
public ResponseEntity<String> create(@RequestBody ReplyVO vo) {
...
}

// 댓글 삭제 부분 (삭제부분은 변수에 @RequestBody ReplyVO vo 도 추가)

@PreAuthorize("principal.username == #vo.replyer")
@DeleteMapping(value = "/{rno}", produces = MediaType.TEXT_PLAIN_VALUE)
public ResponseEntity<String> remove(@PathVariable("rno") Long rno, @RequestBody ReplyVO vo) {
    ...
}

// 댓글 수정 부분

// 댓글 수정
@PreAuthorize("principal.username == #vo.replyer")
@RequestMapping(method = { RequestMethod.PUT, RequestMethod.PATCH }, value = "/{rno}", 
        consumes = "application/json", produces = MediaType.TEXT_PLAIN_VALUE)
public ResponseEntity<String> modify(@RequestBody ReplyVO vo, @PathVariable("rno") Long rno) {
    ...
}

본인이 작성한 댓글이 아닌 댓글을 수정 또는 삭제하려고 하면 삭제가 되지 않도록 변경되었다.

 

 

 

 

 

로그아웃 처리

header.jsp 에서 로그아웃 <a> 태그의 속성에서 data-toggle 값을 지워준다.

계정 로그아웃을 누르면 로그아웃 페이지로 넘어가고, logout버튼을 누르면 정상적으로 로그아웃된다.

 

 

 


 로그인 처리 과정

이전에 web.xml에 security filter를 설정했는데, 이게 로그인처리가 실질적으로 이뤄질 수 있는 핵심이라고 할 수 있다.

관련 내용은 다음 사이트에서 참고할 수 있다.

https://sjh836.tistory.com/165

session은 보안이 높은 등급의 데이터를 유지하는 데 유리하다.

그래서 로그인 처리는 session 처리를 주로 이용하게 되며, 로그인 하기 전에 session으로 체크하는 원리이다.

보통 예민하고 높은 보안등급을 요하는 일은 session으로, 그렇지 않은 일은 cookie로 처리하게 된다.

인증과 관련된 정보는 CustomUserDatailesService 에서 오류체크 후 null이 아니라면 넘겨주도록 설정하고 > CustomUser 에서 일치하는지를 확인한다.

로그인 처리는 지금까지 이런 과정으로 처리해 보았다.

 

 

 

 

댓글