ログインフォーム

タグの編集
投稿者 ど素人  (社会人) 投稿日時 2018/5/22 11:09:54
VB.NETであるプログラムを作成しています。
起動時メニュー画面が立ち上がる前にログイン画面を立ち上げるというのを用意されている機能を使い作成しています。

ログイン時ID,PassがDBに接続してあればログイン可能。そして、この時ID,Passが管理者のものであれば権限を持たせるというものを作りたいです。(フラグなどで)

Public Class LoginForm

    Private user1() As String = {"a", "a"}

    Private Sub OK_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles OK.Click
        If UsernameTextBox.Text = user1(0) AndAlso
            PasswordTextBox.Text = user1(1) Then
            MessageBox.Show("ユーザー名とパスワードを受け付けました。")
            Me.Close()

        Else
            MessageBox.Show("認証できません。")
            UsernameTextBox.Clear()
            PasswordTextBox.Clear()
            UsernameTextBox.Focus()
        End If
    End Sub

    Private Sub Cancel_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Cancel.Click
        Application.Exit()
    End Sub

    Private Sub Login_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Me.ControlBox = False
    End Sub
End Class

今現在は指定したID,Passならログイン可能となっています。
これをDB参照+権限を持たせるという風にしたいです。

VB.NETは初めての言語でさっぱりなのでお願いします。

VS2013 DB:SQLserver 
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2018/5/23 01:54:31
通常、パスワードは平文で保存するべきではなく、
ソルト+ハッシュ処理を施してから(できればスクレイピングも)
格納するべきとされます。ソルトやハッシュという言葉はご存知でしょうか。

一例:
https://www.websec-room.com/2013/02/27/239


> この時ID,Passが管理者のものであれば権限を持たせるというものを作りたいです。(フラグなどで)
たとえばデータベース上のテーブルに対して
 ID列
 Password 列(平文は避ける)
 権限列
という列定義にしておき、これを取得する感じですかね。

「権限」の管理方法をどのように考えるかは、一概には言えません。
単純なフラグ管理(管理者 or 一般ユーザー、とか、読取専用 or 読書可能 or アクセス不可、とか)で済ませる場合もあれば、ロールベースのアクセス制御にする場合もありますし、もっと細かなパーミッション設定をとることもあります。このあたりの匙加減は要件次第といったところです。


でもってデータベースに対しては、先に用意しておいたテーブルの値を得るために
 SELECT Password, 権限 FROM テーブル WHERE ID = @入力された値
のような、バインド変数を通じた問い合わせを行います。

どこまで理解できているのか分かりませんが、データベースの操作に
慣れていないのであれば、まずは下記などを参考にして、ADO.NET の
利用方法を学んでみてください。
https://code.msdn.microsoft.com/windowsdesktop/10-ADONET-VB-653328c7
http://www.my-hobby.jp/2010/01/20/vbnetdb/
投稿者 ど素人  (社会人) 投稿日時 2018/5/23 11:27:39
魔界の仮面弁士さま

>ソルトやハッシュという言葉はご存知でしょうか。
言葉自体は知ってはいるのですがまだ未熟で組んだこともないので今回はしないということで進めさせてください。

>「権限」の管理方法をどのように考えるかは、一概には言えません。
>単純なフラグ管理(管理者 or 一般ユーザー、とか、読取専用 or 読書可能 or アクセス不可、とか)で済ませる場合もあれば、ロールベースのアクセス制御にする場合もありますし、もっと細かなパーミッション設定をとることもあります。このあたりの匙加減は要件次第といったところです。
管理者or一般ユーザーという感じでログイン成功後メニュー画面で検索、登録などのボタンを作り管理者でないと登録のボタンは作動しないという感じで考えています

Imports System.Data.SqlClient

