コンテンツへスキップ

開発者ドキュメント

春のセキュリティ:ログイン試行の制限の例

  • 投稿日: 2019-11-05 2019-11-05
  • タグ: login, Spring Security


spring-security-limit-login-attempts-locked、width = 525、height = 480

このチュートリアルでは、Spring Securityのログイン試行を制限する方法を示します。つまり、ユーザーが無効なパスワードで3回以上ログインしようとすると、システムはユーザーをロックしてログインできなくなります。

使用される技術とツール:

  1. Spring 3.2.8.RELEASE

  2. 春のセキュリティ3.2.3.RELEASE

  3. Spring JDBC 3.2.3.RELEASE

  4. Eclipse 4.2

  5. JDK 1.6

  6. Maven 3

  7. MySQLサーバ5.6

  8. Tomcat 7(Servlet 3.x)

このチュートリアルのいくつかの簡単なメモ:

  1. MySQLデータベースが使用されます.

  2. これはSpring Securityのアノテーションに基づいた例です.

  3. 列「accountNonLocked」を持つ「ユーザー」表を作成します.

  4. 無効なログイン試行を保存するための “user__attempts”テーブルを作成します.

  5. Spring JDBCが使用されます.

  6. 返された例外に基づいてカスタムエラーメッセージを表示します.

  7. カスタマイズされた “authenticationProvider”

1.解決策

既存のSpring Securityの認証クラスを確認すると、「ロックされた」機能が既に実装されています。制限ログイン試行を有効にするには、 `UserDetails.isAccountNonLocked`をfalseに設定する必要があります。

DaoAuthenticationProvider.java

package org.springframework.security.authentication.dao;

public class DaoAuthenticationProvider
    extends AbstractUserDetailsAuthenticationProvider {
   //...
}

AbstractUserDetailsAuthenticationProvider.java

package org.springframework.security.authentication.dao;

public abstract class AbstractUserDetailsAuthenticationProvider
    implements AuthenticationProvider, InitializingBean,
    MessageSourceAware {

    private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
        public void check(UserDetails user) {
          if (!user.isAccountNonLocked()) {
              logger.debug("User account is locked");

          throw new LockedException(
              messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked",
                 "User account is locked"), user);
          }
          //...
        }
    }

2.プロジェクトデモ

3.プロジェクトディレクトリ

最終的なプロジェクト構造を見直す(注釈ベース):


spring-security-limit-login-attempts-directory、width = 526、height = 753

4.データベース

ここに、users、user

roles、およびuser

attemptsテーブルを作成するためのMySQLスクリプトがあります。

4.1カラム “accountNonLocked”を持つ “users”テーブルを作成します。

users.sql

CREATE  TABLE users (
  username VARCHAR(45) NOT NULL ,
  password VARCHAR(45) NOT NULL ,
  enabled TINYINT NOT NULL DEFAULT 1 ,
  accountNonExpired TINYINT NOT NULL DEFAULT 1 ,
  accountNonLocked TINYINT NOT NULL DEFAULT 1 ,
  credentialsNonExpired TINYINT NOT NULL DEFAULT 1,
  PRIMARY KEY (username));

4.2 “user__roles”テーブルを作成します。

user__roles.sql

CREATE TABLE user__roles (
  user__role__id int(11) NOT NULL AUTO__INCREMENT,
  username varchar(45) NOT NULL,
  role varchar(45) NOT NULL,
  PRIMARY KEY (user__role__id),
  UNIQUE KEY uni__username__role (role,username),
  KEY fk__username__idx (username),
  CONSTRAINT fk__username FOREIGN KEY (username) REFERENCES users (username));

4.3 “user__attempts”テーブルを作成します。

user__attempts.sql

CREATE TABLE user__attempts (
  id int(11) NOT NULL AUTO__INCREMENT,
  username varchar(45) NOT NULL,
  attempts varchar(45) NOT NULL,
  lastModified datetime NOT NULL,
  PRIMARY KEY (id)
);

4.4テスト用のユーザーを挿入します。

