コラム

Vue.jsで権限により表示可能画面の制御

Vue.js、Spring Bootシステムでの認証方法の続きで今回は認証で取得した権限毎にアクセス可能な画面を用意し、アクセス制御してみたいと思います。
前回作成した物に追加、修正を加えて行きたいと思います。

Vue.js側

処理の流れ

  • 今回は前回作成したモジュールに商品画面と権限エラー画面、ヘッダー部にメニューを追加し、画面遷移の為にVue Routerも使用します。
  • ログイン時にサーバから取得した権限情報をVuexに設定します。
  • メニューには権限に応じたボタンが表示されます。adminの場合はユーザ画面、商品画面のボタン。guestの場合は商品画面へのボタンを表示します。
  • Vue Routerでは直接URL指定された時の為に指定URLに移行する権限があるかチェックし、問題なければそのまま画面遷移、権限が無い場合は権限エラー画面へ遷移します。

作成モジュール一覧

Vue.js、Spring Bootシステムでの認証方法からのVue.js部分の追加モジュールです。
今回Spring Boot部分の追加部分はありません。

モジュール名追加・修正説明
App.vue追加ヘッダーを既存のApp.vueに入れる
このvueに<router-view/>タグを入れてVue Routerで遷移した画面が表示される。
router/index.js追加Vue Routerを使用しての画面遷移部分の記述、URLと権限の関係をチェックし、問題があればAuthorityError.vueへ遷移する処理を記述する
GoodsList.vue追加権限管理の動きを確認する画面が必要なので、何の画面でも良いですが、今回は商品リスト画面を用意します
GoodsRepository.js追加商品情報をサーバから取得するモジュール
AuthorityError.vue追加権限エラーを検知した際に遷移する画面です
AccountRepository修正ログアウト部分を追加する

実装

App.vue

画面のstyleは見やすいように定義しています。

<template>
  <div>
    <header class="header">
     <div class="headerNav">
      <nav>
     <!--ログイン画面の場合はメニューを表示しない -->
        <ul v-show="$route.path !== '/'" class="b">
           
          <!-- Vuexのロール情報を取得し、adminの場合のみユーザ一覧へのリンクを表示 -->
          <li v-show="store.getters.getRole=='admin'"><a href="#" @click.prevent.stop="trasPage('accountList')">ユーザ一覧</a></li>
          
          <!-- 商品一覧とログアウトは全てのユーザが閲覧可能 --> 
          <li><a href="#" @click.prevent.stop="trasPage('goodsList')">商品一覧</a></li>
          <li><a href="#" @click.prevent.stop="logout">ログアウト</a></li>
        </ul>
      </nav>
      </div>
    </header>
   
    <!--Vue Router用のタグ、URLに対応した画面を表示 -->
    <router-view/>
    
  </div>
</template>

<script>

import { useStore } from 'vuex'
import { useRouter  } from 'vue-router'
import accountRepository from './composables/AccountRepository'

export default ({
  name: 'App',
  setup(){
  
     const router = useRouter()
     const store = useStore()
     const { logoutAccount } = accountRepository()
     
     //メニューのリンクが押下された時に呼ばれる
     //router.pushメソッドで画面遷移依頼
     const trasPage = ( page ) => {
      if( page == "goodsList"){
        router.push("/goodsList")
      }else if( page == "accountList"){
        router.push("/accountList")
      }
    }

  //ログアウト処理用のコールバック
    const callback = ()=>{
      router.push("/")
    }

    //ログアウト押下時の処理
    const logout = ()=>{
      logoutAccount( callback )
    }
  
    return{
      trasPage,
      store
    }
  }
})
</script>

<style>

.header{
  display: flex;
  width: 100%;
  height:90px;
  padding: 0%;
  justify-content: space-between;
  background-color:#ff69b4;
  maring: 0px 0%;
}

.b{
  display: flex;
  font-size: 18px;
  text-transform: uppercase;
  margin-top: 30px;
  margin-right: 30px;
  list-style: none;
}
.b li{
  margin-left: 30px;
}
.b li a{
  color: #0000ff;
  text-decoration: none;
}

.headerNav{
  display: flex;
  width: 100%;
  height:90px;
  padding: 0%;
  justify-content: space-between;
   maring:0%;
}