Public Class LoginForm

    Private Sub OK_Click(sender As Object, e As EventArgs) Handles OK.Click

        Dim cnstr As String = "<接続文字列>"

        Dim cn = New SqlConnection(cnstr)
        cn.Open() ' 接続のオープン

        Dim sqlQuery As String = "SELECT Password, 権限 FROM Table WHERE ID = @Id.Text"
        Dim cmd As New SqlCommand(sqlQuery, cn) ' コマンドの作成

        Dim rd As SqlDataReader = cmd.ExecuteReader() ' コマンドの実行

        Dim Pass As String
        Dim Flag As String = 0

        While rd.Read() ' 1 行読み込み
            Pass = ※取得したもの
            Flag = ※取得したもの
        End While

        rd.Close()
        cn.Close() ' 接続のクローズ

        If Password.Text = Pass Then
            MessageBox.Show("ユーザー名とパスワードを受け付けました。")
            Me.Close()

        Else
            MessageBox.Show("認証できません。")
            Id.Clear()
            Password.Clear()
            Username.Focus()
        End If
    End Sub

End Class

参考を元にわかる範囲で作ってみたのですがどうでしょうか?
全体像としてはこんな感じとなるのですが、、、
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2018/5/24 02:57:35
> 今回はしないということで進めさせてください。
今後の課題ということですね。一応指摘してみたというだけで、特に強制するというものではありません。
(標準の MembershipProvider でも、平文モード/暗号化モード/ハッシュモードが選択できますし)


> 参考を元にわかる範囲で作ってみたのですがどうでしょうか?
概要としてはそれで良いと思いますが、以下、細かい点について。


> Dim Flag As String = 0
代入式の左辺と右辺の型が一致していませんね。

実際の SQL Server 側の権限列の型にもよりますが、
 Dim Flag As String = "0"
あるいは
 Dim Flag As Integer = 0
などではありませんか?


> Dim cn = New SqlConnection(cnstr)
参考にされた元コードは、実行後即座に終了するコンソールアプリでしたが
今回は実行後もアプリが動き続ける Windows Forms アプリなので、
Connection / Command / DataReader に関しては、
Using ブロックで囲った方が良いかも知れません。


> While rd.Read() ' 1 行読み込み
>  Pass = ※取得したもの
>  Flag = ※取得したもの
> End While
ここは While ループである必要は無いですね。

ID が主キーとなる場合、抽出結果は 1 件または 0 件なので、
ループで繰り返し取得する必要は無く、If で十分なはずです。

If rd.Read() Then
 Pass = rd.GetString("Password")
 Flag = rd.GetString("権限")
Else
 '該当者がいない場合の処理
End If



> If Password.Text = Pass Then
>   MessageBox.Show("ユーザー名とパスワードを受け付けました。")
>   Me.Close()
> Else
これだと、OK ボタンを押さずに、画面右上の×で閉じた場合にも
認証処理が通過してしまうのではありませんか?
投稿者 ど素人  (社会人) 投稿日時 2018/5/24 10:47:37
魔界の仮面弁士さま

 >Connection / Command / DataReader に関しては、Using ブロックで囲った方が良いかも知れませ.ん。
Usingブロックに関して調べて囲ったほうがいいということはわかったのですがどのように囲えばいいでしょうか?

>これだと、OK ボタンを押さずに、画面右上の×で閉じた場合にも認証処理が通過してしまうのではありませんか? 
最初のようにControlBoxをなくしボタンで終了するようにしました。

・指摘されたとこを修正後

 Private Sub OK_Click(sender As Object, e As EventArgs) Handles OK.Click

        Dim cnstr As String = "<接続文字列>"

        Dim cn = New SqlConnection(cnstr)
        cn.Open() ' 接続のオープン

        Dim sqlQuery As String = "SELECT Password, 権限 FROM [dbo].[Table] WHERE ID = @Id.Text"
        Dim cmd As New SqlCommand(sqlQuery, cn) ' コマンドの作成

        Dim rd As SqlDataReader = cmd.ExecuteReader() ' コマンドの実行

        Dim Pass As String
        Dim Flag As String = "0"

        If rd.Read() Then 
            Pass = rd.GetString("Password")
            Flag = rd.GetString("権限")
        End If

        rd.Close()
        cn.Close() ' 接続のクローズ

        If Password.Text = Pass Then
            MessageBox.Show("ユーザー名とパスワードを受け付けました。")
            Me.Close()

        Else
            MessageBox.Show("認証できません。")
            Id.Clear()
            Password.Clear()
            Username.Focus()
        End If
    End Sub

現状これを動かすと        
Dim sqlQuery As String = "SELECT Password, 権限 FROM [dbo].[Table] WHERE ID = @Id.Text"

