2부에 이어서 Resource Server를 구현하고 Authorization Server와

함께 토큰 발급과 토큰을 이용한 리소스 접근에 대한 테스트를 진행해보겠습니다.

 

개발 환경 : STS 4.8.1, JDK 1.8, Spring Boot 2.1.6, Mybatis 2.1, MySQL Community 5.6, Maven 4.0.0

2부에서 생성한 테이블과 테스트 데이터를 이용합니다.

 

1. Resource Server 구현을 위한 Spring Boot 프로젝트 생성 및 의존성 설정

프로젝트, 패키지 명은 적절하게 선택하여 Spring Stater Project 생성합니다.

의존성 설정은 아래의 pom.xml 파일을 참고해주세요.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.6.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.codejcd</groupId>
	<artifactId>SpringbootMybatisMysqlOauth2.0-ResourceServer</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>SpringbootMybatisMysqlOauth2.0-ResourceServer</name>
	<description>SpringbootMybatisMysqlOauth2.0-ResourceServer</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.0</version>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
	    <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.6.RELEASE</version>
        </dependency>
        <!-- @ConfigurationProperites 사용 시 클래스 패스 설정 -->
   		<dependency>
   			 <groupId>org.springframework.boot</groupId>
    		 <artifactId>spring-boot-configuration-processor</artifactId>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

 

2. Resource Server 프로젝트 구조

아래 이미지와 같은 구조를 가지게 됩니다.

- com.codejcd.config : 각종 설정 파일들의 패키지

- com.codejcd.dao : DB 액세스를 파일들을 위한 패키지

- com.codejcd.entity : 엔티티를 파일들을 위한 패키지

- com.codejcd.service : 서비스 레이어 패키지

- com.codejcd.controller : 컨트롤러 레이어 패키지


3. Mybatis 설정

MybatisConfiguration 클래스를 생성하고 Properties에서 읽어올 설정 값과

SQL Session과 Mapper 설정을 합니다.

2부에서 설정과 동일하여 별도의 추가 설명은 하지 않겠습니다.

package com.codejcd.config;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import com.zaxxer.hikari.HikariDataSource;
/**
 * Mybatis 설정
 * @author Jeon
 *
 */
@Configuration
@MapperScan(basePackages = "com.codejcd.*.dao.**", sqlSessionFactoryRef = "sqlSessionFactory")
public class MybatisConfiguration {

	@Bean(name = "dataSource")
	@ConfigurationProperties(prefix = "spring.datasource.hikari")
	public DataSource dataSource() {
		return new HikariDataSource();
	}
	
	@Bean(name = "sqlSessionFactory")
	public SqlSessionFactory sqlSessionFactory (
			@Qualifier("dataSource") DataSource dataSource,
			ApplicationContext applicationContext) throws Exception {
		
			SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
			sqlSessionFactory.setDataSource(dataSource);
			sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("config/mapper/*.xml"));
			
		return sqlSessionFactory.getObject(); 
	}
	
	@Bean(name = "sqlSession")
	public SqlSessionTemplate sqlSession(SqlSessionFactory sqlSessionFactory) throws Exception {
		return new SqlSessionTemplate(sqlSessionFactory);
	}
	
}

 

4. application.properties

MySQL DB에 접근하기 위한 설정과 JWT 설정을 추가합니다.

테스트 설정이니 참고만 해주시고 절대로 상용에서 사용하면 안 되는 비밀번호입니다.

server.port=8094
spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.jdbc-url=jdbc:mysql://localhost:3306/oauth2?characterEncoding=UTF-8&serverTimezone=Asia/Seoul
spring.datasource.hikari.username=root
spring.datasource.hikari.password=1111
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.connection-test-query=SELECT 1 FROM DUAL
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=100000
security.oauth2.signkey=testsignkey123!@
security.oauth2.client.client-id=testClientId
security.oauth2.client.client-secret=testSecret
security.oauth2.resource.token-info-uri=http://localhost:8093/oauth/check_token

 

