본문 바로가기

학원/복기

[Spring] Spring Security (users테이블, security_users 테이블 이용한 인증)

인증을 위한 사용자 테이블 생성

테이블명은 무조건 USERS로 지정해야 한다.

CREATE TABLE users(username varchar2(100) primary key, password varchar2(100), enabled varchar2(1));

 

삽입

INSERT INTO users VALUES ('abc123', '123456', '1');
INSERT INTO users VALUES ('xyz789', '123456', '1');
INSERT INTO users VALUES ('opq456', '123456', '1');
INSERT INTO users VALUES ('test', '123456', '0');

 

AUTHORITIES 테이블 및 인덱스 생성

CREATE TABLE authorities(username VARCHAR2(100), authority VARCHAR2(50)
    , CONSTRAINT auth_username_fk FOREIGN KEY(username) REFERENCES users(username));
    
CREATE UNIQUE INDEX auth_username_index ON authorities(username, authority);

 

권한부여

INSERT INTO authorities VALUES ('abc123', 'ROLE_USER');
INSERT INTO authorities VALUES ('abc123', 'ROLE_MANAGER');
INSERT INTO authorities VALUES ('xyz789', 'ROLE_MANAGER');
INSERT INTO authorities VALUES ('opq456', 'ROLE_ADMIN');
INSERT INTO authorities VALUES ('test', 'ROLE_USER');

 

 

security-context.xml

 

 

 jdbc-user-service : Spring Security가 JDBC를 이용하는 인증 처리를 하는 경우 사용하는 경우

=> JdbcUserDetailsManager 클래스를 사용하여 인증과 인가 처리한다. 

=> USERS 테이블을 생성하여 사용자 정보를 저장하고 AUTHORITIES 테이블을 생성하여 사용자 권한 정보를 저장할 경우

  SQL 명령을 제공하지 않아도 기본적으로 제공되는 SQL 명령으로 인증 처리 후 사용자 정보(UserDetils 객체)를 반환한다.

 

data-source-ref 속성 : Connection 객체를 제공하기 위한 DataSource 관련 클래스에 대한 Spring Bean의 식별자(beanName)을 속성값으로 설정

<authentication-manager>
	<authentication-provider>
		<jdbc-user-service data-source-ref="dataSource"/>
	</authentication-provider>
</authentication-manager>

root-context.xml에 있는 dataSource를 가져다 사용한다.

 

 

 

 

 

CustomPasswordEncoder

//비밀번호를 전달받아 암호화 처리하여 반환하거나 암호화 처리된 비밀번호를 비교한 결과를 반환
//하는 메소드를 제공하는 클래스
// => PasswordEncoder 인터페이스를 상속받아 작성한다.
public class CustomPasswordEncoder implements PasswordEncoder {
	//매개변수로 전달받은 문자열을 암호화 처리하여 반환하는 메소드
	@Override
	public String encode(CharSequence rawPassword) {
		return rawPassword.toString();
	}
	
	//매개변수로 전달받은 암호화된 문자열과 일반 문자열을 비교하여 결과를 논리값으로 반환하는 메소드
	@Override
	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		return rawPassword.toString().equals("encodedPassword");//암호화 처리되어있지 않은 비밀번호 비교
	}
	
}

 

 

security-context.xml

 

password-encoder : 암호화 처리된 비밀번호를 비교하기 위한 기능을 제공하는 엘리먼트 

ref 속성 : PasswordEncoder 인터페이스를 상속받은 클래스에 대한 Spring Bean의 식별자를 속성값으로 설정

 

<authentication-manager>
	<authentication-provider>
		...
        <password-encoder ref="customPasswordEncoder"/>
	</authentication-provider>
</authentication-manager>
	
<!-- Spring Security 관련 클래스를 Spring Bean으로 등록 -->
...
<beans:bean class="xyz.itwill.security.CustomPasswordEncoder" id="customPasswordEncoder"/>

 


 

테이블 생성

CREATE TABLE security_users(userid VARCHAR2(100) PRIMARY KEY, passwd VARCHAR2(100)
    , name VARCHAR2(50), email VARCHAR2(100), enabled VARCHAR2(1));
    
