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

저번 글에 이어서 Oauth 2.0를 구현해볼 예정입니다.

Spring Boot 기반 프로젝트를 생성하고 Spring Security Oauth를 사용하여 구현하는 방법과

Spring Security Oauth를 사용하지 않는 방법 2가지로 나눠서 소개드리려고 합니다.

본격 들어가기전에 앞서 먼저 최근 소식을 전해드리면 Spring Security Oauth 프로젝트는 

2019년 11월에 로드맵을 발표하면서 지원 종료 계획을 발표했습니다.

자세한 내용은 링크를 참고해주시구요.

제가 2019년 8월 경에 Spring Security Oauth Demo를 Github에 작성했었는데요.

지금 글을 작성하는 시점에서는 지원 종료될 예정에 프로젝트니 Spring Security Oauth 프로젝트에서

이렇게 구현을 했었구나 정도 참고만 하고 실무에서 사용하는 것은 권장하지 않습니다.

 

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

MySQL Community 5.6 이 미리 설치됐다는 가정하고 진행해보겠습니다.

먼저 Github에서 src/main/resources/db/scheme.sql 과 test_data.sql 을 실행하여

필요한 테이블 및 테스트 데이터를 생성합니다.

oauth_acces_token 테이블과 테스트 데이터는 JWT 방식이 아닐 경우

설정에서 필요한 데이터이기 때문에 JWT만 테스트할 경우 필수 생성하지 않아도 됩니다.

나머지 oauth_client_details 테이블의 client 정보와

user 테이블의 사용자 정보는 password credential 방식에서 필수 정보이므로 생성합니다.

 

1. 구현 내용

먼저 Spring Security Oatuh을 이용하여 Authorization Server와

Resource Server를 구현해볼 예정입니다.

유저 정보, 토큰을 저장/조회하기 위해 MySQL DB를 사용할 예정입니다.

물론 MySQL 이외에 다른 DB를 사용해도 되지만 실무에서 가장 많이 접하고 있는 게

현재 MySQL이라서 사용하게 됐습니다.

사용하기 편한 DB를 사용하면 되고, DB 구조는 알맞게 수정하면 됩니다.

본격 구현에 들어가기에 앞서 Spring Security Oauth 구조와 흐름에 대해서 간단하게 알아보겠습니다.

복잡해 보이지만 사용자 요청 → UsernamePasswordAuthenticationFilter 구현체

→ AuthenticationProvider 구현체 → UserDetailsService 구현체로 흘러가는 흐름입니다.

이중 Filter는 커스텀하지 않고 AuthenticationProvider, UserDetailsService, User를 커스텀하겠습니다.

 

 

 

2. Authorization 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</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>SpringbootMybatisMysqlOauth2.0</name>
	<description>SpringbootMybatisMysqlOauth2.0</description>

	<properties>
		<java.version>11</java.version>
	</properties>

	<dependencies>
		<dependency>
			<!-- Spring Security 의존성 설정 -->
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- mybatis 사용을 위한 설정 -->
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.0</version>
		</dependency>
		<!-- mysql connector 설정 -->
		<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>
		<!-- oauth2.0 기능 구현을 위한 의존성 설정 -->
		 <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>

 

3. Authorization Server 프로젝트 구조

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

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

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

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

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

 

4. Mybtais 설정

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

SQL Session과 Mapper 설정을 합니다.

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 Configuration
 * @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);
	}
	
}

 

  • MapperScan Annotation
    basePackages의 지정된 com.codejcd.*.dao** 패키지에 대해 Mapper를 검색하여 등록합니다.
    SqlSessionFactoryRef로 별칭을 지정합니다.

  • ConfigurationProperties Annotation
    spring.datasource.hikari 로 시작하는DataSource 설정을
    application.properties 파일에 등록된 설정 정보를 주입합니다.

  • sqlSessionFactory Method
    SqlSessioFactoryBean을 생성하고 Mapper 파일들이 위치하는 경로를 설정한다.

  • sqlSession Method
    SqlSessionTemplate SqlSession을 구현하고 코드에서 SqlSession을 대체하는 역할을 합니다.
    SqlSessionTemplat e은 스레드에 안전하고 여러 개의 DAO나 Mapper에서 공유할 수 있습니다.
    그리고 세션의 생명주기를 관리하며, 스프링 트랜잭션에서 사용될 수 있도록 보장합니다.

