vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

Angularのセキュリティ対策について調べてみる その3

はじめに

続きです。

今回はサニタイズせずに値を渡す方法、CSRF (XSRF) などを追いかけてみます。

Trusting safe values

以前Templateを使ってHTMLタグを挿入する場合、自動でサニタイズされることを学びました。

では信頼したHTMLなどの値を、サニタイズせずにページに反映するにはどうすべきか。

DomSanitizerのbypassSecurityTrustHtmlやbypassSecurityTrustUrlを使用します。

  • bypassSecurityTrustHtml
  • bypassSecurityTrustScript
  • bypassSecurityTrustStyle
  • bypassSecurityTrustUrl
  • bypassSecurityTrustResourceUrl

bypass-security.component.ts

import { Component, OnInit } from '@angular/core';
import { DomSanitizer, SafeHtml, SafeUrl } from '@angular/platform-browser';

@Component({
  selector: 'app-bypass-security',
  templateUrl: './bypass-security.component.html',
  styleUrls: ['./bypass-security.component.css']
})
export class BypassSecurityComponent implements OnInit {
  // 元の文字列. そのままTemplateに渡すとサニタイズされる.
  private dangerousHtml: string;
  private dangerousUrl: string; 

  // 信頼された値としてサニタイズされずに表示される.
  private trustedHtml: SafeHtml;
  private trustedUrl: SafeUrl;

  constructor(private sanitizer: DomSanitizer) { }

  ngOnInit() {
    this.dangerousHtml = '< button onclick="alert(0)">Click me!< /button>';
    this.dangerousUrl = 'javascript:alert("Hi there")';
    
    this.trustedHtml = this.sanitizer.bypassSecurityTrustHtml(this.dangerousHtml);
    this.trustedUrl = this.sanitizer.bypassSecurityTrustUrl(this.dangerousUrl);
  }
}

bypass-security.component.html

< h4>An untrusted HTML:< /h4>
< iframe [srcdoc]='dangerousHtml'>< /iframe>
< h4>An trusted HTML:< /h4>
< iframe [srcdoc]='trustedHtml'>< /iframe>
< h4>An untrusted URL:< /h4>
< p>< a [href]="dangerousUrl">Click me< /a>< /p>
< h4>A trusted URL:< /h4>
< p>< a [href]="trustedUrl">Click me< /a>< /p>

これを実行すると以下のようになります。

f:id:mslGt:20170727071523j:plain

trustedHtml、trustedUrlの値がサニタイズされずに表示されているのが確認できます。

{{trustedHtml}}

さて、以前確認していたように、 < p>{{trustedHtml}}< /p> とするとどうなるでしょうか。

bypass-security.component.html

〜省略〜
< p>{{trustedHtml}}< /p>

結果は下記のようなメッセージが表示されます。

SafeValue must use [property]=binding: < button onclick="alert(0)">Click me!< /button> (see http://g.co/ng/security#xss)

つまり、{{trustedHtml}}として表示した場合は値を信頼している・していないに関係なくHTMLが反映されず、
文字列でHTMLのソースを渡して表示したい場合は、iframeなどを使って信頼した値として渡してね、ということのようです。

HTTP-level vulnerabilities

さて、ここからはサーバー側のお話となります。

当然サーバー側の対応はAngularではなく(今回は)Spring bootでの対応となるわけですが、
Cross-site request forgery (CSRF or XSRF) と Cross-site script inclusion (XSSI) についてはサーバー側の処理を補助する仕組みがあるとのことです。

エラーページ

完全に話がずれてしまいますが、引っかかったので書き残しておきます。

前回のCSPの話の中で、「WebSecurityConfig」というクラスを作ってURLごとに閲覧権限を付与していましたが、 例えば必要な権限を持たない状態でページにアクセスすると、Spring Securityがデフォルトで提供する(と思う)エラーページが表示されます。

これを自分で用意する、という場合についてです。

WebSecurityConfig.java

〜省略〜
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser("user1")
                .password("1234")
                .roles("USER");
        auth
                .inMemoryAuthentication()
                .withUser("user2")
                .password("1234")
                .roles("ADMIN");
    }
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/menulist").hasAuthority("ADMIN") 
                .and().formLogin()
                .and().exceptionHandling()
                .accessDeniedPage("/accessdenied.html");
    }
}
  • 上記ではLogin画面はデフォルトのものが表示されますが、「formLogin()」のあとに「.loginPage(“/login”)」のようにログインページのパスを指定することで、
    (ルーティングはControllerクラスで実行)自分で用意したログインフォームを表示できます。
  • 上記では未ログイン時はまずログインフォームが表示され、ログイン後にAdmin権限を持っていない場合(つまりuser1でログインした場合)は「accessDeniedPage()」で指定したページが表示されます。

失敗1: ログインフォームを表示していない

WebSecurityConfig.java

〜省略〜
@Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/menulist").hasAuthority("ADMIN") 

                .and().exceptionHandling()
                .accessDeniedPage("/accessdenied.html");
    }
  }

上記のように、ログインフォームを表示する「.and().formLogin()」を入れずにaccessDeniedPageを指定したところ、
デフォルトのエラーページしか表示されませんでしたorz (未ログイン状態でアクセスして確認)

実際の使用ではまずログインさせるであろう、ということで、
特に問題は無いのですが、ちゃんとドキュメント等を読まずに適当にやると失敗する、という例でした。

失敗2: AccessDeniedPageの表示にAngularのルーティングを使おうとした

WebSecurityConfig.java

〜省略〜
@Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/menulist").hasAuthority("ADMIN") 
                .and().formLogin()
                .and().exceptionHandling()
                .accessDeniedPage("/accessdenied");
    }
  }

以前トライしたように、アクセス拒否のエラーでもAngularのルーティング機能を使ってページを表示しようとしたところ、
Admin権限でないと見られないはずの「/menulist」のページまで一般ユーザー権限のユーザーが閲覧できるようになってしまいましたorz

Spring boot側で「/accessdenied」に切り替える前に、Angularで「/menulist」を表示してしまうため、
ということなのでしょうか。

まぁそもそもエラーページは通常起こり得ない異常が発生したときに表示するものだと考えれば、
通常通り処理しようとするな、ということでもありますorz

ログインやログインフォームなどについては別途詳しく追いかけてみたいと思います。

Cross-site request forgery (CSRF or XSRF)

さて、元の話題に戻ります。

CSRFの説明は安全なウェブサイトの作り方の1.6などを参照していただけたらと思いますが、
簡単にまとめると

ユーザーが ①のページ を開いてログインを行い、ログインした状態のまま別のタブなどで ②罠の仕掛けられたページ を開いた場合に、
②のページから①のページに対し、ログイン済みの情報を利用して不正なリクエストを送ることができてしまう、という問題です。

この対策として、以下のような方法があります。(防ぎたいリクエストの種類にもよりますが)

  1. サーバー側とクライアント側でセッションID、乱数などのトークンを共有・確認する
  2. ユーザーに再ログインを求める
  3. CAPTCHA機能を利用する
  4. Referer情報から正しいURLから遷移したかを確認する

