Spring Securityを使用したSpring MVCアプリでは、カスタムを使用してAuthenticationProvider
チェックしたいn 数デフォルトusername
と以外の追加フィールドpassword
。たとえば、ユーザーが認証する場合、ユーザー名とパスワードに加えて、電子メールで受信した PIN コード、テキストで受信した PIN コード、およびn
その他の資格情報をいくつか入力する必要があります。ただし、この質問を絞り込むために、ログインに 1 つのピンを追加することに焦点を当て、後で他の n 個の資格情報を簡単に追加できるように設定しましょう。
Java 構成を使用したいです。
カスタムAuthenticationProvider
、カスタムAuthenticationFilter
、カスタムUserDetailsService
、およびその他の変更をいくつか作成しました。
しかし、以下の問題を再現するための手順のスクリーンショットに示されているように、ユーザーが有効な資格情報を持っているかどうかに関係なく、ユーザーがログインしようとすると、アプリはアクセスを許可します。カスタム n 要素認証が適切に機能するには、共有しているコードに具体的にどのような変更を加える必要がありますか?
私のテスト プロジェクトの構造は、次のスクリーン ショットに示されています。
Eclipse プロジェクト エクスプローラーの Java コード構造は次のとおりです。
{ 画像ホストが利用できません }
XML 構成ファイルは、プロジェクト エクスプローラーで下にスクロールして次の内容を表示することで見つけることができます。
{ 画像ホストが利用できません }
ビュー コードは、次のようにプロジェクト エクスプローラーで少し下にスクロールすると見つかります。
{ 画像ホストが利用できません }
動作する Eclipse プロジェクトで、このコードをすべてダウンロードして調べることができます。
{ ファイルは削除されました }
CustomAuthenticationProvider.java
は:
package my.app.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
public class CustomAuthenticationProvider implements AuthenticationProvider{
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
List<GrantedAuthority> grantedAuths = new ArrayList<>();
if (name.equals("admin") && password.equals("system")) {
grantedAuths.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
if(pincodeEntered(name)){
grantedAuths.add(new SimpleGrantedAuthority("registered"));
}
Authentication auth = new UsernamePasswordAuthenticationToken(name, password, grantedAuths);
return auth;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
private boolean pincodeEntered(String userName){
// do your check here
return true;
}
}
MessageSecurityWebApplicationInitializer.java
は:
package my.app.config;
import org.springframework.core.annotation.Order;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
@Order(2)
public class MessageSecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
}
TwoFactorAuthenticationFilter.java
は:
package my.app.config;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
public class TwoFactorAuthenticationFilter extends UsernamePasswordAuthenticationFilter
{
private String extraParameter = "extra";
private String delimiter = ":";
/**
* Given an {@link HttpServletRequest}, this method extracts the username and the extra input
* values and returns a combined username string of those values separated by the delimiter
* string.
*
* @param request The {@link HttpServletRequest} containing the HTTP request variables from
* which the username client domain values can be extracted
*/
@Override
protected String obtainUsername(HttpServletRequest request){
String username = request.getParameter(getUsernameParameter());
String extraInput = request.getParameter(getExtraParameter());
String combinedUsername = username + getDelimiter() + extraInput;
System.out.println("Combined username = " + combinedUsername);
return combinedUsername;
}
/**
* @return The parameter name which will be used to obtain the extra input from the login request
*/
public String getExtraParameter(){
return this.extraParameter;
}
/**
* @param extraParameter The parameter name which will be used to obtain the extra input from the login request
*/
public void setExtraParameter(String extraParameter){
this.extraParameter = extraParameter;
}
/**
* @return The delimiter string used to separate the username and extra input values in the
* string returned by <code>obtainUsername()</code>
*/
public String getDelimiter(){
return this.delimiter;
}
/**
* @param delimiter The delimiter string used to separate the username and extra input values in the
* string returned by <code>obtainUsername()</code>
*/
public void setDelimiter(String delimiter){
this.delimiter = delimiter;
}
}
SecurityConfig.java
は:
package my.app.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public void registerGlobalAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider());
}
@Bean
AuthenticationProvider customAuthenticationProvider() {
CustomAuthenticationProvider impl = new CustomAuthenticationProvider();
return impl ;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/secure-home")
.usernameParameter("j_username")
.passwordParameter("j_password")
.loginProcessingUrl("/j_spring_security_check")
.failureUrl("/login")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.and()
.authorizeRequests()
.antMatchers("/secure-home").hasAuthority("registered")
.antMatchers("/j_spring_security_check").permitAll()
.and()
.userDetailsService(userDetailsService());
}
}
User.java
は:
package my.app.model;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
@Entity
@Table(name="users")
public class User implements UserDetails{
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue
private Integer id;
@Column(name= "email", unique=true, nullable=false)
private String login;//must be a valid email address
@Column(name = "password")
private String password;
@Column(name = "phone")
private String phone;
@Column(name = "pin")
private String pin;
@Column(name = "sessionid")
private String sessionId;
@ManyToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
@JoinTable(name="user_roles",
joinColumns = {@JoinColumn(name="user_id", referencedColumnName="id")},
inverseJoinColumns = {@JoinColumn(name="role_id", referencedColumnName="id")}
)
private Set<Role> roles;
public Integer getId() {return id;}
public void setId(Integer id) { this.id = id;}
public String getPhone(){return phone;}
public void setPhone(String pn){phone = pn;}
public String getPin(){return pin;}
public void setPin(String pi){pin = pi;}
public String getSessionId(){return sessionId;}
public void setSessionId(String sd){sessionId = sd;}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
//roles methods
public void addRole(Role alg) {roles.add(alg);}
public Set<Role> getRoles(){
if(this.roles==null){this.roles = new HashSet<Role>();}
return this.roles;
}
public void setRoles(Set<Role> alg){this.roles = alg;}
public boolean isInRoles(int aid){
ArrayList<Role> mylgs = new ArrayList<Role>();
mylgs.addAll(this.roles);
for(int a=0;a<mylgs.size();a++){if(mylgs.get(a).getId()==aid){return true;}}
return false;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// TODO Auto-generated method stub
return null;
}
@Override
public String getUsername() {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean isAccountNonExpired() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean isAccountNonLocked() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean isCredentialsNonExpired() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean isEnabled() {
// TODO Auto-generated method stub
return false;
}
}
XML 構成は次のとおりbusiness-config.xml
です。
<beans profile="default,spring-data-jpa">
<!-- lots of other stuff -->
<bean class="my.app.config.SecurityConfig"></bean>
</beans>
<!-- lots of unrelated stuff -->
さらに、mvc-core-config.xml
次のものが含まれます。
<!-- lots of other stuff -->
<mvc:view-controller path="/" view-name="welcome" />
<mvc:view-controller path="/login" view-name="login" />
そしてlogin.jsp
次のようになります:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Custom Login page</title>
<style>.error {color: red;}</style>
</head>
<body>
<div class="container">
<h1>Custom Login page</h1>
<p>
<c:if test="${error == true}">
<b class="error">Invalid login or password or pin.</b>
</c:if>
</p>
<form method="post" action="<c:url value='j_spring_security_check'/>" >
<table>
<tbody>
<tr>
<td>Login:</td>
<td><input type="text" name="j_username" id="j_username"size="30" maxlength="40" /></td>
</tr>
<tr>
<td>Password:</td>
<td><input type="password" name="j_password" id="j_password" size="30" maxlength="32" /></td>
</tr>
<tr>
<td>Pin:</td>
<td><input type="text" name="pin" id="pin"size="30" maxlength="40" /></td>
</tr>
<tr>
<td colspan=2>
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="Login" /></td>
</tr>
</tbody>
</table>
</form>
</div>
</body>
</html>
'pom.xml' 内の Spring Security 依存関係は次のとおりです。
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>3.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>3.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>3.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>3.2.2.RELEASE</version>
</dependency>
ダウンロードしてマシンに再現する
ローカルの開発ボックスで問題を再現するために必要な最小限のコードを含む、動作する Eclipse プロジェクトもアップロードしました。Eclipse プロジェクトはここからダウンロードできます。
{ ファイルは削除されました }
圧縮されたプロジェクトをダウンロードしたら、次の手順に従ってマシン上で問題を再現できます。
1.) zipファイルを新しいフォルダに解凍します
2.) Eclipseでは、File > Import > Existing Maven Projects
3.) をクリックしますNext
。解凍されたプロジェクトのフォルダーを参照します。ウィザードを完了してプロジェクトをインポートします。
4.) Eclipseでプロジェクト名を右クリックしてMaven > Download sources
5.) Eclipseでプロジェクト名を再度右クリックして、Maven > Update project
6.) MySQLを開き、空の新しいデータベースを作成します。somedb
7.) Eclipse プロジェクトで、data-access.properties
次の図に示すように開き、someusername
をsomepassword
MySQL の実際のユーザー名とパスワードに変更します。
{ 画像ホストが利用できません }
8.) Eclipse でプロジェクトを右クリックし、 を選択しますRun As .. Run on server..
。これによりアプリが起動し、ブラウザのhttp://localhost:8080/n_factor_auth/
URL に次の内容が表示されます。
{ 画像ホストが利用できません }
9.) URL を に変更して、ユーザー名とパスワードに加えて PIN を必要とするサンプルのカスタム ログイン ページhttp://localhost:8080/n_factor_auth/secure-home
にリダイレクトされたことを確認します。結果は、単一の PIN コードを追加するだけでなく、http://localhost:8080/n_factor_auth/login
対応するものである必要があることに注意してください。n-factors
{ 画像ホストが利用できません }
10.) 次の SQL コマンドを実行して、MySQL データベースにテスト資格情報を挿入します。このコマンドはファイルに記述して.sql
、コマンドを使用して MySQL コマンド ラインから実行できますsource
。この例を簡略化するために が有効になっているため、アプリが起動するたびにデータベース オブジェクトが削除され、空で再作成されることhbm2ddl
に注意してください。したがって、Eclipse でアプリを再ロードするたびに、次の SQL コマンドを再実行する必要があります。
SET FOREIGN_KEY_CHECKS=0;
INSERT INTO `roles` VALUES (100,'registered');
INSERT INTO `user_roles` VALUES (100,100);
INSERT INTO `users` (id, email,password, phone, pin) VALUES (100,'[email protected]','somepassword','xxxxxxxxxx', 'yyyy');
SET FOREIGN_KEY_CHECKS=1;
11.) ログインを試みるどれでも資格情報(有効または無効)を入力し、次のログイン成功画面が表示されます(有効な資格情報を入力するかどうかに関係なく、ユーザーはログインできることに注意してください)。
{ 画像ホストが利用できません }
これで完了です。これで、上記のすべてのコードを含む問題が、動作する最小限の Eclipse プロジェクトでマシン上で再現されました。では、上記の OP にどう答えますか? 上記のコードにどのような変更を加え、ログイン時にカスタム認証を作動させるために他に何を行いますか?
n 要素認証を有効にするために、最小限のダウンロード アプリにどのような具体的な変更を加える必要があるかを知りたいと思っています。自分のマシンのサンプル アプリで提案を確認して検証します。
この投稿に示されている現在のバージョンを作成するために、冗長な XML 構成を削除することを提案してくれたさまざまな人々 (M.Deinum を含む) に感謝します。
ベストアンサー1
まず、使用しているインターフェースと、認証プロセスでそれらが果たす役割について説明します。
Authentication
- ユーザの認証結果を表します。そのユーザに付与された権限と、ユーザについて必要となる可能性のある追加の詳細を保持します。フレームワークにはどのような詳細が必要になるかを知る方法がないため、認証オブジェクトにはgetDetails
任意のオブジェクトを返すことができるメソッドAuthenticationProvider
- 何らかの方法でオブジェクトを作成できるオブジェクトAuthentication
。再利用性を高めるために、一部の(またはほとんどの)アプリケーションでは、各アプリケーションが特定のユーザーの詳細を必要とする可能性があるため、AuthenticationProvider
オブジェクトにユーザーの詳細を設定することを控えていますAuthentication
。代わりに、ユーザーの詳細を解決するプロセスを設定可能なメソッドに委任します。UserDetailsService
UserDetailsService
- 1つの戦略アプリケーションに必要なユーザーの詳細を取得します。
したがって、カスタムを作成する場合、AuthenticationProvider
を必要とする方法で実装する必要すらない場合がありUserDetailsService
ます。決定はあなた次第であり、他のプロジェクトで実装を再利用する予定があるかどうかによって異なります。
コードのコンパイルの問題については、 を提供する 2 つの方法が混在していますUserDetailsService
。 では、フィールドにアノテーションCustomAuthenticationProvider
を付けています。つまり、コンテナー (この場合は Spring アプリケーション コンテキスト) は適切な実装を見つけ、実行時にリフレクションを使用してそのフィールドにそれを注入します。 コンテキストによってこのフィールドを設定するプロセスは、依存性注入と呼ばれます。 クラスでは、クラスに存在しないメソッドを通じてフィールドを設定することで、自分で実装を提供しようとしています。userService
@Inject
SecurityConfig
setUserDetailsService
この問題を解決するには、UserDetails サービスを提供する方法の 1 つを選択し、次のいずれかを実行する必要があります。
- アノテーションを削除し
@Inject
てメソッドを作成するsetUserDetailsService
か、 - 存在しないメソッドを呼び出す行を削除し、実装を
UserDetailsService
Beanとして宣言します。
どちらの方法を選択すべきかという点については、クラスをSecurityConfig
他のプロジェクトで再利用できるようにする方法が見つかる場合は、依存性注入の方法のほうが適している可能性があります。その場合は、(@Import
アノテーションを使用して) インポートし、次のアプリケーションで別のUserDetailsSerice
実装を Bean として宣言して動作させることができます。
通常、 のようなクラスはSecurityConfig
実際には再利用できないため、セッターを作成し、依存性注入を削除することがおそらく最初の選択肢になります。
編集
実用的な、しかし単純な実装(このブログの記事) だろう:
public class CustomAuthenticationProvider implements AuthenticationProvider{
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
List<GrantedAuthority> grantedAuths = new ArrayList<>();
if (name.equals("admin") && password.equals("system")) {
grantedAuths.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
if(pincodeEntered(name)){
grantedAuths.add(new SimpleGrantedAuthority("ROLE_PINCODE_USER"));
}
Authentication auth = new UsernamePasswordAuthenticationToken(name, password, grantedAuths);
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
private boolean pincodeEntered(String userName){
// do your check here
return true;
}
}
次に、構成クラスで次のメソッドを変更します。
@Bean
AuthenticationProvider customAuthenticationProvider() {
return new CustomAuthenticationProvider();
}