[Eclipse]_Spring Security Handler 구현 1단계 (로그인 과정 구현)

2022. 12. 22. 14:48[Spring]_/[Eclipse]

728x90
반응형

개요

Intellij를 사용하다가 다음 프로젝트가 Eclipse 환경으로 인해

Eclipse 에 spring 최신버전을 설치하였고

SpringSecurity  에서 제공하는

handler 들을 커스텀화 하여 적용하는 방법 서울

 

1단계에서는 로그인 과정 핸들러를 구현합니다.

 

환경 

Eclipse 최신버전 2022-09 (4.25.0)

Tomcat - 10.0.23  v

maven Artifact ID : maven-archetype-webapp 1.4v

Spring 6.0.2 ver

Spring Security 6.0.0 ver

Jakarta 5.0.0 ver

 

특이사항

이전 포스트 에서 바뀐점이 있습니다.

Tomcat 10 버전 사용시 필요한 servlet 버전은 5 입니다.

이전까지는 Tomcat 에 내장된 jakarta 를 사용하였고

이번에 maven build 에 해당 servlet 이 없을 경우 오류가 발생합니다.

 

다음 포스트 참고

https://yn971106.tistory.com/167

 

determined from the Dynamic Web Module facet version (2.3), was not found on the Java Build Path

환경 Eclipse 최신버전 2022-09 (4.25.0) Tomcat - 10.0.23 v maven Artifact ID : maven-archetype-webapp 1.4v Spring 6.0.2 ver Spring Security 6.0.0 ver spring-jdbc : 6.0.2 ver mybatis : 3.5.11ver mybatis-spring : 3.0.1ver 문제상황 오류코드 deter

yn971106.tistory.com

 

또한 본문에 springSecurty 6.0.0 버전에 맞는 DTD도 기술하였습니다.

 

해당 포스트는 하위 포스트의 후속내용입니다.

https://yn971106.tistory.com/163

 

[eclipse]_spring_security 설치

개요 Intellij를 사용하다가 다음 프로젝트가 Eclipse 환경으로 인해 Eclipse 에 spring 최신버전을 설치하였고 SpringSecurity 를 설치 하는 과정 설명 환경 Eclipse 최신버전 2022-09 (4.25.0) Tomcat - 10.0.23 v maven Ar

yn971106.tistory.com


1. security.context 수정

 

기존의 DTD 의 경우 이전버전으로 6.0.0 에 맞는 DTD 작성

DTD 의 경우 다음 링크 참고

https://docs.spring.io/spring-security/reference/6.0.0-M3/servlet/appendix/namespace/http.html#nsa-http-attributes

 

Web Application Security :: Spring Security

Adds support for concurrent session control, allowing limits to be placed on the number of active sessions a user can have. A ConcurrentSessionFilter will be created, and a ConcurrentSessionControlAuthenticationStrategy will be used with the SessionManagem

docs.spring.io

코드 + 주석 설명

<?xml version="1.0" encoding="UTF-8"?>
<b:beans xmlns="http://www.springframework.org/schema/security"
	xmlns:b="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	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">


	

	<!-- http 요청시 처리 로직 -->
	<http request-matcher="ant" auto-config="true">
	
	<!-- url intercept 설정 -->
		<intercept-url pattern="/login/**"  access="permitAll" />
		<intercept-url pattern="/menu1" access="hasRole('ROLE_ADMIN')" />
		<intercept-url pattern="/**"  access="permitAll" />
	
	<!-- 로그인 페이지 설정 -->	
		<form-login login-page="/login/login"
			username-parameter="username" password-parameter="password"
			login-processing-url="/loginProcessing"
			authentication-success-handler-ref="loginSuccessHandler"
			authentication-failure-handler-ref="loginFailureHandler" />
		<!--  엑세스 거부시 설정 -->
		<access-denied-handler ref="accessDeniedHandler" />
		
		<!--  로그아웃 설정 페이지 아직 미구현 -->
		<logout logout-url="/logout" logout-success-url="/login/login"
			invalidate-session="true" />
		
	</http>

	<!-- 엑세스 거부 프로세스 객체 (빈) 등록 -->
	<!-- 구현체의 defaultDeniedUrl 에 value 의 값을 대입 -->
	<b:bean id="accessDeniedHandler"
		class="com.yoon.loginHelp.security.CustomAccessDeniedHandler">
		<b:property name="defaultDeniedUrl" value="/login/login"></b:property>
	</b:bean>
	