型 'System.Data.SqlClient.SqlException' のハンドルされていない例外が System.Data.dll で発生しました

追加情報:Must declare the scalar variable "@Username

Dim sqlQuery As String = "SELECT Password, 権限 FROM [dbo].[Table] WHERE ID = Id.Text"

型 'System.Data.SqlClient.SqlException' のハンドルされていない例外が System.Data.dll で発生しました

追加情報:The multi-part identifier "Id.Text" could not be bound.

となります

投稿者 (削除されました)  () 投稿日時 2018/5/24 13:07:17
(削除されました)
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2018/5/24 13:12:51
>> Connection / Command / DataReader に関しては、Using ブロックで囲った方が良いかも知れませ.ん。
> Usingブロックに関して調べて囲ったほうがいいということはわかったのですがどのように囲えばいいでしょうか?
Dim cn As New SqlConnection(cnstr)
  :
cn.Close()
の部分を
Using cn As New SqlConnection(cnstr)
  :
 cn.Close()  'Close は呼んでも呼ばなくても良い 
End Using
のようにします。SqlCommand / SqlDataReader も同様にして
「Using cmd As New SqlCommand(sqlQuery, cn)」などとします。

もしくは Using ブロックの代わりに Try~Finally を使うこともできます。


> ・指摘されたとこを修正後
Password.Text を空欄( "" ) にしておき、Id に「存在しない値」が指定された場合、
今の実装だと、Dim Pass As String の内容は Nothing のままなので、
 If Password.Text = Pass Then
のチェックを通過してしまいます。これだと認証をすり抜けてしまいませんか?

なので前回のコードでは、Read メソッドの呼び出しに対する Else 句を用意していたわけで。



> Dim sqlQuery As String = "SELECT Password, 権限 FROM [dbo].[Table] WHERE ID = @Id.Text"

ID 値は Id という名の TextBox から入力するのでしょうか。
だとしたらこんな感じ。

Dim sqlQuery As String = "SELECT Password, 権限 FROM [dbo].[Table] WHERE ID = @Id"
Using cmd As New SqlCommand(sqlQuery, cn)
    cmd.Parameters.Add("@Id", SqlDbType.VarChar, 8).Value = Id.Text
    Using rd = cmd.ExecuteReader()
  '  : 
    End Using
End Using


上記は、ID 列が VACHAR(8) 型のフィールドだった場合のコードなので、
適宜、実際のデータベース定義にあわせて修正してみてください。
投稿者 ど素人  (社会人) 投稿日時 2018/5/24 14:48:32
>Password.Text を空欄( "" ) にしておき、Id に「存在しない値」が指定された場合、
>今の実装だと、Dim Pass As String の内容は Nothing のままなので、
> If Password.Text = Pass Then
>のチェックを通過してしまいます。これだと認証をすり抜けてしまいませんか?

すり抜けてしまいました。
すみません!処理をどうしようと考えて書くの忘れていました。

・修正後
    Private Sub OK_Click(sender As Object, e As EventArgs) Handles OK.Click

        Dim Pass As String
        Dim Flag As Integer = 0

        Dim cnstr As String = "<接続文字列>"

        Using cn = New SqlConnection(cnstr)
            cn.Open() ' 接続のオープン

            Dim sqlQuery As String = "SELECT Password, 権限 FROM [dbo].[Table] WHERE ID = @Id"
            Using cmd As New SqlCommand(sqlQuery, cn) ' コマンドの作成
                cmd.Parameters.Add("@Id", SqlDbType.VarChar, 50).Value = Id.Text

                Using rd As SqlDataReader = cmd.ExecuteReader() ' コマンドの実行

                    If rd.Read() Then
                        Pass = rd.GetString("Password")
                        Flag = rd.GetString("権限")
         Else
                    End If

                    rd.Close()  'Close は呼んでも呼ばなくても良い 
                End Using
            End Using

            cn.Close()  'Close は呼んでも呼ばなくても良い 
        End Using


        If Password.Text = Pass Then
            MessageBox.Show("ユーザー名とパスワードを受け付けました。")
            Me.Close()

        Else
            MessageBox.Show("認証できません。")
            Id.Clear()
            Password.Clear()
            Username.Focus()
        End If
    End Sub

