본문으로 바로가기

728x90

Spring Boot with OAuth2 - 1. OAuth2 Server 에서 구현한 코드를 확장하는 방법에 대해서 설명합니다.

 

데이터베이스 연결(MariaDB + Hibernate)

Hibernate를 통하여 MariaDB에 접속하는 예제를 구현합니다. application.properties에 데이터베이스 연결 정보만 추가하고 일부 몇몇 코드만 쿠가하면 자동으로 데이터베이스에 연결하여 OAuth2 서비스가 가능합니다.

관련 라이브러리 설정

pom.xml에 관련 라이브러리를 추가합니다.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
		
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
		
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
		
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
</dependency>

application.properties에 데이터베이스 연결정보를 설정합니다.

spring.jpa.generate-ddl= false
spring.jpa.hibernate.ddl-auto= none
#spring.jpa.show-sql: true

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://host:port/database_name?characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=oauth2
spring.datasource.password=password

 

아래 SQL문을 실행하여 데이터베이스 테이블을 생성합니다.

CREATE TABLE `oauth_access_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` mediumblob DEFAULT NULL,
  `authentication_id` varchar(256) NOT NULL,
  `user_name` varchar(256) DEFAULT NULL,
  `client_id` varchar(256) DEFAULT NULL,
  `authentication` mediumblob DEFAULT NULL,
  `refresh_token` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`authentication_id`)
);

CREATE TABLE `oauth_approvals` (
  `userId` varchar(256) DEFAULT NULL,
  `clientId` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `status` varchar(10) DEFAULT NULL,
  `expiresAt` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
  `lastModifiedAt` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
);