2、3はパスワードなどの変更や入出金などを行う時に見かけるように思います。

で、今回は1について追いかけてみることにします。

Angular

下記によると、AngularのHttpClient(後述しますが、正確にはHttpClientXsrfModule)にはCSRF(XSRF)の対策としてトークンを発行する機能があります。

クッキーの値は同じドメインのコードからしか読めないことから、そのリクエストが正しくユーザーからのものであり、
攻撃者からではないことが確認できます。

とのことです。

確認してみましょう。

Postリクエストを送信してみる(失敗)

ということで、Postリクエストを送信してみてどのようなHTTPヘッダにデータが渡されているかを見てみることにします。

まずはAngular側から。

post-page.component.ts

import { Component, OnInit } from '@angular/core';
import { Http } from '@angular/http';

@Component({
  selector: 'app-post-page',
  templateUrl: './post-page.component.html',
  styleUrls: ['./post-page.component.css']
})
export class PostPageComponent implements OnInit {

  constructor(private http_: Http) { }

  ngOnInit() {
    this.http_.post("/postsample", {id: 0, createdDate: "2017年6月24日", title: "ニュースだよ", article: "中身です"})
      .subscribe();
  }
}
  • 実際にはせっかく作成したinterface(top-news.ts)を利用したり、ボタン操作によって送信を行いたいところですが、
    今回は確認作業の簡略化のためngOnInitで実行しています。
  • Http.postの戻り値は「Observable」であり、実行には「subscribe」が必要です。

あとは任意のページからpost-page.componentが表示・実行されるようにしておけばOKです。

次にSpring bootです。
RestControllerクラスにPostメソッド用の処理を追加します。

import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;

@RestController
public class CrudSampleController {
    
    〜省略〜

    @PostMapping("/postsample")
    public TopNews postsample(@RequestBody TopNews news){
        System.out.println(news.article);
        return news;
    }
}
  • こちら側も今回は確認のため、特に何も処理を行わずに返しています。

さてこれで準備はOKです。
確認してみましょう!!

404エラーになりましたorz

Postメソッドの送信先が見つからない原因

ズバリSpring SecurityのCSRF対策のための機能です/(^o^)\

そのため、下記のようにCSRF対策を無効にすると正しくアクセスできます。

WebSecurityConfig.java

import org.springframework.context.annotation.Configuration;
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;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

〜省略〜

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/menulist").hasAuthority("ADMIN")
                .and().formLogin().loginPage("/login")
                .and().exceptionHandling()
                .accessDeniedPage("/accessdenied.html");


       // CSRF対策を無効にする.
       http
                .csrf()
                .disable();

    }
}

これで不正アクセスしようとする攻撃者の気持ちが体験できたわけですね/(^o^)\

…というところでここからが本題です。

Angularでトークンを発行する

HttpClientXsrfModule を使って、CSRF対策のためのトークンを発行できます。

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { HttpClientXsrfModule } from '@angular/common/http'

import { AppComponent } from './app.component';
import { PostPageComponent } from './post-page/post-page.component';

@NgModule({
  declarations: [
    AppComponent,
    PostPageComponent
  ],
  imports: [
    BrowserModule,
    HttpModule,
    
    HttpClientXsrfModule.withOptions({
      cookieName: 'My-Xsrf-Cookie',
      headerName: 'My-Xsrf-Header',
    }),
    
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

この後後述のSpring boot側でAngularのトークンを使う設定を行うと、
Postリクエスト送信時のRequestHeaderで「XSRF-TOKEN」(Cookie)、「X-XSRF-TOKEN」というトークンが付与されます。

ここで一点注意があります。

HttpClient - Angular などでは「HttpClientXsrfModule.withConfig」としてcookieNameなどを設定していますが、
「withOptions」でないとエラーになります。

仕様変更があったのでしょうか。
HttpClientXsrfModule - Angularでは「withOptions」になっていますね。

ドキュメントの修正依頼ってプルリクになるのでしょうか。。。?

Spring bootでAngularのトークンを使うようにする

Spring bootでAngularのトークンを使用し、Postリクエストを有効にします。

WebSecurityConfig

〜省略〜
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser("user1")
                .password("1234")
                .roles("USER");
        auth
                .inMemoryAuthentication()
                .withUser("user2")
                .password("1234")
                .roles("ADMIN");
    }
    @Override
    public void configure(HttpSecurity http) throws Exception {
        
        http
                .httpBasic().and()
                .authorizeRequests()
                .antMatchers("/").permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .and().csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
                
    }
}
  • トークンの指定は「csrf()」以降です。
  • Angularのトークンを使用するためには、CookieのHttpOnlyはFalseにしておく必要があるようです。
    「.csrfTokenRepository(new CookieCsrfTokenRepository)」のようにすると403エラーが発生しました。
  • 上記ではPostリクエストを送信する前にログインが必要で、「.anyRequest().authenticated()」を、
    「.anyRequest().permitAll()」にすればいけるのでは?と思いましたが、403エラーが発生しましたorz

おわりに

CSRF対策についてはとりあえずPostリクエストを送信できるようになりましたが、
現状だと全てのページでログインを行うか、CSRF対策を無効にする必要が出てきてしまいます。

次回は特定のページでのみCSRF対策を有効にし、ログインしていなくてもPost送信ができるようにする方法を調べてみます。

また、Angularのドキュメントの最後の項目、Cross-site script inclusion (XSSI)についても追いかけてみます。

参照

BypassSecurity

Spring Security

CSRF (XSRF)

Angularのセキュリティ対策について調べてみる その2

はじめに

前回の続きです。

今回はContent security policy(CSP)、Offline template compilerについて追いかけてみました。

Content security policy(CSP)について

CSPは、画像やJavascriptなどのファイルを読み込むことのできるサイト(ドメイン)を指定するためのものです。

もしこれらのファイルを同一ドメインのみから読み込む場合、CSPで同一ドメインのみに制限をかけることで、
万一クロスサイトスクリプティング(XSS)の対応漏れがあったとしても、
(外部のスクリプトファイルが無効なため)危険を回避できる可能性が高まります。

で、このCSPを有効にするには、Webサーバーから Content-Security-Policy HTTP ヘッダを返す必要があります。

今回はSpring boot(Spring Framework)を使用するため、
Spring Securityで設定を行うことになります。

Spring Securityを使用する

※今回はあくまでCSPの確認を行うことができれば良い。という方針で設定しています。
Spring Securityの正しい使い方については公式ドキュメントや本記事の参照リンクなどを確認してください。

今回は以前作成したプロジェクトを流用することにします。

といってもやっているのはコントローラークラスのルーティング設定ぐらいですが。

ではまずSpring Securityを使えるようにしてみます。

build.gradle

buildscript {
    ext {
        springBootVersion = '1.5.4.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}
dependencies {
    compile('org.springframework.boot:spring-boot-starter-security')
    compile('org.springframework.boot:spring-boot-starter-web')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}
  • dependenciesにSpring Securityを追加するだけです。
    これを起動すると、Spring Securityが有効になってトップページからログインを求められるようになります。
    (ユーザー名: User、パスワードはSpring bootのプロジェクト起動中にログに出力されます)

Configクラスの作成

このままだとログインが必要ないページでも全てログインしないと表示できなくなるので問題です。
また、(今回は使用しませんが)ユーザーのパスワードが、ビルドごとに変更されるのも不便ですね。

このような設定を行うため、xmlファイルまたはclassを作成します。
(今回はclassを使用することにしました)

WebSecurityConfig.java

import org.springframework.context.annotation.Configuration;
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;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 1. ユーザーの設定.
        auth
                .inMemoryAuthentication()
                .withUser("user")
                .password("1234")
                .roles("USER");
    }
    @Override
    public void configure(HttpSecurity http) throws Exception {
        // 2. ページの閲覧権限の設定.
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/menulist").authenticated();
    }
}
  • Spring SecurityのConfigクラスとして認識させるためには、 WebSecurityConfigurerAdapter の継承、
    @Configuration 、 @EnableWebSecurity の付与が必要です。
  • 1でユーザー名、パスワードを設定し、2でページ(トップ、/menulist)の閲覧権限を指定しています。
  • (上記では仮に /menulist をログイン中でしか表示できないようにしていますが)
    今回は全てのユーザーに表示して良いページしかないため、1、2は無くても問題ありません。

X-Frame-Options

実はこの状態で下記のような iframe を含んだHTMLを表示させようとしても、
iframe がブロックされ、表示されません。

main-page.component.html

< img src='https://pbs.twimg.com/media/DC_pK4OUMAAmy_B.jpg' />
< img src='assets/img/sake.jpg' />
< iframe srcdoc="< img src='https://pbs.twimg.com/media/DC_pK4OUMAAmy_B.jpg' />">< /iframe>

それは、Spring Securityではデフォルトで X-Frame-Options がDENYに設定され、iframeが無効になっているためです。

有効にするためにはConfigクラスの configure(HttpSecurity http) で設定を変更します。

WebSecurityConfig.java

〜省略〜
    @Override
    public void configure(HttpSecurity http) throws Exception {
        〜省略〜
        http
                .headers()
                .frameOptions()
                .sameOrigin();
    }
}
  • これで同一ドメインから追加された iframe は有効になり表示されるようになります。
  • ただし iframe が不要なのであればDENYのままの方が良いですね。

