I T H

[포트폴리오 프로젝트 5] 로그인 페이지 구현(spring security포함) 본문

Spring MyPortfolio Project

[포트폴리오 프로젝트 5] 로그인 페이지 구현(spring security포함)

thdev 2024. 1. 24. 10:34
로그인 구현에 앞서 폴더 구성을 변경하고자 한다.
아래와 같이 index.jsp를 제외한 앞으로 신규 생성될 jsp 파일들은 WEB-INF/views 폴더 아래에 위치시키도록 한다.

 

 

- 앞서 admin.jsp 페이지 위치가 다르게 설정되어 있었으므로 아래와 같이 컨트롤러도 수정하여 준다.

 

 

- 로그인 구현을 위한 화면 UI를 작성한다.

[ JSP ]

WEB-INF\views\cLogin.jsp

아이디, 비밀번호 입력 창이 가운데 배치가 되도록 스타일 수정을 해주었다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="kr">
<%@include file="/resources/inc/header.jsp"%>

<style>
.row {
	justify-content: center;
	display: flex;
	padding: 5px;
}
</style>
<body class="home">
	<%@include file="/resources/inc/top.jsp"%>

	<main id="main">

		<div class="container">

			<div class="section featured topspace">
				<h2 class="section-title">
					<span>로그인 페이지</span>
				</h2>
				<div class="row">
					<div class="col-sm-6 col-md-6">
						<h3 class="text-center">로그인 정보를 입력하세요.</h3>
					</div>
				</div>
				<div style="" class="col-md-4 col-sm-4"></div> <!-- 빈 div 태그로 여백주기 -->
				<form id="loginForm" method="POST" action="/login">
					<!-- SecurityConfig에 .loginProcessingUrl("/login")으로 등록되어있기 때문에 action시 AuthProvider를 타게됨!.-->

					<div class="col-md-4 col-sm-4"
						style="border: 1px solid #c5cfcb; border-radius: 5px; padding: 15px;">
						<div class="row">
							<div class="col-sm-12 col-md-12">
								<label class="form-label" for="txtId">아이디</label> <input
									type="text" id="txtId" name="userId" class="form-control">
							</div>
						</div>

						<div class="row">
							<div class="col-sm-12 col-md-12">
								<label class="form-label" for="txtPassword">비밀번호</label> <input
									type="password" id="txtPassword" name="password"
									class="form-control">
							</div>
						</div>
						<p></p>
						<div class="row">
							<div class="text-center col-sm-12 col-md-12">
								<a id="btnLogin" class="btn btn-block w-btn-green button-hover">로그인</a>
							</div>
						</div>
					</div>
				</form>

			</div>
			<!-- / section -->

		</div>
		<!-- /container -->

	</main>


	<%@include file="/resources/inc/footer.jsp"%>
	<%@include file="/resources/inc/incJs.jsp"%>

	<!-- page script -->
	<script src="/resources/views/login.js"></script>

</body>
</html>

 

다음으로 스크립트를 작성한다.

아이디와 비밀번호를 입력 후 로그인 버튼을 클릭하면 서버사이드 컨트롤러를 호출하여 로그인 체크를 하도록 해준다.

[ JS ]

webapp\resources\views\login.js

/*******************************************************************************
 * login.js
 * @author thevalue
 * @since 2023
 * @DESC 로그인 화면 스크립트
 ******************************************************************************/
(function(){
	 
	 function Login(){
		 
		//private variables
		
		//초기화 메서드
		function _init(){
			//이벤트 처리 함수 호출
			bindEvent();
			
			LoginTest();
		}
		
		function bindEvent(){
			
			$("#btnLogin").on("click", function(){
				var userId = $("#txtId").val();
				if(userId == ""){
					alert("사용자 아이디를 입력하시기 바랍니다.");
					return;
				}
				var password = $("#txtPassword").val();
				if(password == ""){
					alert("사용자 비밀번호를 입력하시기 바랍니다.");
					return;
				}
				
				$("#loginForm").submit(); 
				
			});
			
			
		}
		function LoginTest(){
			var LoginParameter = window.location.href;
			//alert(LoginParameter);
			if(LoginParameter.indexOf('error') > 0){
				alert("입력하신 사용자 정보가 없습니다.");
			}
		}
		
		function _finalize() {
		}

		return {
			init: _init,
			finalize: _finalize
		} 
	 }
	 
	 var login = new Login();
	 login.init();
	 
 })();

 

- 시큐리티 환경설정 클래스를 작성해준다.

[SecurityInitializer.java]

 src\main\java\kr\co\values\security\SecurityInitializer.java