CREATE TABLE `oauth_client_details` (
  `client_id` varchar(256) NOT NULL,
  `resource_ids` varchar(256) DEFAULT NULL,
  `client_secret` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `authorized_grant_types` varchar(256) DEFAULT NULL,
  `web_server_redirect_uri` varchar(256) DEFAULT NULL,
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additional_information` varchar(4096) DEFAULT NULL,
  `autoapprove` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
);

CREATE TABLE `oauth_code` (
  `code` varchar(256) DEFAULT NULL,
  `authentication` mediumblob DEFAULT NULL
);

CREATE TABLE `oauth_refresh_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` mediumblob DEFAULT NULL,
  `authentication` mediumblob DEFAULT NULL
);

CREATE TABLE `oauth_user_authorities` (
  `username` varchar(256) NOT NULL,
  `authority` varchar(256) NOT NULL,
  PRIMARY KEY (`username`,`authority`)
);

CREATE TABLE `oauth_user_details` (
  `username` varchar(256) NOT NULL,
  `password` varchar(256) NOT NULL,
  `enabled` tinyint(1) DEFAULT NULL,
  `account_non_expired` tinyint(1) DEFAULT NULL,
  `account_non_locked` tinyint(1) DEFAULT NULL,
  `credentials_non_expired` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`username`)
);

 

기본 데이터를 아래와 같이 입력합니다.

insert  into `oauth_client_details`(`client_id`,`resource_ids`,`client_secret`,`scope`,`authorized_grant_types`,`web_server_redirect_uri`,`authorities`,`access_token_validity`,`refresh_token_validity`,`additional_information`,`autoapprove`)
      values ('client',NULL,'{noop}secret','read_profile','authorization_code,password,client_credentials,implicit,refresh_token','http://localhost:9000/callback',NULL,36000,2592000,NULL,'true');

insert  into `oauth_user_details`(`username`,`password`,`enabled`,`account_non_expired`,`account_non_locked`,`credentials_non_expired`) values ('admin','{noop}pass',1,1,1,1);
insert  into `oauth_user_details`(`username`,`password`,`enabled`,`account_non_expired`,`account_non_locked`,`credentials_non_expired`) values ('user','{noop}pass',1,1,1,1);

insert  into `oauth_user_authorities`(`username`,`authority`) values ('admin','ADMIN');
insert  into `oauth_user_authorities`(`username`,`authority`) values ('admin','USER');
insert  into `oauth_user_authorities`(`username`,`authority`) values ('user','USER');

AuthorizationServerConfig 수정

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
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.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

	@Autowired
	private AuthenticationManager authenticationManager;
	
	@Autowired
	private AuthorizationCodeServices authorizationCodeServices;
	
	@Autowired
	private ApprovalStore approvalStore;

	@Autowired
	private TokenStore tokenStore;

	@Bean
	public AuthorizationCodeServices jdbcAuthorizationCodeServices(DataSource dataSource) {
		return new JdbcAuthorizationCodeServices(dataSource);
	}
	
	@Bean
	public ApprovalStore jdbcApprovalStore(DataSource dataSource) {
		return new JdbcApprovalStore(dataSource);
	}

	@Bean
	public TokenStore jdbcTokenStore(DataSource dataSource) {
		return new CustomJdbcTokenStore(dataSource);
	}
	
	@Bean
	@Primary
	public ClientDetailsService jdbcClientDetailsService(DataSource dataSource) {
		return new JdbcClientDetailsService(dataSource);
	}

/*
데이터베이스를 사용하여 사용자를 관리하므로 이 코드는 삭제처리합니다.
	@Autowired
	@Qualifier("userDetailsService")
	private UserDetailsService userDetailsService;
*/	
	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		endpoints
			.authenticationManager(authenticationManager)
			.authorizationCodeServices(authorizationCodeServices)
			.tokenStore(tokenStore)
			.approvalStore(approvalStore)
//			.userDetailsService(userDetailsService)
			;
	}
/*
데이터베이스를 사용하므로 이 코드는 삭제처리합니다.
	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		clients
			.inMemory()
			.withClient("client")
//			.secret("{bcrypt}$2a$10$goA9F/Q./Ml8lYvuO1tj6OKA5K6VVM/jmUcdIp1AMzqtXHsuo68/W")		// secret
			.secret("{noop}secret")		// secret
			.redirectUris("http://localhost:9000/callback")
			.authorizedGrantTypes("authorization_code", "implicit", "password", "client_credentials", "refresh_token")
			.accessTokenValiditySeconds(120)
			.refreshTokenValiditySeconds(240)
			.scopes("read_profile");
	}
*/
}

/*
CustomJdbcTokenStore를 재구현하지 않을 경우
Failed to find access token for token null 오류가 출력됩니다.
*/
class CustomJdbcTokenStore extends JdbcTokenStore {
	private static final Logger log = LoggerFactory.getLogger(CustomJdbcTokenStore.class);
	public CustomJdbcTokenStore(DataSource dataSource) {
		super(dataSource);
	}

	@Override
	public OAuth2AccessToken readAccessToken(String tokenValue) {
		OAuth2AccessToken accessToken = null;

		try {
			accessToken = new DefaultOAuth2AccessToken(tokenValue);
		} catch (EmptyResultDataAccessException e) {
			if (log.isInfoEnabled()) {
				log.info("Failed to find access token for token " + tokenValue);
			}
		} catch (IllegalArgumentException e) {
			log.warn("Failed to deserialize access token for " + tokenValue, e);
			removeAccessToken(tokenValue);
		}

		return accessToken;
	}
}

WebSecurityConfig 수정

import java.io.UnsupportedEncodingException;
import java.util.Base64;
import java.util.Base64.Encoder;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;

import lombok.AllArgsConstructor;

@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	//
	private static final Logger log = LoggerFactory.getLogger(WebSecurityConfig.class);
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.csrf().disable()
			.authorizeRequests().anyRequest().authenticated()
			.and()
			.formLogin()
			.and()
			.httpBasic();
		
		makeAuthorizationRequestHeader();
	}

	@Bean
	public PasswordEncoder passwordEncoder() {
		return PasswordEncoderFactories.createDelegatingPasswordEncoder();
	}

	/*
	데이터베이스를 사용하므로 이 코드는 삭제처리합니다.
	@Bean
	public UserDetailsService userDetailsService() {
		PasswordEncoder encoder = passwordEncoder();
		String password = encoder.encode("pass");
		log.debug("PasswordEncoder password : [{}] ", password);					// {bcrypt}$2a$10$q6JJMlG7Q7Gt4n/76ydvp.Vk9pWVcTfCQ4NtWyBzNtWOmefYNw/wO

		InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
		manager.createUser(User.withUsername("user").password(password).roles("USER").build());
		manager.createUser(User.withUsername("admin").password("{noop}pass").roles("USER", "ADMIN").build());
		return manager;
	}
	*/


	@Autowired
	DataSource dataSource;
	private JdbcUserDetailsManager userDetailsManager;

	// Enable jdbc authentication
	@Autowired
	public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
		this.userDetailsManager = auth
		.jdbcAuthentication()
		.dataSource(dataSource)
		.usersByUsernameQuery("select username, password, enabled from oauth_user_details where username = ?")
		.authoritiesByUsernameQuery("select username, authority from oauth_user_authorities where username = ?")
//		.rolePrefix("ROLE_")
		.getUserDetailsService();
//		필요할 경우 아래의 코드들을 주석해제합니다.
//		.userExistsSql("select username from oauth_user_details where username = ?")
//		.createUserSql("insert into oauth_user_details (username, password, enabled) values (?,?,?)")
//		.createAuthoritySql("insert into oauth_user_authorities (username, authority) values (?,?)")
//		.updateUserSql("update oauth_user_details set password = ?, enabled = ? where username = ?")
//		.deleteUserSql("delete from oauth_user_details where username = ?")
//		.deleteUserAuthoritiesSql("delete from oauth_user_authorities where username = ?");
	}
	
	@Bean
	public JdbcUserDetailsManager jdbcUserDetailsManager() throws Exception {
//		JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager();
//		jdbcUserDetailsManager.setDataSource(dataSource);
//		
//		this.userDetailsManager.setUsersByUsernameQuery("select username, password, enabled from oauth_user_details where username = ?");
		this.userDetailsManager.setUserExistsSql("select username from oauth_user_details where username = ?");
		this.userDetailsManager.setCreateUserSql("insert into oauth_user_details (username, password, enabled) values (?,?,?)");
		this.userDetailsManager.setCreateAuthoritySql("insert into oauth_user_authorities (username, authority) values (?,?)");
		this.userDetailsManager.setUpdateUserSql("update oauth_user_details set password = ?, enabled = ? where username = ?");
		this.userDetailsManager.setDeleteUserSql("delete from oauth_user_details where username = ?");
		this.userDetailsManager.setDeleteUserAuthoritiesSql("delete from oauth_user_authorities where username = ?");
//		
//		return jdbcUserDetailsManager;
		return this.userDetailsManager;
	}

	
	private static void makeAuthorizationRequestHeader() {
		String oauthClientId = "client";
		String oauthClientSecret = "secret";

		Encoder encoder = Base64.getEncoder();
		try {
			String toEncodeString = String.format("%s:%s", oauthClientId, oauthClientSecret);
			String authorizationRequestHeader = "Basic " + encoder.encodeToString(toEncodeString.getBytes("UTF-8"));
			log.debug("AuthorizationRequestHeader : [{}] ", authorizationRequestHeader);			// Y2xpZW50OnNlY3JldA==
		} catch (UnsupportedEncodingException e) {
			log.error(e.getMessage(), e);
		}
	}


	/**
	 * Need to configure this support password mode support password grant type
	 * 
	 * @return
	 * @throws Exception
	 */
	@Override
	@Bean
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}
}

 

 

참고 사이트

 

소스코드

소스코드는 여기에서 다운로드 가능합니다.

728x90