상세 컨텐츠

본문 제목

spring boot 3 security 그냥 한번 해보고 한번 더 해보면 잘 될꺼얼!

스프링

by e7e 2024. 3. 19. 21:22

본문

초보자들은 Spring Security를  아주 마니 어려워 하는 경우가 대부분이고,

실행만 잘 되어도 대단한 업적을 쌓은 양~~ 겉으로는 기뻐하면서, 속으로는

잘 모른다는 불안감에 스스로에게  "이 설정파일만 잘 지키면 문제업엉!" 라는

자기 다짐을 다지고 또 또 다진당.~~

 

처음 설정이라면 당연히 익숙치 않은 설계 다이어그램, 인터페이스, 클래스등등..

지나친 낯설음 연속에 뇌가 연속 스트레스에 피로감을 느끼는 건 안 피한당!!.

편하게 해주려고, 지켜주려고  맹글었단 사실에만 먼저 기뽀하장.

 

보안에 열심인 사람들이 이미 기본적으로 필요한 걸 다 맹글어 놓았공,

난 커스터마이징 할 수 있게, 큰 그림을  이해하고, 필요한 부분 찾아서 세팅하면

일단  OK당, 시간을 두고 점점 더 익숙해 지면 점점 스스로 더 보여준당.

 

사실 시큐리티는 어렵다라기 보다 설정 방법이 너무 다양해서 설정에

어려움이 있다라는 표현이 아마도 그게 더 어울릴 거시기당 

오늘 기본틀이 될 만한  기본 시나리오와 설정파일을 챙겨보장!

 

일단 보안에선 인증(Authentication -> 너 진짜 e7e 맞앙? ) 이 가장 중요하당!

인증방법(AuthenticationProvider)에는 오른쪽 눈 윙크시 홍채 ,

왼쪽 무좀 새끼 발가락 지문, 분노했을 때 목소리 피크, DB에 저장한

암호화된 암호 체크등등이 이쓸 수 있당.

인증된 정보로 부터 권한(Authorization)을 가져와  허락/거절을 택할 수 있당.

이해 안 될 꺼이 없당.~~ 오켕?

 

오늘의 스토리와 목표는  이러해도 될 정도로 이러하게 초 간단하나

요거이 기본 사용패턴이당.

-. 로그인 화면에 ID와 암호를 넣고  submit하면 

   해당 id에 해당하는 사용자 정보(권한포함)를 DB에서 불러와 인증처리

-. 목표는 백지에서 안 출발,기본설정된 설정파일에서 필요한 부분 수정가능!

 

간단해도 글로 설명하는 거슨 중노동일지도.. ㅠㅠ 그럼에도 Go! Go!

개인적인 생각은 시큐리티 공부는 전체 틀을 먼저 복사 붙여넣기 방식으로

큼직큼직하게 받아들인 다음에 세부적인 부분으로 들어가는 거슬 추천한당.

한줄 한줄 코딩으로 시작하는 건 거의 가성비가 없다는 느낌이당.

만약 누군가  한줄 한줄 따박 따박 코딩하면서 설명한다면 거부해랑

설정은 프로그램 로직이 아니당. 사용방법일 뿐이당. 으미만 챙기장

 

 

이제 스프링 시큐리티 공부시 뇌에 새기면 좋은 아래 그림(인증 과정)을 보장.

ID  PASSWORD 인증 방식( UsernamePasswordAuthenticationToken방식)이당

(대략적으로 뭉개서 추상적으로 설명할꺼닝, 너무 꼬치꼬치 따지면 미울거당!)

(성공이면 1-7까지 갈거공, 실패면  Exception처리가 당근 되어야 할끼당!)


 

 

① 인터셉트 :                                                                                                                                                                    스프링 시큐리티에는 필터체인(FilterChain)이란 것이  있는뎅,                                                              여러개의 Filter(현실에 비유하면 검문소)가 줄줄이 비엔나로 있고,                                                          (Tomcat에 온 사용자 요청이 Spring으로 Dispatch(전달) 될 때 필터에 검문당한당).

 

② 책임전가: 

         여러개의 Filter중 AuthenticationFilter를 만나면,  요 필터는

         AuthenticationManager에게 인증책임을 전가한당.

 

③ 인증방법: 

         AuthenticationManager는 적절한 인증방법을 선택(여기선 UserName/Password)

 

④ User찾기/암호비교:                                                                                                                                                UserDetailsService(유저상세내용가져오는서비스)를 이용                                                                    User정보가 있고, 암호가 일치하는지 PasswordEncoder로 비교

⑤ 결과Return: ④의 결과를 전달(화살표방향 참고)

⑥ 결과Return: 의 결과를 전달(화살표방향 참고)

⑦ 결과Return: 의 결과를  SecurityContext에 저장

 ※ 낯설지 않을 만큼만 몇번 더 눈으로 보장

                      

 

그럼 이제  코드를 두근되는 맘으로 시작해 보장! (실습 마구 따라하깅)

JDK는 17 이나 21 이 깔려 있어야 하공, 아래 2개의 플러그인이 설치되어 있어야 한당.

 

 STS4에서 HTML/JSP 파일을 만들수 있게 도와준당. (추가하면 encoding 세팅도 추가하장)

 