package kr.co.values.security;

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

/* Spring Security를 사용하려면 AbstractSecurityWebApplicationInitializer를 
 * 상속받는 클래스를 반드시 작성해야 한다. 이 클래스가 있을 경우 
 * Spring Security가 제공하는 필터들을 사용할 수 있도록 활성화 해준다.
 */
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer{

}

[ SecurityConfig.java ]

src\main\java\kr\co\values\security\SecurityConfig.java

로그인 페이지 및 로그인 프로세스를 처리할 url 매핑이 configure 메소드에 정의되어 있어야 한다.

package kr.co.values.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;



/* Spring Security를 이용해 로그인/로그아웃/인증/인가 등을 처리하기 위한 설정 파일이다.
 * @EnableWebSecurity가 붙어 있을 경우 Spring Security를 구성하는 기본적인 Bean들을 자동으로 구성해준다.
 * WebSecurityConfigurerAdapter를 상속받으면 특정 메소드를 오버라이딩 함으로써 좀 더 손쉽게 설정할 수 있다.
 */

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	/* 
	 * 주의) configure() 메소드는 파라미터 타입이 다른 동일한 메소드명이 존재하므로 확인 필수
	 * 인증 및 인가가 필요없는 url 을 지정하여 시큐리티 미적용되도록 처리한다.
	 * ex > /resources /css /img 등등
	 */
	
	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring().antMatchers("/webjars/**");
	}
	
	/* configure(HttpSecurity http) 메소드를 오버라이딩 한다는 것은 인증/인가에 대한 설정을 한다는 의미이다. 
     * 가장 중요한 메소드로 볼 수 있다.
     *
     * http.csrf().disable()는 csrf() 기능을 끄라는 설정이다.
     * csrf는 보안 설정 중 post방식으로 값을 전송할 때 token을 사용해야하는 보안 설정이다.
     * csrf은 기본으로 설정되어 있는데 사용시 보안성은 높아지지만 
     * 개발초기에는 불편함이 있다는 단점이 있어서 기능을 끈 것이다.
     */
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.csrf().disable()
		.authorizeRequests()
		.antMatchers("/admin/**").hasRole("ADMIN") // .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')") 이렇게도 사용 가능하다.
		//url주소가 /admin/에 모든것들로 접속하는것들은 "ADMIN"만 가능
		//AuthProver에서 이부분 => roles.add(new SimpleGrantedAuthority("ROLE_" + user.getUserAuth())); 
		.anyRequest().permitAll(); // permitAll() : 이 외 모든 url 접근 허용 / authenticated() : 이 외 모든 url 접근은 로그인 후 사용 가능하도록 설정
		
		//login 설정
		http
		.formLogin()
		.loginPage("/login") //GET 요청 (login form을 보여줌)
		.loginProcessingUrl("/login") //POST 요청(login 창에 입력한 데이터를 처리)
		.usernameParameter("userId") //login에 필요한 id값으 userId로 설정( default는 username)
		.passwordParameter("password") //login에 필요한 password 값을 password(default)로 설정
		.defaultSuccessUrl("/") //login에 성공하면 /로 redirect
		.successHandler(new UserLoginSuccessHandler()); // 로그인 성공 후 해당 클래스 호출하여 세션에 로그인 정보 담기
	}
	
	 /*
     * 비밀번호 암호화를 위해 사용한다.
     */
	@Bean
	public PasswordEncoder encoder() {
		return new BCryptPasswordEncoder();
	}
	
}

[ AuthProvider.java ]

src\main\java\kr\co\values\security\AuthProvider.java

로그인 인증을 담당하는 클래스로 화면에서 전달받은 아이디를 통해 mybatis를 활용하여 사용자 정보를 조회 후 비밀번호가 일치하는지 체크하는 로직을 담고 있다.

package kr.co.values.security;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import kr.co.values.login.domain.User;
import kr.co.values.login.service.UserService;

@Component
public class AuthProvider implements AuthenticationProvider {
    @Autowired
    private UserService userService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String id = (String) authentication.getPrincipal(); // 로그인 창에 입력한 id 
        String password = (String) authentication.getCredentials(); // 로그인 창에 입력한 password
        
        PasswordEncoder passwordEncoder = userService.passwordEncoder();    
        UsernamePasswordAuthenticationToken token;
        
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("userId", id);
        // 화면에서 입력받은 아이디를 통해 사용자 정보가 있는지 조회한다.
        User user = userService.getUserById(map);
//        System.out.println("user" + user.getUserEmail());

        if(user == null) {
        	throw new UsernameNotFoundException("No User Id");
        }
        
        if (user != null && passwordEncoder.matches(password, user.getUserPwd())) { // 일치하는 user 정보가 있는지 확인
            List<GrantedAuthority> roles = new ArrayList<>();
            roles.add(new SimpleGrantedAuthority("ROLE_" + user.getUserAuth())); // 권한 부여

            token = new UsernamePasswordAuthenticationToken(user, null, roles); 
            // 인증된 user 정보를 담아 SecurityContextHolder에 저장되는 token

            return token;
        }

        throw new BadCredentialsException("No such user or wrong password."); 
        // Exception을 던지지 않고 다른 값을 반환하면 authenticate() 메서드는 정상적으로 실행된 것이므로 인증되지 않았다면 Exception을 throw 해야 한다.
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }
}

[ UserLoginSuccessHandler.java ]

src\main\java\kr\co\values\security\UserLoginSuccessHandler.java

로그인 성공했을 때 타는 클래스로 인증된 데이터를 세션에 담는다.

package kr.co.values.security;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import kr.co.values.login.domain.User;

//스프링 시큐리티를 통한 로그인 접속이 ^성공^ 한 경우 해당 클래스를 호출
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler{

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		// 로그인 인증이 성공한 이후 
		// 접속한 로그인 정보 중 아이디, 이름, 이메일을 세션에 담아
		// 화면에서 jstl을 통해 세션 정보를 가져와서 사용할 것이다.
		
		HttpSession session = request.getSession(true);
		if(session == null) {
			session = request.getSession();
		}
        
		User userInfo = (User)authentication.getPrincipal();
		session.setAttribute("userId", userInfo.getUserId());
		session.setAttribute("userName", userInfo.getUserName());
		session.setAttribute("userEmail", userInfo.getUserEmail());
		
		response.sendRedirect(request.getContextPath() + "/"); // localhost:8080 + "/"
	}

}

 

- 시큐리티 설정이 끝났으면

Kr\co\values\init\WebConfig.java 일에 SecurityConfig.class 를 추가해준다.

	@Override
	protected Class<?>[] getRootConfigClasses() {
		return new Class[] {RootConfig.class, SecurityConfig.class};
	}

 

다음으로는 컨트롤러를 비롯한 클래스 파일들을 작성한다.

최종적으로 아래와 같은 패키지 구조로 클래스가 작성될 것이다.

 

 

[ 컨트롤러 구현 ]

kr\co\values\login\web\LoginController.java

로그인 프로세스 url로 들어온 경우 앞서 작성한 Authentication 클래스 파일의 시큐리티 설정을 통해 로그인 체크가 이루어진 다음에 로그인이 정상적으로 성공할 경우 메인 ("/")로 이동하도록 설정해주었다.

package kr.co.values.login.web;

import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginController {
	// 사용자가 브라우저를 통해 /login url로 접속했을때 타는 컨트롤러(get방식)
	// 주의 : cLogin.jsp에서 post방식으로 호출되는 것은 SecurityConfig에서 .loginProcessingUrl("/login") //Post방식 <-- 이부분으로 인해 시큐리티 AuthProvider를 타게됨!

	@GetMapping("/login")
	public String loginPage(Model model) {
		// 로그인되지 않은 상태이면 로그인 페이지를, 로그인된 상태이면 메인 페이지를 보여줌
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if(authentication instanceof AnonymousAuthenticationToken) {
			return "cLogin";
		}
		return "redirect:/";
		
	}
}

[ 서비스 구현 ]

인증 처리를 담당하는 부분에서 호출할 서비스 클래스 및 메소드를 구현하여 준다.

사용자 아이디를 통해 데이터베이스에서 사용자 정보가 있는지 조회하는 로직을 담고 있다.

kr\co\values\login\service\UserService.java

package kr.co.values.login.service;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import kr.co.values.login.domain.User;
import kr.co.values.login.persistence.UserMapper;

@Service
public class UserService {
	@Autowired
	private UserMapper userMapper;
	
	private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
	
	public User getUserById(Map<String, Object> map) {
		return userMapper.getUserById(map);
	}
	
	public PasswordEncoder passwordEncoder() {
		return this.passwordEncoder;
	}
}

[ 매퍼 인터페이스 및 쿼리 작성 ]

kr\co\values\login\persistence\UserMapper.java

package kr.co.values.login.persistence;

import java.util.Map;

import kr.co.values.login.domain.User;

public interface UserMapper {
	