CREATE TABLE security_auth(userid VARCHAR2(100), auth VARCHAR2(50)
    , CONSTRAINT auth_userid_fk FOREIGN KEY(userid) REFERENCES security_users(userid));

CREATE UNIQUE INDEX auth_userid_index ON security_auth(userid, auth);

 

 

DTO 생성

@Data
@NoArgsConstructor
@AllArgsConstructor //객체를 생성할 때 입력받아 저장할 수 있도록 하기 위해 선언
public class SecurityUsers {
	private String userid;
	private String passwd;
	private String name;
	private String email;
	private String enabled;
	private List<SecurityAuth> securityAuthList; //조인할때 사용
}


@Data
@NoArgsConstructor
@AllArgsConstructor
public class SecurityAuth {
	private String userid;
	private String auth;
}

생성자는 안만들어도 됨

 

 

Mppaer

 

 

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xyz.itwill.mapper.SecurityUsersMapper">
	<insert id="insertSecurityUsers">
		insert into security_users values(#{userid}, #{passwd}, #{name}, #{email}, #{enabled})
	</insert>
	
	<insert id="insertSecurityAuth">
		insert into security_auth values(#{userid}, #{auth})
	</insert>
	
	<resultMap type="SecurityUsers" id="securityUsersResultMap">
		<id column="userid" property="userid"/>
		<result column="passwd" property="passwd"/>
		<result column="name" property="name"/>
		<result column="email" property="email"/>
		<result column="enabled" property="enabled"/>
		<collection property="securityAuthList" ofType="SecurityAuth">
			<result column="userid" property="userid"/>
			<result column="auth" property="auth"/>
		</collection>
	</resultMap>
	
	<select id="selectSecurityUsersByUserid" resultMap="securityUsersResultMap">
		select users.userid, passwd, name, email, enabled, auth from security_users users
			left join security_auth auth on users.userid=auth.userid where users.userid=#{userid}
	</select>
</mapper>

 

interface

public interface SecurityUsersMapper {
	int insertSecurityUsers(SecurityUsers users);
	int insertSecurityAuth(SecurityAuth auth);
    SecurityUsers selectSecurityUsersByUserid(String userid);
}

 

 

DAO

 

 

SecurityUsersRepository

SecurityUsersRepositoryImpl

public interface SecurityUsersRepository {
	int insertSecurityUsers(SecurityUsers users);
	int insertSecurityAuth(SecurityAuth auth);
    SecurityUsers selectSecurityUsersByUserid(String userid);
}



@Repository
@RequiredArgsConstructor
public class SecurityUsersRepositoryImpl implements SecurityUsersRepository {
	private final SqlSession sqlSession;

	@Override
	public int insertSecurityUsers(SecurityUsers users) {
		return sqlSession.getMapper(SecurityUsersMapper.class).insertSecurityUsers(users);
	}

	@Override
	public int insertSecurityAuth(SecurityAuth auth) {
		return sqlSession.getMapper(SecurityUsersMapper.class).insertSecurityAuth(auth);
	}

	@Override
	public SecurityUsers selectSecurityUsersByUserid(String userid) {
		return sqlSession.getMapper(SecurityUsersMapper.class).selectSecurityUsersByUserid(userid);
	}
}

 

 

 

Service

 

//인터페이스
public interface SecurityUsersService {
	void addSecurityUsers(SecurityUsers users);
	void addSecurityAuth(SecurityAuth auth);
	SecurityUsers getSecurityUsers(String userid);



@Service
@RequiredArgsConstructor
public class SecurityUsersServiceImpl implements SecurityUsersService {
	private final SecurityUsersRepository securityUsersRepository;
	
	@Override
	public void addSecurityUsers(SecurityUsers users) {
		securityUsersRepository.insertSecurityUsers(users);
	}

	@Override
	public void addSecurityAuth(SecurityAuth auth) {
		securityUsersRepository.insertSecurityAuth(auth);
	}

	@Override
	public SecurityUsers getSecurityUsers(String userid) {
		return securityUsersRepository.selectSecurityUsersByUserid(userid);
	}

}

 

 

 

 

 

컨트롤러

 

@Controller
@RequiredArgsConstructor
public class SecurityUsersController {
	private final SecurityUsersService securityUsersService;
	private final PasswordEncoder passwordEncoder;
	
	@RequestMapping(value = "/security/add", method = RequestMethod.GET)
	@ResponseBody //뷰 만들지 않고 처리하기 위함
	public String addUsers() {
		SecurityUsers user1=new SecurityUsers("abc123", 
				passwordEncoder.encode("123456"), "홍길동", "abc@itwill.xyz", "1", null);
		SecurityUsers user2=new SecurityUsers("xyz789", 
				passwordEncoder.encode("123456"), "임꺽정", "xyz@itwill.xyz", "1", null);
		SecurityUsers user3=new SecurityUsers("opq456", 
				passwordEncoder.encode("123456"), "전우치", "opq@itwill.xyz", "1", null);
		
		securityUsersService.addSecurityUsers(user1);
		securityUsersService.addSecurityUsers(user2);
		securityUsersService.addSecurityUsers(user3);
		
		SecurityAuth auth1=new SecurityAuth("abc123", "ROLE_USER");
		SecurityAuth auth2=new SecurityAuth("abc123", "ROLE_MANAGER");
		SecurityAuth auth3=new SecurityAuth("xyz789", "ROLE_MANAGER");
		SecurityAuth auth4=new SecurityAuth("opq456", "ROLE_ADMIN");
		
		securityUsersService.addSecurityAuth(auth1);
		securityUsersService.addSecurityAuth(auth2);
		securityUsersService.addSecurityAuth(auth3);
		securityUsersService.addSecurityAuth(auth4);
		
		return "사용자를 성공적으로 등록 하였습니다.";
	}
}

 

 

환경설정

 

 

passwordEncoder를 스프링 빈으로 등록

 

security-context.xml

<beans:bean class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" id="passwordEncoder"/>

 

BCryptPasswordEncoder : 문자열을 암호화 처리하거나 비교 결과를 제공하기 위한 클래스 

 

 

servlet-context.xml

<context:component-scan base-package="xyz.itwill.auth" />
<context:component-scan base-package="xyz.itwill.repository" />
<context:component-scan base-package="xyz.itwill.service" />

 


 

http://localhost:8000/auth/security/add 경로를 입력하면

 

select * from security_users;
select * from security_auth;

테이블에 정보가 삽입된 것을 확인할 수 있다.

 


security-context.xml

 

password-encoder ref를 passwordEncoder로 바꿔준다 (암호화)

	<authentication-manager>
		<authentication-provider>
			<jdbc-user-service data-source-ref="dataSource"
				users-by-username-query="select userid, passwd, enabled from security_users where userid=?"
				authorities-by-username-query="select userid, auth from security_auth where userid=?"/>
			<password-encoder ref="passwordEncoder"/>
		</authentication-provider>
	</authentication-manager>

 

users-by-username-query : 아이디를 전달받아 아이디(userid), 비밀번호(passwd), 활성화 상태(enabled) 를 검색하는 SELECT 명령을 속성값으로 설정 - 인증처리

authorities-by-username-query : 아이디를 전달받아 아이디, 권한을 검색하는 SELECT 명령을 속성값으로 설정한다. - 인증 성공한 사용자에게 권한 제공

 

 

 

 

abc123 계정으로 로그인 해보자

 

권한을 확인할 수 있다. (ROLE_MANAGER, ROLE_USER)

 

이처럼 직접 만든 테이블을 이용해 인증과 인가를 제공받을 수 있다.

 


User 클래스를 사용하지 않고 UserDetails 인터페이스를 상속받는 클래스를 선언해서 인증된 사용자 정보를 저장해보자

(User 클래스를 상속받아 작성하는 것도 가능하다.)

 

 

CustomUserDetails

//인증된 사용자 정보를 저장하기 위한 클래스
// => UserDetails 인터페이스를 상속받아 작성 
// => UserDetails 인터페이스 대신 UserDetails 인터페이스를 상속받은 User 클래스를 상속받아 작성하는 것도 가능하다.
@Data
public class CustomUserDetails implements UserDetails {
	private static final long serialVersionUID = 1L;
	
	//인증된 사용자 정보가 저장될 필드 선언 (필드명은 자유롭게 작성 가능) - 세션에 저장될 정보
	private String userid;
	private String passwd;
	private String name;
	private String email;
	private String enabled;
	//인증된 사용자의 권한 정보가 저장될 필드 선언
	private List<GrantedAuthority> securityAuthList;
	
	//매개변수로 전달받은 SecurityUsers 객체의 필드값을 CustomUserDetails 클래스의 필드에 저장
	public CustomUserDetails(SecurityUsers users) {
		this.userid=users.getUserid();
		this.passwd=users.getPasswd();
		this.name=users.getName();
		this.email=users.getEmail();
		this.enabled=users.getEnabled();
		
		//검색된 사용자의 권한(String 객체)은 GrantedAuthority 객체로 생성하여 List 객체의 요소로 저장
		this.securityAuthList=new ArrayList<GrantedAuthority>();
		for(SecurityAuth auth : users.getSecurityAuthList()) {
			securityAuthList.add(new SimpleGrantedAuthority(auth.getAuth()));
		}
	}
	
	//인증 사용자의 권한 정보를 반환하는 메소드
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return securityAuthList;
	}
	
	//인증 사용자의 비밀번호를 반환하는 메소드
	@Override
	public String getPassword() {
		return passwd;
	}
	
	//인증 사용자의 식별자(아이디)를 반환하는 메소드
	@Override
	public String getUsername() {
		return userid;
	}
	
	//인증 사용자의 계정 만료 관련 상태 정보를 반환하는 메소드
	// => false : 계정 사용 유효기관 초과 상태, true : 계정의 사용 유효기간 미만 상태
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}
	
	//인증 사용자의 계정 잠금 상태 정보를 반환하는 메소드
	// => false : 계정 잠금 상태, true : 계정 해제 상태
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}
	
	//인증 사용자의 비밀번호 유효기간 상태 정보를 반환하는 메소드 
	// => false : 비밀번호 유효기간 초과 상태, true : 비밀번호 유효기간 미만 상태
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}
	
	//인증 사용자의 활성화 상태 정보를 반환하는 메소드
	// => false : 사용자 비활성화 상태, true : 사용자 활성화 상태
	@Override
	public boolean isEnabled() {
		if(enabled.equals("0")) {
			return false;
		} else {
			return true;
		}
	}
}

 

 

