본문 바로가기
java

스프링 시큐리티

by rewind 2024. 1. 30.

스프링 시큐리티

스프링에서 제공하는 프레임 워크중 하나로써 스프링 기반 어플리케이션의 보안(인증, 인가 , 권한)에 관한 처리들을 담당한다. 스프링 시큐리티를 사용하지 않으면 세션체크등 관련 내용들을 직접 구현해야한다.

 

자주사용되는 용어

Principal(접근주체) : 보호된 대상에 접근하는 유저

Authentication(인증) : 어떤 유저가 애플리케이션의 작업을 수행 할 수 있는 주체인지 증명하는 과정?

Authorization(인가) : 권한부여/허가와 동일한 의미, 현재 유저가 애플리케이션에 접근 할 수 있는 권한이 있는지를 검사

권한 : 인증된 주체가 애플리케이션의 동작을 수행할 수 있도록 허락 되어있는지를 결정

권한부여의 두가지 영역 -> 웹 요청 권한 , 메소드 호출 및 도메인 인스턴스에 대한 접근 권한 부여

 

webapp\WEB_INF\spring 폴더내에 security-context.xml 파일 생성

	<security:authentication-manager>
		<security:authentication-provider>
			<security:jdbc-user-service data-source-ref=""/>		
<!-- 			<security:user-service> -->
<!-- 				<security:user name="member" password="{noop}java" authorities="ROLE_MEMBER" /> -->
<!-- 				<security:user name="admin"  password="{noop}java" authorities="ROLE_MEMBER,ROLE_ADMIN" /> -->

<!-- 			</security:user-service> -->
		</security:authentication-provider>
	</security:authentication-manager>
</beans>

 

AuthenticationManager - 인증 처리하는 filter로부터 첫번째로 지시받는 클래스

	<bean id="dataSource"
		class="org.apache.commons.dbcp2.BasicDataSource"
		destroy-method="close">
		<property name="driverClassName"
			value="oracle.jdbc.driver.OracleDriver" />
		<property name="url"
			value="jdbc:oracle:thin:@localhost:1521:xe" />
		<property name="username" value="jspexam" />
		<property name="password" value="java" />
	</bean>

 

 

data-source-ref="dataSource" 작성시 root-context의 dataSource에 해당하는 객체정보를 모두 끌고온다

	<security:authentication-manager>
		<security:authentication-provider>
			<security:jdbc-user-service data-source-ref="dataSource"/>		
<!-- 			<security:user-service> -->
<!-- 				<security:user name="member" password="{noop}java" authorities="ROLE_MEMBER" /> -->
<!-- 				<security:user name="admin"  password="{noop}java" authorities="ROLE_MEMBER,ROLE_ADMIN" /> -->
<!-- 			</security:user-service> -->
			<security:password-encoder ref="customPasswordEncoder" />
		</security:authentication-provider>
	</security:authentication-manager>
</beans>

 

사용자 정의 패스워드 인코더 -> 패스워드 인증 직접 구현

 

dataSource-> 사용자 정보

customPasswordEncoder-> 패스워드암호

 

빈 객체 추가/구현 CustomNoOpPasswordEncoder -> no option

<bean id="customPasswordEncoder" class="kr.or.ddit.security.CustomNoOpPasswordEncoder"></bean>

 

 

CustomNoOpPasswordEncoder 클래스 생성

package kr.or.ddit.security;

import org.springframework.security.crypto.password.PasswordEncoder;

import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomNoOpPasswordEncoder implements PasswordEncoder{

	@Override
	public String encode(CharSequence rawPassword) {
		// 비밀번호를 받아서 인코딩 해주는 메소드 , but 아무것도 하지 않는다
		log.info("before encode : " + rawPassword);	
		return rawPassword.toString();
	}
	@Override
	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		// 비밀번호를 받은 것과 DB의 비밀번호랑 비교
	      log.warn("matches : " + rawPassword + " : " + encodedPassword);
	    // 일치하면 true를 리턴, 불일치하면 false를 리턴
		return rawPassword.toString().equals(encodedPassword);
	}
}

 

 

부모테이블의 기본키가 자식테이블의 외래키가 되고,
자식테이블의 외래키가 부모테이블의 기본키를 참조

 

참조무결성에 위배된다 -> 4의 부모키 존재하지않는다

	<security:authentication-manager>
		<security:authentication-provider user-service-ref="customUserDetailsService">
