####1. 認可サーバ設定(LINE)
LINE Developers:
https://developers.line.biz/ja
・Channel Type: LINE Login
・App Types: Web app
・Callback URL: http://localhost:8080/redirect (認証後のリダイレクト先URL)
####2. クライアント実装(Spring Boot/Security)
2-1. 依存ライブラリの追加
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
#####2-2. application.ymlの設定
以下内容をもとに、ユーザーをLINE認証画面に遷移させ、LINE API を呼び出してユーザー情報を取得
security.oauth2.client.clientId
- Channnel ID
security.oauth2.client.clientSecret
- Channnel Secret
security.oauth2.client.accessTokenUri
- アクセストークン発行の際に呼び出すエンドポイント
security.oauth2.client.userAuthorizationUri
- 認証先エンドポイント
security.oauth2.client.pre-established-redirect-uri
- 認証後のリダイレクト先URL (※LINE側で設定したCallback URL)
security.oauth2.client.scope
- トークンをリクエストする際にクライアントが使えるスコープのリスト
security.oauth2.client.clientAuthenticationScheme
- OAuth2 Token Endpoint 呼び出しに使うフォーマット(※LINEはFORMに対応)
security.oauth2.resource.userInfoUri
- ユーザー情報(ユーザーID・ユーザ名・プロフィール画像)取得のために提供されるエンドポイント
spring:
main:
allow-bean-definition-overriding: true
security:
oauth2:
client:
clientId: [CLIENT_ID]
clientSecret: [CLIENT_SECRET]
accessTokenUri: https://api.line.me/oauth2/v2.1/token
userAuthorizationUri: https://access.line.me/oauth2/v2.1/authorize
use-current-uri: false
pre-established-redirect-uri: https://localhost:8443/redirect
authorized-grant-types:
- authorization_code
- refresh_token
clientAuthenticationScheme: form
scope:
- openid
- email
- profile
resource:
userInfoUri: https://api.line.me/oauth2/v2.1/userinfo
preferTokenInfo: true
server:
port: 8443
ssl:
key-store: [.p12ファイルパス]
key-store-password: [パスワード]
keyStoreType: PKCS12
keyAlias: MyAlias
logging:
level:
org.springframework.web: DEBUG
#####2-3. application.ymlから変数情報を取得
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
@Component
@PropertySource("classpath:application.yml")
@ConfigurationProperties(prefix = "security.oauth2.client")
public class ConfigComponent {
private String accessTokenUri;
private String clientId;
private String clientSecret;
@Value("${security.oauth2.client.pre-established-redirect-uri}")
private String redirecturi;
@Value("${security.oauth2.resource.userInfoUri}")
private String userInfoUri;
public String getAccessTokenUri() {
return this.accessTokenUri;
}
public void setAccessTokenUri(String accessTokenUri) {
this.accessTokenUri = accessTokenUri;
}
public String getClientId() {
return this.clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getRedirecturi() {
return this.redirecturi;
}
public void setRedirecturi(String redirecturi) {
this.redirecturi = redirecturi;
}
public String getClientSecret() {
return this.clientSecret;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public String getUserInfoUri() {
return this.userInfoUri;
}
public void setUserInfoUri(String userInfoUri) {
this.userInfoUri = userInfoUri;
}
}
#####2-4. @Beanの登録
- 上記で設定した変数情報取得用クラスをBeanに登録
package com.example.demo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanConfig {
@Bean
public ConfigComponent configComponent() {
return new ConfigComponent();
}
@Bean
public LoginRestClient loginRestClient() {
return new LoginRestClient();
}
}
#####2-5. ユーザが指定パス(/login)にアクセスした際にOAuth認証を実行
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
@EnableWebSecurity
@EnableOAuth2Sso
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/images/**",
"/css/**",
"/javascript/**",
"/webjars/**",
"/favicon.ico");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/redirect/**")
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/login/**")
.authenticated();
}
}
#####2-5. @Controllerの定義
- 認証完了後に、認可コードを取得し、アクセストークン発行/ユーザ情報取得リクエストを実行
- @AutowiredでLoginRestClientクラスをInject
- ModelAndViewクラスでThymeleafテンプレートへ取得したオブジェクトを渡す
import static java.util.Objects.requireNonNull;
import java.io.IOException;
import java.util.stream.IntStream;
import com.example.demo.LoginRestClient;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class LoginController {
@Autowired
LoginRestClient loginRestClient;
@RequestMapping(value = "/redirect", method = RequestMethod.GET)
public String getToken(@RequestParam("code") String code,
HttpServletRequest req, HttpServletResponse res) throws IOException{
LineToken responseToken = loginRestClient.getToken(code);
String accessTK = responseToken.getAccessTK();
String refreshTK = responseToken.getRefreshTK();
String idTK = responseToken.getIdTK();
Cookie accessTKCookie = new Cookie("access_token", accessTK);
accessTKCookie.setSecure(true);
Cookie refreshTKCookie = new Cookie("refresh_token", refreshTK);
refreshTKCookie.setSecure(true);
Cookie idTKCookie = new Cookie("id_token", idTK);
idTKCookie.setSecure(true);
res.addCookie(accessTKCookie);
res.addCookie(refreshTKCookie);
res.addCookie(idTKCookie);
return "redirect:/";
}
@RequestMapping(value = "/userinfo", method = RequestMethod.GET)
public ModelAndView getSubInfo(ModelAndView mv, HttpServletRequest req) throws IOException {
Cookie cookies[] = req.getCookies();
for(Cookie c: cookies) {
if(c.getName().equals("access_token")) {
System.err.println(c.getValue());
JSONObject userInfo =
new JSONObject(loginRestClient.getUserInfo(c.getValue()).getBody());
String sub = userInfo.getString("sub");
String name = userInfo.getString("name");
String picture = userInfo.getString("picture");
mv.addObject("sub", sub);
mv.addObject("name", name);
mv.addObject("picture", picture);
mv.setViewName("userinfo");
}
}
return mv;
}
@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView getTopPage(ModelAndView mv, HttpServletRequest req) {
Cookie cookies[] = req.getCookies();
for(Cookie c: cookies) {
if(c.getName().equals("access_token")) {
mv.addObject("access_token", c.getValue());
} else if (c.getName().equals("refresh_token")) {
mv.addObject("refresh_token", c.getValue());
} else if (c.getName().equals("id_token")) {
mv.addObject("id_token", c.getValue());
}
}
mv.setViewName("index");
return mv;
}
}
#####2-6. アクセストークン発行/ユーザ情報取得をリクエスト実行クラス
- getTokenメソッドで認可コードを引数にアクセストークン発行APIをリクエスト
- getUserInfoメソッドにてアクセストークンをAuthorizationヘッダーに付与することでユーザ情報を取得
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
@Component
public class LoginRestClient {
final static RestTemplate rs = new RestTemplate();
@Autowired
private ConfigComponent cc;
public LineToken getToken(String code) throws IOException {
code = Objects.requireNonNull(code);
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth(cc.getClientId(), cc.getClientSecret());
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
params.add("code", code);
params.add("redirect_uri", cc.getRedirecturi());
params.add("grant_type", "authorization_code");
HttpEntity<MultiValueMap<String, Object>> requestEntity
= new HttpEntity<>(params, headers);
LineToken response = rs
.postForObject(cc.getAccessTokenUri(), requestEntity, LineToken.class);
return response;
}
public ResponseEntity<String> getUserInfo(String accessToken) throws IOException {
accessToken = Objects.requireNonNull(accessToken);
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
HttpEntity<MultiValueMap<String, Object>> requestEntity
= new HttpEntity<>(headers);
ResponseEntity<String> response =
rs.exchange(cc.getUserInfoUri(), HttpMethod.GET, requestEntity, String.class);
System.err.println(response);
return response;
}
}
#####2-7. アクセストークン/リフレッシュトークン/IDトークンをPOJO変換
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import static java.util.Objects.requireNonNull;
public final class LineToken {
public final String accessToken;
public final String idToken;
public final String refreshToken;
@JsonCreator
LineToken(
@JsonProperty("access_token") String accessToken,
@JsonProperty("id_token") String idToken,
@JsonProperty("refresh_token") String refreshToken
) {
this.accessToken = requireNonNull(accessToken, "accessToken");
this.idToken = requireNonNull(idToken, "idToken");
this.refreshToken = requireNonNull(refreshToken, "refreshToken");
}
public String getAccessTK() {return this.accessToken;}
public String getIdTK() {return this.idToken;}
public String getrefreshTK() {return this.refreshToken;}
}
####3. 検証
#####3-1. https://localhost:8443/loginへアクセス
#####3-3. ログイン実行
- リフレッシュトークンにより、アクセストークンの有効期限満了後(30日後)も、当該トークンのリクエストによりアクセストークンの再発行が可能。
- リフレッシュトークンはアクセストークン満了後、最長10日間有効。
#####3-4. id_tokenをデコード
ID TokenがJWT形式で返却されているため、ヘッダー・ペイロード部分をBase64デコードし、中身を参照
ヘッダー:
{"typ":"JWT","alg":"HS256"}
ペイロード:
{"iss":"https://access.line.me","sub":"**********","aud":"1653797412","exp":1579957272,"iat":1579953672,"amr":["linesso"],"name":"Yu","picture":"https://profile.line-scdn.net/0ho4KGrFFFMBt2PhvCq9pPTEp7PnYBEDZTDg98LVpsanhTXn4aTFx5Lwdpbn9SWnEYHwgtfFs3Znhd"}
クレーム名 | クレーム内容 |
---|---|
iss | トークンの発行者(ISSuer) |
sub | トークンの対象者(SUBubect) |
aud | トークンの受け手(AUDience) |
exp | トークンの有効期限(EXPiration time) |
iat | トークンの発行時タイムスタンプ(Issued-AT) |