CSPの設定

さていよいよCSPです。
こちらはデフォルトでは有効にならないため、 configure(HttpSecurity http) で設定を行います。

WebSecurityConfig.java

〜省略〜
    @Override
    public void configure(HttpSecurity http) throws Exception {
    〜省略〜
        http
            .headers()
            .contentSecurityPolicy("script-src 'self' 'unsafe-eval'; img-src 'self';");
    }
}
  • スクリプト、画像を同一ドメインのみ有効にしています。
  • 各要素は ; (セミコロン)で区切られます。
  • AngularでCSPを使用する場合、'self'以外に'unsafe-eval'(文字列からコードを生成する eval() や類似メソッドの使用許可)を指定しておく必要があります。
    これが抜けていると、 vendor.bundle.js でエラーが発生し、ページが表示されません。
  • スクリプト、画像の他にドメインを制限したい場合は下記などを参照してください。
    Content Security Policy (CSP) - Web セキュリティ - MDN

Offline template compilerについて

【個人的理解に基づく意訳】

Offline template compilerを使うことで、Template Injectionと呼ばれる脆弱性(下記参照)の回避とパフォーマンスの向上が見込まれます。

プロダクトレベル、特にテンプレートがユーザーデータを含む場合、動的にテンプレートを生成するのではなく、
Offline template compilerを使ってください。

Angularではテンプレートを信頼しているため、動的に生成したテンプレートに含まれる問題に対して組み込みのセキュリティ機能が働かないためです

動的にフォームを生成する必要がある場合は、Dynamic Formsのガイドページを参照してください。

ということですので、Dynamic Formsについてもおいおい調べてみたいと思います(そんなのばっかりですね。。。)。

サーバー側のXSS対策について

【個人的理解に基づく意訳】

Angularで構築したWebアプリケーションに、サーバー側からテンプレート言語(Spring Frameworkだと JSP や Thymeleaf ?)を使ってテンプレートのコードを入れてしまうと、
XSSによってWebアプリケーションが自由にコントロールされてしまう危険があります。

これを防ぐため、サーバー側からテンプレート言語を使って渡す値をエスケープ処理し、 サーバー側でAngularのテンプレートを生成するのを避けてください。

フロント側はAngularに任せて、サーバー側からAngularCLIで生成したHTMLなどにあれこれするのはやめてね。ということでしょうか。

参照

Angularのセキュリティ対策について調べてみる その1

はじめに

フロントエンドで行うセキュリティ対策について、Angularではどのようにしているのかなぁ〜、と疑問に思ったので調べてみました。

まずはAngularの公式ドキュメントを元に、あれこれ試してみることにします。

https://angular.io/guide/security

雑な翻訳で進めているため、間違いなどありましたらご指摘のほどよろしくお願いいたしますm( )m

XSSについて

ドキュメントではまずXSSへの対応について書かれています。

Angularではプロパティやスタイルなどの値はTemplateから挿入され、それらの値は全てデフォルトでサニタイズされます。
そのため、仮にユーザーが入力した値をそのままページ上に表示したとしても、Javascriptなどのコードは実行されません。

Angularのセキュリティの文脈で扱われる要素は以下の4つです。

  • HTML
  • Style (CSS)
  • URL (例:< a href>)
  • Resource URL (例:< script src>)

untrustedなコードがサニタイズされた場合、ConsoleにはWarningが出力されます。

ということで、src/app/inner-html-binding.component.htmlを試してみました。

< h3>Binding innerHTML
< p>Bound value:< /p>
< p>{{htmlSnippet}}< /p>
< p>Result of binding to innerHTML:< /p>
< p [innerHTML]="htmlSnippet">< /p>
  • クラスは今回特に関係がないと考えたため、簡略化のため省いています。
  • {{htmlSnippet}}と[innerHTML]に後述のHTMLなどを含めた値を渡し、どのように反映されるかを確認します。
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-sample-from-docs',
  templateUrl: './sample-from-docs.component.html',
  styleUrls: ['./sample-from-docs.component.css']
})
export class SampleFromDocsComponent implements OnInit {

  
  htmlSnippet = 'Template < script>alert("0wned")< /script>< b>Syntax< /b>';
  

  constructor() { }
  ngOnInit() {
  }
}
  • htmlSnippetにHTMLタグを含めてどう表示されるかを確認しています。
    上記はブログ内でHTMLタグが反映されないように「<」の後にスペースを入れていますが、実際には削除してください。

結果

{{htmlSnippet}}

  • 入力されたHTMLタグは反映されず、記号も含めてそのまま表示される。

[innerHTML]

  • scriptタグは除去され、bタグは反映される。

色々試してみた

[innerHTML]の方はセキュリティ上問題ないとされたHTMLタグは反映されました。

では、他にどのようなものが反映されるか、または反映されないのかを調べてみました。