<!-- 사용자의 디테일 서비스를 직접 구현 -->
<!-- 			<security:jdbc-user-service data-source-ref="dataSource"/>		 -->
<!-- 			<security:user-service> -->
<!-- 				<security:user name="member" password="{noop}java" authorities="ROLE_MEMBER" /> -->
<!-- 				<security:user name="admin"  password="{noop}java" authorities="ROLE_MEMBER,ROLE_ADMIN" /> -->
<!-- 			</security:user-service> -->
<!-- 1. 비밀번호 암호화 사용하지 않는다 -->
<!-- 			<security:password-encoder ref="customPasswordEncoder" /> -->
<!-- 2. 비밀번호 암호화 사용(직접구현x) -->
			<security:password-encoder ref="passwordEncoder" />
		</security:authentication-provider>
	</security:authentication-manager>
</beans>

 

security user <--> member 와의 매핑작업

customUserDetailsService에서

1.select

2.super(부모)

<bean id="customUserDetailsService" class="kr.or.ddit.security.CustomUserDetailsService"></bean>

 

빈객체 추가

 

UserDetailsService -> 사용자 상세정보를 가지고 있는 인터페이스(스프링 시큐리티에서 제공)

package kr.or.ddit.controller;

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

import lombok.Data;
@Data
public class MemberVO {
	
	private int userNo;
	private String userId;
	private String userPw;
	private String userName;
	private int coin;
	private Date regDate;
	private Date updDate;
	private String enabled;
	
	// 회원 : 권한 = 1 : N
	// 중첩된 자바빈
	private List<MemberAuthVO> memberAuthVOList;

}

 

package kr.or.ddit.controller;

import lombok.Data;

@Data
public class MemberAuthVO {
	
	private int userNo;
	private String auth;

}

 

MemberVO , MemberAuthVO 생성

 

package kr.or.ddit.controller;

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 org.springframework.stereotype.Service;

import kr.or.ddit.dao.MemberDao;
import lombok.extern.slf4j.Slf4j;

/*
UserDetailsService : 스프링 시큐리티에서 제공해주는 사용자 상세 정보를 가지고 있는 인터페이스
 */
@Slf4j
@Service
public class CustomUserDetailsService implements UserDetailsService {

	//DI Dependency Injection : 의존성 주입
	//IoC Inversion of Control : 제어역전
	@Autowired
	private MemberDao memberDao;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// 1) 사용자 정보를 검색
		// String username : 로그인 시 입력받은 회원의 아이디  ↓↓↓
		//					 <input type="text" name="username"> 에서 username에 해당하는 부분
		
		MemberVO memberVO = this.memberDao.detail(username);
		log.info("memberVO : " + memberVO);
		// MVC에서는 Controller로 리턴하지 않고, CustomUser로 리턴함
	    // CustomUser : 사용자 정의 유저 정보. extends User를 상속받고 있음
	    // 2) 스프링 시큐리티의 User 객체의 정보로 넣어줌 => 스프링에서 해당 유저를 관리
	    // User : 스프링 시큐리티에서 제공해주는 사용자 정보 클래스(최상위클래스)
	    /*
	       memberVO(우리) -> user(시큐리티)
	       -----------------
	       userId        -> username
	       userPw        -> password
	       enabled       -> enabled
	       auth들                -> authorities
	    */
		return memberVO == null ? null : CustomUser(memberVO);
        // CustomUser 클래스는 상속받아서 직접 선언/
		
	}
}

 

package kr.or.ddit.dao;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import kr.or.ddit.controller.MemberVO;
//자바빈 선언
@Repository
public class MemberDao {

	// 쿼리 실행 객체 선언, DI
	@Autowired
	SqlSessionTemplate sqlSessionTemplate;
	
	// 로그인 회원 검색
	public MemberVO detail(String username) {
		// selectOne("namespace.id" , 파라미터)
		return this.sqlSessionTemplate.selectOne("member.detail", username);
	}
}

 