>Dim Pass As String = ""
>Dim Flag As Integer = 0
はPass = rd.GetString("Password")で宣言がないとなるのでだしました。


実行しますとPass = rd.GetString("Password")の部分で下記のようになります。入力は(a,a)など

型 'System.InvalidCastException' のハンドルされていない例外が Microsoft.VisualBasic.dll で発生しました
追加情報:String "Password" から型 'Integer' への変換は無効です。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2018/5/24 16:31:15
データ型を意識したコーディングが行われていないように見えます。

Form の先頭に、「Option Strict On」という宣言を付けておき、
それでコンパイルが通るようなコードになるよう、見直してみてください。
(現状は、コンパイル オプションが Option Strict Off になっているはず)


>> これだと認証をすり抜けてしまいませんか?
> すり抜けてしまいました。

なのでこの場合、「If Password.Text = Pass Then」の判定処理を末尾に置くのではなく、
それを If rd.Read() Then のブロック内に移動させてしまえば良いと思います。


ついでに、「該当する Id が無い」状態と、「Id は正しいがパスワードが間違っている」状態を
区別するか同一視するかも、アプリケーション設計時点で決めておきましょう。


> Dim Flag As Integer = 0
> Flag = rd.GetString("権限")

えぇと…。
何故、Integer 型の変数に、String 型の値を代入しようとしているのでしょうか。

そもそも、データベース側の [権限]フィールドのデータ型は何になっていて、
そこには何という値が入っているのでしょうか。


> 追加情報:String "Password" から型 'Integer' への変換は無効です。 

これは文字通りの意味でしょうね。
極端な話、下記のようなコードを書けば、まったく同じエラーが再現されます。

たとえば、"123" という文字列を Integer にすることならできるかもしれませんが、
"Password" という文字列を Integer にすることは、当然できないわけで…。

Dim x As String = "Password"
Dim y As Integer = x  '【InvalidCastException】 
' 「String "Password" から型 'Integer' への変換は無効です。」 
投稿者 ど素人  (社会人) 投稿日時 2018/5/24 17:10:54
魔界の仮面弁士さま

>なのでこの場合、「If Password.Text = Pass Then」の判定処理を末尾に置くのではなく、
>それを If rd.Read() Then のブロック内に移動させてしまえば良いと思います。

修正しました
    
If rd.Read() Then
        Pass = rd.GetString("Password")
         Flag = rd.GetString("権限")

         If Password.Text = Pass Then
                 MessageBox.Show("ユーザー名とパスワードを受け付けました。")
                 Me.Close()

         Else
                 MessageBox.Show("認証できません。")
                 Id.Clear()
                 Password.Clear()
                 Username.Focus()
        End If

        Else
                '該当者がいない場合の処理
        End If

>ついでに、「該当する Id が無い」状態と、「Id は正しいがパスワードが間違っている」状態を区別するか同一視するかも、アプリケーション設計時点で決めておきましょう。

そこまで考えが回っていませんでした。勉強になります。

>何故、Integer 型の変数に、String 型の値を代入しようとしているのでしょうか。
>そもそも、データベース側の [権限]フィールドのデータ型は何になっていて、
>そこには何という値が入っているのでしょうか。

すみませんいじっていた際ミスしたものでした。
Dim Flag As String = "0"
Flag = rd.GetString("権限")
となっています。

投稿したものだと確かにおかしいのはわかるんですが、その前にPass = rd.GetString("Password")の部分で前のエラーが出ているのです。

Passwaod、権限どちらもvarchar(50)です。
値は現状Password{a、b}、権限{0、1}です。


投稿者 魔界の仮面弁士  (社会人) 投稿日時 2018/5/24 17:31:31
> If rd.Read() Then
>   Pass = rd.GetString("Password")
>   Flag = rd.GetString("権限")
>   If Password.Text = Pass Then
>     MessageBox.Show("ユーザー名とパスワードを受け付けました。")
>     Me.Close()

この場合、Pass や Flag 変数はどこで宣言していますか?

Pass については再利用する必要が無いので、ローカル変数で十分ですが、
Flag は、OK_Click 以外の場所でも利用される可能性があるものですから、
フィールド変数なり、Shared にするなりしないといけないですよね。


> その前にPass = rd.GetString("Password")の部分で前のエラーが出ているのです。