#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
}

#nav a.router-link-exact-active {
  color: #42b983;
}
</style>

・router/index.js

import { createRouter, createWebHistory } from 'vue-router'

//ログイン、ユーザ一覧、商品一覧、権限エラーの4つの画面
//の遷移を管理する
import Login from '../views/Login.vue'
import AccountList from '../views/AccountList.vue'
import GoodsList from '../views/GoodsList.vue'
import AuthorityError from '../views/AuthorityError.vue'

import store from '@/store/index';

const routes = [
  {
    path: '/',
    name: 'login',
    component: Login
  },
  {
    path: '/accountList',
    name: 'accountList',
    component: AccountList
  },
  {
    path: '/goodsList',
    name: 'goodsList',
    component: GoodsList
  },
  {
    path: '/authorityError',
    name: 'authorityError',
    component: AuthorityError
  },
]

//historyモードでルーター作成
const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
});

//画面遷移が行われる前に呼ばれるメソッド
//ここでURL直接入力された時などのために、権限チェックを行う
router.beforeEach((to, from, next) => {

 //パスがログインの場合はそのまま遷移
 if( to.path == "/"){
   next()
   return
 }
 
 //非ログイン状態の時はログイン画面へ遷移
 if( !store.getters.getIsLogin ){
   router.push("/")
 }else{
   //ログイン状態の時
  //権限がguestでユーザ一覧画面へ遷移しようとしてる場合は権限エラー画面に遷移
    //それ以外の時はそのまま遷移
   var role = store.getters.getRole
   if( role == "guest" && ( to.path == "/accountList" ) ){
     router.push("/authorityError")
   }else{
     next()
   }
 }
})
export default router

router.beforeEachメソッドは他の箇所に実装する事も可能です。例を下に記述します。

<template>
 省略...
</template>
<script>

import { ref } from 'vue'
import { useRouter  } from 'vue-router'

export default ({
  name: 'App',
  setup(){
    const router = useRouter()
 
   router.beforeEach((to, from, next) => {
     //パスがログインの場合はそのまま遷移
     if( to.path == "/"){
       next()
       return
     }
 
     //非ログイン状態の時はログイン画面へ遷移
     if( !store.getters.getIsLogin ){
       router.push("/")
     }else{
       //ログイン状態の時
     //権限がguestでユーザ一覧画面へ遷移しようとしてる場合は権限エラー画面に遷移
       //それ以外の時はそのまま遷移
       var role = store.getters.getRole
       if( role == "guest" && ( to.path == "/accountList" ) ){
         router.push("/authorityError")
       }else{
         next()
       }
     }
   })
  }
})

・GoodsList.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>コード</th>
						<th>名前</th>
						<th>カテゴリ</th>
						<th>価格</th>
					</tr>
				</thead>
				<tbody>
					<tr  v-for="(data) in goodsList" :key="data.id">
						<td>{{ data.code }}</td>
						<td>{{ data.name }}</td>
						<td>{{ data.category }}</td>
						<td>{{ data.price }}</td>
					</tr>
				</tbody>
			</table>
		</div>
		
	</div>

</template>

<script>
import { ref } from 'vue'
import goodsRepository from '../composables/GoodsRepository'

export default ({
  name: 'GoodsList',
  setup(){
    //商品情報リスト
    const goodsList = ref([])

    //商品取得結果メッセージ
    const message = ref(null)

    //goodsRepositoryモジュールからgetGoodsメソッドを取得
    const { getGoods } = goodsRepository()
    
    //データ取得時のコールバック( GoodsRepositoryのgetGoodsメソッドに渡す)
    const callback = ( result, data )=>{
      //取得成功の場合はgoodsListに取得データを設定
      if( result == "success"){
        message.value = "データ取得完了"
        goodsList.value = data
      }else{
        message.value = result
      }
    }
    
    //ユーザ情報取得
    getGoods( callback )
    
    return{
      goodsList,
      message
    }  
  }
})

</script>

・GoodsRepository

import store from '@/store/index';
import * as axios from 'axios'