INSERT INTO users(username,password,enabled)
VALUES ('mkyong','123456', true);

INSERT INTO user__roles (username, role)
VALUES ('mkyong', 'ROLE__USER');
INSERT INTO user__roles (username, role)
VALUES ('mkyong', 'ROLE__ADMIN');

5. UserAttemptsクラス

このクラスは、 “user__attempts”テーブルのデータを表します。

UserAttempts.java

package com.mkyong.users.model;

import java.util.Date;

public class UserAttempts {

    private int id;
    private String username;
    private int attempts;
    private Date lastModified;

   //getter and setter

}

DAOクラス

無効なログイン試行を更新するためのDAOクラスは、コメントを読んでください。

UserDetailsDao.java

package com.mkyong.users.dao;

import com.mkyong.users.model.UserAttempts;

public interface UserDetailsDao {

    void updateFailAttempts(String username);
    void resetFailAttempts(String username);
    UserAttempts getUserAttempts(String username);

}

UserDetailsDaoImpl.java

package com.mkyong.users.dao;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;

import javax.annotation.PostConstruct;
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.security.authentication.LockedException;
import org.springframework.stereotype.Repository;

import com.mkyong.users.model.UserAttempts;

@Repository
public class UserDetailsDaoImpl extends JdbcDaoSupport implements UserDetailsDao {

    private static final String SQL__USERS__UPDATE__LOCKED = "UPDATE USERS SET accountNonLocked = ? WHERE username = ?";
    private static final String SQL__USERS__COUNT = "SELECT count(** ) FROM USERS WHERE username = ?";

    private static final String SQL__USER__ATTEMPTS__GET = "SELECT **  FROM USER__ATTEMPTS WHERE username = ?";
    private static final String SQL__USER__ATTEMPTS__INSERT = "INSERT INTO USER__ATTEMPTS (USERNAME, ATTEMPTS, LASTMODIFIED) VALUES(?,?,?)";
    private static final String SQL__USER__ATTEMPTS__UPDATE__ATTEMPTS = "UPDATE USER__ATTEMPTS SET attempts = attempts + 1, lastmodified = ? WHERE username = ?";
    private static final String SQL__USER__ATTEMPTS__RESET__ATTEMPTS = "UPDATE USER__ATTEMPTS SET attempts = 0, lastmodified = null WHERE username = ?";


    private static final int MAX__ATTEMPTS = 3;

    @Autowired
    private DataSource dataSource;

    @PostConstruct
    private void initialize() {
        setDataSource(dataSource);
    }

    @Override
    public void updateFailAttempts(String username) {

      UserAttempts user = getUserAttempts(username);
      if (user == null) {
        if (isUserExists(username)) {
           //if no record, insert a new
            getJdbcTemplate().update(SQL__USER__ATTEMPTS__INSERT, new Object[]{ username, 1, new Date() });
        }
      } else {

        if (isUserExists(username)) {
           //update attempts count, +1
            getJdbcTemplate().update(SQL__USER__ATTEMPTS__UPDATE__ATTEMPTS, new Object[]{ new Date(), username});
        }

        if (user.getAttempts() + 1 >= MAX__ATTEMPTS) {
           //locked user
            getJdbcTemplate().update(SQL__USERS__UPDATE__LOCKED, new Object[]{ false, username });
           //throw exception
            throw new LockedException("User Account is locked!");
        }

      }

    }

    @Override
    public UserAttempts getUserAttempts(String username) {

      try {

        UserAttempts userAttempts = getJdbcTemplate().queryForObject(SQL__USER__ATTEMPTS__GET,
            new Object[]{ username }, new RowMapper<UserAttempts>() {
            public UserAttempts mapRow(ResultSet rs, int rowNum) throws SQLException {

                UserAttempts user = new UserAttempts();
                user.setId(rs.getInt("id"));
                user.setUsername(rs.getString("username"));
                user.setAttempts(rs.getInt("attempts"));
                user.setLastModified(rs.getDate("lastModified"));

                return user;
            }

        });
        return userAttempts;

      } catch (EmptyResultDataAccessException e) {
        return null;
      }

    }