member_SQL.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">
<!-- namespace : xml파일은 여러개일 수 있음. 이를 구별하기 위한 식별 용도로 사용 -->
<mapper namespace="member">
	<!-- member 테이블과 memberAuth 테이블이 조인되어야 하기때문에 resultType이 아니라 resultMap -->

	<resultMap type="memberVO" id="memberMap">
		<result property="userNo" column="USER_NO" />
		<result property="userId" column="USER_ID" />
		<result property="userPw" column="USER_PW" />
		<result property="userName" column="USER_NAME" />
		<result property="coin" column="COIN" />
		<result property="regDate" column="REG_DATE" />
		<result property="updDate" column="UPD_DATE" />
		<result property="enabled" column="ENABLED" />
		<collection property="memberAuthVOList" resultMap="memberAuthMap"></collection>
	</resultMap>
	
	<resultMap type="MemberAuthVO" id="memberAuthMap">
		<result property="userNo" column="USER_NO"/>
		<result property="auth" column="AUTH"/>
	</resultMap>

	<!-- 로그인 처리를 위해 회원 검색 -->
	<select id="detail" parameterType="String" resultMap="memberMap">
		SELECT
		A.USER_NO, A.USER_ID, A.USER_PW, A.USER_NAME, A.COIN , A.REG_DATE,
		A.UPD_DATE, A.ENABLED , B.AUTH
		FROM MEMBER A, MEMBER_AUTH B
		WHERE
		A.USER_NO = B.USER_NO
		AND A.ENABLED = '1'
		AND A.USER_ID = 'admin';

	</select>
</mapper>

 

equl join = 동등조인 = 내부조인 = inner join

조인테이블

SELECT A.USER_NO, A.USER_ID, A.USER_PW, A.USER_NAME, A.COIN
         , A.REG_DATE, A.UPD_DATE, A.ENABLED
         , B.USER_NO, B.AUTH
FROM  MEMBER A, MEMBER_AUTH B
WHERE A.USER_NO = B.USER_NO
AND    A.ENABLED = '1'
AND    A.USER_ID = 'admin';

쿼리문에서 중복되는 부분은 제거 해준다

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<!-- 
	[마이바티스] 스프링에서 "_"를 사용한 컬럼명을 사용 시(BOOK 테이블의 BOOK_ID)
	카멜케이스로 읽어줌(bookId)
	
	ex) 테이블 컬러명이 member_id인 경우 jsp화면단에서 이 값을 사용 시 memberId로 사용
	-->
	<settings>
		<setting name="mapUnderscoreToCamelCase" value="true"/>
	</settings>
	<!-- 자주 사용하는 타입의 별칭을 세팅 -->
	<typeAliases>
		<typeAlias type="kr.or.ddit.vo.BookVO" alias="bookVO" />
		<typeAlias type="kr.or.ddit.vo.StudVO" alias="studVO" />
		<typeAlias type="kr.or.ddit.vo.LprodVO" alias="lprodVO" />
		<typeAlias type="kr.or.ddit.vo.HobbyVO" alias="hobbyVO" />
		<typeAlias type="kr.or.ddit.vo.MemberVO" alias="memberVO" />
		<typeAlias type="kr.or.ddit.vo.MemberAuthVO" alias="MemberAuthVO" />
	</typeAliases>
	
</configuration>

 

mybatisAlias에서 VO alias 추가

 

일대다의 관계이기때문에 resultMap사용

 

memberVO에는 list의 형태로 권한이 존재(memberVO의 memberAuthVOList) ↓ ↓ ↓

 

 

 

스프링에서 제공하는 superclass 의 user를 상속받아서 customuser 클래스 생성

package kr.or.ddit.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 kr.or.ddit.vo.MemberVO;

//사용자가 유저를 정의함
//memVO(사용자 정의한 유저)정보를 User(스프링 시큐리티에서 정의된 유저) 객체 정보에 연계하여 넣어줌
//CustomUser의 객체 = principal
public class CustomUser extends User {
	/*
	private String password;
	private final String username;
	private final Set<GrantedAuthority> authorities;
	private final boolean accountNonExpired;
	private final boolean accountNonLocked;
	private final boolean credentialsNonExpired;
	private final boolean enabled;
	*/
	
	private MemberVO memberVO;
	
	// USER의 생성자 오버로딩
	public CustomUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
		super(username, password, authorities);
	}
	
	public CustomUser(MemberVO memberVO) {
		// 사용자가 정의한 (select한) MemVO 타입의 객체 memVO를
	    // 스프링 시큐리티에서 제공해주고 있는 UsersDetails 타입으로 변환
	    // 회원정보를 보내주면 스프링에서 직접 관리
		super(memberVO.getUserNo()+ "" , memberVO.getUserPw(), memberVO.getMemberAuthVOList().stream().
				map(auth -> new SimpleGrantedAuthority(auth.getAuth())).collect(Collectors.toList())
				);
		// 리스트를 스트림으로 받는다 -> auth 객체에 넣어준다
			this.memberVO = memberVO;
		}
	public MemberVO getMemberVO() {
		return memberVO;
	}
	
	public void setMemberVO(MemberVO memberVO) {
		this.memberVO = memberVO;
	}

}

 

스프링 시큐리티를 사용하여 별도의 인증/권한을 체크하는 이유?