〜省略〜
export class SampleFromDocsComponent implements OnInit {

  
  htmlSnippet = '< div class="testclass" id="testid" style="color:red;">Test< br>Test2\nTest3< /div>< font color=red>Test4< /font>< ul>< li>t< /li>< li>e< /li>< /ul>< h1>H1< /h1>< img src="#" onerror=alert(0) alt="test" />< video src="#">< /video>< a href="/">Top< /a>< a href="javascript:alert(0)">script< /a>';
  

  constructor() { }
  ngOnInit() {
  }
}

結果

{{htmlSnippet}}

  • 先程と同じくタグは全て反映されず、記号も含めて入力された値がそのまま表示される。

[innerHTML]

反映されたもの
* divタグ
* class
* brタグ
* fontタグ
* ulタグ
* liタグ
* h1タグ
* imgタグ
* videoタグ
* aタグ
反映されなかったもの
* id
* style
* 改行文字(\n)
* onerror
* href="javascript:alert(0)"
* iframeタグ
  • 反映されなかったものの大部分はただ除去されただけですが、「href=“javascript:alert(0)"」は「href="unsafe:javascript:alert(0)"」に置き換えられ、
    リンクをクリックすると「xdg-openを開きますか?」と聞かれ、「xdg-openを開く」としてもJavascriptの実行はされませんでした。
  • classが反映されてidが除去されるのが謎ですね。。。何かidに関連する脆弱性があったのでしょうか。
  • classは反映されるものの、例えばCSSにこのクラスに対して何かを書いても反映はされませんでした。おそらくCSSと上記コードの実行順によるものだと思います。
  • aタグはそのまま反映されるということで、ユーザーの入力した値をそのまま反映する場合は、クッションのページなどを挟んだ方が良いかも知れません。

FormsModuleについて

ドキュメントからは外れますが、Formに入力されたデータも見てみました。

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { SampleFromDocsComponent } from './sample-from-docs/sample-from-docs.component';
import { SampleForFormComponent } from './sample-for-form/sample-for-form.component';

@NgModule({
  declarations: [
    AppComponent,
    SampleFromDocsComponent,
    SampleForFormComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
  • ngModelを使って2 way bindingを行うために、FormsModuleのインポートが必要です。

sample-for-form.component.ts

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-sample-for-form',
  templateUrl: './sample-for-form.component.html',
  styleUrls: ['./sample-for-form.component.css']
})
export class SampleForFormComponent implements OnInit {
  private inputValue: string;
  constructor() { }

  ngOnInit() {
  }
  onSubmitButtonClicked(){
    console.log(this.inputValue);
  }
}
  • 入力された値を保持する変数と、ボタンをクリックした時に入力されたデータをログに出力するようにしました。

sample-for-form.component.html

< textarea [(ngModel)]='inputValue'>< /textarea>
< textarea [(ngModel)]='inputValue'>< /textarea>
< div>{{inputValue}}< /div>
< button (click)='onSubmitButtonClicked()'>Submit< /button>
  • 2つある内どちらかのTextareaに値を入力すると、もう一方にも反映される、という内容です。

確認内容

HTMLタグ、改行、絵文字を入力して、2 way bindingでの動作とボタン押下時のログを見てみます。

結果

HTMLタグ、絵文字はそのまま、改行も反映された状態で出力されました。

ということで、HTTPヘッダ、メールヘッダにユーザーが入力した値をそのまま反映する場合、
改行を取り除く必要があります(HTTPヘッダインジェクション、メールヘッダインジェクション防止のため)。

ま、そもそも値をそのまま反映するな、という話ではありますが。

暗号化

Angularに限ったことではありませんが、例えばログインを行う場合、
入力されたパスワードは暗号化してサーバー側に投げるのかな?と思っていました。

が、実際にはSSLで第三者に読み取られないようにした上で、サーバー側で必要に応じて暗号化を行う、というのが主流のようです。

ま、そりゃそうか。

ちなみにJavasciriptで暗号化とググるとCrypto.jsと、
Web Crypto APIが出てきます。

こちらもおいおい調べていきたいところ。

おわりに

結局ドキュメントからは話が逸れてしまいましたが、XSSについてはデフォルトでも対策がとられていることがわかりました。

自分でサニタイズをするかどうかは、AngularのTemplateだけを使って値を表示する場合は不要ですが、
そうでない場合はサニタイズしておくか、またはユーザーが入力した値がそのまま保存されていることを明示しておいた方が良さそうです。

次はドキュメントの続きを。。。

参照

AngularのAnimationsで画像をスライドさせたい

はじめに

AngularのAnimationsで画像をスライドさせてみたら色々大変でした、というお話です。

※2017.7.6 各コードの解説などを追記しました

今回実現したかったことは、Unity日本語サイトトップのバナーのような、
表示中の画像に、画面外から次の画像が重なってくるアニメーションです。

http://japan.unity3d.com/

結局うまく再現できなかったため、表示中の画像が画面外にスライドすると同時に次の画像がスライドしてくる、という内容にしました。

アニメーション1(失敗)

以前は表示中の画像をフェードアウトのアニメーションで非表示にしたあと、
次の画像を表示する、という内容にしていました。

ということは、次の画像を表示するアニメーションを画面外から移動してくるようにして、
フェードアウトと同時に実行すれば完成じゃね?

と思い、そして失敗しましたorz

main-page.component.html

< section id="top-banner-area" >
  < div *ngFor='let bannerImage of bannerImages' >
    < a href='bannerImage.transitionPath'>
      < img [src]="bannerImage.imagePath" class="top-banner" [@bannerState]="bannerImage.bannerState" (@bannerState.done)="onBannerChanged()" />
    < /a>
  < /div>
< /section>
〜省略〜

main-page.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Title } from '@angular/platform-browser';
import { BannerImage } from '../banner-image' 

@Component({
  selector: 'app-main-page',
  templateUrl: './main-page.component.html',
  styleUrls: ['./main-page.component.css'],
  animations: [
    trigger('bannerState',
      [
        state('active', style({
          zindex: 1,
          display: 'block',
          transform: 'translateX(0%)',
          filter: 'brightness(100%)',
          
        })),
        state('inactive', style({
          zindex: 0,
          filter: 'brightness(20%)',
          display: 'none',
        })),
        state('beforeActive', style({
          transform: 'translateX(100%)',
          filter: 'brightness(100%)',
          zindex: 1,
        })),
        transition('active => inactive', animate('400ms ease-in')),
        transition('inactive => beforeActive', animate('1ms ease-in')),
        transition('beforeActive => active', animate('400ms ease-in'))
      ]
    )]
})
export class MainPageComponent implements OnInit {
  private bannerImages: BannerImage[];
  private activeImageNum: number;
  private nextImageNum: number;
  
  constructor(private titleService: Title) {}
  ngOnInit() {
    this.titleService.setTitle('メインページ');

    this.bannerImages = [
      {imagePath: "assets/img/banner01.jpg", transitionPath: '', bannerState: 'inactive'},
      {imagePath: "assets/img/sake.jpg", transitionPath: '', bannerState: 'inactive'}
    ];
    this.activeImageNum = -1;
    this.nextImageNum = 0;
    
    this.onBannerChanged();
  }
  @HostListener('document:keyup', ['$event'])
  onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
      this.nextImageNum = (this.activeImageNum < this.bannerImages.length - 1)? this.activeImageNum + 1 : 0;
      this.bannerImages[this.activeImageNum].bannerState = 'inactive';
      this.bannerImages[this.nextImageNum].bannerState = 'beforeActive';
    }
    else if(event.keyCode === 37){ // LeftArrow
      this.nextImageNum = (this.activeImageNum > 0)? this.activeImageNum - 1: this.bannerImages.length - 1;
      this.bannerImages[this.activeImageNum].bannerState = 'inactive';
      this.bannerImages[this.nextImageNum].bannerState = 'beforeActive';
    }
  }
  onBannerChanged(){
    if(this.activeImageNum === this.nextImageNum){
      return;
    }
    this.bannerImages[this.nextImageNum].bannerState = 'active';
    this.activeImageNum = this.nextImageNum;
  }
}

main-page.component.css

#top-banner-area{
    background-color: #000;
    width: 81%;
    min-width: 1200px;
    height: 600px;

}
.top-banner{
    height: 600px;
    position:absolute;
    margin-left: auto;
    margin-right: auto;
}

内容

  • active -> inactive -> beforeActive -> activeの繰り返し
  • inactiveで画像の明るさを落としたあと非表示
  • beforeActiveで画像をX軸方向に枠外まで移動 + z-indexを1にしてinactiveより前に表示
  • activeでbeforeActiveの位置から画面中央に移動

うまくいかなかったところ

  • z-indexが効かず、beforeActive -> activeで移動する画像が表示中の画像より前に表示されたり後ろに表示されたりする
  • 上記のコードで再確認したところ再現はしなかったのだが(stateが2つの場合?)アニメーションの実行完了速度が画像(配列の順番?)によって異なる現象が発生した

画像をスライドさせる

ということで、表示中の画像が画面外にスライドすると同時に次の画像がスライドしてくる、という内容のアニメーションを作ってみることにしました。

main-page.component.html

アニメーション1(失敗)と同じです。

main-page.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Title } from '@angular/platform-browser';
import { BannerImage } from '../banner-image' 

@Component({
  selector: 'app-main-page',
  templateUrl: './main-page.component.html',
  styleUrls: ['./main-page.component.css'],
  animations: [
    trigger('bannerState',
      [
        state('active', style({
          display: 'block',
          transform: 'translateX(0%)',
          filter: 'brightness(100%)',
        })),
        state('moveLeft', style({
          transform: 'translateX(-100%)',
          filter: 'brightness(20%)',
          display: 'none'
        })),
        state('fromRight', style({
          transform: 'translateX(100%)',
        })),
        state('moveRight', style({
          transform: 'translateX(100%)',
          filter: 'brightness(20%)',
          display: 'none'
        })),
        state('fromLeft', style({
          transform: 'translateX(-100%)',
        })),
        transition('active => *', animate('400ms ease-in')),
        transition('moveLeft => fromRight', animate('1ms ease-in')),
        transition('fromRight => active', animate('400ms ease-in')),
        transition('moveRight => fromLeft', animate('1ms ease-in')),
        transition('fromLeft => active', animate('400ms ease-in'))
      ]
    )]
})
export class MainPageComponent implements OnInit {
  private bannerImages: BannerImage[];
  private activeImageNum: number;
  private nextImageNum: number;
〜省略〜
  ngOnInit() {
〜省略〜
    this.bannerImages = [
      {imagePath: "assets/img/banner01.jpg", transitionPath: '', bannerState: 'inactive'},
      {imagePath: "assets/img/sake.jpg", transitionPath: '', bannerState: 'inactive'}
    ];
    this.activeImageNum = -1;
    this.nextImageNum = 0;
    
    this.onBannerChanged();
  }
  @HostListener('document:keyup', ['$event'])
  onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
      this.nextImageNum = (this.activeImageNum < this.bannerImages.length - 1)? this.activeImageNum + 1 : 0;
      this.bannerImages[this.activeImageNum].bannerState = 'moveLeft';
      this.bannerImages[this.nextImageNum].bannerState = 'fromRight';
    }
    else if(event.keyCode === 37){ // LeftArrow
      this.nextImageNum = (this.activeImageNum > 0)? this.activeImageNum - 1: this.bannerImages.length - 1;
      this.bannerImages[this.activeImageNum].bannerState = 'moveRight';
      this.bannerImages[this.nextImageNum].bannerState = 'fromLeft';
    }
  }
  onBannerChanged(){
    if(this.activeImageNum === this.nextImageNum){
      return;
    }
    this.bannerImages[this.nextImageNum].bannerState = 'active';
    this.activeImageNum = this.nextImageNum;
  }
}

main-page.component.css

#top-banner-area{
    background-color: #000;
    width: 100%;
    min-width: 1200px;
    height: 600px;
    margin-left: auto;
    margin-right: auto;
    overflow: hidden;
    position: relative;
}
.top-banner{
    position: absolute;
    display: block;
    margin-left: auto;
    margin-right: auto;
    height: 600px;
}
  • class:top-banner-areaで「overflow: hidden;」を指定することで、
    アニメーション中に「translateX(100%)」を実行したときもブラウザの横方向のスライドバーが表示されるのを防ぎます。
  • 最初は横方向のスライドバーの表示を抑えるためclass:top-bannerのpositionを「fixed」にしていたのですが、
    画面に固定されてしまうため、positionは「absolute」に変更し、overflowを指定しました。
  • 「overflow: hidden;」は、一番外側の要素に設定しないと効果が無いようです。
  • スライドする画像を全て同じ位置に表示するためpositionを「absolute」にする必要があり、
    他の要素を並べるためにはheightを固定値で持つ必要があります(%指定だと正しく表示できない)。

背景画像をスライドさせる

画像の高さを固定にすると、画像の比率を保つためには幅も固定にする必要がでてきます。

この場合ここまで作成したようにimgタグを使用していると、
(少なくとも今の私の知識では)画面の幅が狭いデバイスで見たとき画像の左側だけが見える、という状態になります。

これを例えば画像の中央部分を表示するよう指定するためには、
imgタグで画像を表示せず、要素の背景画像として指定する、という方法があります。

main-page.component.html

< section id="top-banner-area" >
  < div *ngFor='let bannerImage of bannerImages' >
    < a href='bannerImage.transitionPath'>
      < div [style.background-image]="'url(' + bannerImage.imagePath + ')'" class="top-banner" [@bannerState]="bannerImage.bannerState" (@bannerState.done)="onBannerChanged()">
      < /div>
    < /a>
  < /div>
< /section>
〜省略〜
  • ngForを使って画像のURLを指定したかったため、HTMLでbackground-imageを設定しています
  • background-imageのURLを「"‘url(bannerImage.imagePath)’“」のようにしてしまうと、
    「bannerImage.imagePath」がngForで得られた内容ではなく、「bannerImage.imagePath」という文字列として扱われ、エラーになります。

main-page.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Title } from '@angular/platform-browser';
import { BannerImage } from '../banner-image' 

@Component({
  selector: 'app-main-page',
  templateUrl: './main-page.component.html',
  styleUrls: ['./main-page.component.css'],
  animations: [
    trigger('bannerState',
      [
        state('active', style({
          zIndex: 1,
          display: 'block',
          transform: 'translateX(0%)',
          filter: 'brightness(100%)',
        })),
        state('moveLeft', style({
          zIndex: 0,
          transform: 'translateX(-100%)',
          filter: 'brightness(20%)',
          display: 'none'
        })),
        state('fromRight', style({
          transform: 'translateX(100%)',
        })),
        state('moveRight', style({
          zIndex: 0,
          transform: 'translateX(100%)',
          filter: 'brightness(20%)',
          display: 'none'
        })),
        state('fromLeft', style({
          transform: 'translateX(-100%)',
        })),
        state('start', style({
          transform: 'translateX(0%)',
          display: 'none'
        })),
        transition('active => *', animate('400ms ease-in')),
        transition('moveLeft => fromRight', animate('1ms ease-in')),
        transition('fromRight => active', animate('400ms ease-in')),
        transition('moveRight => fromLeft', animate('1ms ease-in')),
        transition('fromLeft => active', animate('400ms ease-in')),
        transition('* => start', animate('1ms ease-in')),
        transition('start => *', animate('1ms ease-in')),
      ]
    )]
})
export class MainPageComponent implements OnInit {
  private bannerImages: BannerImage[];
  private activeImageNum: number;
  private nextImageNum: number;
  
  constructor(private titleService: Title) {}
  ngOnInit() {
    this.titleService.setTitle('メインページ');

    this.bannerImages = [
      {imagePath: "assets/img/banner01.jpg", transitionPath: '', bannerState: 'start'},
      {imagePath: "assets/img/sake.jpg", transitionPath: '', bannerState: 'start'},
      {imagePath: 'assets/img/pic_1466247624676.jpg', transitionPath: '', bannerState: 'start'}
    ];
    this.activeImageNum = -1;
    this.nextImageNum = 0;
    
    this.onBannerChanged();
  }
  @HostListener('document:keyup', ['$event'])
  onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
      this.nextImageNum = (this.activeImageNum < this.bannerImages.length - 1)? this.activeImageNum + 1 : 0;
      this.bannerImages[this.activeImageNum].bannerState = 'moveLeft';
      this.bannerImages[this.nextImageNum].bannerState = 'fromRight';
    }
    else if(event.keyCode === 37){ // LeftArrow
      this.nextImageNum = (this.activeImageNum > 0)? this.activeImageNum - 1: this.bannerImages.length - 1;
      this.bannerImages[this.activeImageNum].bannerState = 'moveRight';
      this.bannerImages[this.nextImageNum].bannerState = 'fromLeft';
    }
  }
  onBannerChanged(){
    if(this.activeImageNum === this.nextImageNum){
      return;
    }
    this.bannerImages[this.nextImageNum].bannerState = 'active';
    this.activeImageNum = this.nextImageNum;
  }
}
  • 先程はうまく活躍できなかったz-indexですが、ここではactive状態の場合のみ1にして、他より前に表示されるようにしておくことで、
    移動時の画像などが表示されてしまうことによるチラツキを抑えることができます。

main-page.component.css

#top-banner-area{
    background-color: #000;
    width: 80%;
    height: 600px;
    display: block;
    margin-top: 90px;
    margin-left: auto;
    margin-right: auto;
    overflow: hidden;
    position: relative;
}
.top-banner{
    position: absolute;
    background-size: cover;
    background-position: center;
    display: block;
    width: 100%;
    height: 600px;
}

参照

Angularで引っかかったあれこれ

はじめに

相変わらず上の続きなのですが、Angular周りで細々したことに引っかかりまくったため、ここにまとめることとします。

静的なファイルの読み込み

たとえばヘッダーのロゴ画像など、静的にファイルを読み込みたい場合。

普通にHTMLで書くときと同じように、HTMLファイルと同じ階層に画像ファイルを置いても読み込むことはできません。

angular-cliを使っている場合、ファイルの置き場所は.angular-cli.jsonの中の、app > assets の中で指定されます。

.angular-cli.json

〜省略〜
  "apps": [
    {
〜省略〜
      "assets": [
        "assets",
        "favicon.ico"
      ],
      "index": "index.html",
      "main": "main.ts",
      "polyfills": "polyfills.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.app.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "app",
      "styles": [
        "styles.css"
      ],
      "scripts": [],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    }
  ],
  〜省略〜

デフォルトでは favicon.ico と src > assets が指定されており、
例えば src > assets > img > image01.jpg となるようフォルダと画像ファイルを追加した場合、
「< img src=“assets/img/image01.jpg”>」で表示できるようになります。

動的にページタイトルを設定する

ルーティングは以前やりましたが、ページが切り替わったらタイトルも変更したいところ。

ページタイトルを切り替えるには、対象のページのクラスにDIで「Title」クラスを注入し、
「setTitle()」を使って設定します。

main-page.component.ts

import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';

@Component({
  selector: 'app-main-page',
  templateUrl: './main-page.component.html',
  styleUrls: ['./main-page.component.css']
})
export class MainPageComponent implements OnInit {

  constructor(private titleService: Title) {}
  ngOnInit() {
    this.titleService.setTitle('メインページ');
  }

}

IE対応

気になってはいたのです。。。
IEでちゃんと開けるのかということに。

とはいえ、下記を見ればIE9以降であればサポートはされているようなので、
諸事情によりサポート切れのブラウザで表示する必要が。。。みたいな環境でなければ問題ないはずw

ところが、IE11で開くとなにやらエラーが発生してページが正しく動作しませんorz

IEに対応するためには、プロジェクト > src にあるpolyfills.tsの下記の部分をアンコメントする必要があります。

polyfills.ts

/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';

ちなみにここで登場するcore-jsは、ブラウザ間の差異を吸収してくれるもののようです。
(この辺もちゃんと調べたいと思いつつ何もできてないマンですはい)

デフォルトでこれがオンになってない理由は、余計なファイルを含めたくない、ということなのでしょうか。

Componentをどう分けるか

これまで1ページの中の要素でも、ひとかたまりごとにComponentに分けて作っていました。

ただ、この辺りのお話を見ていると、そうではなく複数の箇所から共通で使う部分だけを切り分ける、
とした方が良いように思えてきました。

今から思えば、たしかにハンズオンも、先にまとめて作ったあと切り分けていたなぁ、と。

これもバランスだとは思いますが、今後はComponentをもう少しまとめてみようと思います。

画像切り替え

以前は画像のパスをimgタグ渡して、動的に画像を切り替える、ということをしていましたが、
これには問題がありました。

最初にページを表示した時に、画像がキャッシュされていないので画像切り替え時に一瞬前の画像が見えたりしていたのです。

大量に画像を表示するなどの場合は別ですが、今回のように決まった数を表示する場合は、
画像を全てキャッシュしておいて、それを切り替えた方が良さそうです。

banner-image.ts

export interface BannerImage {
    imagePath: string;
    transitionPath: string;
    bannerState: string;
}

main-page.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Title } from '@angular/platform-browser';
import { BannerImage } from '../banner-image' 

@Component({
  selector: 'app-main-page',
  templateUrl: './main-page.component.html',
  styleUrls: ['./main-page.component.css'],
  animations: [
    trigger('bannerState',
      [
        state('active', style({
          transform: 'scale(1.0)',
          filter: 'brightness(100%)',
          display: 'block'
        })),
        state('inactive', style({
          transform: 'scale(0.6)',
          filter: 'brightness(20%)',
          display: 'none'
        })),
        transition('active => inactive', animate('400ms ease-in')),
        transition('inactive => active', animate('10ms ease-in'))
      ]
    )]
})
export class MainPageComponent implements OnInit {
  private bannerImages: BannerImage[];
  private activeImageNum: number;
  private nextImageNum: number;
  
  constructor(private titleService: Title) {}
  ngOnInit() {
    this.titleService.setTitle('メインページ');

    this.bannerImages = [
      {imagePath: "assets/img/banner01.jpg", transitionPath: '', bannerState: 'inactive'},
      {imagePath: "assets/img/banner01.jpg", transitionPath: '', bannerState: 'inactive'},
      {imagePath: "assets/img/banner01.jpg", transitionPath: '', bannerState: 'inactive'},
    ];
    this.activeImageNum = - 1;
    this.nextImageNum = 0;
    this.onBannerChanged();
  }
  @HostListener('document:keyup', ['$event'])
  onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
      this.nextImageNum = (this.activeImageNum < this.bannerImages.length - 1)? this.activeImageNum + 1 : 0;
      this.bannerImages[this.activeImageNum].bannerState = 'inactive';
    }
    else if(event.keyCode === 37){ // LeftArrow
      this.nextImageNum = (this.activeImageNum > 0)? this.activeImageNum - 1: this.bannerImages.length - 1;
      this.bannerImages[this.activeImageNum].bannerState = 'inactive';
    }
  }
  onBannerChanged(){
    if(this.activeImageNum === this.nextImageNum){
      return;
    }
    this.bannerImages[this.nextImageNum].bannerState = 'active';
    this.activeImageNum = this.nextImageNum;
  }
}

main-page.component.html

< section id="top-banner-area" >
  < div *ngFor='let bannerImage of bannerImages' >
    < a href='bannerImage.transitionPath'>
      < img [src]="bannerImage.imagePath" class="top-banner" [@bannerState]="bannerImage.bannerState" (@bannerState.done)="onBannerChanged()" />
    < /a>
  < /div>
< /section>
< app-top-news>< /app-top-news>

参照

静的ファイルの読み込み

ページタイトル

ブラウザ対応

Component分割

AngularとSpringBootでWebページを作ってみたい その3

はじめに

今回は、Spring boot側のルーティングと、特定の要素をマウスオーバーした時に、メニューを表示する方法について調べたことをまとめます。

ルーティング

以前Angular側ではルーティングを行ったわけですが、トップページから遷移したときは問題なくても、
「トップドメイン/contact」 など、トップから遷移したあとのURLに直接アクセスしようとするとエラーになってしまいます。

これを回避するためには、Spring boot側でトップドメイン以下のページのURLにアクセスしたときもAngularのindex.htmlを返すようにする必要があります。

MainController.java

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.InternalResourceView;

@Controller
public class MainController {
    @RequestMapping(value = {"/", "/menulist", "/contact"})
    public View redirect() {
        return new InternalResourceView("/index.html");
    }
}

htmlを表示するために使っているInternalResourceViewですが、下記などを見ると、JSPを表示する場合に使用されるもののようです。

これを使うと、ng buildでビルドしたファイルを全部 src/main/resources/static 以下に入れればOKなのですが、
これが正しい方法なのか、というところはちょっと不安が残っています。

アイテムがマウスオーバーされた時にメニューを表示する

ボタンをマウスオーバーした時に、メニューを表示してみます。

具体的には https://www.jetbrains.com/ の上部メニューの、「IDEs」「.NET & VISUAL STUDIO」などをマウスオーバーした時に表示されるメニュー(ウィンドウ)を真似したいということですw

ngIfなどを使ったり試行錯誤していたのですが、ぐぐってみるとCSSでシンプルに解決する方法が見つかりましたorz

global-header.component.html

< header id="global-header">
〜省略〜
  < div id="header-button-menulist-area">
    < button routerLink="./menulist" class="header-button">Menu< /button>
    < app-global-header-menulist-window id="global-header-menulist-parent">< /app-global-header-menulist-window>
  < /div>
〜省略〜
< /header>

global-header.component.css

#global-header{
    background-color: #2980B9;
    width: 100%;
    min-width: 1200px;
    height: 90px;
    position: fixed;
    top: 0;
    z-index: 1;
}
〜省略〜
.header-button{
    background-color: #2980B9;
    border-style: none;
    color: #FFFFFF;
    font-size: 18px;
    width: 160px;
    height: 60px;
}
.header-button:hover{
    background-color: #FFF;
    color: #000;
    font-weight: bold;
}
〜省略〜
#header-button-menulist-area{
    width: 160px;
    height: 60px;
    float: left;
    margin: 30px 20px 0px 0px;
}
〜省略〜

#header-button-menulist-area #global-header-menulist-parent{
    visibility: hidden;
}
#header-button-menulist-area:hover #global-header-menulist-parent{
    visibility: visible;   
}

global-header-menulist-window.component.html

< div id="global-header-menulist-window-frame">
  < button class="menulist-window-button">
    < div>Menu 1< /div>
    < div>1つめのメニューはカレーうどんについて。
いや〜、うまいっすね。
みなさんも白い服を着ているときはカレーうどんに決まり! < /div> < /button> < button class="menulist-window-button"> < div>Menu 2< /div> < div>2つめのメニューはかけうどん。
いいっすよ、かけうどん。
かけっこしながらかけうどん! < /div> < /button> < button class="menulist-window-button"> < div>Menu 3< /div> < div>3つめのメニューは月見うどん。
月が、きれいですね。 < /div> < /button> < button class="menulist-window-button"> < div>Menu 4< /div> < div>4つめのメニューはきつねうどん
場所によってはきつねそばもあるらしいです。 < /div> < /button> < /div>

global-header-menulist-window.component.css

#global-header-menulist-window-frame{
    width: 100%;
    height:300px;
    background-color: #FFF;
    position: absolute;
    left: 0;
    padding: 40px;
}
.menulist-window-button{
    border-style: none;
    width: 400px;
    height: 120px;
    margin-left: 60px;
    margin: 10px;
    float: left;
}
  • メニュー自体は適当にボタンを並べているだけです。
  • global-header-menulist-window.component.htmlが表示されている間は、
    マウスオーバーの領域が div id:header-button-menulist-area に加えて global-header-menulist-window.component.html に広がるため、メニュー内のボタンも選択可能、というわけですね。

おわりに

次は入力フォーム辺りを触ってみたいところ。

参照

KANJAVA PARTYに参加してきました

はじめに

6/24に行われたKANJAVA PARTYに参加してきました。

今回は関ジャバとしては初となる複数トラック制の、
参加人数も100名超と、普段より少し規模の大きい勉強会でした。

関ジャバとJava

www.slideshare.net

まずは関ジャバについてのお話から。

簡単な歴史や、関ジャバこと関西ジャバエンジニアの会は、あくまで関西中心のジャバエンジニアのための会であって、
必ずしもジャバだけを勉強する会ではない(のでフロントエンドなどの勉強会も行う)、といったお話をされていました。

私も過去に何度も参加させてもらいながら、歴史については知らずにいたためなるほど〜、と感心。

一点、もとの題名であった「すべらない話」を期待していたのは内緒ですw
(念の為書いておくと、滑ってはいませんでしたw)

Spring Securityにできること・できないこと

qiita.com

IPAが公開している安全なWebサイトの作り方で指摘されている脆弱性の内、
Spring Securityが対策してくれることとそうでないことを、脆弱性対策しているサイトとそうでないサイトのサンプルを用意して、比較しながら説明されていました。

Spring Security対策してくれるところ、そうでないところでも保険的なフォローをしてくれるところといった内容を、
デモを交えて説明してくれたのがとてもわかりやすかったです。

あと実践で使用するにあたっては、そのフレームワークや技術がどのように動いているのか、
ということをしっかりと理解できていなければいけない、と改めて感じました。

どうも自分は表面的なところだけで次に進んでしまいがちなので、
セキュリティはもちろん、それ以外も深く追っていかねばと思いました。

安全なWebサイトの作り方も早いうちに読んでおきたいと思います。

JUnit5の味見

speakerdeck.com

ここも相当迷いましたがJUit5に。

このセッションではJUnit5のユーザーガイドをベースに、
かみくだいた内容から気になるところをピックアップする、という方法で進められていました。

ようやくJUnit4を触りだした私としては、話についていけたとは言い難いのですが、
話されていない内容も含めてガッツリ資料にまとめてくれていると、あとから追えるので大変助かります^^;
(翌日次のバージョンが出る、という悲しいオチもありましたが。。。)

Java8に対応したり、便利になっているということはわかった(気がする)ので、
特にプライベートでは積極的にトライしていきたいと思います。

ストリーム処理ことはじめ 〜Akka StreamとRxJava

speakerdeck.com

Java Stream APIの基礎的な説明から、forではなくストリーム処理が何故良いのか、というお話へ。
さらに、Stream APIの抱える課題点と、Akka Streamがどうそれを解決するか、といった流れで説明されていました。

mapなどの処理の中身をStreamから分離する、というのは処理が複雑になったり、
複数ヶ所で同じ処理を行う時に力を発揮しそうですし、これはC#Linqを使うときなんかにも使えるのでは、と感じました。

処理が進んで値が流れていく様子がスライドで表現されていたのも良かったと思いました。

ちなみにAkka Streamを調べるとScalaだけでなくJavaもあるのですが、
Java版も同じように使えるのでしょうか。

その辺りも試してみたいところです。

Kafkaを使ってイベントを中心にしたアプリケーションを作ってみる。Spring Cloudにのせて

bufferings.hatenablog.com

スライドがなく、マインドマップ?をベースにやってみたこと、
困ったことや考えなどを話されていて、ある意味異色のセッションでしたw

Kafkaについての前知識が全くない状態で聴いていたため、
本当にぼんやりとこんな感じかなぁ。。。?と思っているくらいですが、とにかく楽しいセッションでした。

まぁKafkaについても近い内触ってみる、ということで。

あと今回に限らないのですが、bufferingsさんの資料やデモに、
娘さんが描かれたという絵が載せられていて、うらやましいです。

うちの子がもう少し大きくなって、絵を掻けるようになったら真似したいな〜と思っているのですが、いつになるやらw

関西Java女子部ショートセッション大会

関西Java女子部の方々によるショートセッション × 3 + 1

まずは関西Java女子部の活動の説明から、システムに対して理解が薄い顧客に対して自分たちの仕事をどう理解してもらうか、
リファクタリングのススメ、ハンズオン(講師側)のススメといった内容でした(順番違うかも)。

関西Java女子部はTwitterなどで情報は流れてくるものの、
一緒に参加してくれる女性の知り合いはおらんしな〜と思っていたので、活動内容が知れてよかったです。

自分たちの仕事をどう理解してもらうか、というところは、他人事でなく自分も取り組まなきゃいけない内容ですが、
相手も勉強会に賛同してくれた、という話は少しうらやましく感じました。

リファクタリングについては、とにかくネーミングの面白さがwww

speakerdeck.com

真面目なところとしては、私もリファクタリングしようとして、
歯止めが効かなくなって延々と作業し続けてしまうことがあるので、
うまくいかなかったら素直に元に戻す、というのは気をつけていきたいと思います。

ハンズオンについては、講師側を務めたことがないのですが、
今後自分が参加するだけでなく、サポートする側(ハンズオンだけでなく)にもまわってみようかなぁ、と思います。

自分も学ばなきゃいけないことがありすぎるので、バランスが難しそうではありますが(; ・`ω・´)

はてなブックマークAndroidアプリへのKotlinの導入について

speakerdeck.com

以前関モバ(だったと思います)で、Kotlinを使いたい、という話を聴いていたので、
最初このセッションのタイトルを見た時に「おぉ!ついに!」と思っておりました。

というところで前半はKotlin導入への経緯と、既存のJavaコードをどのようにKotlinに置き換えていくか、というお話が、 後半はKotlinの代表的な言語機能をピックアップして説明されていました。

前半については、やっぱり情熱って大事だなぁと感じました。
少々リスクや課題があったとしても、やりたい欲と技術があればまぁなんとかなりますよね。

後半については、プロパティがいまひとつよくわかっていなかったので、
そこに触れてもらえたのが助かりました。

Kansai.kt #3も行われるというし、またKotlinで何か作ってみるかなぁ。

ビアバッシュ

あっという間にセッションもおわり、ビアバッシュに。

今回も懲りずにLTに挑戦しました。

speakerdeck.com

本来の予定では、もう少しJavaよりにするつもりだったのですが、
進捗の具合からAngularメインのお話となりました。

時間は問題なかったのですが、特にビアバッシュに行うLTは、
もう少し聴いている人を楽しませる、ということにも気を遣ってみようと思いました。

まぁビールを出すとか、おっさんギャグを言うとかじゃなくて良いのですがw

あとはせっかくなので、もう少しいろんな人に話しかけるようにしたいところ。

あと岡山の勉強会参加したい。

おわりに

今回に限りませんが、やっぱり複数トラックでセッションがあると、
どれを聴くか迷ったり、自分が聴いている方も良いのだけどあっちも聴きたかった!となりますね。

スタッフの皆様、参加された皆様、会場を提供していただいた日本マイクロソフト様、ありがとうございました!