    @Override
    public void resetFailAttempts(String username) {

      getJdbcTemplate().update(
             SQL__USER__ATTEMPTS__RESET__ATTEMPTS, new Object[]{ username });

    }

    private boolean isUserExists(String username) {

      boolean result = false;

      int count = getJdbcTemplate().queryForObject(
                            SQL__USERS__COUNT, new Object[]{ username }, Integer.class);
      if (count > 0) {
        result = true;
      }

      return result;
    }

}

7. UserDetailsS​​ervice

デフォルトでは、

JdbcDaoImpl`は常に

accountNonLocked`をtrueに設定します。これは私たちが望むものではありません。ソースコードを確認します。

JdbcDaoImpl.java

package org.springframework.security.core.userdetails.jdbc;

public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService {
 //...
  protected List<UserDetails> loadUsersByUsername(String username) {
    return getJdbcTemplate().query(usersByUsernameQuery, new String[]{username}, new RowMapper<UserDetails>() {
      public UserDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
        String username = rs.getString(1);
        String password = rs.getString(2);
        boolean enabled = rs.getBoolean(3);
        return new User(username, password, enabled, true, true, true, AuthorityUtils.NO__AUTHORITIES);
      }

  });
}

開発時間を節約するために、

JdbcDaoImpl`を拡張し、

loadUsersByUsername`と

createUserDetails`の両方をオーバーライドしてカスタマイズされた

UserDetails`を得ることができます。

CustomUserDetailsS​​ervice.java

