Spring Security + OAuth2でシングルサインオンSSO機能を実装してみる

  今日はシングルサインオンSSOのことをサンプルを作りながら説明します。
### SSOとは
  Single Sign Onの略。1つのIDとパスワードを入力して、複数のWebサービスやアプリケーションにログインする仕組み。入力や管理の手間を省き、セキュリティを強化することができる。
  イメージは以下のように参照しましょう。
![SSO流程图.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225740/fcad37ae-d5ae-943d-e059-cd89c166591c.png)


### SS0機能実装
  本記事はサンプルプロジェクトを作って説明します。主にSpring Security Oauth2を活用してSSO機能を実現します。

#### 環境準備

  予めspring security、oauth2.0などの知識を持っていればよいです。

- Eclipse
- Java8
- Spring
- OAuth2.0

#### 親プロジェクトを作る
mavenで親のプロジェクトを作りました。sso-oauth2-demoをネーミングします。
pom.xmlに下記のように配置します。

```xml:pom.xml
<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>

    <groupId>com.example</groupId>
    <artifactId>sso-oauth2-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
        <spring-boot.version>2.1.9.RELEASE</spring-boot.version>
        <spring-security.version>2.1.9.RELEASE</spring-security.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>${spring-security.version}</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

```

#### SSO認証サーバを作る
##### SSO認証サーバ配置
（１）．上記の親プロジェクトの下に子供のモジュールを作って、auth-serverをネーミングします。
auth-serverプロジェクトのpom.xmlに下記のように配置します。

```xml: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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>sso-oauth2-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <artifactId>auth-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>auth-server</name>
    <description>Demo project for Spring Boot</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

```

（２）．application.ymlを作成して下記のように配置します。
ここでは、sso認証サーバーのポート（8300）を指定します。

```yml:application.yml
server:
  port: 8300
  servlet:
    context-path: '/auth'
```

##### SSO認証サーバ実装
（１）．mainクラスを実装
　　　下記のとおり、実装してみよう。
     @EnableResourceServerアノテーションをmainクラスの上に付けます。このアノテーションが重要です。

```java:AuthServerApplication.java
@SpringBootApplication
@EnableResourceServer
public class  AuthServerApplication 
{
    public static void main( String[] args )
    {
     SpringApplication.run(AuthServerApplication.class, args);
    }
}
```

（２）．authクラスを実装

```java:AuthServerConfig.java

/**
 * SSO認証サーバー
 * @author hyman
 */
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Override
    public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.tokenKeyAccess("permitAll")
                .checkTokenAccess("isAuthenticated()");
    }

    @Override
    public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("SampleClientId")
                .secret(passwordEncoder.encode("secret"))
                .authorizedGrantTypes("authorization_code")
                .scopes("user_info")
                .autoApprove(true)
                .redirectUris("http://localhost:8301/login", "http://localhost:8302/login");
    }


}

```

（３）．securityクラスを実装
ここでユーザidとパスワードはソースの中で定義します。
>ユーザーID：hyman
>パスワード：hyman123

```java:SecurityConfig.java

/**
 * security配置
 * ここでユーザidとパスワードはソースの中で定義します。
 * @author hyman
 *
 */
@Configuration
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requestMatchers()
                .antMatchers("/login", "/oauth/authorize")
                .and()
                .authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .permitAll()
                .and().csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("hyman") // ユーザーID：hyman
                .password(passwordEncoder().encode("hyman123"))  // パスワード：hyman123
                .roles("USER");
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

```

（３）．SSO認証APIクラスを実装
SSO認証サーばーへアクセスURLは**http://localhost:8300/auth/user/me** となります。

```java:UserAction.java
@RestController
@RequestMapping(value = "user")
public class UserAction {

    @GetMapping(value = "me")
    public Principal me(Principal principal) {
        System.out.println("アクセスユーザー情報：" + principal);
        return principal;
    }
}

```

ここまで、SSO認証サーバーの実装は終りました。パッケージなどの階層は以下のとおり、参照ください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225740/b66dbcb9-9d0e-bcef-c873-01c8c665c159.png)

#### クライアントアプリケーションを作る
ここから、クライアントアプリケーションを作り始めます。本記事は二つのクライアントアプリケーションをつくります。client-aとclient-bです。
##### client-aアプリケーション
###### client-a配置
　（１）．親のsso-oauth2-demoプロジェクトの下にclient-aモジュールを作成します。
　client-aモジュールのpom.xmlに下記のように配置します。

```xml: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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>sso-oauth2-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <artifactId>client-a</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>client-a</name>
    <description>Demo project for Spring Boot</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
```

（２）．application.xml配置
下記のとおり、client-aを配置します。
ここでclient-aのポートを8301で定義します。

```yml:application.xml
server:
  port: 8301
  servlet:
    session:
      cookie:
        name: CLIENT_A_SESSION

security:
  oauth2:
    client:
      client-id: SampleClientId
      client-secret: secret
      access-token-uri: http://localhost:8300/auth/oauth/token
      user-authorization-uri: http://localhost:8300/auth/oauth/authorize
    resource:
      user-info-uri: http://localhost:8300/auth/user/me # SSO認証サーバーのアドレス

spring:
  thymeleaf:
    cache: false
```

###### client-a実装
（１）．securityクラスの実装
@EnableOAuth2Ssoアノテーションが重要ですから、忘れずに、クラスの上に付けます。

```java:MySecurityConfig.java
@EnableOAuth2Sso
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/**")
            .authorizeRequests()
            .antMatchers("/", "/login**")
            .permitAll()
            .anyRequest()
            .authenticated();
    }

}
```

（２）．画面の実装
resourcesフォルダの下にtemplaesフォルダを作って、画面表示用のindex.htmlファイルを作ります。
下記の内容をindex.htmlに書き込みます。

```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Spring Security SSO</title>
<link rel="stylesheet"
	href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" />
</head>

<body>
<div class="container">
	<div class="col-sm-12">
		<h1>Spring Security SSO クライアントアプリケーションA</h1>
		<a class="btn btn-primary" href="securedPage">Login</a>
	</div>
</div>
</body>
</html>
```

上記と同じの場所でsecuredPage.htmlを作って下記のように実装します。

```html:securedPage.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Spring Security SSO</title>
<link rel="stylesheet"
	href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" />
</head>

<body>
<div class="container">
	<div class="col-sm-12">
		<h1>Secured Page, Client A</h1>
		Welcome, <span th:text="${#authentication.name}">Name</span>
	</div>
</div>
</body>
</html>
```

（２）．Controllerクラス実装

```java:IndexAction.java
@Controller
public class IndexAction {

    @GetMapping(value = "")
    public String index() {
        System.out.println("ClientAのindex画面へ遷移する");
        return "index.html";
    }

    @GetMapping(value = "securedPage")
    public String home() {
        System.out.println("ClientAのsecuredPage画面へ遷移する");
        return "securedPage.html";
    }
}

```

ここまで行くと、client-aは終りました。

##### client-bアプリケーション
client-bアプリケーションの作り方はclient-aと同じですから、ここで略です。
ただ、client-bアプリケーションを作成する際にあるところでclient-bの名称を書き換えるが必要です。
たとえば、client-bのポートを8302に変わってます。

ここでSS0機能実装は全部完了しました。

### SS0機能検証
まず、それぞれ三つのプロジェクトを起動します。

- auth-server
- client-a
- client-b

あと、client-aをアクセスします。urlはhttp://localhost:8301となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225740/a29fc34a-bcd3-c9d2-e119-ad1c9ecad4ae.png)

Loginボタンをクリックします。Login画面でユーザーID(hyman)とパスワード(hyman123)を入力してログインします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225740/9b29dc23-7e86-80ea-0dde-f5b762522a80.png)

ログインができたら、securedPage画面へ行きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225740/d1bb1907-527a-8a57-af0c-d35ef6e88c61.png)

ここまで、client-aを経由してログインできましたので、SSO機能の影響でclient-bをアクセスするときにclient-bのindex画面でLoginボタンをクリックしてユーザーIDとパスワードを入力ずに、ログインできるはずです。

じゃあ、検証してみましょう。

http://localhost:8302/ を経由してclient-bをアクセスします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225740/b19f69aa-1171-caea-3f42-1729e4122da9.png)

Loginボタンをクリックすると、ログイン画面へ遷移しないでclient-bのsecuredPage画面にいきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225740/0e0a849a-71d0-1368-e4c0-30b1770e53b2.png)

だから、SSOの機能はうまくいけました。

### 最後に
最後まで読んでいただき、ありがとうございます。
ソースコードは[こちらで](https://github.com/Hyman1993/Demo-Projects/tree/master/sso-oauth2-demo)ダウンロードできます。

ソースコード：https://github.com/Hyman1993/Demo-Projects/tree/master/sso-oauth2-demo