	// 사용자 정보 가져오기
	User getUserById(Map<String, Object> map);
	
}

 

kr\co\values\login\persistence\UserMapper.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.co.values.login.persistence.UserMapper"> 
	
    <!-- 회원 정보 가져오기 -->
    <select id="getUserById" parameterType="hashmap" resultType="kr.co.values.login.domain.User">
        SELECT USER_ID
             , USER_PWD
             , USER_NAME
             , USER_EMAIL
             , CASE WHEN USER_AUTH = '1' THEN 'USER' ELSE 'ADMIN' END AS USER_AUTH
             , USER_TEL
             , USER_EMAIL
        FROM TBL_USER_INFO
        WHERE USER_ID = #{userId}
    </select>
	
</mapper>

[ 모델빈 (도메인) 클래스 작성 ]

- 사용자 정보를 담기 위한 빈(bean) 클래스

- 이미 전 챕터에서 모델빈을 이용하였기때문에 작성했으므로 추가 작성할 필요는 없다.

kr\co\values\login\domain\User.java

package kr.co.values.login.domain;

/*
 * 사용자 테이블 컬럼 구조 
 *  `USER_ID` varchar(20) NOT NULL,
  `USER_PWD` varchar(100) NOT NULL,
  `USER_NAME` varchar(100) DEFAULT NULL,
  `USER_AUTH` char(1) DEFAULT NULL,
  `USER_TEL` varchar(20) DEFAULT NULL,
  `USER_EMAIL` varchar(50) DEFAULT NULL,
  `INPUT_DATETIME` datetime DEFAULT NULL,
 */
public class User {
	private String userId;
	private String userPwd;
	private String userName;
	private String userAuth;
	private String userTel;
	private String userEmail;
	
	public String getUserId() {
		return userId;
	}
	public void setUserId(String userId) {
		this.userId = userId;
	}
	public String getUserPwd() {
		return userPwd;
	}
	public void setUserPwd(String userPwd) {
		this.userPwd = userPwd;
	}
	public String getUserName() {
		return userName;
	}
	public void setUserName(String userName) {
		this.userName = userName;
	}
	public String getUserAuth() {
		return userAuth;
	}
	public void setUserAuth(String userAuth) {
		this.userAuth = userAuth;
	}
	public String getUserTel() {
		return userTel;
	}
	public void setUserTel(String userTel) {
		this.userTel = userTel;
	}
	public String getUserEmail() {
		return userEmail;
	}
	public void setUserEmail(String userEmail) {
		this.userEmail = userEmail;
	}
}

[ 로그인 테스트 ]

/login 으로 접속하여 로그인 테스트를 진행한다.

로그인시 메인페이지로 정상 이동했다면 정상작동!

 

< 추가 >

-  incJs.jsp 상단에는 UTF-8 설정을 통해 한글 저장이 가능하도록 아래 코드도 입력하여 준다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

 

- 헤더 배경이미지 바꾸기

1. 마음에 드는 이미지를 선택후 bg_head2.jpg 이름으로 저장한다.

2. myPortfolio\src\main\webapp\resources\images 폴더 아래에 bg_head2.jpg 파일을 붙여넣는다.

3. myPortfolio\src\main\webapp\resources\css\styles.css

   위 파일의 228라인의 스타일 정보를 아래와 같이 수정한다.

#head {
  background: #f4f4f4 url(../images/bg_head2.jpg) top center;
  background-size: cover;
  color: #7C7C7C;
  padding: 30px 0 35px 0;
}
#head img.img-circle {
  display: block;
  width: 140px;
  height: 140px;
  overflow: hidden;
  border: 9px solid rgba(0, 0, 0, 0.05);
  margin: 0 auto;
}
#head .title {
  font-family: Alice, Georgia, serif;
  font-size: 49px;
  font-size: 3.0625rem;
}
#head .title a {
  text-decoration: none;
  color: #333333;
}
#head .tagline {
  display: block;
  font-size: 14px;
  font-size: 0.875rem;
  line-height: 1.2em;
  color: #333;
  margin: 5px 0 0;
  font-weight: bold;
  text-shadow: 2px -2px 2px #eef1ec;
}
#head .tagline b {
  font-weight: normal;
}
#head .tagline a {
  color: #333333;
}
.home #head {
  padding: 90px 0;
}
.home #head .title {
  font-size: 49px;
  font-size: 3.0625rem;
  color: #0c0c0c;
  text-shadow: 2px -2px 2px #f5f5f5;
}
.home #head .tagline {
  font-size: 16px;
  font-size: 1rem;
  margin: 15px 0 0;
}