<!-- 로그인 성공시 작동할 객체(빈) 등록-->
	<b:bean id="loginSuccessHandler"
		class="com.yoon.loginHelp.security.LoginSuccessHandler">
	</b:bean>

<!-- 로그인 실패시 작동할 객체(빈) 등록-->
<!-- 해당 클래스의(구현체의) defaultFailureUrl 에 value 값 대입-->
	<b:bean id="loginFailureHandler"
		class="com.yoon.loginHelp.security.LoginFailureHandler">
		<b:property name="defaultFailureUrl" value="/login/error" />
	</b:bean>

<!-- 로그인 프로세스-->
<!-- 구현체에 기존 security 에서 작동하는 메소드 오버라이드 하여 커스텀화 가능-->
	<authentication-manager>
		<authentication-provider
			ref="customAuthenticationProvider" />  <!-- 로그인 관련 로직 구현 -->
	</authentication-manager>

<!-- 로그인 구현체에 등록한 객체의 빈 등록 -->
 <!-- 로그인 관련 로직 구현 -->
	<b:bean id="customAuthenticationProvider"
		class="com.yoon.loginHelp.security.CustomAuthenticationProvider" />

</b:beans>

 

등록해야 하는 빈 객체

  • loginSuccessHandler : 로그인 성공시 핸들러
  • loginFailureHandler : 로그인 실패시 핸들러 
  • accessDeniedHandler : 접근 거부 ( 예시 permitall 이 아닌 곳 직접 url 치고 들어올 경우)
  • CustomAuthenticationProvider : 로그인 하는 과정 직접 오버라이드 하여 구현 < NOW

2 -1  CustomAuthenticationProvider.java작성

<!-- 로그인 프로세스-->
<!-- 구현체에 기존 security 에서 작동하는 메소드 오버라이드 하여 커스텀화 가능-->
	<authentication-manager>
		<authentication-provider
			ref="customAuthenticationProvider" />  <!-- 로그인 관련 로직 구현 -->
	</authentication-manager>

<!-- 로그인 구현체에 등록한 객체의 빈 등록 -->
 <!-- 로그인 관련 로직 구현 -->
	<b:bean id="customAuthenticationProvider"
		class="com.yoon.loginHelp.security.CustomAuthenticationProvider" />

securityContext 에 작성한 내용으로

class 경로와 똑같은 위치에 

 

CustomAuthenticationProvider.java 를 생성합니다.

CustomAuthenticationProvider.java 소스코드

package com.yoon.loginHelp.security;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.AuthorityUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.yoon.user.service.UserService;

import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;

public class CustomAuthenticationProvider implements AuthenticationProvider{
	
	final Logger logger = LoggerFactory.getLogger(getClass());

	
	@Resource
	private UserService userService;
	
	@Override
	public boolean supports(Class<?> authentication) {
		// TODO Auto-generated method stub
		return authentication.equals(UsernamePasswordAuthenticationToken.class);	
		}

	
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//		HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();

		String username = (String) authentication.getPrincipal();
		String password = (String) authentication.getCredentials();
		
		
		System.out.println("고객번호/비밀번호. {}/{}" +username +":"+password);
		
		Map<String,String> param = new HashMap<>();
		param.put("username", username);
		param.put("password", password);
		
