- 已关闭**。此问题需要超过focused。当前不接受答案。
- 想要改进此问题吗?**更新此问题,使其仅关注editing this post的一个问题。
七年前就关门了。
Improve this question
在一个使用Spring Security的Spring MVC应用程序中,我想使用一个定制的AuthenticationProvider
来检查默认username
和password
之外的n个附加字段。例如,如果一个用户想进行身份验证,我想让她在用户名和密码的后面加上一个通过电子邮件接收到的pin码、一个通过文本接收到的pin码、和n
个其他凭据。但是,为了缩小这个问题的范围,让我们只关注向登录添加一个额外的pin,但让我们以一种使我们能够在以后轻松添加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安全依赖项为:
<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项目,其中包含在本地devbox上重现问题所需的最少代码。您可以在此处下载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更改为http://localhost:8080/n_factor_auth/secure-home
,以查看您是否已被重定向到http://localhost:8080/n_factor_auth/login
,该页面提供示例自定义登录页面,该页面除了用户名和密码之外还需要PIN。请注意,结果需要适应n-factors
,而不是简单地添加单个PIN代码:
{映像主机不可用}
10.)通过运行以下SQL命令将测试凭据插入MySQL数据库,您可以将这些命令放入.sql
文件中,然后使用source
命令从MySQL命令行运行。请注意,每次启动应用程序时都会删除数据库对象并重新创建为空,因为启用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,'me@mydomain.com','somepassword','xxxxxxxxxx', 'yyyy');
SET FOREIGN_KEY_CHECKS=1;
11.)尝试使用任何凭据(有效或无效)登录,并获得以下成功登录屏幕(请注意,无论用户是否提供有效凭据,他们都会登录):
{映像主机不可用}
就是这样。现在你已经在你的机器上重新创建了这个问题,包括上面显示的所有代码,但是是在一个工作的极简主义eclipse项目中。那么现在你如何回答上面的OP呢?你对上面的代码做了什么修改,你还做了什么来让自定义身份验证器在登录时使用?
我很想知道为了启用n因子身份验证,需要对极简主义下载应用进行哪些具体更改。我将通过检查我机器上的示例应用中您的建议来验证。
感谢许多人(包括M. Deinum),他们建议删除多余的XML配置,以创建本文中显示的当前版本。
4条答案
按热度按时间cmssoen21#
首先,解释一下您正在使用的接口及其在身份验证过程中所起的作用:
Authentication
-表示验证用户的结果。保存授予该用户的权限以及可能需要的有关该用户的任何其他详细信息。由于框架无法知道需要哪些详细信息,因此验证对象具有可以返回任何对象的getDetails
方法AuthenticationProvider
-可以以某种方式创建Authentication
对象的对象。为了使它们更可重用,一些(或大多数)AuthenticationProvider
避免在Authentication
对象上设置用户详细信息,因为每个应用程序可能需要特定的用户详细信息。相反,它们将解析用户详细信息的过程委托给可设置的UserDetailsService
UserDetailsService
-strategy,用于检索应用程序所需的用户详细信息。所以,如果你正在创建一个定制的
AuthenticationProvider
,你甚至不需要用一种需要UserDetailsService
的方式来实现它,这个决定取决于你是否计划在其他项目中重用你的实现。至于代码中的编译问题,您混合了两种提供
UserDetailsService
的方式,在CustomAuthenticationProvider
中,您使用@Inject
注解了userService
字段,这意味着,该容器(在您的案例中是Spring应用程序上下文)是找到一个合适的实现,并在运行时使用反射将其注入到该字段中。通过上下文设置该字段的过程称为依赖注入,在SecurityConfig
类中,您试图通过setUserDetailsService
方法设置字段来自己提供实现,而setUserDetailsService
方法在您的类中不存在。要解决此问题,您需要决定使用以下方法之一提供UserDetails服务:
@Inject
注解并创建setUserDetailsService
方法,或者UserDetailsService
的实现声明为bean时,请删除该行至于你应该选择哪种方法,如果你能找到一种方法让你的
SecurityConfig
类在其他项目中可重用,那么依赖注入的方法可能会更好。在这种情况下,你可以直接导入它(通过使用@Import
注解),并在你的下一个应用程序中声明一个不同的UserDetailsSerice
实现作为bean,并让它工作。通常,像
SecurityConfig
这样的类并不是真正可重用的,因此创建setter并删除依赖注入可能是我的第一选择。一个有效的、虽然过于简单的实现(主要基于blog entry)是:
然后在config类中更改以下方法:
3j86kqsm2#
我们需要做的第一件事是扩展UsernamePasswordAuthenticationFilter类,以便它能够处理第二个输入字段。
然后将这两个值连接成一个字符串,用分隔符字符串(默认为冒号)分隔它们。
然后返回这个组合的字符串。从其中读取"extra"输入字段的参数默认为extra。
将给定的用户名拆分为两个部分:用户名和额外字段。在本例中,额外字段是用户的公司域。
一旦我们有了用户名和域,我们就可以使用DAO来查找匹配的用户。
最后一个谜题:
在twoFactorAuthenticationFilterbean定义中,我们将extraParameter属性设置为"domain",这是要在登录表单中使用的输入字段的名称。
编辑:
查看User类的构造函数。
如果你不知道什么是授权进入一个看看下面的链接:
http://docs.spring.io/autorepo/docs/spring-security/3.2.1.RELEASE/apidocs/org/springframework/security/core/GrantedAuthority.html
你的代码给出了一个不同的模式,只适用于正常的用户名和密码。我的代码工作的n因素身份验证。尝试切换到我的代码,如果任何问题继续存在。
wlp8pajw3#
我很清楚这篇文章已经被编辑了28次,所以我可能错过了一些上下文。我也很清楚你把其他答案中的一些代码合并到了你的问题中,这个问题已经从“为什么一个有效的用户不能认证?”变成了“为什么每个用户都要认证?"
当前问题。
然而,正如所写的,你的
CustomAuthenticationProvider.authenticate()
方法将总是返回一个Authentication
对象,该对象返回auth.isAuthenticated() == true
,因为你使用这个方法示例化,它警告你关于那件事。即使你作为第三个参数传入的collection
是空的,情况也会是这样。事实上,集合总是包含一个GrantedAuthority
,表示“registered”,因为pincodeEntered(name)
总是返回true
。因此,您需要更正这些方法中的逻辑。如果身份验证不成功,authenticate()
应该返回null
。后续步骤
您在评论中指出,您想要的是多因素身份验证的参考实现。这是有问题的--对于构成这样一个东西的内容不一定有一致意见。例如,有些人会认为多因素应该包括拥有因素,而不是在一个登录页面上的n个知识因素。它也不适合于一个SO答案,因为它需要一个博客文章(或一系列)-无论赏金多么慷慨。
在web上的spring中有多因素身份验证的工作示例,例如here和here,后者我想你一定已经发现了,因为你似乎正在使用那里的一些代码。
使
CustomAuthenticationProvider
工作可能需要几个小时。调试可能需要更长的时间,因为您的示例中混合了多种方法-这不是最小的。特别是,TwoFactorAuthenticationFilter
类应该用于拦截来自登录页面的请求输入,并连接用户名和pin。在博客的示例中,这是set up in XML-你可以把security
命名空间添加到你的business-config.xml
中,并把那些bean添加到那里。但是,
SecurityConfig
类和CustomAuthenticationProvider
又是一种不同的方法。接下来,你的项目代码引用了一个
j_security_check
url,但是那个URL没有被任何东西处理。我不确定它背后的意图,或者它来自哪里。最后,URL路由的MVC配置添加了另一个元素-一个我不熟悉的元素。我已经用了你的例子一段时间了,有太多的混合方法和太多的复杂性,我无法快速修复-也许其他人可以。
我***强烈***建议你从博客中的例子开始,然后在上面添加你想要的mvc配置。
注意:为其他尝试运行示例的用户设置
在设置项目时有一些问题--它对
javax.mail
有一个不需要和不满意的依赖项,您需要将maven依赖项发布到服务器(在项目-〉属性-〉部署程序集中),如果您还没有适配器,则需要下载并安装用于Tomcat服务器的适配器。您还需要在数据库中创建表和列。
n6lpvg4x4#
使用java config进行n因素身份验证的最简单方法是从使用java config的单因素身份验证(用户名和密码)的工作示例开始。然后,您只需进行一些非常小的更改:假设您有一个使用Java配置的单因素身份验证应用程序,步骤很简单:
首先,定义分层角色,每个因子对应一个角色。如果只有双因子验证,请在数据库中保留现有的一个角色,然后创建第二个角色,该角色具有完全访问权限,只能在运行时分配。因此,当用户登录时,他们将登录到存储在数据库中的最小角色,并且该最小角色只能访问一个视图。这是一个允许用户输入pin码的表单,您的管理员刚刚通过文本或电子邮件或其他方式发送了pin码。这些分层角色在
SecurityConfig.java
中定义如下:其次,将在成功输入正确的pin码后将用户角色升级为完全访问权限的代码添加到处理pin码输入表单
POST
的控制器代码中。在控制器中手动分配完全访问权限的代码为:您可以在
/getpin
之后添加任意多个层,也可以支持多个授权角色,并使其尽可能复杂,但这个答案给出了使用java config运行它的最简单方法。