
このチュートリアルでは、Spring Securityのログイン試行を制限する方法を示します。つまり、ユーザーが無効なパスワードで3回以上ログインしようとすると、システムはユーザーをロックしてログインできなくなります。
使用される技術とツール:
-
Spring 3.2.8.RELEASE
-
春のセキュリティ3.2.3.RELEASE
-
Spring JDBC 3.2.3.RELEASE
-
Eclipse 4.2
-
JDK 1.6
-
Maven 3
-
MySQLサーバ5.6
-
Tomcat 7(Servlet 3.x)
このチュートリアルのいくつかの簡単なメモ:
-
MySQLデータベースが使用されます.
-
これはSpring Securityのアノテーションに基づいた例です.
-
列「accountNonLocked」を持つ「ユーザー」表を作成します.
-
無効なログイン試行を保存するための “user__attempts”テーブルを作成します.
-
Spring JDBCが使用されます.
-
返された例外に基づいてカスタムエラーメッセージを表示します.
-
カスタマイズされた “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.プロジェクトディレクトリ
最終的なプロジェクト構造を見直す(注釈ベース):
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. UserDetailsService
デフォルトでは、
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`を得ることができます。
CustomUserDetailsService.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.デモ
11.1、最初の無効なログイン試行では、通常のエラーメッセージが表示されます。
11.2、無効なログイン試行の最大回数がヒットした場合、エラーメッセージ “ユーザーアカウントがロックされています”が表示されます。
11.3、ユーザーが「ロックされた」状態で、まだログインを再試行している場合。ロックされた詳細が表示されます。
11.4「ユーザー」テーブルを見直してください。「accountNonLocked」= 0またはfalseの場合、このユーザーはロックされた状態です。
ソースコードをダウンロードする
ダウンロードする – リンク://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)
参考文献
: 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]