MyBatis를 아주 편하게(SQL 로그 포함) 사용할 수 있도록 도와주는 플러그인


STS4에서 File ->  New-> Spring Starter Project 를 누르고,

일단 아래 그림처럼 헷갈리지 않게 나와 똑같은 이름과 package로 시작하장

 

Dependency도 아래 그림과 같이 나와 똑같은 것만 추가하장.

 

 

jstl, security tag, jsp 사용을 위한 아래 라이브러리  pom.xml에  추가하장

		<!-- jstl을 boot3에서 사용하기 위한 라이브러리 -->
		<dependency>
			<groupId>jakarta.servlet.jsp.jstl</groupId>
			<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
		</dependency>

		<dependency>
			<groupId>jakarta.servlet</groupId>
			<artifactId>jakarta.servlet-api</artifactId>
			<scope>provided</scope>
		</dependency>

		<dependency>
			<groupId>org.glassfish.web</groupId>
			<artifactId>jakarta.servlet.jsp.jstl</artifactId>
		</dependency>

        <!-- jsp에서 spring security tag를 사용하기 위한 라이브러리 -->
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-taglibs</artifactId>
		</dependency>
		
        <!-- boot에서 jsp를 사용하기 위한 라이브러리 -->
		<dependency>
			<groupId>org.apache.tomcat.embed</groupId>
			<artifactId>tomcat-embed-jasper</artifactId>
		</dependency>

     

 여기서 잠깐 귀찮지만   jstl 사용시 warning이 뜰 수 있는데, 아래 처리를 먼저 해놓장

Winodw-> preference에서  validation으로 검색후

Web -> Jsp Files -> Validation -> Custom actions 에서

Other problems with TagExtraInfo class의  선택을 ignore로 바꾸어서 warning을 없앤당!

 

src/main/resources/application.properties 파일에  기본적으로 필요한 거 설정하장

(주석 참공)

# loggin 레벨 설정
logging.level.com.e7e.secsample=debug
logging.level.org.springframework.security=debug

# mvc jsp viewResolver 설정
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

# database 설정
spring.datasource.url=jdbc:oracle:thin:@localhost:1521/xe
spring.datasource.username=java   # 본인 DB  유저(스키마)명
spring.datasource.password=oracle # 본인 암호