Spirng Security 필터는 root-context.xml 또는 security-context.xml에 권한 설정 해주어야 한다.

 

security-context.xml

<context:component-scan base-package="xyz.itwill.security"/>
<context:component-scan base-package="xyz.itwill.repository" />

 

CustomUserDetailsService

//인증 처리 후 인증된 사용자 정보와 권한을 저장한 UserDetails 객체로 반환하는 기능을 제공하는 클래스 
// => UserDetailsService 인터페이스를 상속받아 작성
@Service
@RequiredArgsConstructor //DAO를 가져다 쓰기 위함
public class CustomUserDetailsService implements UserDetailsService {
	private final SecurityUsersRepository securityUsersRepository;
	
	//매개변수로 아이디(username)를 전달받아 DB에 저장된 사용자 정보를 검색하여 검색된 사용자 정보를  
	//UserDetails 객체를 생성하여 반환하는 메소드
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		SecurityUsers users=securityUsersRepository.selectSecurityUsersByUserid(username);
		
		if(users==null) {
			throw new UsernameNotFoundException(username);
		}
		return new CustomUserDetails(users);//사용자 정보가 검색된 경우 UserDetails 객체로 반환
	}
}

 

 

security-context.xml

 

 user-service-ref 속성 : 인증에 필요한 사용자 및 권한 정보(UserDetails 객체)가 저장된 객체를 반환하는 클래스(UserDetailsService)에 대한 Spring Bean의 식별자(beanName)을 속성값으로 설정

<authentication-manager>
	<authentication-provider user-service-ref="customUserDetailsService">
		<password-encoder ref="passwordEncoder"/>
	</authentication-provider>
</authentication-manager>

 

 

 

abc123으로 로그인