・その時に発生している例外も「InvalidCastException」で、メッセージも完全に同一の
 「String "Password" から型 'Integer' への変換は無効です。」 なのですか?

・代入操作前に、MsgBox( "[" & rd.GetString("Password") & "]" ) を実行すると、
 画面上には何が表示されますか?

・左辺の Pass が As String であることは間違いないですか?
投稿者 ど素人  (社会人) 投稿日時 2018/5/24 17:44:45
魔界の仮面弁士さま

>この場合、Pass や Flag 変数はどこで宣言していますか?
今はクラスの下です(ボタンの処理の上)

>その時に発生している例外も「InvalidCastException」で、メッセージも完全に同一の
>「String "Password" から型 'Integer' への変換は無効です。」 なのですか?
同じものですね

>代入操作前に、MsgBox( "[" & rd.GetString("Password") & "]" ) を実行すると、
>画面上には何が表示されますか?
表示がされる前に同じエラーがでます

>左辺の Pass が As String であることは間違いないですか? 
間違いありません
投稿者 (削除されました)  () 投稿日時 2018/5/24 17:56:44
(削除されました)
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2018/5/24 18:08:11
>> 代入操作前に、MsgBox( "[" & rd.GetString("Password") & "]" ) を実行すると、
>> 画面上には何が表示されますか?
> 表示がされる前に同じエラーがでます

コンパイルエラーではなく、実行時エラーになるのですね。

ということは、先に述べたコンパイルオプションの
「Option Strict On」を指定されていないのではないでしょうか。

SqlDataReader の GetString メソッドは、引数として列名(String)を渡すのではなく、
引数として列番号(Integer)を渡す仕様になっていますので、そこに
"Password" という文字列を渡すわけにはいかない…ということです。


たとえば下記の場合は、Option Strict が On/Off いずれでも同じ動作になると思います。

MsgBox( "[" & rd.GetString(0) & "]" )
MsgBox( "[" & rd.GetString(1) & "]" )
MsgBox( "[" & rd.GetOrdinal("Password") & "]" )
MsgBox( "[" & rd.GetString(rd.GetOrdinal("Password")) & "]" )
MsgBox( "[" & CStr(rd.Item("Password")) & "]" )
MsgBox( "[" & CStr(rd("Password")) & "]" )
MsgBox( "[" & CStr(rd!Password) & "]" )
投稿者 ど素人  (社会人) 投稿日時 2018/5/25 10:17:22
魔界の仮面弁士さま

>SqlDataReader の GetString メソッドは、引数として列名(String)を渡すのではなく、引数として列番号(Integer)を渡す仕様になっていますので、そこに"Password" という文字列を渡すわけにはいかない…ということです。
列番号(Integer)を渡すのに列名(String)で渡そうとしているからエラーがでているのですね!

下記のように直したらいけました!
Pass = rd.GetString(0)
Flag = rd.GetString(1)

1つ聞きたいのですが最初はこのようにしていたのですが
Pass = rd.GetString(1)
Flag = rd.GetString(2)
列範囲外とエラーが出たのですが主キー含まれないのでしょうか?
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2018/5/25 11:36:54
GetString メソッドは、0 から始まる列番号を指定します。
列番号の最大値は、検索結果のフィールド数 - 1 です。


実行した SQL が
 SELECT [権限] FROM [dbo].[Table] WHERE [ID] = @ID AND [Password] = @pwd
なのであれば、
 第 0 列が 権限 フィールド
となります。

実行した SQL が
 SELECT [権限], [ID], [Password] FROM [dbo].[Table] WHERE [ID] = @ID AND [Password] = @pwd
なら、
 第 0 列が 権限 フィールド
 第 1 列が ID フィールド
 第 2 列が Password フィールド
です。
投稿者 ど素人  (社会人) 投稿日時 2018/5/25 13:21:46
 魔界の仮面弁士 

>GetString メソッドは、0 から始まる列番号を指定します。
>列番号の最大値は、検索結果のフィールド数 - 1 です。

だから今回
Dim sqlQuery As String = "SELECT Password, 権限 FROM [dbo].[Table] WHERE ID = @Id"
としているため
第 0 列が Password フィールド
第 1 列が 権限 フィールド
となっているのですね!

勉強になりました。ありがとうございます。