		try {
			
			List<Map<String,String>> userInfo = userService.getUserInfo(param);
			
			if(userInfo.get(0) != null) {
				System.out.println("success login");
				System.out.println(userInfo.get(0).toString());
				
				List<GrantedAuthority> roles = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN");
				System.out.println(roles.toString());
				UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(username,password,roles);
				System.out.println(result.toString());
				result.setDetails(new CustomUserDetails(userInfo.get(0),roles));
				System.out.println(result);
				return result;
				
			}else {
				throw new BadCredentialsException("미가입 사용자");
			}
			
			
		}catch (Exception e) {
			throw new BadCredentialsException(e.toString(), e);
		}
		
		
		
	}
	
	
}

AuthenticationProvider 를 implements 로 상속받습니다.

AuthenticationProvider 에 구현된 2가지 메소드를 오버라이드 한 뒤

 

authenticate 메소드에 코드를 작성합니다.

 

Authentication 객체는 spring Security 에서 인증정보의 객체입니다.

해당 인터페이스의 

getPrincipal()

getCredentials()

를 이용하여 로그인한 username 과 password 를 받습니다.

 

이를 Map 형식을 바꾸어 service 에 param을 전달합니다.

List<Map<String,String>> userInfo = userService.getUserInfo(param);

2- 2 DB 조회 로직 (Mapper 가 연동되어 있어야 합니다. ( 이전포스팅 참고 )

service 는 다음과 같습니다.

package com.yoon.user.service;

import java.sql.SQLException;
import java.util.List;
import java.util.Map;

public interface UserService {


	public List<Map<String, String>> getUserInfo(Map<String, String> mapData) throws SQLException;
}

serviceImpl 은 다음과 같습니다.

package com.yoon.user.service.impl;

import java.sql.SQLException;
import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Service;

import com.yoon.mapper.UserMapper;
import com.yoon.user.service.UserService;

import jakarta.annotation.Resource;

@Service
public class UserServiceImpl implements UserService {

	
	@Resource
	private UserMapper userMapper;
	
	@Override
	public List<Map<String, String>> getUserInfo(Map<String,String>mapData) throws SQLException {
		// TODO Auto-generated method stub
		return userMapper.getUserInfo(mapData);
	}

}

mapper interface 는 다음과 같습니다.

package com.yoon.mapper;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {

	public List<Map<String, String>> getUserInfo(Map<String, String> mapData);

}

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="com.yoon.mapper.UserMapper">

	<select id="getUserInfo"  resultType="HashMap">
		select 
			*
		from user_info_manager
		where 
			USER_ID = #{username}
			AND USER_PW = #{password}
	</select>

</mapper>

2-3 다시 CustomAuthenticationProvider.java 로 돌아와서

try 내부 로직은 원하시는대로 작성할 수 있습니다.

 

만약 컬럼중 admin 혹은 user를 구분하는 값이 있다면 

그 컬럼의 유무를 기점으로 roles 을 지정하여

UsernamePasswordAuthenticationToken 를 생성 할 수 있습니다.

 

저는 예시로 모든 값에 ROLE_ADMIN 이라는 역활을 부여하였습니다.

List<GrantedAuthority> roles = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN");

 

이를 반환할 객체에 담고

UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(username,password,roles);

 

여기 까지는 username, password , role 까지 토큰에 저장되고.

 

만약 추가적인 정보를 저장해서 전달하고 싶을 경우

setDetails() 를 활용합니다.

result.setDetails(new CustomUserDetails(userInfo.get(0),roles));

 

2- 4 여기서 CusomerUserDetails 형식의 모델 객체로 만들어서 저장하게 됩니다.

해당 클래스는 다음과 같습니다.

CusomerUserDetails .java 소스코드

package com.yoon.loginHelp.security;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import javax.sound.midi.Soundbank;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import jakarta.annotation.Resource;

public class CustomUserDetails extends User {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1593214568512L; // ?

	
	private String user_id;
	private String user_pw;
	private String user_nm;
	private String user_phone;
	private String user_typ;
	
	
	
	public CustomUserDetails(String username, String password,Collection<? extends GrantedAuthority> authorities) {
		super(username, password,authorities);
		// TODO Auto-generated constructor stub
	};
	
	public CustomUserDetails(Map<String, String> result ,Collection<? extends GrantedAuthority> authorities ) {
		super(result.get("USER_ID"),result.get("USER_PW"),authorities);
		System.out.println("tttt");
		System.out.println(result);
		this.setUser_id(result.get("USER_ID").toString());
		this.setUser_pw(result.get("USER_PW").toString());
		this.setUser_nm(result.get("USER_NM").toString());
		this.setUser_phone(result.get("USER_PHONE").toString());
		this.setUser_typ(result.get("USER_TYP").toString());
	}

	public String getUser_id() {
		return user_id;
	}

	public void setUser_id(String user_id) {
		this.user_id = user_id;
	}

	public String getUser_pw() {
		return user_pw;
	}

	public void setUser_pw(String user_pw) {
		this.user_pw = user_pw;
	}

	public String getUser_nm() {
		return user_nm;
	}

	public void setUser_nm(String user_nm) {
		this.user_nm = user_nm;
	}

	public String getUser_phone() {
		return user_phone;
	}

	public void setUser_phone(String user_phone) {
		this.user_phone = user_phone;
	}

	public String getUser_typ() {
		return user_typ;
	}

	public void setUser_typ(String user_typ) {
		this.user_typ = user_typ;
	}
	
	

}

해당 클래스는 User 를 상속받아서 구현하였습니다.

 

Getter 와 Setter 가 구현되어 있습니다.

해당 모델의 생성자는 다음과 같습니다.

public CustomUserDetails(Map<String, String> result ,Collection<? extends GrantedAuthority> authorities ) {
		super(result.get("USER_ID"),result.get("USER_PW"),authorities);
		System.out.println("tttt");
		System.out.println(result);
		this.setUser_id(result.get("USER_ID").toString());
		this.setUser_pw(result.get("USER_PW").toString());
		this.setUser_nm(result.get("USER_NM").toString());
		this.setUser_phone(result.get("USER_PHONE").toString());
		this.setUser_typ(result.get("USER_TYP").toString());
	}

여기서 map 형식의 result 와 Authority 의 콜렉션을 받도록 하였습니다.

 

이와 같이 

UsernamePasswordAuthenticationToken 의 detail 항목에는 모델의 객체가 들어가 됩니다.

 


정리

securityContext

만약 Mapper 에서 ( 유저가 있는지 확인하는 쿼리 ) 값이 있어 

UsernamePasswordAuthenticationToken  가  리턴 된 경우 -> loginSuccessHandler 호출

 

만약 Mapper 에서 ( 유저가 있는지 확인하는 쿼리 ) 값이 없어

BadCredentialsException 가 일어날 경우 -> loginFailureHandler 호출

 

하게 됩니다.

 


3 Front (jsp)

login.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
     <link type="text/css" rel="stylesheet"  href="/static/assets/css/login.css">
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet">
    <script src="https://kit.fontawesome.com/53a8c415f1.js" crossorigin="anonymous"></script>
   
</head>
<body>
    <div class="wrap">
        <div class="login">
            <h2>Log-in</h2>
            <div class="login_sns">
            <li><a href=""><i class="fab fa-instagram"></i></a></li>
            <li><a href=""><i class="fab fa-facebook-f"></i></a></li>
            <li><a href=""><i class="fab fa-twitter"></i></a></li>
            </div>
            <form action="/loginProcessing" method="POST">
            <div class="login_id">
                <h4>E-mail</h4>
                <input type="text" name="username" id="username" />
            </div>
            <div class="login_pw">
                <h4>Password</h4>
                <input type="password" name="password" id="password" />
            </div>
            
            <div class="submit">
                <input type="submit"  value="로그인"/>
            </div>
            </form>
        </div>
    </div>
    
    <script>
    	
    </script>

 

중요한 것은 이부분입니다.

<form action="/loginProcessing" method="POST">

폼태그 action 을 

securityContext

login-processing-url 과 동일하게 해야 springSecurity 가 인식하게 됩니다.

 


결과

 

감사합니다.

 

728x90
반응형