[Java] Spring Security OAuth2

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 ServerResource 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
> ...

Reference

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *