Safari のプライベートブラウズだと LINEログイン 出来ない
1. 使用技術
Spring Boot Starter Security 2.7.1
2. 原因
Safariのプライベートブライズモードだと、タブ毎にCookie管理を行なっている(はず。。)。
よって、別タブに切り替えるとログインしているSessionの情報を保存している Cookie が無くなる。
LINEログインをした後に、LINEの認可サーバーが別タブでレスポンスを返却する。
そのため、Cookie が消失して、ログインエラーとなる。
3. 備考
3-1. 備考1
LoggerのレベルをTRACEにすると、下記のようなエラーが出る。
****は、省略。
OAuth2LoginAuthenticationFilterを呼び出す際に、Cookie を消失しているので認可リクエストを取得出来ない模様。
2022-08-23 17:27:13.733 DEBUG 394375 --- [http-nio-8080-exec-2] o.s.security.web.FilterChainProxy : Securing GET /login/oauth2/code/line?state=**** 2022-08-23 17:27:13.733 TRACE 394375 --- [http-nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/14) 2022-08-23 17:27:13.733 TRACE 394375 --- [http-nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/14) 2022-08-23 17:27:13.733 TRACE 394375 --- [http-nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking SecurityContextPersistenceFilter (3/14) 2022-08-23 17:27:13.734 TRACE 394375 --- [http-nio-8080-exec-2] w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists 2022-08-23 17:27:13.734 TRACE 394375 --- [http-nio-8080-exec-2] w.c.HttpSessionSecurityContextRepository : Created SecurityContextImpl [Null authentication] 2022-08-23 17:27:13.735 DEBUG 394375 --- [http-nio-8080-exec-2] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext 2022-08-23 17:27:13.735 TRACE 394375 --- [http-nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/14) 2022-08-23 17:27:13.735 TRACE 394375 --- [http-nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/14) 2022-08-23 17:27:13.736 TRACE 394375 --- [http-nio-8080-exec-2] o.s.security.web.csrf.CsrfFilter : Did not protect against CSRF since request did not match CsrfNotRequired [TRACE, HEAD, GET, OPTIONS] 2022-08-23 17:27:13.736 TRACE 394375 --- [http-nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking LogoutFilter (6/14) 2022-08-23 17:27:13.736 TRACE 394375 --- [http-nio-8080-exec-2] o.s.s.w.a.logout.LogoutFilter : Did not match request to Ant [pattern='/logout', POST] 2022-08-23 17:27:13.736 TRACE 394375 --- [http-nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking OAuth2AuthorizationRequestRedirectFilter (7/14) 2022-08-23 17:27:13.737 TRACE 394375 --- [http-nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking OAuth2LoginAuthenticationFilter (8/14) 2022-08-23 17:27:13.748 TRACE 394375 --- [http-nio-8080-exec-2] .s.o.c.w.OAuth2LoginAuthenticationFilter : Failed to process authentication request org.springframework.security.oauth2.core.OAuth2AuthenticationException: [authorization_request_not_found] at org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter.attemptAuthentication(OAuth2LoginAuthenticationFilter.java:173) ~[spring-security-oauth2-client-5.7.2.jar!/:5.7.2] at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:227) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.doFilterInternal(OAuth2AuthorizationRequestRedirectFilter.java:178) ~[spring-security-oauth2-client-5.7.2.jar!/:5.7.2] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.21.jar!/:5.3.21] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:117) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.21.jar!/:5.3.21] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.21.jar!/:5.3.21] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:112) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:82) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:55) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.21.jar!/:5.3.21] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.21.jar!/:5.3.21] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:211) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:183) ~[spring-security-web-5.7.2.jar!/:5.7.2] at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:354) ~[spring-web-5.3.21.jar!/:5.3.21] at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:267) ~[spring-web-5.3.21.jar!/:5.3.21] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.21.jar!/:5.3.21] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.21.jar!/:5.3.21] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.21.jar!/:5.3.21] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.21.jar!/:5.3.21] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.21.jar!/:5.3.21] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.21.jar!/:5.3.21] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:890) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1787) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-9.0.64.jar!/:na] at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.64.jar!/:na] at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
3-2. 備考2
下記のように、2021/06/04 時点で LINEのQ&Aに質問が投稿されているが、まだ対応がされていない模様。
loggin.file.path を指定しても spring.log ファイルが出力されない場合
問題
Spring Boot のドキュメントを参考に、application.properties に下記のように追記した。
****
は、サービスの名前のディレクトリにした。
logging.file.path=/var/log/****
しかし、var/log/**** 配下にログが出力されなかった。
解決方法
/var/log/**** の所有者が root になっており、所有者以外は書き込み、実行が出来ないようになっていた。
そこで、下記のように /var/log/ ディレクトリの所有者を、サービスを実行する user と一緒にした。
は、サービスの名前のディレクトリ。
%%%%は、サービスを実行するuser。
sudo chown %%%% /var/log/****
これで spring.log ファイルが出力されるようになった。
Spring Framework の @ModelAttribute の使い方
概要
@ModelAttribute の使い方には、下記の2つがある。
1. @Controllerのメソッドに使う方法
2. @Controllerのメソッド引数に使う方法
Spring Security OAuth2 Login と LINEログイン を利用してログイン機能を実装する
version
- Spring Boot Starter Security 2.7.1
参考
まずは、下記の動画、documentを理解すると分かりやすいと思う。
- https://www.youtube.com/watch?v=PKPj_MmLq5E
- https://qiita.com/TakahikoKawasaki/items/701e093b527d826fd62c
また、下記のSpring Security OAuth2 LoginとLINEログインのdocumentも理解すると分かりやすいと思う。
- https://spring.pleiades.io/spring-security/reference/servlet/oauth2/login/core.html#oauth2login-sample-redirect-uri
- https://developers.line.biz/ja/docs/line-login/integrate-line-login/
1. LINE Developersに開発者アカウントを作成
下記のdocument通りに開発者アカウントを作成する。
https://developers.line.biz/ja/docs/line-developers-console/login-account/
2. LINE Developersの開発者アカウントにProviderを作
Providerとは、
LINEプラットフォームを通じてサービスを提供する個人、企業、またはそのほかの組織を意味する情報です。
下記のdocument通りにProviderを作成する。
https://developers.line.biz/ja/docs/liff/getting-started/#creating-provider-and-channel
3. LINE Developersの開発者アカウントのProviderにChannelを作成する
Channelとは、
LINEプラットフォームが提供する機能を、プロバイダーが開発するサービスで利用するための通信路です。
おそらくChannelは、サービスの機能毎に作ることを想定されてそう。
例えばAサービスのLINEログイン, BサービスのLINEログイン, CサービスのMessaging APIそれぞれが個別のChannelを持つように作ると良さそう。
下記のdocument通りにChannelを作成する。
https://developers.line.biz/ja/docs/liff/getting-started/#creating-provider-and-channel
4. ChannelのCallback URLを設定
4-1. Callback URLとは
LINEログインの文脈でのCallbackとは、リダイレクションエンドポイントを指すと思われる。
リダイレクションエンドポイントは、Spring Security OAuth2 Loginの文脈では、リダイレクトURIと呼ばれる。
要は、LINE側が認証を行った後にブラウザにレスポンスするリダイレクト先のエンドポイントである。
このリダイレクションエンドポイントの実際のURLは、後述の5で定義する。
このリダイレクションエンドポイントは、OAuth2LoginAuthenticationFilterに引っかかる。
4-2. 定義する値
ChannelのLINEログインのタブ内に下記のような形式でCallback URLを定義する。
***は、スキーム + ドメイン
である。
***/login/oauth2/code/line
ただし、ローカル環境のリダイレクトエンドポイントの場合、下記のように記述すると有効なURLとみなされない。
http://localhost:8080/login/oauth2/code/login
よって、ローカル環境のリダイレクトエンドポイントの場合、下記のように記述する。
http://127.0.0.1:8080/login/oauth2/code/line
5. Spring Security OAuth2 Loginの設定
ClientRegistrationの設定を行う。
ClientRegistrationのClientとは、本記事で言えば LINEログイン Channel を指す。
設定項目の説明は、下記を参照すると良い。
https://spring.pleiades.io/spring-security/reference/servlet/oauth2/login/core.html#oauth2login-boot-property-mappings
https://spring.pleiades.io/spring-security/reference/servlet/oauth2/client/core.html#oauth2Client-client-registration
具体的には、application.propertiesに下記の設定をする。
key | 指定するvalueについて |
---|---|
spring.security.oauth2.client.registration.line.client-id | Channel の Basic Settings の ChannelID を指定する。 |
spring.security.oauth2.client.registration.line.client-secret | Channel の Basic Settings の Channel secret を指定する。 |
spring.security.oauth2.client.registration.line.authorization-grant-type | authorization_code を指定する。LINEログインの処理は、認可コード付与のフローに基づくためである。 |
spring.security.oauth2.client.registration.line.redirect-uri | http://127.0.0.1:8080/login/oauth2/code/line を指定する。これによりcode(認可コード)とstateを付与したリクエストをリダイレクションエンドポイントに送る。これによりOAuth2LoginAuthenticationFilterに引っかかる。 |
spring.security.oauth2.client.registration.line.scope | profile を指定する。これによりprofileを取得する権限を得る。他に指定出来るスコープは、こちらを参照する。 |
spring.security.oauth2.client.provider.line.authorization-uri | https://access.line.me/oauth2/v2.1/authorize を指定する。これにより認可エンドポイントのリクエスト先が決まる。LINEログインにおける 認可エンドポイント は、こちらに記載される https://access.line.me/oauth2/v2.1/authorize である。 |
spring.security.oauth2.client.provider.line.token-uri | https://api.line.me/oauth2/v2.1/token を指定する。これによりトークンエンドポイントへのリクエスト先が決まる。LINEログインにおけるトークンエンドポイントが、こちらに記載される https://api.line.me/oauth2/v2.1/token である。 |
spring.security.oauth2.client.provider.line.user-info-uri | https://api.line.me/v2/profile を指定する。これによりUserInfo Endpointへのリクエスト先が決まる。LINEログインにおけるUserInfo Endpointが、こちらに記載される https://api.line.me/v2/profile である。 |
spring.security.oauth2.client.provider.line.user-name-attribute | userId を指定する。上記のUserInfo Endpointから返される識別子の属性の名前を記述する。これは、こちらに記載される userId である。 |
6. SpringSecurityFilterChain のBeanをDIコンテナに登録
具体的には、下記のようなConfigクラスを実装する。
下記の実装は、最低限の設定である。
@EnableWebSecurity public class SpringSecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .oauth2Login(customizer -> customizer .loginPage("/login") .defaultSuccessUrl("/") ) .logout(customizer -> customizer .logoutSuccessUrl("/login") ) .authorizeHttpRequests(customizer -> customizer .mvcMatchers("/css/**", "/favicon.ico").permitAll() .mvcMatchers("/login").permitAll() .anyRequest().authenticated() ) .build(); } }
7. Lineログインテスト
7-1. 最初のリクエスト
ローカル環境で開発をしている場合、Spring Security OAuth2 Loginのデフォルトでは、下記のURLにリクエストを送ることで OAuth2AuthorizationRequestRedirectFilter に引っかかり、必要なパラメータを付与したLINEログインの認可エンドポイント(https://access.line.me/oauth2/v2.1/authorize)にリクエストを送る。
http://127.0.0.1:8080/oauth2/authorization/line
7-2. 7-2の後
LINEログインの認可エンドポイントにリクエストを送ると、上記4-1に記述した通りリダイレクトのレスポンスを返す。
リダイレクトのURLは、上記5で spring.security.oauth2.client.registration.line.redirect-uri
に設定したURLである。
このURLにリクエストをすると、OAuth2LoginAuthenticationFilterに引っかかる。
8. LINEログインのユーザーIDを取得
下記のようなログインしているLINEのユーザーIDを取得出来るコンポーネントを作成する。
@Component public class LineLoginContext { OAuth2AuthorizedClientService oAuth2AuthorizedClientService; LineLoginContext(OAuth2AuthorizedClientService oAuth2AuthorizedClientService) { this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService; } public LINEUserId LINEのユーザーID() { DefaultOAuth2User ログインユーザー = (DefaultOAuth2User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); return LINEUserId.of(ログインユーザー.getName()); } }
9. LINEログイン 関連の Resource(リソース)
下記からダウンロード可能。
https://developers.line.biz/ja/docs/line-login/login-button/
デザイン基本対応表
1. レスポンシブ対応
デバイス | 大きさ | Tailwind CSS |
---|---|---|
スマートフォン縦 | 320px ~ 639px | なし |
スマートフォン横 | 640px ~ 767px | sm |
タブレット縦 | 768px ~ 1023px | md |
タブレット横 | 1024px ~ 1279px | lg |
PC小 | 1280px ~ 1535px | xl |
PC大 | 1536px ~ | 2xl |
2. 文字の大きさ
タグ | スマートフォンの場合の大きさ | Tailwind CSS | PCの場合の大きさ | Tailwind CSS |
---|---|---|---|---|
h1 | 1.25rem, bold | text-xl, font-bold | 1.5rem, bold | text-2xl, font-bold |
h2 | 1.125rem, bold | text-lg, font-bold | 1.25rem, bold | text-xl, font-bold |
h3 | 1rem, bold | text-base, font-bold | 1.125rem, bold | text-lg, font-bold |
p | 1rem | text-base | 1.125rem | text-lg |
3. 余白の大きさ
余白の場所 | スマートフォンの場合の大きさ | Tailwind CSS | PCの場合の大きさ | Tailwind CSS |
---|---|---|---|---|
ヘッダーと初期コンテンツの間 | 2rem | mt-8 | 4rem | mt-16 |
タイトルとコンテンツの間 | 1rem | mt-4 | 2rem | mt-8 |
コンテンツの中の要素の間 | 2rem | mt-8 | 2rem | mt-8 |
コンテンツとコンテンツの間 | 4rem | mt-16 | 8rem | mt-32 |