Motivation of this article
由於公司最近有類似的需求,因此這段時間開始研究 OAuth2 Server。由於不是串接別人的 OAuth2 Server。因此第一步就是先了解 OAuth2 是什麼。
可參考 OAuth 2.0 筆記 (1) 世界觀 系列文章
這邊文章主要是記錄一些學習過程中的內容。剛開始遇到滿多困難,但慢慢地整理出一些基本的設定,希望能幫助到一些有類似需求的人。
The goal
這篇文章的目標是讓大家能簡單快速地用 Spring 建立一個 Authorization server
和一個 Resource server
。
Prerequisites
由於本篇內容都是建立在用 Spring 的基礎上,因此需要了解一些 Spring 的基本觀念。
How to start
git clone https://github.com/teyushen/oauth2.git
Step 1: Basic setup
設定如下:
pom.xml(Authorization Server and Resource Server) :
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
Authorization Server
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("sample").secret("samplesecret")
.authorizedGrantTypes("authorization_code", "refresh_token", "password").scopes("read", "delete");
}
}
@SpringBootApplication
@RestController
@EnableResourceServer
public class AuthorizationApplication {
private static final Log log = LogFactory.getLog(AuthorizationApplication.class);
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext ctx = SpringApplication.run(AuthorizationApplication.class, args);
}
@RequestMapping("/user")
public Principal user(Principal user) {
log.debug("user: " + user.toString());
return user;
}
}
Resource Server
@SpringBootApplication
@RestController
@EnableResourceServer
public class ResourceApplication {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext ctx = SpringApplication.run(ResourceApplication.class, args);
}
private String message = "Hello world!";
@RequestMapping(value = "/", method = RequestMethod.GET)
public Map<String, String> home() {
return Collections.singletonMap("message", message);
}
@RequestMapping(value = "/user", method = RequestMethod.GET)
public Map<String, String> user(Principal user) {
return Collections.singletonMap("message", "user is: " + user.toString());
}
}
application.properties:
由於我們 token
的資訊都是儲存在 Authorization Server
,因此當 client 端
拿著 token
去跟 Resource Server
要資料時,Resource Server
需利用以下設定與 Authorization Server
做溝通。
security.oauth2.resource.user-info-uri=http://localhost:8888/authorization-server/user
Time to test
$ git checkout BasicSetup-inMemory
Authorization Server:
$ cd authorization-server
$ mvn spring-boot:run
取得 code:
http://localhost:8888/authorization-server/oauth/authorize?response_type=code&client_id=sample&redirect_uri=http://example.com
用 code 換 token:
$ curl sample:samplesecret@localhost:8888/authorization-server/oauth/token -d grant_type=authorization_code -d client_id=sample -d redirect_uri=http://example.com -d code=kd1Rg1
(code 換成自己取得的)
Resource Server:
$ cd resource-server
$ mvn spring-boot:run
$ TOKEN=180f45c8-078a-4695-8a36-07773e658232
$ curl -H "Authorization: Bearer $TOKEN" -v localhost:9999/resource-server/
$ curl -H "Authorization: Bearer $TOKEN" -v localhost:9999/resource-server/user
Step 2: Use DB
在 step 1 時,我們是幾 Client 端
的資料存在 Memory 裡頭。Step 2 主要是讓我們將這些資訊該存在 DB 中。
這裡我們用的是 mysql。
CREATE DATABASE oauth2db;
CREATE USER 'oauth2Admin'@'localhost' IDENTIFIED BY 'admin123';
GRANT ALL ON oauth2db.* TO 'oauth2Admin'@'localhost';
設定如下:
pom.xml(Authorzation Server):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Bean
public JdbcClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 這部分改從 DB 撈取處理
clients.jdbc(dataSource).clients(jdbcClientDetailsService());
}
}
讓 JPA 幫我們建立好 Table
@Entity
public class OauthClientDetails extends BaseClientDetails {
private static final long serialVersionUID = -8290003991325296682L;
@Id
@Column(length = 256)
private String clientId;
@Column(length = 256)
private String resourceIds;
@Column(length = 256)
private String clientSecret;
@Column(length = 256)
private String scope;
@Column(length = 256)
private String authorizedGrantTypes;
@Column(length = 256)
private String webServerRedirectUri;
@Column(length = 256)
private String authorities;
@Column(length = 4096)
private String additionalInformation;
@Column(length = 256)
private String autoapprove;
}
application.properties:
spring.jpa.hibernate.ddl-auto=create
spring.datasource.url=jdbc:mysql://localhost:3306/oauth2db
spring.datasource.username=oauth2Admin
spring.datasource.password=admin123
新增資料
insert into oauth_client_details (client_id, client_secret, scope, authorized_grant_types) values ('sample', 'samplesecret', 'read,delete', 'authorization_code,password');
Time to test
$ git checkout BasicSetup-inDB
Authorization Server:
$ cd authorization-server
$ mvn spring-boot:run
取得 code:
http://localhost:8888/authorization-server/oauth/authorize?response_type=code&client_id=sample&redirect_uri=http://example.com
用 code 換 token:
$ curl sample:samplesecret@localhost:8888/authorization-server/oauth/token -d grant_type=authorization_code -d client_id=sample -d redirect_uri=http://example.com -d code=kd1Rg1
(code 換成自己取得的)
Resource Server:
$ cd resource-server
$ mvn spring-boot:run
$ TOKEN=180f45c8-078a-4695-8a36-07773e658232
$ curl -H "Authorization: Bearer $TOKEN" -v localhost:9999/resource-server/
$ curl -H "Authorization: Bearer $TOKEN" -v localhost:9999/resource-server/user
Step 3: Use JWT(JSON Web Token)
這部分我們是利用 JWT
來串接 Authorization Server
與 Resource Server
。
-
優點: 當
Client 端
拿著token
去跟Resource Server
要資訊時, 因為所有資訊都包含在JWT
裡,所以Resource Server
不需要一直去問Authorization Server
這個token
是否有效,以及是否到期等等。 -
缺點: 如果在想要在
token
到期前就終止這個token
的效用,是比較困難的。
要使用 JWT Tokens
的話,在我們的 Authorization Server
要有 JwtTokenStore
。這部分我們只需產生 JwtAccessTokenConverter
即可, 若我們沒有產生 JwtAccessTokenConverter
,Spring 底層預設幫我們產生 InMemoryTokenStore
。
由於經過 Authorization Server
利用 private key 做加簽;我們的 Resource Server
需要拿對應的 public key 去且解開這個 token
。取得 public key 的部分是透過 Authorization Server
中 /oauth/token_key
的 endpoint 去取得。
設定如下:
pom.xml(Authorization Server and Resource Server) :
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
Authorization Server
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private Environment environment;
@Bean
public JwtAccessTokenConverter jwtTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
String pwd = environment.getProperty("keystore.password");
KeyPair keyPair = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), pwd.toCharArray()).getKeyPair("jwt");
converter.setKeyPair(keyPair);
return converter;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager).accessTokenConverter(jwtTokenConverter());
}
// 這部分是讓 Resource Server 可以透過 /oauth/token_key 來取得 public key
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
}
Time to test
$ git checkout useJWT
Authorization Server:
$ cd authorization-server
$ mvn spring-boot:run
取得 code:
http://localhost:8888/authorization-server/oauth/authorize?response_type=code&client_id=sample&redirect_uri=http://example.com
用 code 換 token:
$ curl sample:samplesecret@localhost:8888/authorization-server/oauth/token -d grant_type=authorization_code -d client_id=sample -d redirect_uri=http://example.com -d code=kd1Rg1
(code 換成自己取得的)
Resource Server:
$ cd resource-server
$ mvn spring-boot:run
$ TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTg2NjkxNDQsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiMmU0N2FiZWItYjQzNC00MzlhLWI0NDUtN2U0YTlhNzE4ODlkIiwiY2xpZW50X2lkIjoic2FtcGxlIiwic2NvcGUiOlsicmVhZCIsImRlbGV0ZSJdfQ.YjKEO_p765h1_XJxEbv1jXSqFptMPuIj9kW47enGNXUu3Vxh0er2BGY4pGVYWtgLCALcGgagC_JcB6JWfqo0oGlRJOM5SeHGj-jh2-Zk-bTf7RfXPlRxqfZEg3bCUL19NwCbMvzqm3vVq6CuDYF3UYSOLCphRnhW1rKtj3MVQKHN2MIHVXHW9aPxvc4A3olpkbbTtHzhJ6WzptCqmlfW9buN57mUnRWC6rExDb0aCD_dZXd0EHJdQJCjHaMXLHZpnjWkCvfA_8KDs2lkcGw8Xrk120Cp7fIYMJDLtQ8Q_yEtc26AODmj5uDSnhI8vjnkUKl4w_juXxcyxJhq4hwSgg
$ curl -H "Authorization: Bearer $TOKEN" -v localhost:9999/resource-server/
$ curl -H "Authorization: Bearer $TOKEN" -v localhost:9999/resource-server/user
How to create your own key store
generate keystore:
$ keytool -genkeypair -alias jwt -keyalg RSA -keystore jwt.jks -keypass mySampleKey -storepass mySampleKey -dname "CN=sample, OU=keeplearning, C=Taipei, C=TW"
you can see your java keystore:
$ keytool -list -v -keystore jwt.jks
> Enter keystore password:
> Keystore type: JKS
> Keystore provider: SUN
> our keystore contains 1 entry
> ...