Spring Boot with OAuth2 - 1. OAuth2 Server 에서 구현한 코드를 확장하는 방법에 대해서 설명합니다.
OAuth2 Server 확장
실제 운영을 위해서는 InMemory방식에서 데이터베이스를 이용한 확장이 필요합니다.
본 글에서는 OAuth Server와 데이터베이스 연결을 설명합니다.
- 데이터베이스 연결(MariaDB + myBatis)
- AuthorizationCodeServices 데이터베이스 연결
- ApprovalStore 데이터베이스 연결
- TokenStore 데이터베이스 연결
- ClientDetailsService 데이터베이스 연결
데이터베이스 테이블 생성(MariaDB + MyBatis)
MyBatis를 통하여 MariaDB에 접속하는 예제를 구현합니다. 이 후의 글에서는 본 프로젝트를 기준으로 설명합니다.
데이터베이스 연결 및 설정은 "Spring Boot - MyBatis & log4jdbc 설정"를 참고하십시오.
아래 SQL문을 실행하여 데이터베이스 테이블을 생성합니다.
CREATE TABLE `oauth_access_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` varchar(4096) DEFAULT NULL,
`authentication_id` varchar(256) NOT NULL,
`username` varchar(256) DEFAULT NULL,
`client_id` varchar(256) DEFAULT NULL,
`authentication` varchar(4096) 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,
`redirect_uris` 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,
`auto_approve` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
);
CREATE TABLE `oauth_code` (
`code` varchar(256) DEFAULT NULL,
`authentication` varchar(4096) DEFAULT NULL
);
CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` varchar(4096) DEFAULT NULL,
`authentication` varchar(4096) DEFAULT NULL
);
CREATE TABLE `oauth_user_details` (
`username` varchar(256) NOT NULL,
`password` varchar(256) NOT NULL,
`enabled` tinyint(1) DEFAULT NULL,
`authority` varchar(256) DEFAULT NULL,
`account_non_expired` tinyint(1) DEFAULT NULL,
`account_non_locked` tinyint(1) DEFAULT NULL,
`credentials_non_expired` tinyint(1) DEFAULT NULL,
`name` varchar(256) DEFAULT NULL,
PRIMARY KEY (`username`)
);
아래의 SQL문을 수행하여 기본 데이터를 삽입합니다.
insert into `oauth_client_details`(`client_id`,`resource_ids`,`client_secret`,`scope`,`authorized_grant_types`,`redirect_uris`,`authorities`,`access_token_validity`,`refresh_token_validity`,`additional_information`,`auto_approve`) values ('client',NULL,'{bcrypt}$2a$10$goA9F/Q./Ml8lYvuO1tj6OKA5K6VVM/jmUcdIp1AMzqtXHsuo68/W','read, write, read_profile','authorization_code, implicit, password, client_credentials, refresh_token','http://localhost:9000/callback','ROLE_YOUR_CLIENT',36000,2592000,NULL,'true');
insert into `oauth_user_details`(`username`,`password`,`enabled`,`authority`,`account_non_expired`,`account_non_locked`,`credentials_non_expired`,`name`) values ('admin','{noop}pass',1,'user,admin',1,1,1,'관리자');
insert into `oauth_user_details`(`username`,`password`,`enabled`,`authority`,`account_non_expired`,`account_non_locked`,`credentials_non_expired`,`name`) values ('user','{noop}pass',1,'user',1,1,1,'사용자');
ClientDetailsService 구현
ClientDetailsService 는 OAuth를 사용하려는 제3의 어플리케이션을 관리하는 서비스입니다. 아래의 내용에서는 제3의 어플리케이션을 데이터베이스를 통해 관리하는 방법에 대해 설명합니다.
ClientDetailsService 생성
ClientDetailsService 인터페이스를 확장하여 새로운 어플리케이션관리 서비스를 생성합니다.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.stereotype.Service;
import kr.ejsoft.oauth2.server.dao.OAuthClientDetailsDAO;
import kr.ejsoft.oauth2.server.model.OAuthClientDetails;
@Service("oauthClientDetailsService")
public class OAuthClientDetailsService implements ClientDetailsService {
private static final Logger log = LoggerFactory.getLogger(OAuthClientDetailsService.class);
@Autowired
@Qualifier("oauthClientDetailsDAO")
private OAuthClientDetailsDAO clientDetailsDAO;
@Override
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
log.debug("Client Id : {}", clientId);
OAuthClientDetails client = clientDetailsDAO.getClientById(clientId);
if (client == null) {
log.debug("Client : null");
throw new ClientRegistrationException(clientId);
}
log.debug("Client : {}", client.toString());
return client;
}
}
ClientDetailsDAO 생성
데이터베이스에서 어플리케이션 정보를 읽어오는 코드를 작성합니다.
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import kr.ejsoft.oauth2.server.model.OAuthClientDetails;
import kr.ejsoft.oauth2.server.model.OAuthUserDetails;
@Repository("oauthClientDetailsDAO")
public class OAuthClientDetailsDAO {
@Autowired
private SqlSessionTemplate sqlSession;
public OAuthClientDetails getClientById(String username) {
return sqlSession.selectOne("client.selectClientById", username);
}
}
ClientDetails 생성
어플리케이션 정보를 저장하기위한 ClientDetails 인터페이스를 구현한 model 객체를 생성합니다.
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.provider.ClientDetails;
@SuppressWarnings("serial")
public class OAuthClientDetails implements ClientDetails {
private String clientId;
private String clientSecret;
private String scope;
private String resourceIds;
private boolean secretRequired;
private boolean scoped;
private String authorizedGrantTypes;
private String redirectUris ;
private String authorities;
private String additionalInformation;
private int accessTokenValidity;
private int refreshTokenValidity;
private boolean autoApprove;
@Override
public String getClientId() {
return clientId;
}
@Override
public String getClientSecret() {
return clientSecret;
}
public String getPassword() {
return clientSecret;
}
@Override
public Set<String> getResourceIds() {
return toSet(resourceIds);
}
@Override
public boolean isSecretRequired() {
return secretRequired;
}
@Override
public boolean isScoped() {
return scoped;
}
@Override
public Set<String> getScope() {
return toSet(scope);
}
@Override
public Set<String> getAuthorizedGrantTypes() {
return toSet(authorizedGrantTypes);
}
@Override
public Set<String> getRegisteredRedirectUri() {
return toSet(redirectUris);
}
@Override
public Collection<GrantedAuthority> getAuthorities() {
ArrayList<GrantedAuthority> auth = new ArrayList<GrantedAuthority>();
auth.add(new SimpleGrantedAuthority(authorities));
return auth;
}
@Override
public Integer getAccessTokenValiditySeconds() {
return accessTokenValidity;
}
@Override
public Integer getRefreshTokenValiditySeconds() {
return refreshTokenValidity;
}
@Override
public boolean isAutoApprove(String scope) {
return autoApprove;
}
@Override
public Map<String, Object> getAdditionalInformation() {
// return additionalInformation;
return null;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder
.append("OAuthClientDetails [")
.append("clientId=").append(clientId)
.append(", clientSecret=").append(clientSecret)
.append(", scope=").append(scope)
.append(", resourceIds=").append(resourceIds)
.append(", secretRequired=").append(secretRequired)
.append(", scoped=").append(scoped)
.append(", authorizedGrantTypes=").append(authorizedGrantTypes)
.append(", authorities=").append(authorities)
.append(", redirectUris=").append(redirectUris)
.append(", accessTokenValidity=").append(accessTokenValidity)
.append(", refreshTokenValidity=").append(refreshTokenValidity)
.append(", additionalInformation=").append(additionalInformation)
.append("]");
return builder.toString();
}
private static Set<String> toSet(String data) {
if(data == null) return null;
String[] arr = data.split(",");
if(arr != null && arr.length > 0) {
Set<String> set = new HashSet<>();
// Collections.addAll(set, arr);
for(String e : arr) {
if(e == null || "".equals(e)) continue;
set.add(e.trim());
}
return set;
}
return null;
}
}
client_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">
<mapper namespace="client">
<select id="selectClientById" resultType="kr.ejsoft.oauth2.server.model.OAuthClientDetails">
<![CDATA[
SELECT
*
FROM
oauth_client_details
WHERE
client_id = #{clientId}
]]>
</select>
</mapper>
AuthorizationServerConfig 수정
어플리케이션 관리 서비스에 접속하기 위한 코드를 아래와 같이 수정합니다. inMemory방식은 더 이상 사용하지 않습니다.
@Autowired
@Qualifier("oauthClientDetailsService")
private ClientDetailsService clientDetailsService;
@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")
//// .authorizedGrantTypes("authorization_code", "implicit")
//// .authorizedGrantTypes("authorization_code", "implicit", "password")
//// .authorizedGrantTypes("authorization_code", "implicit", "password", "client_credentials")
// .authorizedGrantTypes("authorization_code", "implicit", "password", "client_credentials", "refresh_token")
// .accessTokenValiditySeconds(120)
// .refreshTokenValiditySeconds(240)
// .scopes("read_profile");
clients.withClientDetails(clientDetailsService);
}
UserDetailsService 구현
UserDetailsService 는 OAuth에 등록된 사용자의 인증정보를 관리하는 서비스입니다. 아래의 내용에서는 데이터베이스를 통해 사용자를 관리하는 방법에 대해 설명합니다.
UserDetailsService 생성
UserDetailsService 인터페이스를 구현한 사용자 관리 서비스를 아래와 같이 생성합니다.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.ejsoft.oauth2.server.dao.OAuthUserDetailsDAO;
import kr.ejsoft.oauth2.server.model.OAuthUserDetails;
@Service("oauthUserDetailsService")
public class OAuthUserDetailsService implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(OAuthUserDetailsService.class);
@Autowired
@Qualifier("oauthUserDetailsDAO")
private OAuthUserDetailsDAO userAuthDAO;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
OAuthUserDetails user = userAuthDAO.getUserById(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
log.debug("User : {}", user.toString());
return user;
}
}
UserDetailsDAO 생성
데이터베이스에서 사용자정보를 읽어오는 객체를 생성합니다.
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import kr.ejsoft.oauth2.server.model.OAuthUserDetails;
@Repository("oauthUserDetailsDAO")
public class OAuthUserDetailsDAO {
@Autowired
private SqlSessionTemplate sqlSession;
public OAuthUserDetails getUserById(String username) {
return sqlSession.selectOne("user.selectUserById", username);
}
}
UserDetails 생성
UserDetails 인터페이스를 구현한 사용자정보 저장객체를 생성합니다.
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.Data;
@SuppressWarnings("serial")
public @Data class OAuthUserDetails implements UserDetails {
private String username;
private String password;
private String authority;
private boolean enabled;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private String name;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
ArrayList<GrantedAuthority> auth = new ArrayList<GrantedAuthority>();
auth.add(new SimpleGrantedAuthority(authority));
return auth;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
public String getName() {
return name;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder
.append("OAuthUserDetails [")
.append("username=").append(username)
.append(", password=").append(password)
.append(", authority=").append(authority)
.append(", enabled=").append(enabled)
.append(", accountNonExpired=").append(accountNonExpired)
.append(", accountNonLocked=").append(accountNonLocked)
.append(", credentialsNonExpired=").append(credentialsNonExpired)
.append(", name=").append(name)
.append("]");
return builder.toString();
}
}
user_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">
<mapper namespace="user">
<select id="selectUserById" resultType="kr.ejsoft.oauth2.server.model.OAuthUserDetails">
<![CDATA[
SELECT
*
FROM
oauth_user_details
WHERE
username = #{username}
]]>
</select>
</mapper>
WebSecurityConfig 수정
기존의 InMemory방식은 더 이상 사용하지 않습니다. 기존 코드를 주석처리하거나 삭제하십시오.
// 아래의 코드들은 더 이상 사용되지 않습니다.
// @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;
// return new OAuthUserDetailsService();
// }
AuthorizationServerConfig 수정
AuthorizationServer 환경설정에 사용자관리 서비스를 등록합니다.
@Autowired
@Qualifier("oauthUserDetailsService")
private UserDetailsService userDetailsService;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
TokenStore, CodeService, ApprovalStore 구현
TokenStore, CodeService, ApprovalStore 는 OAuth의 허가정보를 관리하는 서비스들로 토큰에 대한 만료 및 접근 토큰 발급과 관련된 승인코드 들을 관리합니다.
TokenStoreService 생성
TokenService 인터페이스를 구현하는 접근 토큰, 갱신 토큰을 관리 서비스를 구현합니다.
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.stereotype.Service;
import kr.ejsoft.oauth2.server.dao.OAuthTokenStoreDAO;
import kr.ejsoft.oauth2.server.model.OAuthAccessToken;
import kr.ejsoft.oauth2.server.model.OAuthRefreshToken;
@Service("oauthTokenStoreService")
public class OAuthTokenStoreService implements TokenStore {
private static final Logger log = LoggerFactory.getLogger(OAuthTokenStoreService.class);
@Autowired
@Qualifier("oauthTokenStoreDAO")
private OAuthTokenStoreDAO tokenStoreDAO;
private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
@Override
public OAuth2Authentication readAuthentication(OAuth2AccessToken accessToken) {
return readAuthentication(accessToken.getValue());
}
@Override
public OAuth2Authentication readAuthentication(String token) {
OAuthAccessToken accessToken = tokenStoreDAO.findByTokenId(extractTokenKey(token));
if (accessToken != null) {
return accessToken.getAuthenticationObject();
}
return null;
}
@Override
public void storeAccessToken(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String refreshToken = null;
if (accessToken.getRefreshToken() != null) {
refreshToken = accessToken.getRefreshToken().getValue();
}
if (readAccessToken(accessToken.getValue()) != null) {
this.removeAccessToken(accessToken);
}
OAuthAccessToken cat = new OAuthAccessToken();
cat.setId(UUID.randomUUID().toString() + UUID.randomUUID().toString());
cat.setTokenId(extractTokenKey(accessToken.getValue()));
cat.setTokenObject(accessToken);
cat.setAuthenticationId(authenticationKeyGenerator.extractKey(authentication));
cat.setUsername(authentication.isClientOnly() ? null : authentication.getName());
cat.setClientId(authentication.getOAuth2Request().getClientId());
cat.setAuthenticationObject(authentication);
cat.setRefreshToken(extractTokenKey(refreshToken));
tokenStoreDAO.saveAccessToken(cat);
}
@Override
public OAuth2AccessToken readAccessToken(String tokenValue) {
OAuthAccessToken accessToken = tokenStoreDAO.findByTokenId(extractTokenKey(tokenValue));
if (accessToken != null) {
return accessToken.getTokenObject();
}
return null;
}
@Override
public void removeAccessToken(OAuth2AccessToken oAuth2AccessToken) {
OAuthAccessToken accessToken = tokenStoreDAO.findByTokenId(extractTokenKey(oAuth2AccessToken.getValue()));
if (accessToken != null) {
tokenStoreDAO.deleteAccessToken(accessToken);
}
}
@Override
public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
OAuthRefreshToken crt = new OAuthRefreshToken();
crt.setId(UUID.randomUUID().toString() + UUID.randomUUID().toString());
crt.setTokenId(extractTokenKey(refreshToken.getValue()));
crt.setTokenObject(refreshToken);
crt.setAuthenticationObject(authentication);
tokenStoreDAO.saveRefreshToken(crt);
}
@Override
public OAuth2RefreshToken readRefreshToken(String tokenValue) {
OAuthRefreshToken refreshToken = tokenStoreDAO.findRefreshTokenByTokenId(extractTokenKey(tokenValue));
return refreshToken != null ? refreshToken.getTokenObject() : null;
}
@Override
public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken refreshToken) {
OAuthRefreshToken rtk = tokenStoreDAO.findRefreshTokenByTokenId(extractTokenKey(refreshToken.getValue()));
return rtk != null ? rtk.getAuthenticationObject() : null;
}
@Override
public void removeRefreshToken(OAuth2RefreshToken refreshToken) {
OAuthRefreshToken rtk = tokenStoreDAO.findRefreshTokenByTokenId(extractTokenKey(refreshToken.getValue()));
if (rtk != null) {
tokenStoreDAO.deleteRefreshToken(rtk);
}
}
@Override
public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
OAuthAccessToken token = tokenStoreDAO.findByRefreshToken(extractTokenKey(refreshToken.getValue()));
if (token != null) {
tokenStoreDAO.deleteAccessToken(token);
}
}
@Override
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
OAuth2AccessToken accessToken = null;
String authenticationId = authenticationKeyGenerator.extractKey(authentication);
OAuthAccessToken token = tokenStoreDAO.findByAuthenticationId(authenticationId);
if (token != null) {
accessToken = token.getTokenObject();
if (accessToken != null && !authenticationId.equals(this.authenticationKeyGenerator.extractKey(this.readAuthentication(accessToken)))) {
this.removeAccessToken(accessToken);
this.storeAccessToken(accessToken, authentication);
}
}
return accessToken;
}
@Override
public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) {
Collection<OAuth2AccessToken> tokens = new ArrayList<OAuth2AccessToken>();
List<OAuthAccessToken> result = tokenStoreDAO.findByClientIdAndUsername(clientId, userName);
result.forEach(e -> tokens.add(e.getTokenObject()));
return tokens;
}
@Override
public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {
Collection<OAuth2AccessToken> tokens = new ArrayList<OAuth2AccessToken>();
List<OAuthAccessToken> result = tokenStoreDAO.findByClientId(clientId);
result.forEach(e -> tokens.add(e.getTokenObject()));
return tokens;
}
private String extractTokenKey(String value) {
if (value == null) {
return null;
} else {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException var5) {
throw new IllegalStateException("MD5 algorithm not available. Fatal (should be in the JDK).");
}
try {
byte[] e = digest.digest(value.getBytes("UTF-8"));
return String.format("%032x", new Object[] { new BigInteger(1, e) });
} catch (UnsupportedEncodingException var4) {
throw new IllegalStateException("UTF-8 encoding not available. Fatal (should be in the JDK).");
}
}
}
}
CodeService 생성
RandomValueAuthorizationCodeServices를 상속하는 코드 관리 서비스를 구현합니다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.code.RandomValueAuthorizationCodeServices;
import org.springframework.stereotype.Service;
import kr.ejsoft.oauth2.server.dao.OAuthTokenStoreDAO;
import kr.ejsoft.oauth2.server.model.OAuthCode;
@Service("oauthCodeService")
public class OAuthCodeService extends RandomValueAuthorizationCodeServices {
@Autowired
@Qualifier("oauthTokenStoreDAO")
private OAuthTokenStoreDAO dao;
@Override
protected void store(String code, OAuth2Authentication authentication) {
OAuthCode approval = new OAuthCode();
approval.setCode(code);
approval.setAuthenticationObject(authentication);
dao.save(approval);
}
public OAuth2Authentication remove(String code) {
OAuth2Authentication authentication = null;
try {
OAuthCode oauthcode = dao.findByCode(code);
authentication = oauthcode != null ? oauthcode.getAuthenticationObject() : null;
} catch (Exception e) {
return null;
}
if (authentication != null) {
dao.delete(code);
}
return authentication;
}
}
ApprovalService 생성
ApprovalStore 인터페이스를 구현하는 승인 관리 서비스를 구현합니다.
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import static org.springframework.security.oauth2.provider.approval.Approval.ApprovalStatus.APPROVED;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.oauth2.provider.approval.Approval;
import org.springframework.security.oauth2.provider.approval.Approval.ApprovalStatus;
import org.springframework.security.oauth2.provider.approval.ApprovalStore;
import org.springframework.stereotype.Service;
import kr.ejsoft.oauth2.server.dao.OAuthTokenStoreDAO;
import kr.ejsoft.oauth2.server.model.OAuthApproval;
@Service("oauthApprovalStoreService")
public class OAuthApprovalStoreService implements ApprovalStore {
private static final Logger log = LoggerFactory.getLogger(OAuthTokenStoreService.class);
@Autowired
@Qualifier("oauthTokenStoreDAO")
private OAuthTokenStoreDAO dao;
private boolean handleRevocationsAsExpiry = false;
public void setHandleRevocationsAsExpiry(boolean handleRevocationsAsExpiry) {
this.handleRevocationsAsExpiry = handleRevocationsAsExpiry;
}
@Override
public boolean addApprovals(Collection<Approval> approvals) {
if (log.isDebugEnabled()) {
log.debug(String.format("adding approvals: [%s]", approvals));
}
boolean success = true;
for (Approval approval : approvals) {
OAuthApproval appr = new OAuthApproval();
appr.setExpiresAt(approval.getExpiresAt().getTime());
appr.setStatus((approval.getStatus() == null ? APPROVED : approval.getStatus()).toString());
appr.setLastModifiedAt(approval.getLastUpdatedAt().getTime());
appr.setUserId(approval.getUserId());
appr.setClientId(approval.getClientId());
appr.setScope(approval.getScope());
if (!refreshApproval(appr)) {
if (!saveApproval(appr)) {
success = false;
}
}
}
return success;
}
private boolean refreshApproval(final OAuthApproval approval) {
if (log.isDebugEnabled()) {
log.debug(String.format("refreshing approval: [%s]", approval));
}
int refreshed = dao.refreshApproval(approval);
if (refreshed != 1) {
return false;
}
return true;
}
private boolean saveApproval(final OAuthApproval approval) {
if (log.isDebugEnabled()) {
log.debug(String.format("refreshing approval: [%s]", approval));
}
int refreshed = dao.saveApproval(approval);
if (refreshed != 1) {
return false;
}
return true;
}
@Override
public boolean revokeApprovals(Collection<Approval> approvals) {
if (log.isDebugEnabled()) {
log.debug(String.format("Revoking approvals: [%s]", approvals));
}
boolean success = true;
for (final Approval approval : approvals) {
if (handleRevocationsAsExpiry) {
OAuthApproval appr = new OAuthApproval();
appr.setExpiresAt(System.currentTimeMillis());
appr.setUserId(approval.getUserId());
appr.setClientId(approval.getClientId());
appr.setScope(approval.getScope());
int refreshed = dao.expireApproval(appr);
if (refreshed != 1) {
success = false;
}
}
else {
OAuthApproval appr = new OAuthApproval();
appr.setUserId(approval.getUserId());
appr.setClientId(approval.getClientId());
appr.setScope(approval.getScope());
int refreshed = dao.deleteApproval(appr);
if (refreshed != 1) {
success = false;
}
}
}
return success;
}
@Override
public Collection<Approval> getApprovals(String userId, String clientId) {
List<Approval> coll = null;
List<OAuthApproval> list = dao.findByUserIdAndClientId(userId, clientId);
if(list != null && list.size() > 0) {
coll = new ArrayList<Approval>();
for(OAuthApproval approval : list) {
String userId1 = approval.getUserId();
String clientId1 = approval.getClientId();
String scope = approval.getScope();
Date expiresAt = new Date(approval.getExpiresAt().getTime());
String status = approval.getStatus();
Date lastUpdatedAt = new Date(approval.getLastModifiedAt().getTime());
coll.add(new Approval(userId1, clientId1, scope, expiresAt, ApprovalStatus.valueOf(status), lastUpdatedAt));
}
}
return coll;
}
}
AccessToken 생성
접근 토큰을 저장하는 객체를 생성합니다. 데이터베이스 저장시 데이터는 객체를 직렬화한 상태로 저장합니다.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import kr.ejsoft.oauth2.server.util.SerializableObjectConverter;
public class OAuthAccessToken {
private String id;
private String tokenId;
private String token;
private String authenticationId;
private String username;
private String clientId;
private String authentication;
private String refreshToken;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTokenId() {
return tokenId;
}
public void setTokenId(String tokenId) {
this.tokenId = tokenId;
}
public String getToken() {
return token;
}
public OAuth2AccessToken getTokenObject() {
return token != null ? SerializableObjectConverter.deserializeAccessToken(token) : null;
}
public String getAuthenticationId() {
return authenticationId;
}
public void setAuthenticationId(String authenticationId) {
this.authenticationId = authenticationId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public void setTokenObject(OAuth2AccessToken token) {
this.token = SerializableObjectConverter.serializeAccessToken(token);
}
public void setToken(String token) {
this.token = token;
}
public OAuth2Authentication getAuthenticationObject() {
return SerializableObjectConverter.deserializeAuthentication(authentication);
}
public void setAuthenticationObject(OAuth2Authentication authentication) {
this.authentication = SerializableObjectConverter.serializeAuthentication(authentication);
}
public String getAuthentication() {
return authentication;
}
public void setAuthentication(String authentication) {
this.authentication = authentication;
}
}
RefreshToken 생성
갱신 토큰을 저장하는 객체를 생성합니다.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import kr.ejsoft.oauth2.server.util.SerializableObjectConverter;
public class OAuthRefreshToken {
private static final Logger log = LoggerFactory.getLogger(OAuthRefreshToken.class);
private String id;
private String tokenId;
private String token;
private String authentication;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTokenId() {
return tokenId;
}
public void setTokenId(String tokenId) {
this.tokenId = tokenId;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public OAuth2RefreshToken getTokenObject() {
return token != null ? SerializableObjectConverter.deserializeRefreshToken(token) : null;
}
public void setTokenObject(OAuth2RefreshToken token) {
this.token = SerializableObjectConverter.serializeRefreshToken(token);
}
public OAuth2Authentication getAuthenticationObject() {
return SerializableObjectConverter.deserializeAuthentication(authentication);
}
public void setAuthenticationObject(OAuth2Authentication authentication) {
this.authentication = SerializableObjectConverter.serializeAuthentication(authentication);
}
public String getAuthentication() {
return this.authentication;
}
public void setAuthentication(String authentication) {
this.authentication = authentication;
}
}
Approval 생성
승인정보를 저장하는 객체를 생성합니다.
import java.sql.Timestamp;
public class OAuthApproval {
private String userId;
private String clientId;
private String scope;
private String status;
private Timestamp expiresAt;
private Timestamp lastModifiedAt;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Timestamp getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(Timestamp expiresAt) {
this.expiresAt = expiresAt;
}
public void setExpiresAt(long time) {
this.expiresAt = new Timestamp(time);
}
public Timestamp getLastModifiedAt() {
return lastModifiedAt;
}
public void setLastModifiedAt(Timestamp lastModifiedAt) {
this.lastModifiedAt = lastModifiedAt;
}
public void setLastModifiedAt(long time) {
this.lastModifiedAt = new Timestamp(time);
}
}
Code 생성
코드 정보를 저장하는 객체를 생성합니다.
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import kr.ejsoft.oauth2.server.util.SerializableObjectConverter;
public class OAuthCode {
private String code;
private String authentication;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getAuthentication() {
return authentication;
}
public void setAuthentication(String authentication) {
this.authentication = authentication;
}
public OAuth2Authentication getAuthenticationObject() {
return SerializableObjectConverter.deserializeAuthentication(authentication);
}
public void setAuthenticationObject(OAuth2Authentication authentication) {
this.authentication = SerializableObjectConverter.serializeAuthentication(authentication);
}
}
TokenStoreDAO 생성
Token, Approval, Code를 데이터베이스에 질의하는 객체를 생성합니다.
import java.util.List;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import kr.ejsoft.oauth2.server.model.OAuthAccessToken;
import kr.ejsoft.oauth2.server.model.OAuthApproval;
import kr.ejsoft.oauth2.server.model.OAuthCode;
import kr.ejsoft.oauth2.server.model.OAuthRefreshToken;
@Repository("oauthTokenStoreDAO")
public class OAuthTokenStoreDAO{
@Autowired
private SqlSessionTemplate sqlSession;
public List<OAuthAccessToken> findByClientId(String clientId) {
return sqlSession.selectList("oauth.findTokenByClientId", clientId);
}
public List<OAuthAccessToken> findByClientIdAndUsername(String clientId, String username) {
OAuthAccessToken token = new OAuthAccessToken();
token.setClientId(clientId);
token.setUsername(username);
return sqlSession.selectList("oauth.findTokenByClientIdAndUsername", token);
}
public OAuthAccessToken findByTokenId(String tokenId) {
return sqlSession.selectOne("oauth.findTokenByTokenId", tokenId);
}
public OAuthAccessToken findByRefreshToken(String refreshToken) {
return sqlSession.selectOne("oauth.findTokenByRefreshToken", refreshToken);
}
public OAuthAccessToken findByAuthenticationId(String authenticationId) {
return sqlSession.selectOne("oauth.findTokenByAuthenticationId", authenticationId);
}
public int saveAccessToken(OAuthAccessToken oauthAccessToken) {
return sqlSession.insert("oauth.saveAccessToken", oauthAccessToken);
}
public int deleteAccessToken(OAuthAccessToken oauthAccessToken) {
return sqlSession.delete("oauth.deleteAccessToken", oauthAccessToken);
}
public OAuthRefreshToken findRefreshTokenByTokenId(String tokenId) {
return sqlSession.selectOne("oauth.findRefreshTokenByTokenId", tokenId);
}
public int saveRefreshToken(OAuthRefreshToken refreshToken) {
return sqlSession.insert("oauth.saveRefreshToken", refreshToken);
}
public int deleteRefreshToken(OAuthRefreshToken refreshToken) {
return sqlSession.delete("oauth.deleteRefreshToken", refreshToken);
}
public OAuthCode findByCode(String code) {
return sqlSession.selectOne("oauth.findAuthenticationByCode", code);
}
public int save(OAuthCode approval) {
return sqlSession.insert("oauth.saveCode", approval);
}
public int delete(String code) {
return sqlSession.delete("oauth.deleteCode", code);
}
public List<OAuthApproval> findByUserIdAndClientId(String userId, String clientId) {
OAuthApproval approval = new OAuthApproval();
approval.setClientId(clientId);
approval.setUserId(userId);
return sqlSession.selectList("oauth.findByUserIdAndClientId", approval);
}
public int saveApproval(OAuthApproval approval) {
return sqlSession.insert("oauth.saveApproval", approval);
}
public int refreshApproval(OAuthApproval approval) {
return sqlSession.update("oauth.refreshApproval", approval);
}
public int expireApproval(OAuthApproval approval) {
return sqlSession.update("oauth.expireApproval", approval);
}
public int deleteApproval(OAuthApproval approval) {
return sqlSession.delete("oauth.deleteApproval", approval);
}
}
SerializableObjectConverter 생성
각종 객체를 직렬화/반직렬화하는 코드를 생성합니다.
import java.util.Base64;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.common.util.SerializationUtils;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
public class SerializableObjectConverter {
public static String serializeAccessToken(OAuth2AccessToken object) {
try {
byte[] bytes = SerializationUtils.serialize(object);
return Base64.getEncoder().encodeToString(bytes);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
public static OAuth2AccessToken deserializeAccessToken(String encodedObject) {
try {
byte[] bytes = Base64.getDecoder().decode(encodedObject);
return (OAuth2AccessToken) SerializationUtils.deserialize(bytes);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
public static String serializeRefreshToken(OAuth2RefreshToken object) {
try {
byte[] bytes = SerializationUtils.serialize(object);
return Base64.getEncoder().encodeToString(bytes);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
public static OAuth2RefreshToken deserializeRefreshToken(String encodedObject) {
try {
byte[] bytes = Base64.getDecoder().decode(encodedObject);
return (OAuth2RefreshToken) SerializationUtils.deserialize(bytes);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
public static String serializeAuthentication(OAuth2Authentication object) {
try {
byte[] bytes = SerializationUtils.serialize(object);
return Base64.getEncoder().encodeToString(bytes);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
public static OAuth2Authentication deserializeAuthentication(String encodedObject) {
try {
byte[] bytes = Base64.getDecoder().decode(encodedObject);
return (OAuth2Authentication) SerializationUtils.deserialize(bytes);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
}
oauth_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">
<mapper namespace="oauth">
<select id="findTokenByClientId" resultType="kr.ejsoft.oauth2.server.model.OAuthAccessToken">
<![CDATA[
SELECT
token_id, token, authentication_id, username, client_id, authentication, refresh_token
FROM
oauth_access_token
WHERE
client_id = #{clientId}
]]>
</select>
<select id="findTokenByClientIdAndUsername" resultType="kr.ejsoft.oauth2.server.model.OAuthAccessToken">
<![CDATA[
SELECT
token_id, token, authentication_id, username, client_id, authentication, refresh_token
FROM
oauth_access_token
WHERE
client_id = #{clientId}
AND username = #{username}
]]>
</select>
<select id="findTokenByTokenId" resultType="kr.ejsoft.oauth2.server.model.OAuthAccessToken">
<![CDATA[
SELECT
token_id, token, authentication_id, username, client_id, authentication, refresh_token
FROM
oauth_access_token
WHERE
token_id = #{tokenId}
]]>
</select>
<select id="findTokenByRefreshToken" resultType="kr.ejsoft.oauth2.server.model.OAuthAccessToken">
<![CDATA[
SELECT
token_id, token, authentication_id, username, client_id, authentication, refresh_token
FROM
oauth_access_token
WHERE
refresh_token = #{refresToken}
]]>
</select>
<select id="findTokenByAuthenticationId" resultType="kr.ejsoft.oauth2.server.model.OAuthAccessToken">
<![CDATA[
SELECT
token_id, token, authentication_id, username, client_id, authentication, refresh_token
FROM
oauth_access_token
WHERE
authentication_id = #{authenticationId}
]]>
</select>
<insert id="saveAccessToken" keyProperty="token_id">
insert into oauth_access_token
(token_id, token, authentication_id, username, client_id, authentication, refresh_token)
values
(#{tokenId}, #{token}, #{authenticationId}, #{username}, #{clientId}, #{authentication}, #{refreshToken})
</insert>
<delete id="deleteAccessToken">
delete from oauth_access_token where token_id = #{tokenId}
</delete>
<select id="findRefreshTokenByTokenId" resultType="kr.ejsoft.oauth2.server.model.OAuthRefreshToken">
<![CDATA[
SELECT
*
FROM
oauth_refresh_token
WHERE
token_id = #{tokenId}
]]>
</select>
<insert id="saveRefreshToken" keyProperty="token_id">
insert into oauth_refresh_token
(token_id, token, authentication)
values
(#{tokenId}, #{token}, #{authentication})
</insert>
<delete id="deleteRefreshToken">
delete from oauth_refresh_token where token_id = #{tokenId}
</delete>
<select id="findAuthenticationByCode" resultType="kr.ejsoft.oauth2.server.model.OAuthCode">
<![CDATA[
select code, authentication from oauth_code where code = #{code}
]]>
</select>
<insert id="saveCode" keyProperty="token_id">
insert into oauth_code (code, authentication) values (#{code}, #{authentication})
</insert>
<delete id="deleteCode">
delete from oauth_code where code = #{code}
</delete>
<select id="findByUserIdAndClientId" resultType="kr.ejsoft.oauth2.server.model.OAuthApproval">
<![CDATA[
SELECT
expiresAt, status, lastModifiedAt, userId, clientId, scope
FROM
oauth_approvals
WHERE 1=1
AND userId = #{userId}
AND clientId = #{clientId}
]]>
</select>
<insert id="saveApproval" keyProperty="token_id">
insert into oauth_approvals
(expiresAt, status, lastModifiedAt, userId, clientId, scope)
values
(#{expiresAt, javaType=java.sql.Timestamp, jdbcType=TIMESTAMP},
#{status},
#{lastModifiedAt, javaType=java.sql.Timestamp, jdbcType=TIMESTAMP},
#{userId},
#{clientId},
#{scope})
</insert>
<update id="refreshApproval">
UPDATE oauth_approvals
SET expiresAt=#{expiresAt, javaType=java.sql.Timestamp, jdbcType=TIMESTAMP},
status=#{status},
lastModifiedAt=#{lastModifiedAt, javaType=java.sql.Timestamp, jdbcType=TIMESTAMP}
WHERE 1=1
AND userId = #{userId}
AND clientId = #{clientId}
AND scope = #{scope}
</update>
<update id="expireApproval">
UPDATE oauth_approvals
SET expiresAt = #{expiresAt}
WHERE 1=1
AND userId = #{userId}
AND clientId = #{clientId}
AND scope = #{scope}
</update>
<delete id="deleteApproval">
DELETE FROM oauth_approvals
WHERE 1=1
AND userId = #{userId}
AND clientId = #{clientId}
AND scope = #{scope}
</delete>
</mapper>
AuthorizationServerConfig 수정
생성한 서비스들을 AuthorizationServerConfig에 등록합니다.
@Autowired
@Qualifier("oauthTokenStoreService")
private TokenStore tokenStoreService;
@Autowired
@Qualifier("oauthApprovalStoreService")
private ApprovalStore approvalStore;
@Autowired
@Qualifier("oauthCodeService")
private AuthorizationCodeServices authorizationCodeServices;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.tokenStore(tokenStoreService)
.approvalStore(approvalStore)
.authorizationCodeServices(authorizationCodeServices)
.userDetailsService(userDetailsService);
}
참고사이트
- Spring Security - 인증 절차 인터페이스 구현 (1) UserDetailsService, UserDetails
https://to-dy.tistory.com/86?category=720806 - Spring Security - 인증 절차 인터페이스 구현 (2) AuthenticationProvider
https://to-dy.tistory.com/87?category=720806 - Spring Boot, OAuth 2.0 서버 구현하기(with Redis)
https://jsonobject.tistory.com/363 - Spring Boot로 만드는 OAuth2 시스템 5
https://brunch.co.kr/@sbcoba/5 - OAuthTokenStore.groovy
https://gist.github.com/jsureshchandra/3184012 - spring-projects/spring-security-oauth
https://github.com/spring-projects/spring-security-oauth/tree/master/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider - Springboo2+springcloud+security-oauth2+redis for single sign-on
http://www.programmersought.com/article/4375850051/ - How to create a custom Token Store for Spring-securtiy-oauth2 | OAuth Part 2 https://blog.couchbase.com/custom-token-store-spring-securtiy-oauth2/
소스코드
소스코드는 여기에서 다운로드 가능합니다.
'Development > Spring Framework, Security, OAuth2' 카테고리의 다른 글
Spring Boot with OAuth2 - 6. OAuth2 Server 접근/권한 제어 (0) | 2019.11.27 |
---|---|
Spring Boot with OAuth2 - 5. OAuth2 Resource Server 분리 (0) | 2019.11.27 |
Spring Boot with OAuth2 - 4. OAuth2 Server 확장 3 - JwtTokenStore (0) | 2019.11.26 |
Spring Boot with OAuth2 - 2. OAuth2 Server 확장 1 - Hibernate + MariaDB 연결 (0) | 2019.11.20 |
Spring Boot with OAuth2 - 1. OAuth2 Server 구현 (0) | 2019.11.18 |