이 세 값은 JWT 일 경우는 해당 사항이 없다.

JWT는 검증을 위해  Authorization Serve를 호출하는 과정이 없기 때문이다.

단 JWT 가 아닌 일반적인 token을 사용하는 경우에는 호출한다.

 

  • security.oauth2.signkey
    JWT에 사용할 sign key 설정 값으로 복호화에 사용.
    이 데모 앱에서는 대칭 키를 사용하기 때문에 동일합니다.

  • security.oauth2.client.client-id=testClientId
    client id이다. token 검증을 위한 호출 시 사용됩니다.

  • security.oauth2.client.client-secret=testSecret
    client secret. token 검증을 위한 호출 시 사용됩니다.

  • security.oauth2.resource.token-info-uri=http://localhost:8093/oauth/check_token
    token 검증을 위한 Authroization Server의 token 검증 URI를 설정한다.
    token 검증 시 사용된다.

5. Resource Server 설정

Oauth2 Resource 서버 설정을 구현한다.

EnableResourceServer 어노테이션과 ResourceServerConfigureAdaptor를 상속받아

간단하게 핵심적인 기능을 구현이 가능하다. 

package com.codejcd.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

@Configuration
@EnableResourceServer
public class Oauth2ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
	
	@Override
	public void configure(HttpSecurity http) throws Exception {
		    // http.headers().frameOptions().disable(); // X-Frame-Options 설정
	        http.authorizeRequests()
	        		// access token이 있어야 접근 가능
	                //.antMatchers("/user/list1").access("isAuthenticated()"); // 인증 받은 경우 해당 URI 에 접근 가능
	                .anyRequest().authenticated(); // 모든 요청은 인증 받아야 접근 가능
    }	
}

 

.antMatchers("/user/list 1").access("isAuthenticated()")와.anyRequest().authenticated() 설정에

따라서 뒤에서 테스트 해볼 결과가 달라질 수 있다.

모든 자원에 인증이 필요한 경우가 있을 수 있고
특정 자원만 인증이 필요한 경우가 있을 수 있으니

해당 설정은 그런 상황에서 리소스 접근에 대한 관리가 용이합니다.

 

6. User Entity 구현  

Authroization Server의 Entitiy와 동일합니다.

UserDetails를 구현하는 객체입니다.

package com.codejcd.entity;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

public class User implements UserDetails {
	
	private static final long serialVersionUID = -4591689732776493890L;

	private int userSeq;
	
	private String userId;
	
	private String password;
	
	private String name;
	
	private String status;
	