# mybatis 설정
mybatis.type-aliases-package=com.e7e.secsample.vo
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.jdbc-type-for-null=varchar
mybatis.mapper-locations=classpath:mybatis/mapper/*_SQL.xml

 

위  설정에 맞겡  src/main/webapp

                          src/main/webapp/WEB_INF

                          src/main/webapp/WEB-INF/views

폴더 순차적으로 맹글어주공,

 

com.e7e.secsample.vo 패키지도  맹글어 주공,

                        src/main/resources/mybatis

                        src/main/resources/mybatis/mapper

폴더 순차적으로 맹글어 주어야 한당.

 

만약 이게 뭐얌? 뇌에서 삐딱 삐딱 삐걱되는  소리가 나면

2023.07.03 - [스프링부트] - sts4 spring boot 첫번쨍  부터 세번째 글까지 먼저 읽장

 

시작 준비 완룡!!! (넘 수꼬 해쪄용! 당신은 개발자의 근성이 있당)

 

이제 진짜 본격적으로 실질적으로 시작해 보장

(앞에서도 이야기 했지만, 처음은 그냥 복사/ 붙여넣기 방식으로 큼직큼직하게 가는 게 좋당)


우선 사용할 DB부터 준비하장! 

아래 sql 누느로 휘리릭 읽고, sql developer에서 복사/붙여넣기 하장

사용자/권한 을 위한 테이블이당. (remember-me 기능을 위한 테이블도 그냥 미리 생성)

사용자는 여러권한을  가질 수 있는 1:N으로 설계되어 이땅. (CEO이면서 MANAGER..)

tb_members 테이블은 mem_id와 mem_pw만  필수공, 나머진 있어도 되고 없어도 되공

피료한 게 있으면 마구 더 추가해도 된당! 

(내 맘대로 한게 아니공, security 문서 기준이당) 

 

-- 시규리티용 억지 member 테이블
create table tb_members(
    mem_id varchar2(10) not null primary key,
    mem_name varchar2(50) not null,
    mem_pw varchar2(60) not null,
    mem_enable char(1) DEFAULT '1',
    mem_regdate date default sysdate
);


-- 시큐리티용 억지 권한 테이블
create table tb_auths(
    mem_id VARCHAR2(10) not null,
    auth_name VARCHAR2(20) not null,
    CONSTRAINT fk_memid FOREIGN key(mem_id) REFERENCES tb_members(mem_id)
);
create UNIQUE index index_mem_id_auth_name on tb_auths(mem_id,auth_name);


-- remember-me 토큰관리용 테이블
create table persistent_logins (
    username varchar2(64) not null,
    series varchar2(64) primary key,
    token varchar2(64) not null,
    last_used timestamp not null
)

 

테이블을  맹글었으닝,당근  VO도 맹글장 MemberVO (권한을 담을 속성 authList 추가)

package com.e7e.secsample.vo;

import java.util.List;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class MemberVO {
	private String memId;
	private String memName;
	private String memPw;
	private boolean memEnable;
	private String memRegdate;
	
	private List<AuthVO> authList;
}

 

권한 테이블용 AuthVO

package com.e7e.secsample.vo;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Setter
@Getter
@ToString
public class AuthVO {
	private String memId;
	private String authName;
}

 

일단 tb_membesr와 tb_auths 테이블에 데이터를 insert하는 메소드와,

Member정보를 읽어오는 메소드 총 3개의 메소드를 가진 MemberMapper정의 하공

package com.e7e.secsample.mapper;

import org.apache.ibatis.annotations.Mapper;

import com.e7e.secsample.vo.AuthVO;
import com.e7e.secsample.vo.MemberVO;

@Mapper
public interface MemberMapper {
	public int insertMember(MemberVO memberVO);
	public int insertAuth(AuthVO authVO);
	public MemberVO read(String memId);
}

 

MemberMapper의 각 메소드에 연결될 SQL문을  작성하장.

<?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.e7e.secsample.mapper.MemberMapper">
	<resultMap type="MemberVO" id="memberMap">
		<id column="mem_id" property="memId" />
		<result column="mem_id" property="memId" />
		<result column="mem_pw" property="memPw" />
		<result column="mem_name" property="memName" />
		<result column="mem_regdate" property="memRegdate" />
		<collection property="authList" resultMap="authMap" />
	</resultMap>
	
	<resultMap type="AuthVO" id="authMap">
		<result column="mem_id" property="memId" />
		<result column="auth_name" property="authName" />
	</resultMap>
	
	<insert id="insertMember" parameterType="MemberVO">
	   insert into tb_members(mem_id,mem_name,mem_pw) 
	          values(#{memId},#{memName},#{memPw})
	</insert>
	
	<insert id="insertAuth" parameterType="AuthVO">
		insert into tb_auths(mem_id, auth_name) values(#{memId},#{authName})
	</insert>
	
	<select id="read" parameterType="string" resultMap="memberMap">
		select
				m.mem_id, mem_pw, mem_name, mem_enable, mem_regdate, auth_name
				from tb_members m , tb_auths a 
				where m.mem_id = #{memId} and m.mem_id = a.mem_id
	</select>
</mapper>

사용자별 권한은 스프링 security에서 ROLE로 많이 표현되는 데, 원하는 이름으로 마음껏

정하면 되는뎅, 으미가 보이게 지는 게 좋을 것이당. CEO, MANAGER, SAWON, USER등등

resultMap id=memberMap의 <collection property="authList" resultMap="authMap" />

에 주목,  여러개  권한(ROLE)을 가질 수 있당.

 

Mapper를 맹글면,  동작 검증은 SpringBootTest를 사용하는 것을 권장한당!(왱?..)

SpringBootTest와 MemberMapper를 이용하여 DB에 일단 필요한 값을 넣공,

select도 잘 되는 징 테스토하장! (Tomcat 구동 안하공, 마우스 오른쪽 버튼 Junit을 쓴당!)

 

src/test/java 아래 com.e7e.secsample.mapper 패키지를 맹글고 그 안에

MemberMapperTest.java를 아래 소스를 복사/붙여넣기로 맹근당!

각각의 @Test가 붙은 메소드의 @Disabled 를 오직 1개씩만 주석 처리해서, 실행하공

insertMember후에 insertAuth 후에 (외래키문제), read를 테스트 해야함,  

insertMember와 insertAuth는 sql developer에서 꼭 결과를 확인하공,

read는 그냥 출력결과를 확인하장.(시름 말공!~~~)

package com.e7e.secsample.mapper;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Iterator;
import java.util.List;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;

import com.e7e.secsample.vo.AuthVO;
import com.e7e.secsample.vo.MemberVO;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@SpringBootTest
public class MemberMapperTest {
		
	// 암호화및 암호체크용
	private static PasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder(); 
	
	@Autowired
	private MemberMapper memberMapper;
	
	@Test
	@DisplayName("member(사용자) 등록 test")
	@Disabled
	public void insMemberTest() {
		
		// 샘플용 Id & Name
		List<String> memberIds   = List.of("e7e","roze","jenni","nobi");
		List<String> memberNames = List.of("272","로제","제니","노비");
		
		MemberVO memberVO = null;				
		int totalRow = 0;
		
		for(int index=0; index < memberNames.size(); index++) {
			memberVO = new MemberVO();
			memberVO.setMemId(memberIds.get(index));
			memberVO.setMemName(memberNames.get(index));
			// 암호 인코딩 필요!, 암호는 Id와 그냥 같겡
			memberVO.setMemPw(bcryptPasswordEncoder.encode(memberIds.get(index)));
			totalRow +=memberMapper.insertMember(memberVO);			

		}		
		assertEquals(memberNames.size(), totalRow);
	}
	
	@Test
	@DisplayName("auth(권한) 등록 test")
	@Disabled
	public void insAuthTest() {
		      
		AuthVO authVO = new AuthVO();
		MemberVO memberVO = new MemberVO();
		
		List<String> memberIds   = List.of("e7e","roze","jenni","nobi","ksj");
		int totalRow = 0;
		
		for (String memId : memberIds) {
			switch (memId) {
			case "e7e": 
				authVO.setMemId(memId);
                		authVO.setAuthName("ROLE_CEO");
				totalRow += memberMapper.insertAuth(authVO);
				
				authVO.setAuthName("ROLE_MANAGER");
				totalRow += memberMapper.insertAuth(authVO);				
				break;
				
			case "roze" :
			case "jenni": 
				authVO.setMemId(memId);
                		authVO.setAuthName("ROLE_MANAGER");
				totalRow += memberMapper.insertAuth(authVO);				
				break;
			
			case "nobi": 
				authVO.setMemId(memId);
                		authVO.setAuthName("ROLE_ALBA");
				totalRow += memberMapper.insertAuth(authVO);								
				break;
			
			default: // 그냥 괘니 member, auth 양쪽에 1번에 등록
				memberVO.setMemId(memId);
				memberVO.setMemName("friend_"+memId);
				// 암호 인코딩 필요!, 암호는 Id와 그냥 같겡
				memberVO.setMemPw(bcryptPasswordEncoder.encode(memId));
				totalRow +=memberMapper.insertMember(memberVO);			
				
				authVO.setMemId(memId);
                		authVO.setAuthName("ROLE_USER");
				totalRow += memberMapper.insertAuth(authVO);									
			}
			
		}
				
		assertEquals(7, totalRow);
	}
	
	
	@Test
	@DisplayName("read member(사용자검색) test")
	@Disabled
	public void readMember() {
		
		MemberVO memberVO = memberMapper.read("e7e");
		
		//암호 확인!
		log.debug("암호일치: {}",bcryptPasswordEncoder.matches("e7e", memberVO.getMemPw()));
		
		log.debug("ck: {}",memberVO);
		memberVO.getAuthList().forEach(authVO -> log.debug("{}",authVO));
		
		assertEquals(2, memberVO.getAuthList().size());		
	}
	
}

 

private static PasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder();

부분은 사용자 암호를 DB에 저장시에도 다른사람들이 알수없게 암호화하는데

사용되는 것으로 BcryptPasswordEncoder가 대표적이당.!

 

 

스프링 security 설정에선 나중에  아래 처럼 Bean으로 등록해서 사용하게 된당

 

@Bean // 암호 인코더 기본필요
protected PasswordEncoder passwordEncoder() {
         return new BCryptPasswordEncoder();
}

 

 

아마도 아래 그림과 같은 결과가 보일 거시당.(아니라면 디버깅하장!)

 

 

 

그럼 DB의 데이타도 준비가 되었으닝, 본격적으로 security 설정으로 들어가장

src/main/java 아래에 com.e7e.sesample.config 패키지를 만들고 그 아래에 SecurityConfig.java를

맹글공, 아래 코드를  복사 부쳐너킹 하장.

(참고로 deprecated 된 부분을 모두 upgrade 했당. 참고하기 좋을 거시당~~ ㅋㅋ)

먼저 전체 구성을 보는 것이 중요하당. (오타 찾기 하지 말장!)

 

package com.e7e.secsample.config;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.HttpSessionEventPublisher;

import com.e7e.secsample.security.CustomAccessDeniedHandler;
import com.e7e.secsample.security.CustomLoginFailureHandler;
import com.e7e.secsample.security.CustomLoginSuccessHandler;
import com.e7e.secsample.security.CustomUserDetailsService;

import jakarta.servlet.DispatcherType;

@Configuration
@EnableWebSecurity(debug = false)
@EnableMethodSecurity  						// @preAuthorize/@postAuthorize 사용
public class SecurityConfig {

	@Autowired
	private DataSource dataSource; // application.properties에 설정한 spring.datasource D.I

	@Bean
	protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		return http.csrf(csrf -> csrf.disable()).httpBasic(hbasic -> hbasic.disable())
				.headers(config -> config.frameOptions(customizer -> customizer.sameOrigin()))
				.authorizeHttpRequests(
						authz -> authz.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ASYNC).permitAll() // forward																														// 허가
								.requestMatchers("/","/login", "/error").permitAll()
								.requestMatchers("/ceo/**").hasRole("CEO")
								.requestMatchers("/manager/**").hasAnyRole("CEO","MANAGER")
								.anyRequest().authenticated())
				.formLogin(form -> form.loginPage("/login")
						               .successHandler(customLoginSuccessHandler())
						               .failureHandler(customLoginFailureHandler()))
				.sessionManagement(session -> session.maximumSessions(1))
				.exceptionHandling(ex -> ex.accessDeniedHandler(customAccessDeniedHandler()))
				.logout(Customizer.withDefaults()).rememberMe(config -> config.key("e7eKey").tokenValiditySeconds(86400)
						.tokenRepository(persistentTokenRepository()))
				.build();
	}

	@Bean // remember-me db 연결
	protected PersistentTokenRepository persistentTokenRepository() {
		JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
		tokenRepository.setDataSource(dataSource);
		return tokenRepository;
	}

	@Bean // 사용자 정의 UserDetailsService
	protected UserDetailsService customUserDetailsService() {
		return new CustomUserDetailsService();
	}

	@Bean // 인증실패 핸들러, 안 맹글어도 크게 안 불편
	protected AuthenticationFailureHandler customLoginFailureHandler() {
		return new CustomLoginFailureHandler();
	}

	@Bean // 인증성공 핸들러, 성공시 URL분배하려면 필요
	protected AuthenticationSuccessHandler customLoginSuccessHandler() {
		return new CustomLoginSuccessHandler();
	}

	@Bean // 접근거부 처리 핸들러, 필요
	protected AccessDeniedHandler customAccessDeniedHandler() {
		return new CustomAccessDeniedHandler();
	}

	@Bean // 암호 인코더 기본필요
	protected PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Bean // 인증매니저 스프링 문서 참조, global 설정 복사해옴
	protected AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
			throws Exception {
		return authenticationConfiguration.getAuthenticationManager();
	}

	@Bean // 인증제공자 인증처리
	protected AuthenticationProvider authenticationProvider() {
		DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
		authenticationProvider.setUserDetailsService(customUserDetailsService());
		authenticationProvider.setPasswordEncoder(passwordEncoder());
		return authenticationProvider;

	}

	@Bean // 동시 로그인수 제어을 위해 필요
	protected HttpSessionEventPublisher httpSessionEventPublisher() {
		return new HttpSessionEventPublisher();
	}

	@Bean // webSecurity는 전체적인 설정(구성)에,httpSecurity는 구체적인 세부URL에
	protected WebSecurityCustomizer webSecurityCustomizer() {
		return (web) -> web.debug(false).ignoring().requestMatchers("/css/**", "/js/**", "/img/**", "/favicon.ico");
	}

}

 

우엑! 할 수도 있당. 차분히 뽀인또만 챙겨 보도록 하장.

filterChain 메소드가 핵심 뽀인또당.  filter는 검문소에 비교 될 수 있고, 

그런 필터들이 Tomcat과 Spring사이에 여러개 나열되어 있어서 filterChain이라 불리공

filterChain 메소드는 바로 filter들을 제어해서 시큐리티설정을 원하는 대로 

커스터마이징 하는 부분이당.    몇개를 눈여겨 보장

 

.formLogin(form -> form.loginPage("/login")   =>  로그인 페이지를 지정하는 부분이당.

컨트롤러에서 해당 URL("/login")이 오면 사용자가 ID와 암호를 입력할 수 있는

페이지로 갈 수 있도록 맹글어 주어야 한당.

 

.requestMatchers("/","/login", "/error").permitAll()

.requestMatchers("/ceo/**").hasRole("CEO")

.requestMatchers("/manager/**").hasAnyRole("CEO","MANAGER")

.anyRequest().authenticated())

 

"/","/login","/error" 은 누구가 접근 할 수 있는 URL,

"/ceo" 가 붙은 URL은 CEO ROLE가진 자만 접근 가능

"/manager"로 시작하는 URL은 CEO, MANAGER  ROLE을 가진자만 접근

그외 URL은 오직 인증된 사용자만 접근 가능하다는 설정이당.

 

그외 나머지는 지금은 무시하장! (이야기가 길면 얘기 하는 사람도 졸리당)

 

src/main/java 아래에 com.e7e.sesample.security 패키지를 만들고 그 아래에 CustomUser.java를

맹글공, 아래 소스 코드를 복사 붙여넣깅 한당.

package com.e7e.secsample.security;

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.e7e.secsample.vo.MemberVO;

import lombok.Getter;

@Getter
public class CustomUser extends User {

	private static final long serialVersionUID = 1L;
	private MemberVO member;

	public CustomUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
		super(username, password, authorities);
	}
	
	
	public CustomUser(MemberVO memberVO) {
		super(memberVO.getMemId(),memberVO.getMemPw(),
			  memberVO.getAuthList().stream().map(auth -> new SimpleGrantedAuthority(auth.getAuthName())).collect(Collectors.toList()));
		this.member = memberVO;
	}
	
}

 

spring security에서 사용자를 정의 할 규칙을 담은 UserDetails란 인터페이스와 이를 구현한 User라는 

class를 제공해 주는뎅, 요걸 그대로 쓰면 ID/암호/권한만(사실 몇개 더있당) 담아서 써야한당.

그건 넘 불편하당. 그래서 User를 상속받아서, DB에서 읽어온 MemberVO로 User를 생성하고,

MemberVO을 속성으로 가지는 CustomUser를 맹글어 쓰는게 일반적이당.

 

 

좀전 맹근  com.e7e.sesample.security 패키지 아래에 CustomUserDetailsService.java를

맹글공, 아래 소스 코드를 복사 붙여넣깅 한당.

package com.e7e.secsample.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 com.e7e.secsample.mapper.MemberMapper;
import com.e7e.secsample.vo.MemberVO;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CustomUserDetailsService implements UserDetailsService{

	@Autowired
	private MemberMapper memberMapper;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		log.debug("CKK1 {}",username);
	
		MemberVO memberVO = memberMapper.read(username);
		if(memberVO != null) {
			return new CustomUser(memberVO);
		}else {
			throw new UsernameNotFoundException(username);
		}
	}
}

CustomUserDetailsService는 보면 알겠지만, 스프링 시큐리티가 제공(강요)하는 

UserDetailsService(사용자정보) 인터페이스를 구현하여, DB에서 MemberVO을 읽어와

CustomUser를 생성하는 역할을 한당. 스프링 시큐리티가 사용자가 ID와 암호를 

입력하면 요 CustomUserDetailsService를 이용해서 DB에서 사용자 정보를 읽어와 

ID와 Password를 비교하여 인증 여부를 결정하게 되는 거시였던 거시당.(일단 오켕!)

 

좀전 맹근  com.e7e.sesample.security 패키지 아래에 CustomLoginSuccessHandler.java를

맹글공, 아래 소스 코드를 복사 붙여넣깅 한당.

package com.e7e.secsample.security;

import java.io.IOException;

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

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
	
	
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		    log.debug("인증되셨어용 {}",authentication);
		    
		    response.sendRedirect("/auth");
	}
	
}

요건 꼭 만들어야 되는 건 아닌 뎅,  일반적으로 인증 성공 후

ROLE 별로 이동해야 하는 페이지가 다를 경우 만들게 된당

SecurityConfig.java 파일의 filterChain 메서드의 아래 부분에 연결된당.

.formLogin(form -> form.loginPage("/login")

                              .successHandler(customLoginSuccessHandler())

 

좀전 맹근  com.e7e.sesample.security 패키지 아래에 CustomLoginFailureHandler.java를

맹글공, 아래 소스 코드를 복사 붙여넣깅 한당.

package com.e7e.secsample.security;

import java.io.IOException;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CustomLoginFailureHandler implements AuthenticationFailureHandler {
	
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		log.debug("인증 실패하셨어용",exception.getMessage());
		response.sendRedirect("/login");
	}

}

역시 꼬옥 맹글어야 되는 건 아닌뎅(난 안 만든당)

인증 실패 후 어케 할 건지를 결정하는 내용을 담는당. 여기선 실패하면

다시 login 페이지로 돌렸당 (스프링 시큐리티의 기본값이라, 이럴거면 안 맹글어도 된당)

SecurityConfig.java 파일의 filterChain 메서드의 아래 부분에 연결된당.

.formLogin(form -> form.loginPage("/login")

                              .failureHandler(customLoginFailureHandler()))

 

좀전 맹근  com.e7e.sesample.security 패키지 아래에 CustomAccessDeniedHandler.java를

맹글공, 아래 소스 코드를 복사 붙여넣깅 한당.

package com.e7e.secsample.security;

import java.io.IOException;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CustomAccessDeniedHandler implements AccessDeniedHandler{

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response,
			AccessDeniedException accessDeniedException) throws IOException, ServletException {
		log.debug("체킁1: {}",accessDeniedException.getMessage());
		response.sendRedirect("/accessError");
	}
}

요건 접근 권한이 없는 페이지를 접근하려 하면, 실행되는 것으로 ,예를 들면 

USER ROLE을 가진 자가 CEO ROLE이 필요한 페이지로 이동하려 하면 발생하는뎅

꼬옥 맹그는 게 좋당.  접근 권한이 없는 이유를 로그로 찍어볼 수 있어서, 개발에  유용하당.

개발이 잘 되었다면, 에러페이지 1개 맹글어서 그쪽으로 날려버리장! 

 

SecurityConfig.java 파일의 filterChain 메서드의 아래 부분에 연결된당.(예외처링)

.exceptionHandling(ex -> ex.accessDeniedHandler(customAccessDeniedHandler()))

 

src/main/java 아래  com.e7e.sesample.controller 패키지를 맹근담, 그 아래  PageHandler.java를

맹글공, 아래 소스 코드를 복사 붙여넣깅 한당.

package com.e7e.secsample.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Controller
@RequestMapping("/")
public class PageController {
	
	@GetMapping("/")
	public String homePage() {
		log.debug("home.jsp로 포워딩!");
		return "home";
	}
		
	@GetMapping("/login")
	public String loginPage() {
		log.debug("login.jsp로 포워딩!");
		return "login";
	}

	@GetMapping("/accessError")
	public String accessError() {
		log.debug("accessError.jsp로 포워딩!");
		return "accessError";
	}

	@GetMapping("/auth")
	public String authPage() {
		log.debug("auth.jsp로 포워딩!");
		return "auth";
	}

	
	@GetMapping("/ceo")
	public String e7ePage() {
		log.debug("ceo.jsp로 포워딩!");
		return "ceo";
	}

	@GetMapping("/manager")
	public String managerPage() {
		log.debug("manager.jsp로 포워딩!");
		return "manager";
	}

	
	@GetMapping("/user")
	public String userPage() {
		log.debug("user.jsp로 포워딩!");
		return "user";
	}

	@GetMapping("/alba")
	public String albaPage() {
		log.debug("alba.jsp로 포워딩!");
		return "alba";
	}
}

시큐리티를 테스트 하기 위해서 맹근 그냥 컨트롤러당

 

아래는 스프링 security taglibs로 사용되는 대표적 코드 모습이당.

혹 이해가 안된다면 지금 당장 구글에 spring security tag로 검색해서 으미만 깨닫장.

으미만 느끼면 복사 붙여넣기 해도 충분하당

<sec:authentication property="name" var="userName" />
<sec:authentication property="principal.member" />
<sec:authentication property="principal.member.authList" var="authList"/>

<sec:authorize access="isAuthenticated">
    <a href="/user">일반 유저 페이지</a><br>
    <a href="/alba">알바 페이지</a><br>
</sec:authorize>

<sec:authorize url="/ceo" access="hasRole('CEO')">
    <a href="/ceo">사당님 페이지</a><br>
</sec:authorize>

<sec:authorize access="hasRole('CEO') and hasRole('MANAGER')">
     <h4>사장님 강령하소성 ~~ 아붕</h4>
</sec:authorize>

 

security tag가 대략 이해되었다면 테스트용 jsp 파일들을 맹글장!

src/main/webapp/WB-INF/views에

home.jsp를 맹글공, 아래 코드 부쳐너킹!  (테스트용이당)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Welcome To E7E World</title>
</head>
<body>
	<sec:authorize access="!isAuthenticated()">
  		<a href ="/login">로그인 페이지롱</a>
  		<h1>E7E 세상에 오신걸 좋아해용</h1>
	</sec:authorize>
	<sec:authorize access="isAuthenticated()">
		<h1><sec:authentication property="name"/>님!!</h1>
 		<h1>E7E 세상에 오신걸 좋아해용</h1>
 		<a href="/auth">인증받은자여 요기 오랑</a>	
  		<form action="/logout">
  			<sec:csrfInput />
  			<button>로그아웃할꼉</button>
  		</form>
	</sec:authorize>
</body>
</html>

 

 

src/main/webapp/WB-INF/views에

login.jsp를 맹글공, 아래 코드 부쳐너킹! (테스트용이당)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Welcome To E7E World</title>
</head>
<body>
    <h1>E7E World Login</h1>
    <!--  post에 /login은  Spring Security가 자동으로 처리 해줘용!!
          get /login 직접 화면 만드는 URL가 초보자들은 엄칭 헷갈려해용
     -->
    <form action="/login" method='POST'>
        <table>
            <tr>
                <td>멤버:</td>
                <td><input type='text' name='username' value=''></td>
            </tr>
            <tr>
                <td>암호:</td>
                <td><input type='password' name='password' /></td>
            </tr>
            <tr>
                <td>날 기억해줄랭:</td>
                <td><input type="checkbox" name="remember-me" /></td>
            </tr>
            <tr>
            	<sec:csrfInput/> <!-- csrf 토큰 넣깅 -->
                <td><input name="submit" type="submit" value="submit" /></td>
            </tr>
        </table>
    </form>
	<script>
		// AJAX 사용시 csrf 토큰 생성은 꼬옥 이렇게 할 필요는 없음
		const csrfParameter = $("meta[name='_csrf_parameter']").attr("content");
		const csrfHeader = $("meta[name='_csrf_header']").attr("content");
		const csrfToken = $("meta[name='_csrf']").attr("content");

		//서버에서 발행된 헤더네임과 토큰갑사 저장, 요거이 좀더 편함!
		const header = '${_csrf.headerName}';
		const token = '${_csrf.token}';
		console.log("체킁:",header, tokern);  // 누느로 화긴
	</script>
</body>
</html>

 

src/main/webapp/WB-INF/views에

accessError.jsp를 맹글공, 아래 코드 부쳐너킹! (테스트용이당)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Welcome To E7E World</title>
</head>
<body>
	<h1>미안해요 레벨이 안되삼</h1>
	<h1>레벨업 위해 노력하삼</h1>
	<a href="/">홈가깅</a>
</body>
</html>

 

src/main/webapp/WB-INF/views에

auth.jsp를 맹글공, 아래 코드 부쳐너킹! (테스트용이당)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>

<sec:authentication property="name" var="userName" />
<sec:authentication property="principal.member.authList" var="authList"/>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Welcome To E7E World</title>
</head>
<body>
	<h1>추카해용 ${userName }님 인증 OK!</h1>
	<h2>${userName }님 권한은 다음과 같아용</h2>
	<ul>
		<c:forEach items="${authList}" var="auth">
			<li> ${auth } </li>
		</c:forEach>
	</ul>

	<h2>${userName }님 상세정보</h2>
	<p>	<sec:authentication property="principal.member" /></p>
	
	<hr>
	<h2>${userName }님 갈 수 있는 곳은</h2>
	<sec:authorize url="/ceo" access="hasRole('CEO')">
		<a href="/ceo">사당님 페이지</a><br>
	</sec:authorize>

	<sec:authorize url="/manager" access="hasAnyRole('CEO','MANAGER')">
		<a href="/manager">관리자 페이징</a><br>
	</sec:authorize>

	<sec:authorize access="isAuthenticated">
		<a href="/user">일반 유저 페이지</a><br>
		<a href="/alba">알바 페이지</a><br>
	</sec:authorize>

	<a href="/">홈으롱</a><br>
	
</body>
</html>

 

src/main/webapp/WB-INF/views에

ceo.jsp를 맹글공, 아래 코드 부쳐너킹! (테스트용이당)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Welcome To E7E World</title>
</head>
<body>
	<p><sec:authentication property="principal" /></p>

	<sec:authorize access="hasRole('CEO') and hasRole('MANAGER')">
    	 <h4>사장님 강령하소성 ~~ 아붕</h4>
	</sec:authorize>

	<h1><sec:authentication property="name" />사장님</h1>
	<a href="javascript:history.back()">뒤로가깅</a>
	<form action="/logout">
		<sec:csrfInput />
		<button>로그아웃</button>
	</form>
</body>
</html>

 

src/main/webapp/WB-INF/views에

manager.jsp를 맹글공, 아래 코드 부쳐너킹! (테스트용이당)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Welcome To E7E World</title>
</head>
<body>
	<sec:authorize access="hasRole('CEO')">
    	<h4>사장님은 여기 오는거 아니얌~~ 반칙!!</h4>
	</sec:authorize>
	
    <!--
        .requestMatchers("/manager/**").hasAnyRole("CEO","MANAGER")
        FilterChain에 이미 위처럼 등록해  놓았는데, 아래 처럼 사용하는게 으미? 
        전역(대표) 설정과 페이지별 내부 설정의 의미를 한번 생각해 보장(중요)
     -->
	<sec:authorize access="hasRole('MANAGER')">
    	<h4>관리자님 어여 오셔용</h4>
	</sec:authorize>

	<h1><sec:authentication property="name" /> 관리자님</h1>
	<a href="javascript:history.back()">뒤로가깅</a>
	<form action="/logout">
		<sec:csrfInput />
		<button>로그아웃</button>
	</form>
</body>
</html>

 

src/main/webapp/WB-INF/views에

user.jsp를 맹글공, 아래 코드 부쳐너킹! (테스트용이당)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Welcome To E7E World</title>
</head>
<body>
	<sec:authorize access="hasAnyRole('CEO','MANAGER')">
    	<h4>사장님, 매니저님 여기 오는거 아냐!</h4>
	</sec:authorize>

	<sec:authorize access="hasRole('USER')">
        <h1><sec:authentication property="name" /> 유저님 어서오삼</h1>
	</sec:authorize>

	<a href="javascript:history.back()">뒤로가깅</a>
	<form action="/logout">
		<sec:csrfInput />
		<button>로그아웃</button>
	</form>
	
</body>
</html>

 

src/main/webapp/WB-INF/views에

alba.jsp를 맹글공, 아래 코드 부쳐너킹! (테스트용이당)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Welcome To E7E World</title>
</head>
<body>

	<sec:authorize access="!hasRole('ALBA')">
        <h1>인간적으로 알바도 좀 쉽시당!~~ㅠㅠ</h1>
	</sec:authorize>

	<sec:authorize access="hasRole('ALBA')">
        <h1><sec:authentication property="name" /> 알바님 어서오삼</h1>
        <h2>수고가 많으삼 조선판 노비님!</h2>
	</sec:authorize>

	<a href="javascript:history.back()">뒤로가깅</a>
	<form action="/logout">
		<sec:csrfInput />
		<button>로그아웃</button>
	</form>
	
</body>
</html>

 

이제 Spring Boot App을 실행시켜 요래조래 테스트를 하면서 먼 느낌을 점점 가깝게 하장!

 

요기서 주소 표시줄에 http://localhost:8080/ceo 라고 입력해보장, 아직 인증이 안된 사람이라

자동으로 http://localhost:8080/login 으로 이동 하는 걸 확인할 수 있을꺼시당.

 

login에 성공하면  customLoginsuccessHandler에 의해 /auh으로 이동하게 된당. 

 

logout과 다른 사용자로 로그인도 해 보공

날 기억해 줄랭 체크박스도  체크한 상태로 로그인 한 다음에 ,

DB의 persistent_logins 테이블 내용과 F12->application탭 -> cookie도 확인해보장! 

요래조래 요리조리 테스트를 충분히 즐기고 난 다음에

요기까지의 대략 그림이 머리에 그려지면, 차분히 다시 해보길 초 강력 추천한당!

 

아래는  SecuriyConfig.java 파일에 

@EnableMethodSecurity 어노테이션을 붙였기 때문에 사용할 수 있는 global security 메소드

간단 사용법이당. (#과 returnObject에 주목, 요거 2개 알면 다 아는 것임)

기본은 웹은 URL 베이스로 동작하기 때문에, filterChain의 requestMatchers를 이용

URL로 보안 레벨을 정하고, 그것으로 애매한 상황일 경우

@preAuthorize/@postAuthorize를 사용하장

@Secured("ROLE_ADMIN")  //오래 된 애 이젠 잘 안씀

@PreAuthorize("isAuthenticated() and (( #user.name == principal.name ) or hasRole('ROLE_ADMIN'))")
//넘어오는 파라미터에 접근시 #사용
	
@PostAuthorize("isAuthenticated() and (( returnObject.name == principal.name ) or hasRole('ROLE_ADMIN'))")	
//return 하는 값에 접근 returnObject 사용

 

 

설명을 달지 못한 부분도 많지만, 요기까지 이해가 되었다면, 나머지는 가벼운 도전일 거시당.

오늘은 피곤하닝 내일 도전하장!~~ 

 

 

술 퍼도 슬프당

그럼 더 술 퍼랑

 

술 퍼서 슬프지 않당

그럼 더 술 퍼랑

 

슬퍼서 술 푼다면

그럼 더 슬퍼랑

 

슬퍼도 술 푸지않으면

그럼 그만 슬퍼랑

 

이제 슬슬 술 술 술

술 생각이 날거당!~~

 

술퍼 레이디!!~~

 

https://www.youtube.com/watch?v=6f3RzjXPQwA

 

관련글 더보기