5. application.properties

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

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

security.oauth2.signkey는 뒤에 내용에서 JWT에 사용할 설정 값입니다.

server.port=8093
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!@

 

6. UserDetails 

6.1 User 구현 

1번의 구조 이미지에서 User에 해당하는 부분이며, UserDetails를 구현하는 객체입니다.

오버 라이딩 메서드 이외에 생성할 DB 칼럼에 맞춰 필요한 값들을 추가해줍니다.

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>();

	
	public int getUserSeq() {
		return userSeq;
	}

	public void setUserSeq(int userSeq) {
		this.userSeq = userSeq;
	}
	
	@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 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;
	}
}

 

6.2 User SQL Mapper 구현

User 객체에 필요한 값들을 userId를 조건으로 조회할 SQL Mapper을 작성합니다.

이번 데모에서는 Mybatis를 이용하기 때문에 필요한 작업입니다.

<?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="selectUser" parameterType="String" 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
  		WHERE user_id = #{userId}  	 
     </select>
</mapper>

 

6.3 User DAO 구현

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

package com.codejcd.dao;

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 User selectUserByUserId(String userId) {
	    	return sqlSession.selectOne(NAMESPACE + "selectUser", userId);
	   }

}

 

  • selectuserByUserId Method
    userId를 매개변수로 받아 UserMapper에 맵핑된 SQL 조회한 결과 값을 리턴합니다.

6.4 UserDetailService 구현

1번의 구조 이미지에서 UserDetailService에 해당하는 부분으로

UserDetailService 구현하는 클래스입니다.

loadUserByUsername 메서드를 오버 라이딩합니다.

package com.codejcd.service;

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 com.codejcd.dao.UserDao;

@Service
public class CustomUserDetailService implements UserDetailsService {
	
	@Autowired
	private UserDao userDao;
	
	@Override
	public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
		return userDao.selectUserByUserId(userId);
	}

}

 

7. AuthenticationProvider 구현

AuthenticationProvider의 구현하는 클래스입니다.

authenticate 메서드를 오버 라이딩하여 어떻게 인증 처리를 할지를 구현합니다.

유저/패스워드에 대한 간단한 유효성 체크 후 인증 통과 시

UsernamePasswordAuthenticationToken 객체 인스턴스를 리턴합니다.

현업에서는 유저 아이디에 대한 생성 규칙이나 패스워드 정책에 따라

더욱 꼼꼼하게 체크를 해야 하지만 데모 앱이므로 간략화했습니다.

package com.codejcd.config;

import org.springframework.beans.factory.annotation.Autowired;
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.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import com.codejcd.service.CustomUserDetailService;

/**
 * AuthenticationProvider Impl
 * @author Jeon
 *
 */
@Component
public class CustomAuthenticationProvider implements org.springframework.security.authentication.AuthenticationProvider {
	
	@Autowired
	private PasswordEncoder passwordEncoder; 
	 
	@Autowired
	private CustomUserDetailService custumUserDetailService;
	
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		
		String userId = authentication.getName();
		String password = authentication.getCredentials().toString();
		
		UserDetails userDetail = custumUserDetailService.loadUserByUsername(userId);
	
		if(null == userDetail) {
			throw new BadCredentialsException("user is null");
		}
		
		if (!passwordEncoder.matches(password, userDetail.getPassword())) {
			throw new BadCredentialsException("password is invalid");
		}
		
		return new UsernamePasswordAuthenticationToken(userId, password, userDetail.getAuthorities());
	}
	
	@Override
	public boolean supports(Class<?> authentication) { // false 경우 authenticate 메소드 호출하지 않음.
		return authentication.equals(UsernamePasswordAuthenticationToken.class);
	}

}

 

8. WebMvcConfiguration

WebMvcConfigurer 구현체이며, WebApplicationContext에 사용할 설정이다.

이번 데모 앱에서는 특별한 설정을 넣지 않았고, CORS 설정 관련해서는 주석 처리했습니다.