	private List<String> roles = new ArrayList<String>();

	
	@Override
	public String getUsername() {
		return userId;
	}
	
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return this.roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
		
	}
	
	@Override
	public String getPassword() {
		return password;
	}
	
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}
	
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}
	
	@Override
	public boolean isEnabled() {
		return true;
	}

	public int getUserSeq() {
		return userSeq;
	}

	public void setUserSeq(int userSeq) {
		this.userSeq = userSeq;
	}

	public String getUserId() {
		return userId;
	}

	public void setUserId(String userId) {
		this.userId = userId;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public List<String> getRoles() {
		return roles;
	}

	public void setRoles(List<String> roles) {
		this.roles = roles;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getStatus() {
		return status;
	}

	public void setStatus(String status) {
		this.status = status;
	}
}


7. User SQL Mapper 구현

User Data를 리스트 형태로 조회할 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.codejcd.mapper.UserMapper">
     <select id="selectUserList" resultType="com.codejcd.entity.User">
     	 SELECT user_seq as userSeq
  				, user_id as userId
  				, password as password
  				, name as name
  				, status as status
  				, reg_date as regDate
  		  FROM user
     </select>
</mapper>

 

8. User DAO 구현

SQL Mapper에 접근하여 결과 값을 얻을 DAO를 작성합니다.

package com.codejcd.dao;

import java.util.List;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Repository;
import com.codejcd.entity.User;

@Repository
public class UserDao {
	private final static String NAMESPACE = "com.codejcd.mapper.UserMapper.";
	
	@Autowired
	@Qualifier("sqlSession")
	private SqlSessionTemplate sqlSession;
	  
	   public List<User> selectUserList() {
	    	return sqlSession.selectList(NAMESPACE + "selectUserList");
	   }
}

 

9. UserService 구현

UserService 레이어입니다. 

유저 리스트 조회를 위한 UserDao의 selectUserList 메서드를

호출하고 결과 값을 리턴합니다.

여기서 데모 앱이기 때문에 별다른 비즈니스 로직은 없습니다.

package com.codejcd.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.codejcd.dao.UserDao;
import com.codejcd.entity.User;

@Service
public class UserService {

	@Autowired
	private UserDao userDao;
	
	public List<User> selectUserList() {
		return userDao.selectUserList();
	}
}

 

10.  UserController 구현

RestContoller 어노테이션을 사용하여,

JSON  포맷으로 응답 값을 반환하게 설정합니다.

package com.codejcd.controller;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.codejcd.entity.User;
import com.codejcd.service.UserService;

@RestController
public class UserController {
	
	@Autowired
	private UserService userService;
	
    @RequestMapping("/user/list1")
    public List<User> getUserList1() {
    	return userService.selectUserList(); 
    }
    
    @RequestMapping("/user/list2")
    public List<User> getUserList2() {
    	return userService.selectUserList(); 
    }

}

 

  • /user/list1, /user/list2
    유저 리스트를 조회하는 같은 기능을 합니다,
    앞으로 돌아가서 Oauth2ResourceServerConfiguration를 주석 처리된 설정을 통해 
    어떻게 동작하는지 뒤이은 테스트에서 결과 값을 확인해봅니다.

11. 테스트

테스트 툴로는 POSTMAN을 사용했습니다.

먼저 앞에서 구현한 Authrozation Server를 시작하고 

Authrozation Basic에 클라이언트 정보와 Body 정보를 세팅하고 
http://localhost:8093/oauth/token를 호출하여 응답 값으로 토큰 값을 얻습니다.

 

header 정보 세팅

 

body 정보 세팅하고 API 호출

 

앞에서 작성한 자원 서버 API(/user/list1, /user/llist2) 를 먼저 토큰 값을 세팅하지 않고 호출해봅니다.

/user/list1 API 호출
/user/list2 API 호출

 

두 API 모두 접근 권한이 없으므로 Unauthorized 응답 값이 확인됩니다.

Bearer Token Header 값에 세팅하고 다시 두 API를 호출하면 접근 가능한 것을 확인 가능합니다.

/user/list1 API 호출

 

/user/list2 API 호출

 

.antMatchers("/user/list1").access("isAuthenticated()") 주석을 해제하고

.anyRequest().authenticated() 을 주석 처리하고 다시 시도하면 /user/list1은 접근 가능하고
/user/list2는 접근 불가능합니다.

 

12. 마치며

비교적 간단한 Spring Security Oauth2 설정을 사용하여,

password credential을 데모 앱을 구현하고
이에 따른 리소스 접근 제어를 확인해봤습니다.

아쉽지만 이 시리즈 작성을 시작할 때 이야기했듯이
지원 중단이 될 프로젝트이고 디테일한 설정을 하기 위해서는
Spring Security Oatuh2를 더 공부해야 가능할 것 같습니다.
다음은 이 시리즈의 마지막으로 Oauth2 + JWT를
Spring Security Oauth2 없이 직접 구현해보겠습니다.
전체 소스는 Github에서 확인 가능합니다.

※ 참고

https://projects.spring.io/spring-security-oauth/docs/oauth2.html

https://docs.spring.io/spring-security/site/docs/3.2.5.RELEASE/reference/htmlsingle/#el-access

+ Recent posts