package com.mkyong.users.service;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import javax.annotation.PostConstruct;
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.stereotype.Service;
/** **
 **  Reference org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl
 **
 **  @author mkyong
 **
 ** /@Service("userDetailsService")
public class CustomUserDetailsService extends JdbcDaoImpl {

    @Autowired
    private DataSource dataSource;

    @PostConstruct
    private void initialize() {
        setDataSource(dataSource);
    }

    @Override
    @Value("select **  from users where username = ?")
    public void setUsersByUsernameQuery(String usersByUsernameQueryString) {
        super.setUsersByUsernameQuery(usersByUsernameQueryString);
    }

    @Override
    @Value("select username, role from user__roles where username =?")
    public void setAuthoritiesByUsernameQuery(String queryString) {
        super.setAuthoritiesByUsernameQuery(queryString);
    }

   //override to get accountNonLocked
    @Override
    public List<UserDetails> loadUsersByUsername(String username) {
      return getJdbcTemplate().query(super.getUsersByUsernameQuery(), new String[]{ username },
        new RowMapper<UserDetails>() {
          public UserDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
            String username = rs.getString("username");
            String password = rs.getString("password");
            boolean enabled = rs.getBoolean("enabled");
            boolean accountNonExpired = rs.getBoolean("accountNonExpired");
            boolean credentialsNonExpired = rs.getBoolean("credentialsNonExpired");
            boolean accountNonLocked = rs.getBoolean("accountNonLocked");

            return new User(username, password, enabled, accountNonExpired, credentialsNonExpired,
                accountNonLocked, AuthorityUtils.NO__AUTHORITIES);
          }

      });
    }

   //override to pass accountNonLocked
    @Override
    public UserDetails createUserDetails(String username, UserDetails userFromUserQuery,
            List<GrantedAuthority> combinedAuthorities) {
        String returnUsername = userFromUserQuery.getUsername();

        if (super.isUsernameBasedPrimaryKey()) {
          returnUsername = username;
        }

        return new User(returnUsername, userFromUserQuery.getPassword(),
                       userFromUserQuery.isEnabled(),
               userFromUserQuery.isAccountNonExpired(),
                       userFromUserQuery.isCredentialsNonExpired(),
            userFromUserQuery.isAccountNonLocked(), combinedAuthorities);
    }

}

8. DaoAuthenticationProvider

無効なログイン試行ごとにカスタム認証プロバイダを作成し、user__attemptsテーブルに更新し、最大試行が発生した場合は `LockedException`をスローします。

LimitLoginAuthenticationProvider.java

package com.mkyong.web.handler;

import java.util.Date;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

import com.mkyong.users.dao.UserDetailsDao;
import com.mkyong.users.model.UserAttempts;

@Component("authenticationProvider")
public class LimitLoginAuthenticationProvider extends DaoAuthenticationProvider {

    @Autowired
    UserDetailsDao userDetailsDao;

    @Autowired
    @Qualifier("userDetailsService")
    @Override
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        super.setUserDetailsService(userDetailsService);
    }

    @Override
    public Authentication authenticate(Authentication authentication)
          throws AuthenticationException {

      try {

        Authentication auth = super.authenticate(authentication);

       //if reach here, means login success, else an exception will be thrown
       //reset the user__attempts
        userDetailsDao.resetFailAttempts(authentication.getName());

        return auth;

      } catch (BadCredentialsException e) {

       //invalid login, update to user__attempts
        userDetailsDao.updateFailAttempts(authentication.getName());
        throw e;

      } catch (LockedException e){

       //this user is locked!
        String error = "";
        UserAttempts userAttempts =
                    userDetailsDao.getUserAttempts(authentication.getName());

               if(userAttempts!=null){
            Date lastAttempts = userAttempts.getLastModified();
            error = "User account is locked! <br><br>Username : "
                           + authentication.getName() + "<br>Last Attempts : " + lastAttempts;
        }else{
            error = e.getMessage();
        }

      throw new LockedException(error);
    }

    }

}

9.スプリングコントローラ

標準のコントローラクラスで、 `login`メソッドを参照して、セッション値 – ”

SPRING

SECURITY

LAST__EXCEPTION

“の使い方を示し、エラーメッセージをカスタマイズします。

MainController.java

package com.mkyong.web.controller;

import javax.servlet.http.HttpServletRequest;

import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
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 MainController {

    @RequestMapping(value = { "/", "/welcome** ** " }, method = RequestMethod.GET)
    public ModelAndView defaultPage() {

        ModelAndView model = new ModelAndView();
        model.addObject("title", "Spring Security Limit Login - Annotation");
        model.addObject("message", "This is default page!");
        model.setViewName("hello");
        return model;

    }

    @RequestMapping(value = "/admin** ** ", method = RequestMethod.GET)
    public ModelAndView adminPage() {

        ModelAndView model = new ModelAndView();
        model.addObject("title", "Spring Security Limit Login - Annotation");
        model.addObject("message", "This page is for ROLE__ADMIN only!");
        model.setViewName("admin");

        return model;

    }

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public ModelAndView login(
                @RequestParam(value = "error", required = false) String error,
        @RequestParam(value = "logout", required = false) String logout,
                HttpServletRequest request) {

        ModelAndView model = new ModelAndView();
        if (error != null) {
            model.addObject("error",
                           getErrorMessage(request, "SPRING__SECURITY__LAST__EXCEPTION"));
        }

        if (logout != null) {
            model.addObject("msg", "You've been logged out successfully.");
        }
        model.setViewName("login");

        return model;

    }

   //customize the error message
    private String getErrorMessage(HttpServletRequest request, String key){

        Exception exception =
                   (Exception) request.getSession().getAttribute(key);

        String error = "";
        if (exception instanceof BadCredentialsException) {
            error = "Invalid username and password!";
        }else if(exception instanceof LockedException) {
            error = exception.getMessage();
        }else{
            error = "Invalid username and password!";
        }

        return error;
    }

   //for 403 access denied page
    @RequestMapping(value = "/403", method = RequestMethod.GET)
    public ModelAndView accesssDenied() {

        ModelAndView model = new ModelAndView();

       //check if user is login
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (!(auth instanceof AnonymousAuthenticationToken)) {
            UserDetails userDetail = (UserDetails) auth.getPrincipal();
            System.out.println(userDetail);

            model.addObject("username", userDetail.getUsername());

        }

        model.setViewName("403");
        return model;

    }

}

10.春のセキュリティ設定

カスタマイズした `authenticationProvider`を添付しました。

SecurityConfig.java

package com.mkyong.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    @Qualifier("authenticationProvider")
    AuthenticationProvider authenticationProvider;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests().antMatchers("/admin/** ** ")
          .access("hasRole('ROLE__USER')").and().formLogin()
          .loginPage("/login").failureUrl("/login?error")
            .usernameParameter("username")
            .passwordParameter("password")
          .and().logout().logoutSuccessUrl("/login?logout").and().csrf();
    }
}

完了しました。

11.デモ

デモページ –

http://localhost:8080/spring-security-limit-login-annotation/admin

11.1、最初の無効なログイン試行では、通常のエラーメッセージが表示されます。


spring-security-limit-login-attempts-1、width = 534、height = 480

11.2、無効なログイン試行の最大回数がヒットした場合、エラーメッセージ “ユーザーアカウントがロックされています”が表示されます。


spring-security-limit-login-attempts-locked、width = 525、height = 480

11.3、ユーザーが「ロックされた」状態で、まだログインを再試行している場合。ロックされた詳細が表示されます。


spring-security-limit-login-attempts-locked-detail、width = 513、height = 480

11.4「ユーザー」テーブルを見直してください。「accountNonLocked」= 0またはfalseの場合、このユーザーはロックされた状態です。


spring-security-limit-login-attempts-locked-database、width = 649、height = 300

ソースコードをダウンロードする

ダウンロードする – リンク://wp-content/uploads/2014/04/spring-security-limit-login-annotation.zip[spring-security-limit-login-annotation.zip](38 KB)

ダウンロードする –

spring-security-limit-login-xml.zip

(32 KB)

参考文献


  1. http://stackoverflow.com/questions/5351391/how-can-i-limit-login-attempts-in-spring-security

    [StackOverflow

: How can I limit login attempts in Spring Security?].

http://docs.spring.io/spring-security/site/docs/3.2.3.RELEASE/reference/htmlsingle/#core-services

[Spring

公式リファレンス:Core Services – authenticationProvider]。リンク://spring-security/spring-security-custom-login-form-annotation-example/[Spring

セキュリティカスタムログインフォーム注釈の例]。

http://docs.spring.io/spring-security/site/docs/3.2.3.RELEASE/apidocs/org/springframework/security/authentication/dao/DaoAuthenticationProvider.html

[Spring

DaoAuthenticationProvider JavaDoc]。

http://docs.spring.io/spring-security/site/docs/3.2.3.RELEASE/apidocs/org/springframework/security/core/userdetails/jdbc/JdbcDaoImpl.html

[Spring

JdbcDaoImpl JavaDoc]


login


spring security

投稿ナビゲーション

前 前の投稿: Java 8 – インスタントをLocalDateTimeに変換する
次 次の投稿: JSF 2ボタンとcommandButtonの例

getdocs

13036RSS




タグ

Algorithms apache Applications CentOS Core Java Databases debian Development DevOps Docker Hibernate Java Java Collections javascript Let's Encrypt Linux Basics Maven Miscellaneous MongoDB Monitoring mysql Networking nginx Node.js NoSQL Persistence php Programming python React REST Security Spring Spring Boot Spring MVC Spring Security System Tools Testing ubuntu Ubuntu 16.04 Ubuntu 16.04 Ubuntu 18.04 Ubuntu 18.04 Vue.js Weekly Review

最近の投稿

  • arpingコマンド
  • ターミナルからプロセスを完全に切り離す
  • iPerfを使用してネットワークパフォーマンスを測定する方法
  • コマンドラインからLinuxディストリビューションを探す
  • パブリックDMZネットワークアーキテクチャ

60日間無料の$100ドルのクレジットを取得

60日間無料の$100ドルのクレジットを取得

© 2025  開発者ドキュメント. Proudly powered by WordPress. WordStar, Theme by Linesh Jose

モバイルバージョンに移動