package com.codejcd.config;

//import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//import org.springframework.security.crypto.factory.PasswordEncoderFactories;
//import org.springframework.security.crypto.password.PasswordEncoder;
//import org.springframework.web.client.RestTemplate;
//import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
	
	//private static final long MAX_AGE_SECONDS = 3600;

	/*
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(MAX_AGE_SECONDS);
    }
	 */

}

 

9. SecurityConfiguration

WebSecurityConfigurerAdapter 구현체이며, 보안 구성을 설정입니다.

package com.codejcd.config;

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.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * Spring Security Configuration
 * @author Jeon
 *
 */
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
	
	@Autowired
	private CustomAuthenticationProvider customAuthenticationProvider; 

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.authenticationProvider(customAuthenticationProvider);
	}
	
	@Bean
	@Override
	protected AuthenticationManager authenticationManager() throws Exception {
		return super.authenticationManager();
	}
	
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
    
}

 

  • configure Method
    |7번에서 작성한 CustomAuthenticationProvider를 AuthenticationManger 구현체로 설정하고 Bean으로 등록합니다.

  • authenticationManager Method
    변경 없이 그대로 상속받습니다.

  • passwordencdoer Method
    PasswordEncoderFactories.createDelegatingPasswordEncoder()로 생성한 PasswordEncoder는
    값의 prefix인 {id}로 PasswordEncoder 유형에 따라 정의됩니다.
    이번 데모에서 사용하는 테스트 데이터(test_data.sql)에는 bcrypt를 사용합니다.

10. Oauth2 AuthServerConfiguration

AuthorizationServerConfigurerAdapter 구현체이며, 인증 방식에 대해 설정합니다.

JWT 방식에 대해 설정하고 있는데 JWT가 설정하지 않을 경우 Access Token을 획득한 사용자가

Resource Server에 접근하려고 Access Token을 전달했을 경우에 Resource Server는

다시 Authorization Server에 유효한 토큰이 맞는지 체크를 해야 합니다.

JWT 설정한 경우에는 Resource Server에 요청이 들어올 경우 JWT 검증하여 유효한 경우

Authorization Server에 체크하지 않습니다.

유효성은 sign key를 이용하여 복호화를 통해 검증합니다.

package com.codejcd.config;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import com.codejcd.service.CustomUserDetailService;

/**
 * Oauth2 Authorization Server Configuration
 * @author Jeon
 *
 */
@Configuration
@EnableAuthorizationServer
public class Oauth2AuthServerConfiguration extends AuthorizationServerConfigurerAdapter {
	
	@Autowired
	private CustomUserDetailService customUserDetailService;  
	
	@Autowired
	private AuthenticationManager authenticationManager; 
	
	@Autowired
    private DataSource dataSource;
	
	@Autowired
	private PasswordEncoder passwordEncoder; // DI가 없는 이유는 DB 저장시 Prefix를 사용하여 다양한 암호화 방식을 지원한다. ex) {bcrypt}
	