export default function (){

  //商品情報取得
  const getGoods = async( callback ) => {
    try{
      //Spring Bootにaxiosで商品取得依頼
      var token = store.getters.getAuthToken
      console.log( token )
      var goodsList = await requestServerGetWidthToken ( "api/goods/list", token)

      //コールバックに結果を返す
      callback( "success", goodsList )
    }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{
    getGoods
  }
}	

・AuthorityError.vue

<template>
	<div class="contentMainDiv">
		<div style="color:red;">このページにアクセスする権限がありません</div>	
	</div>
</template>

・AccountRepository

ログアウト機能を追加します。(追加部分のみ追記)

  const logoutAccount  = async( callback ) => {
    try{ 
      //サーバにログアウト依頼
      await requestServerPost("logout")
      
      //ログイン、権限、トークン情報を初期化
      store.commit("setIsLogin", false)
      store.commit("setRole", "")
      store.commit("setAuthToken", "")
      
      callback("success")
    }catch(e){
      callback(e)
    }
  }

  return{
    loginAccount,
    getAccount,
    logoutAccount
  }

以上がVue.js側の実装になります。

Spring Boot側

前回のクラスの修正と今回分の追加を合わせて記述します。
修正点は権限管理部分になります。RestController側で権限チェックをし、エラーなら例外を返す形となります。

修正・作成クラス一覧

クラス名追加・修正説明
AccountController修正権限チェックをし、エラーなら例外を返すように修正する
GoodsController追加商品情報取得用のRestController
商品情報一覧画面から呼ばれる。ここは全ユーザがアクセス可能な為権限
チェックを行わない
GoodsRepository追加DBのGoodsテーブルへクエリーを掛けるクラス
Goods追加DBのGoodsテーブルに対応するエンティティクラス
RoleAuthorityException追加権限エラーの際に投げる例外クラス

実装

・AccountController

此方はadmin権限のみ閲覧可能なので、メソッド内で権限チェックをする処理を追加する。

package com.example.authsample.ac;

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.authsample.entity.Account;
import com.example.authsample.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() throws Exception{

		//権限がadmin以外からの受付を拒否する処理
		//セッションから権限情報を取得
		String role = ( String )SecurityContextHolder.getContext().getAuthentication().getAuthorities().iterator().next().getAuthority();
		//ダブルクォート削除処理
		role = role.replaceAll("\"", "");

    //	権限がadminでない場合は例外を発生
		if( !role.equals("admin")) {
			throw new RoleAuthorityException("アクセス権限がありません。");
		}
		return this.accountRepository.findAll();
	}
}

・GoodsController

此方は全ユーザで閲覧可能なのでチェック無し。

package com.example.authsample.ac;

import org.springframework.beans.factory.annotation.Autowired;
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.authsample.entity.Goods;
import com.example.authsample.repository.GoodsRepository;

import reactor.core.publisher.Flux;


@RestController
@RequestMapping("/api/goods")
@CrossOrigin
public class GoodsController {


	@Autowired
	private GoodsRepository goodsRepository;

	/**
	* 商品リストを取得
	* @return
	*/
	@RequestMapping(value = "/list", method = RequestMethod.GET)
	public Flux<Goods> getUserList() throws Exception{

		return this.goodsRepository.findAll();

	}
}

・GoodsRepository

package com.example.authsample.repository;

import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import com.example.authsample.entity.Goods;

public interface GoodsRepository extends ReactiveCrudRepository<Goods, Integer> {
}

・Goods

package com.example.authsample.entity;

import java.math.BigDecimal;
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("goods")
public class Goods {
	@Id
	private Long id;
	private String code;
	private String name;
	private String category;
	private BigDecimal price;
}

以上で権限のチェック処理を追加した例です。
他にも色々方法はあると思いますが、今私がやっている方法を紹介しました。
今回の認証チェック、権限チェックのサンプルは此方からダウンロード可能です。

この記事をシェアする
  • Facebookアイコン
  • Twitterアイコン
  • LINEアイコン

お問い合わせ ITに関するお悩み不安が少しでもありましたら、
ぜひお気軽にお問い合わせください

お客様のお悩みや不安、課題などを丁寧に、そして誠実にお伺いいたします。

お問い合わせはこちら
お電話でのお問い合わせ 03-5820-1777(平日10:00〜18:00)
よくあるご質問