4
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Spring Boot/SecurityでOAuthクライアントを実装してみた(LINEログイン)

Last updated at Posted at 2020-01-25

####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. 依存ライブラリの追加

pom.xml
    <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・ユーザ名・プロフィール画像)取得のために提供されるエンドポイント

application.yml
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から変数情報を取得

ConfigComponent.java
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に登録

ConfigBean.java
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認証を実行

SecurityConfig.java
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テンプレートへ取得したオブジェクトを渡す

LoginController.java
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ヘッダーに付与することでユーザ情報を取得

LoginRestClient.java
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変換

LineToken.java
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-2. LINEログイン画面へリダイレクト
キャプチャ.PNG

#####3-3. ログイン実行
 - リフレッシュトークンにより、アクセストークンの有効期限満了後(30日後)も、当該トークンのリクエストによりアクセストークンの再発行が可能。
 - リフレッシュトークンはアクセストークン満了後、最長10日間有効。
キャプチャ.PNG
キャプチャ.PNG

#####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)
4
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?