	@Value("${security.oauth2.signkey}")
	private String signKey;
    
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource) // Client 인증 시 사용할 datasoruce 설정
        	   .passwordEncoder(passwordEncoder); // 패스워드 인코딩 섷정

    }	
    
    /** 
    * 인증과 토큰 Endpoint 설정
    *  유저 토큰 인증 설정 및 토큰에 대해 DB 사용
    */
    /*@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    	 endpoints.tokenStore(new JdbcTokenStore(dataSource)) // 토큰 DB 설정 
    	 .userDetailsService(custumUserDetailService) // 유저 인증 관리자 및 서비스
    	 .authenticationManager(authenticationManager);  
    }*/
    
    /**
     *  인증과 토큰 Endpoint 설정
     *  JWT와 유저 토큰 인증 설정
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    	super.configure(endpoints);
    	endpoints
    	.userDetailsService(customUserDetailService) // 유저 인증 관리자 및 서비스
    	.authenticationManager(authenticationManager)
    	.accessTokenConverter(jwtAccessTokenConverter()); // Json Web Token 변환 설정
    }
    
    /**
     * Json Web Token 설정
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
    	JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    	converter.setSigningKey(signKey); // 사인 키 설정
    	return converter;
    }
    
    /**
     * 토큰 보안 제약
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    	 security.tokenKeyAccess("permitAll()")
            .checkTokenAccess("isAuthenticated()");
           // .allowFormAuthenticationForClients();
    }
    

}

 

  • configure(ClientDetailsServiceConfigurer clients) Method
    API 요청 클라이언트 세부 정보를 정의.

  • configure(AuthorizationServerEndpointsConfigurer endpoints) Method
    Endpoint 에 대한 설정. TokenStore 또는 JWT 설정 등 토큰 서비스를 정의.
    해당 데모 앱에서는 JWT 설정, userDetailService, authenticationManager를 설정하였습니다.

  • configure(AuthorizationServerSecurityConfigurer security) Method
    Token Endpoint에 대한 액세스 규칙 등 보안 제약 조건을 정의.
    permitAll()은 모두에게 허용이며, isAuthenticated()는 익명이 아닌 인증된 사용자만 허용.
    pertmitAll()이라고 해도 client가 아닌 경우 API를 호출한다고 token key를 보여주지는 않는다.
    자세한 권한에 대한 내용은 해당 링크의 3.1.1 항목을 참고해주기 바랍니다.

11.  테스트

미리 생성해둔 테스트 정보로 테스트를 진행해보겠습니다.

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

Header 에는 Authorization에 Basic Auth 타입으로 Client 정보를 입력해주고

Body 에는 client_id, grant_type, username, password를 Client ID, 인증 방식, 유저 정보를 입력합니다.

Response에 정상적으로 token 정보가 응답 오는 것을 확인 가능합니다.

만약 틀린 정보를 입력하면 다른 응답 값을 리턴하며,

Basic Auth가 틀린 경우 401 응답 코드를 리턴 받게 됩니다.

 

header 정보 세팅
body 정보 세팅

 

응답 값

13. 마치며

생각보다 내용이 길어져서 Resource Server에 대한 구현은 3부에 이어서 진행하겠습니다.

서두에서 이야기했지만 Spring Security Oauth가 이미 지원 종료되는

프로젝트이기 때문에 자세히 설명하지 않았습니다.

어떤 방식으로 Oauth 2.0을 구현을 했는지 구조와 방법을 어느 정도 익힌다면

직접 구현도 어렵지 않을 것으로 보이며, 대체하는 여러 오픈 소스가 있으니

오픈 소스마다 구현 방법이 조금씩 다를 순 있어도 결국에는 인증 방식이라던지

큰 틀은 벗어나지 않을 것이라 생각됩니다.

전체 소스는 링크를 참고해주세요.

 

※ 참고

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

이번에 정리해볼 내용은 Oauth 2.0과 JWT(Json Web Token)입니다.

Spring Boot를 사용하여 구현해볼 예정입니다.

여담으로 실무에서 Oauth 2.0을 먼저 접해보았습니다.

이전 회사에서는 주로 웹 서비스만 다루는 경우가 많아서 Session, Cookie를 사용하여 인증했고

이번 회사에서 일하면서 처음으로 접하게 되면서 공부를 하게 되었습니다.

공부한지는 이미 한참이 되었으나 그동안 하드 드라이브에  Github에 업로드만하고

정리를 못한 저장된 소스를 꺼내어 간단하게 이론

먼저 1부에서 정리한 후에 2부에서 Demo를 만들어보겠습니다.

 

1.Oauth 란?

Oauth는 사용자들이 비밀번호를 제공하지 않고

다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의

접근 권한을 부여할 수 있는 공통적인 수단으로써 사용되는 접근 위임을 위한 개방형 표준입니다.

예를 들면 네이버, 카카오, 구글 아이디로 로그인 같은 서비스를 생각해보면 됩니다.

Oauth 2.0 이 널리 쓰이고 있고 1.0 버전은 웹 이외에 애플리케이션 지원이 부족한 점과 Access Token의

만료가 없는 점 등 여러 가지 단점 때문에 개선된 2.0 버전을 사용하게 됐습니다.

 

2. Oauth 2.0 구조와 흐름

명칭 설명
Resource Owner 보호 자원에 접근을 부여할수 있는 개체
ex) 서비스 이용자
Resource Server 보호 자원에 API는 제공하는 서버
예) 로그인 
Authorization Server 자원 접근 권한을 위임 및 관리 서버
Client Resource Server에 보호 자원을 요청하고 관련 서비스를
제공하는 애플리케이션 
Access Token  자원 접근에 대한 권한을 나타내는 자격 증명
Refresh Token Access Token 만료 시 이를 갱신하기 위한 용도로 사용하는 자격 증명

 

3. Oauth 2.0 승인 방식 종류

명칭 설명
Authorization Code Grant Type 권한 부여 코드 승인 타입.
리소스 접근을 위한 아이디/패스워드를 Authorization
서버에 요청하여 받은 권한 코드를 활용하여 리소스에
대한 access token을 획득하는 방식.
Implicit Grant Type 암묵적 승인.
첫번째 타입과 흐름이 비슷합니다.
인증 후 redirect url 로 직접 access token을 전달받음.
Resource Owner Password Credentials Grant Type 리소스 소유자 암호자격 증명타입.
Resource Owner가 직접 Client에 아이디/패스워드 입력하여
Authorization 서버에 인증받고 access token을 얻는 획득하는 방식.
Client Credentials Grant Type 클라이언트 자격 증명 타입
access token을 얻기 위해 정해진 인증 key(secret)으로 요청.
일반적인 사용보다는 서버간 통신에 주로 사용.

 

3.1. Authorization Code Grant Type


3.2. Implicit Grant Type


3.3. Resource Owner Password Credentials Grant Type


3.4. Client Credentials Grant Type

 

4. JWT(JSON Web Token)

JWT는 일반적으로 클라이언트와 서버, 서비스와 서비스 사이 통신 시 권한 인가를 위해 사용하는 토큰이다. 

URL 안전한 문자로 구성되고 HTTP Header, Body, URL 등에 위치할 수 있습니다.

헤더, 페이로드, 서명 세 부분을 점(.)으로 구분하는 구조입니다.

 

4.1 Header

JWT 검증에 필요한 정보를 가진 JSON 객체를 BASE64 URL-Safe 인코딩한 문자열.

JWT를 어떻게 검증하는지에 대한 내용을 담는다.

 

4.2 Payload

Payload 속성을 Claim Set이라고 부른다. Claim Set은 JWT 대한 생성자, 생성 일자 등 클라이언트와 서버 간

주고받기로 한 값들로 구성.

속성 설명
iss 토큰을 발급한 발급자(Issuer)
sub Claim의 주제로 토큰이 갖는 문맥을 의미
aud 이 토큰을 사용할 수신자
exp 토큰 만료시간
nbf Not Before의 의미로 이 시간 이전에는 토큰을 처리하지
않아야 함을 의미
iat 토큰이 발급된 시간(Issued At)
jti JWT ID로 토큰에 대한 식별자이다.

 

4.3 Signature

점(.)을 구분자로 헤더와 페이로드를 합친 문자열을 서명한 값. 

헤더에 정의된 알고리즘 값과 비밀 키를 이용해 생성하고 Base64 URL-Safe 인코딩한다.

 

JWT는 점(.)을 구분자로 해서 Header, Payload, Signature를 합친 것이다.

간단하게 샘플 JWT을 생성해보고 싶으시다면 아래 URL의 툴을 이용해서 한번 생성해보세요.

https://jwt.io/#debugger

 

5. 마치며

간략하게 이론적인 부분을 정리해보았습니다.

가장 정확한 것은 RFC 문서이지만 다른 자료들을 참고해서 요약정리해봤습니다.

2부에서는 Spring Boot 기반으로 직접 구현해볼 예정입니다.

잘못된 내용이나 문제가 될 소지가 있는 내용이 있으면 언제든지 연락 주세요.

 

※ 참고

https://datatracker.ietf.org/doc/html/rfc6749

https://tools.ietf.org/html/rfc7519
https://tools.ietf.org/html/rfc7515
https://tools.ietf.org/html/rfc7516

https://blog.naver.com/mds_datasecurity/222182943542

https://meetup.toast.com/posts/239

+ Recent posts