- 03-5820-1777平日10:00〜18:00
- お問い合わせ
今回はクライアント側をVue.js、サーバ側をSpringBootを使用した場合のログインでの認証、各アクセスに対しての認証方法の例を書いていきたいと思います。
作成クラス一覧
以下、今回作成するクラス一覧になります。
DBアクセスは今回はR2DBCを使用していますが、認証に関しては効果は発揮できないので違う方法でも良いと思います。
作成クラス | 説明 |
---|---|
AuthenticationFilter | UsernamePasswordAuthenticationFilterクラスを継承するFilterクラス ログイン時に呼ばれれ、認証依頼を掛ける 認証成功時のトークン作成や、ログインのパス指定などをする |
CheckTokenFilter | OncePerRequestFilterクラスを継承するFilterクラス ログイン後にアクセスされた際のトークンチェックを行う |
UserDetailsServiceImpl | UserDetailsServiceインターフェイスを実装するクラス AuthenticationFilterから呼ばれ、ユーザ認証を行う |
AccountRepository | DBのAccountテーブルへクエリーを掛けるクラス 今回はユーザIDを指定して取得するメソッドと、全てのユーザ情報を取得するメソッドを用意する |
Account | DBのAccountテーブルに対応するエンティティクラス |
SecurityConfig | WebSecurityConfigurerAdapterクラスを継承するConfigurationクラス ここでパスに対する上記のFilterを実行するか、CSRFの設定等を行う |
AccountController | ログイン後の認証を確認する為、今回はAccountControllerというユーザ情報取得のRestコントローラを作成しました。 |
実装
・AuthenticationFilter
//UsernamePasswordAuthenticationFilterを継承
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter{
private AuthenticationManager authenticationManager;
public AuthenticationFilter(AuthenticationManager authenticationManager) {
// AuthenticationManagerの設定
this.authenticationManager = authenticationManager;
// ログインパスを設定
setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/api/login","POST"));
// UserDetailsServiceImplでの認証成功時に呼ばれる
// tokenを発行してレスポンスにセットする
this.setAuthenticationSuccessHandler((req,res,ex) -> {
// トークンの作成
String token = JWT.create()
.withIssuer("distant-view") //発行者
.withIssuedAt( new Date() ) //日付
.withClaim("username", ex.getName()) //keyに対してvalueの設定。様々な値を保持できる
.withClaim("role", ex.getAuthorities().iterator().next().toString())
.sign(Algorithm.HMAC256("secret")); // 利用アルゴリズムを指定してJWTを新規作成
System.out.println( token );
// tokeをX-AUTH-TOKENにセットする
// 認証情報はX-AUTO-TOKENに設定する事が良いみたいです。
// 今回のシステムに関しては違う名前でも機能します。
res.setHeader("X-AUTH-TOKEN", token);
res.setStatus(200); //ステータスを正常に設定
);
//ログイン成功という結果とUserDetailsServiceImplで取得した権限情報をクライアントに返す。
res.getWriter().write("Success@" + SecurityContextHolder.getContext().getAuthentication().getAuthorities().iterator().next() );
});
// ログイン失敗時
this.setAuthenticationFailureHandler((req,res,ex) -> {
//権限エラーを返す
res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
});
}
//このメソッドでリクエストからログイン情報の取得、認証依頼を掛けます。
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
ServletInputStream stream = request.getInputStream();
// リクエストのjsonの値をUserFormにマッピングします。
UserForm form = new ObjectMapper().readValue(request.getInputStream(), UserForm.class);
// ここでデフォルトのProviderを利用し、ユーザ認証に関してはUserDetailsServiceの実装クラスのloadUserByUsernameを利用する
return this.authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(form.getUserid(), form.getPassword(), new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
・UserDetailsServiceImpl
UserDetailsServiceのサブクラスは2つあるとエラーになります。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.stereotype.Service;
import com.example.vuexmulti.entity.Account;
import com.example.vuexmulti.repository.AccountRepository;
import reactor.core.publisher.Mono;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
//カウントDBアクセスへのRepositoryクラス
@Autowired
private AccountRepository accountRepository;
//AuthenticationFilterが呼ばれたら、このメソッドが呼ばれる
@Override
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
try{
//アカウントを取得する。R2DBCなのでMono型でかえって来る。
//DBアクセスに関してはJPA等の通常のアクセスでも問題ありません。
Mono<Account> account = accountRepository.findByUserId(userId);
//Mono<Account>からデータを取得し、Userを作成し返す。
//Userを作成する際にアカウントからユーザID、パスワード、権限情報を設定しています。
return accountRepository.findByUserId(userId).
map( a ->
new User( a.getUserid(),
PasswordEncoderFactories.createDelegatingPasswordEncoder().encode( a.getPassword() ),
Collections.singletonList( new SimpleGrantedAuthority( a.getRole() ) )
)
).block();
}catch (Exception e) {
e.printStackTrace();
throw new UsernameNotFoundException("ユーザーが見つかりません");
}
}
}
・AccountRepository
import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import com.example.vuexmulti.entity.Account;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
//ユーザID用のメソッドだけ定義します。
//全ユーザ取得メソッドは用意されています。
public interface AccountRepository extends ReactiveCrudRepository<Account, Integer> {
@Query("select * from account where userid = :userId")
Mono<Account> findByUserId(String userId);
}
・Account
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table("account")
public class Account {
@Id
private Long id;
private String userid;
private String name;
private String password;
private String role;
private String companyname;
private String department;
}
・CheckTokenFilter
import java.io.IOException;
import java.util.Collections;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
public class CheckTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// クライアント側からX-AUTH-TOKENヘッダーでトークンを渡す実装をするので、
// 此方側もX-AUTH-TOKENヘッダーからトークンを取得します。
String token = request.getHeader("X-AUTH-TOKEN");
// トークンが無い場合は何もせずセッションの状態を維持しない
if(token == null ){
filterChain.doFilter(request,response);
return;
}
// Tokenの検証と認証を行う デコード出来ない場合は例外が発生
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256("secret")).build().verify(token);
// usernameの取得
String username = decodedJWT.getClaim("username").toString();
// roleの取得
String role = decodedJWT.getClaim("role").toString();
// ログイン状態を維持すべく設定する。この処理が無いとアクセスエラーを返すようになります。
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(username,null,Collections.singletonList(new SimpleGrantedAuthority( role ) ) ) );
filterChain.doFilter(request,response);
}
}
・SecurityConfig
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Configuration;
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;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity //SpringSecurityを使うために必要
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
//CORS(クロスオリジンリクエスト)の許可設定、必要ないならしなくて良いです。
//http.cors().configurationSource(this.corsConfigurationSource());
// 認証が必要なURL、必要ないURLを設定
//ログイン関係のURL以外をauthenticatedで認証を必要にしています。
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/api/login").permitAll()
.antMatchers("/api/**").authenticated();
//Filterを設定する
//AuthenticationFilterを呼んだ後、CheckTokenFilterが呼ばれるように設定
http.addFilter(new AuthenticationFilter(authenticationManager()));
http.addFilterAfter(new CheckTokenFilter(), AuthenticationFilter.class);
//csrf対策はトークンでやるので無効にする
http.csrf().ignoringAntMatchers("/**");
//ここからはログアウト用の設定
//ログアウト用のハンドラーを用意する。(今回は特に何もしないので親クラスのメソッドを呼ぶだけ)
HttpStatusReturningLogoutSuccessHandler handler = new HttpStatusReturningLogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
super.onLogoutSuccess(request, response, authentication);
}
};
//ログアウトのURL、セッション削除、ハンドラーの設定をする。
http.logout()
.logoutUrl("/logout")
.invalidateHttpSession(true)
.logoutSuccessHandler( handler );
}
//CORSの設定情報を作成
//Vue.jsが別サーバで動作してなければ、この記述は必要ないです。
private CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
//全てのHTTPメソッドを許可
corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
/全てのヘッダー情報の受信を許可
corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
//追加で許可するヘッダー情報
corsConfiguration.addExposedHeader("X-AUTH-TOKEN");
//CORSを許可するドメイン Vue.js側のサーバ側からのアクセスを可能にする
//例はvue.js側のサーバがlocalhostのポート8081の別サーバで動作しているときは下記のような設定が必要です
corsConfiguration.addAllowedOrigin("http://localhost:8081");
//cookie情報の取得を許可するか
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
// CORSリクエストを許可するURLの形式
corsSource.registerCorsConfiguration("/**", corsConfiguration);
return corsSource;
}
}
・AccountController
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.example.vuexmulti.entity.Account;
import com.example.vuexmulti.repository.AccountRepository;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/api/account")
@CrossOrigin
public class AccountController {
@Autowired
private AccountRepository accountRepository;
/**
* ユーザリストを取得
* @return
*/
@RequestMapping(value = "/list", method = RequestMethod.GET)
public Flux<Account> getUserList() {
return this.accountRepository.findAll();
}
}
作成モジュール一覧
以下が作成モジュール一覧になります。
モジュール名 | 説明 |
---|---|
store/index.js | Vuexを使用してセッションに情報を格納するモジュール ログイン情報、トークン、権限を管理する |
Login.vue | ログイン画面 AccountRepository.jsのログイン処理を呼んでログイン機能を実装 |
AccountRepository.js | ログイン処理や、ユーザ情報をサーバから取得するモジュール |
AccountList.vue | ログイン後の認証を確認する為、今回はユーザ情報一覧画面を作成する ログイン画面でログインが正常に通った場合に遷移し、AccountRepository.jsのユーザ 情報処理を呼んで、ユーザ一覧情報を表示する |
実装
・store/index.js
import { createStore } from 'vuex'
import createPersistedState from 'vuex-persistedstate' //vuex-persistedstateをインストールする必要があります
export default createStore({
state: {
isLogin:false, //ログイン状態
authToken:'', //トークン
role:'' //権限(今回はguestかadmin)
},
mutations: {
setAuthToken(state, value){
state.authToken = value
},
setIsLogin(state, value){
state.isLogin = value
},
setRole(state, value){
state.role = value
}
},
actions: {
setAuthToken: function(commit, value) {
commit('setAuthToken', value)
},
setIsLogin: function(commit, value) {
commit('setIsLogin', value)
},
setRole: function(commit, value) {
commit('setRole', value)
}
},
getters:{
getAuthToken(state){
return state.authToken
},
getIsLogin(state){
return state.isLogin
},
getRole(state){
return state.role
}
},
plugins: [createPersistedState(
{
storage: window.sessionStorage
}
)]
})
・Login.vue
<template>
<div>
<div class="inputItemDiv"><div style="color:red;">{{message}}</div></div>
<div class="inputItemDiv"><div class="inputTitleDiv">ID</div><input class="input" type="text" v-model="loginForm.userid"></div>
<div class="inputItemDiv"><div class="inputTitleDiv">パスワード</div><input class="input" type="password" v-model="loginForm.password"></div>
<div class="inputItemDiv" style="text-align:right">
<button type="button" class="button" @click='cancel'>キャンセル</button>
<button type="button" class="button" @click='login'>ログイン</button>
</div>
</div>
</template>
<script>
//アカウント情報をサーバと通信して取得するモジュール
import accountRepository from '../composables/AccountRepository'
//ルート情報を保持するモジュール
import { useRouter } from 'vue-router'
import {ref} from 'vue';
export default ({
name: 'Login',
setup(){
//画面のuserid、passwordとバインドさせる変数
const loginForm = ref( {userid:'', password:''} )
//ログイン結果のメッセージ表示用
const message = ref(null)
//AccountRepositoryモジュールからの変数、メソッドを宣言
const { loginAccount } = accountRepository()
//ルータ使用変数宣言
const router = useRouter()
//ログインメソッド、ログインボタン押下時に呼ばれる
const login = () =>{
//loginAccountメソッドを呼ぶ、loginAccountは非同期メソッドなので、
//処理終了時に呼ばれるコールバックメソッドを渡す
loginAccount( loginForm.value, callback )
}
//キャンセルメソッド、キャンセルボタン押下時に呼ばれる
const cancel = () =>{
loginForm.value.userid = ''
loginForm.value.password = ''
}
//データ取得時のコールバック( AccountRepositoryのloginAccount メソッドに渡す)
const callback = ( result ) =>{
//処理が成功しならユーザリスト画面に遷移
if( result == "Success"){
router.push("/accountList")
}else{
var data = result.response.data
//ステータス401の場合は下記のエラーを出力し終了
if( data.status == 401 ){
message.value = "ユーザID又はパスワードが間違っています。"
}else{
//それ以外の場合は、エラーを出力し終了
message.value = data.error
}
}
}
//template上で使用する変数を宣言
return{
loginForm,
login,
cancel,
message
}
}
})
</script>
・AccountRepository.js
import store from '@/store/index';
import * as axios from 'axios'
export default function (){
//ログインメソッド
const loginAccount = async( loginForm, callback ) => {
try{
//Spring Bootにaxiosでログイン依頼
var result = await requestServerPost ( "api/login", loginForm )
var results = result .split('@')
//ログイン成功の場合はトークン、ログイン情報、権限をvuexに格納
if( results[0] == "Success"){
store.commit("setIsLogin", true)
store.commit("setRole", results[1])
callback( "Success" )
return
}
//コールバックに結果を返す
callback( result )
}catch(e){
callback(e)
}
}
//axiosのポストメソッドでSpring Boot側へリクエストを掛ける
const requestServerPost = ( url, formData ) =>{
return new Promise((resolve, reject) => {
axios
.post('http://localhost:8080/' + url, formData)
.then(response => {
if( response.headers['x-auth-token'] != null ){
store.commit("setAuthToken", response.headers['x-auth-token'])
}
resolve(response.data)
}).catch(error => {
reject(error)
})
}).catch((e) => {
throw e
})
}
//ユーザ情報取得
const getAccount = async( callback ) => {
try{
//Spring Bootにaxiosでユーザ取得依頼
var token = store.getters.getAuthToken
var accountList = await requestServerGetWidthToken ( "api/account/list", token)
//コールバックに結果を返す
callback( "success", accountList )
}catch(e){
callback(e)
}
}
//axiosのGetメソッドでトークンをヘッダーにセットしSpring Boot側へリクエストを掛ける
const requestServerGetWidthToken = ( url, token ) =>{
return new Promise((resolve, reject) => {
axios
.get('http://localhost:8080/' + url
,{
headers: {
"X-AUTH-TOKEN" : token
}
}
)
.then(response => {
resolve(response.data)
}).catch(error => {
reject(error)
})
}).catch((e) => {
throw e
})
}
return{
loginAccount,
getAccount
}
}
・AccountList.vue
<template>
<div>
<div class="inputItemDiv"><div style="color:red;">{{message}}</div></div>
<div class="table" style="max-height:300px;width:900px;">
<table>
<thead>
<tr>
<th>アカウントID</th>
<th>名前</th>
<th>会社名</th>
<th>部署</th>
</tr>
</thead>
<tbody>
<tr v-for="(data) in accountList" :key="data.id">
<td>{{ data.userid }}</td>
<td>{{ data.name }}</td>
<td>{{ data.companyname }}</td>
<td>{{ data.department }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
import { ref } from 'vue'
import accountRepository from '../composables/AccountRepository'
export default ({
name: 'AccountList',
setup(){
//ユーザ情報リスト
const accountList = ref([])
//ユーザ取得結果メッセージ
const message = ref(null)
//accountRepositoryモジュールからgetAccountメソッドを取得
const { getAccount } = accountRepository()
//ユーザ情報取得
getAccount( callback )
//データ取得時のコールバック( AccountRepositoryのgetAccountメソッドに渡す)
const callback = ( result, data )=>{
//取得成功の場合はaccountListに取得データを設定
if( result == "success"){
message.value = "データ取得完了"
accountList.value = data
}else{
message.value = result
}
}
return{
accountList,
message
}
}
})
</script>
以上が私が使用している認証方法でした。
次回は権限による画面や、SpringBootへのアクセス制御をやっていきたいと思います。
今回と次回を合わせたソースは此方からダウンロードできます。