spring security,UserDetailsS​​ervice,authenticationProvider,密码编码器..我迷路了

Posted

技术标签:

【中文标题】spring security,UserDetailsS​​ervice,authenticationProvider,密码编码器..我迷路了【英文标题】:spring security, UserDetailsService, authenticationProvider, pass word encoder.. i'm lost 【发布时间】:2016-12-19 20:01:56 【问题描述】:

首先,我已经阅读/重读(重复 10 次),至少有 6 本关于春季和春季安全的书籍,并在谷歌上搜索了所有内容。

在使用 spring 工作 10 年后,我仍然发现有如此多的注解定义、注入、组件、配置注解魔法正在发生,我对自己理解我的应用程序的信心为 0。 在线示例要么是 xml-config,要么不完整,要么完成 n diff。方式,过于简单化,使用较旧的弹簧,相互冲突,而且根本不是为处理基本的现实用例而构建的。

例如,以下代码尝试处理简单的登录,使用密码编码器对 db 表进行身份验证。 表单帖子包括一个对其进行身份验证的“客户端”、一个持久的 IP 地址和一些用于深度链接帖子登录的 url 路径信息。 (对于当今的单页网络应用来说,所有这些都是非常基本的东西) 我最初使用 xml 配置来工作,但 javaConfig 让我卡住了。

我不知道 userDetailsS​​ervice、AuthenticationManagerBuilder 和 PasswordEncoder 在 SecurityConfiguration 中是如何交互的。我获得了服务的登录数据,但不确定在何时何地应用了 spring authenticationProvider,或者我什至需要一个。

我的用户实现了 UserDetails 并保存了必填字段。 我在我的 CustomUserDetailsS​​ervice 中填充这些并授予权限。 如果我在服务中使用登录名/密码检查数据库,我如何/何时/为什么需要 auth.authenticationProvider(authenticationProvider())?

我的 UserDetailsS​​ervice 现在似乎执行了两次。

spring 如何获取提交的密码,对其进行编码并与存储在数据库中的密码进行比较? 它怎么知道使用与创建用户时创建/持久化 p/w 时使用的相同的盐?

当 authenticationProvider() 也设置了 userDetailsS​​ervice 时,为什么 configureGlobal() 会同时定义 auth.userDetailsS​​ervice 和 auth.authenticationProvider?

为什么我的大脑这么小,我无法理解这一点? :)


@Service
public class CustomUserDetailsService implements UserDetailsService 

@Autowired
private ClientDAO clientDAO;
@Autowired
private UserDAO userDAO;


public UserDetails loadUserByUsername(String multipartLogon) throws UsernameNotFoundException, DataAccessException 

    Boolean canAccess = false;
    Long clientId = null;
    String userLogon = null;
    String password = null;
    String id = null;
    String entryUrl = null;
    String ipAddress = null;
    String urlParam = null;
    String[] strParts = multipartLogon.split(":");
    try 
        userLogon = strParts[0];
        password = strParts[1];
        id = strParts[2];
        entryUrl = strParts[3];
        ipAddress = strParts[4];
        urlParam = strParts[5];
     catch(IndexOutOfBoundsException ioob)  
    Client client = new Client();
    if (!"".equals(id)) 
        clientId = IdUtil.toLong(id);
        client = clientDAO.getClient(clientId);
    

    //BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    //String encodedPassword = passwordEncoder.encode(password);

    //String encodedPassword = "$2a$22$6UiVlDEOv6IQWjKkLm.04uN1yZEtkepVqYQ00JxaqPCtjzwIkXDjy";


    User user = userDAO.getUserByUserLogonPassword(userLogon, password); //encodedPassword?
    user.isCredentialsNonExpired = false;
    Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
    for (UserRole userRole : userDAO.getUserRolesForUser(user)) 
        if (userRole.getRole().getActiveStatus()) 
            authorities.add(new SimpleGrantedAuthority(userRole.getRole().getRoleName()));
            user.isCredentialsNonExpired = true;
        
           
    user.setAuthorities(authorities);
    user.setPassword(password); //encodedPassword?
    user.setUsername(user.getUserLogon());
    user.isAccountNonExpired = false;
    user.isAccountNonLocked = false;

    List<ClientUser> clientUsers = clientDAO.getClientUsersForUser(user);
    for (ClientUser clientUser : clientUsers) 
        if (clientUser.getClient().getClientId().equals(client.getClientId())) 
            canAccess = true;
            break;
        
    

    user.isEnabled = false;
    if (user.getActiveStatus() && canAccess) 
        user.isAccountNonExpired = true;
        user.isAccountNonLocked = true;
        user.isEnabled = true;

        Session session = userDAO.getSessionForUser(user);
        if (session == null)  session = new Session(); 
        session.setUser(user);
        session.setDateLogon(Calendar.getInstance().getTime());
        session.setClient(client);
        session.setEntryUrl(entryUrl);
        session.setUrlParam(urlParam);
        session.setIPAddress(ipAddress);
        session.setActive(true);
        userDAO.persistOrMergeSession(session);
    
    return user;




@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter 

@Autowired
CustomUserDetailsService customUserDetailsService;


@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception 
    auth.userDetailsService(customUserDetailsService);
    auth.authenticationProvider(authenticationProvider());



@Bean
public BCryptPasswordEncoder passwordEncoder() 
    return new BCryptPasswordEncoder();



@Bean
public DaoAuthenticationProvider authenticationProvider() 
    DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
    authenticationProvider.setUserDetailsService(customUserDetailsService);
    authenticationProvider.setPasswordEncoder(passwordEncoder());
    return authenticationProvider;



@Override
protected void configure(HttpSecurity http) throws Exception 
    http
        .csrf().disable()

        .authorizeRequests()
            .antMatchers("/conv/a/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_COURT_ADMIN')")
            .antMatchers("/conv/u/**").access("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN') or hasRole('ROLE_COURT_ADMIN')")
            .antMatchers("/**").permitAll()
            .and()

        .formLogin()
            .loginPage("/conv/common/logon")
            .usernameParameter("multipartLogon")
            .loginProcessingUrl("/conv/common/logon")
            .defaultSuccessUrl("/conv/")
            .failureUrl("/conv/common/logon?error=1")
            .and()

        .logout()
            .logoutUrl("/conv/common/logout")
            .logoutSuccessUrl("/conv/")
            .permitAll()
            .and()

        .rememberMe()
            .key("conv_key")
            .rememberMeServices(rememberMeServices())
            .useSecureCookie(true);



@Override
public void configure(WebSecurity web) throws Exception 
    web.ignoring()
        .antMatchers("/common/**")
        .antMatchers("/favicon.ico");



@Bean
public RememberMeServices rememberMeServices() 
    TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("conv_key", customUserDetailsService);
    rememberMeServices.setCookieName("remember_me_cookie");
    rememberMeServices.setParameter("remember_me_checkbox");
    rememberMeServices.setTokenValiditySeconds(2678400); //1month
    return rememberMeServices;



【问题讨论】:

【参考方案1】:

我的用户实现了 UserDetails 并保存了必填字段。一世 在我的 CustomUserDetailsS​​ervice 中填充这些并授予权限。 我如何/何时/为什么需要一个 auth.authenticationProvider(authenticationProvider()),如果我检查数据库 在我的服务中使用登录名/密码?

我想你想要的是:

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception 
    auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());

userDetailsS​​ervice 方法是创建DaoAuthenticationProvider bean 的快捷方式!你不应该两者都需要,它只是配置同一事物的两种不同方式。 authenticationProvider 方法用于更多的自定义设置。

spring 如何获取提交的密码,对其进行编码并进行比较 存储在数据库中?它怎么知道使用与那相同的盐 在创建用户时创建/持久化 p/w 时使用?

如果您使用 BCrypt,salt 存储在编码的密码值中。盐是第三个 $(美元)符号后的前 22 个字符。 matches 方法负责检查密码。

为什么 configureGlobal() 定义了 auth.userDetailsS​​ervice 和 auth.authenticationProvider 当 authenticationProvider() 也设置 userDetailsS​​ervice?

见上文。这可能是用户详细信息被加载两次的原因。

更新:奇怪的是,您将密码和其他详细信息输入到 UserDetailsS​​ervice 中。这应该只根据用户名加载用户,例如:

User user = userDAO.getUserByUserLogonPassword(userLogon);

返回的用户对象应该包含编码(存储)的密码,而不是输入的密码。 Spring Security 会为您进行密码检查。您不应修改 UserDetailsS​​ervice 中的 User 对象。

【讨论】:

哈! thx,盐的答案非常有帮助。只有 UserDetailsS​​ervice,是的,这就是我原来的方式。我也将 passwordEncoder 定义为与您的完全一样。通过在线示例,人们倾向于以 9 种方式将这些东西放在一起到星期天,希望能奏效。 我的问题是我的 CustomUserDetailsS​​ervice.loadUserByUsername() 采用冒号分隔的字符串,其中包含的不仅仅是用户名。它包含登录:密码:clientId:entryUrl:等。然而之后,当 DaoAuthProvider.retrieveUser(String username..) 被调用时,使用原始的多部分登录到 loadUserByUsername 并且它失败了 auth 实际上 DaoAuthenticationProvider 有正确的loadedUser,但是AbstractDetailsAuthenticationProvider authenticate() 使用原来的冒号分隔的用户名,这就是它验证失败的时候。 用户名,p/w 查找是我自定义的。我更新为只检查用户名。问题是,CustomUserDetailsS​​ervice 工作正常,并且 p/w 已正确自动加密.. 但是随后 DaoAuthenticationProvider.retrieveUser() 被调用并且该方法使用最初提交的 multipartLogon(并且包含:[userName]:[clientId]:[requestedUrl ];[IPAddress]: 等等。这是因为我需要的不仅仅是用户名,来自登录的 p/w 和 userDetails.loadUserByUsername() 只接受单个字符串 arg 并自动传递给 DaoAutehnticationProvider。 @oldMan 再读一遍。不要乱用 UserDetailsS​​ervice 中的密码之类的东西!【参考方案2】:

哇,好的,这是很多问题。我会和这个人说话:

“我不知道 userDetailsS​​ervice、AuthenticationManagerBuilder 和 PasswordEncoder 是如何实现的”

UserDetailsS​​ervice 设置您可以从 Spring 访问的 User。如果您希望在用户的上下文中存储更多用户信息,则需要实现自己的用户并使用自定义用户详细信息服务进行设置。例如

public class CustomUser extends User implements UserDetails, CredentialsContainer 
private Long id;
private String firstName;
private String lastName;
private String emailAddress;
....

然后,在您的自定义 UserDetailsS​​ervice 中,设置属性:

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 
DatabaseEntity databaseUser = this.userRepository.findByUsernameIgnoreCase(username);

customUser customUser = databaseUser.getUserDetails();
customUser.setId(databaseUser.getId());
customUser.setFirstName(databaseUser.getFirstname());
.....

密码编码器是 Spring 用来将纯文本密码与数据库中的加密哈希进行比较的机制。您可以使用 BCryptPasswordEncoder:

@Bean
public PasswordEncoder passwordEncoder()
    return new BCryptPasswordEncoder();

除了将其传递给您的身份验证提供者之外,您还需要做更多的事情。

最后,configureGlobal 是您进行连接的地方。您定义了 Spring 要使用的用户详细信息服务和身份验证提供程序。

就我而言,我使用自定义身份验证提供程序来限制失败的登录尝试:

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

然后我把所有东西都连接起来:

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

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception 
    LimitLoginAuthenticationProvider provider = (LimitLoginAuthenticationProvider)authenticationProvider;
    provider.setPasswordEncoder(passwordEncoder);
    auth.userDetailsService(customUserDetailsService()).passwordEncoder(passwordEncoder);
    auth.authenticationProvider(authenticationProvider);

【讨论】:

感谢 Brian 的回复。是的,我知道我的帖子会如何让人们认为我对 userService、javaConfig 等的基本机制一无所知。 感谢 Brian 的回复。是的,我看到我的帖子如何让人们假设我对 userService、javaConfig 等的基本机制一无所知。但正如我包含的代码所证明的那样,我拥有你提到的所有设置。我实际上调用了一个使用用户名和密码来获取用户(实现用户详细信息)的 dao。当仅通过用户名加载用户时,我看不到安全性如何工作。是否使用密码在 UserDetails 上再次查找?它是如何/何时加密的(因为它与 db 中的加密值相比)?弹簧自动处理这个吗?春天怎么知道使用相同的盐? 断点显示检索和填充的用户在服务中。我什至尝试手动将encodedPassword 设置为db-stored 值。我得到了一个用户,但仍然没有通过 spring 身份验证。我不得不认为编码器没有被应用。我无法想象我手动添加:new BCryptPasswordEncoder() in service 以加密提交的 p/w,因为盐可能是 diff。 在 DaoAuthenticationProvider 中,会进行实际的密码检查。请参阅附加的AuthenticationChecks()。在我的情况下,使用我的自定义身份验证提供程序,我调用 super:@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException try Authentication auth = super.authenticate(authentication); 就我而言,在实施新的 UserDetailService 后,我没有更新 auth.userDetailsS​​ervice(XXX) 下的类名,因此我一直收到Authentication failed: password does not match stored value的错误

以上是关于spring security,UserDetailsS​​ervice,authenticationProvider,密码编码器..我迷路了的主要内容,如果未能解决你的问题,请参考以下文章

Spring mvc / security:从spring security中排除登录页面

Spring Security:2.4 Getting Spring Security

没有 JSP 的 Spring Security /j_spring_security_check

Spring-Security

Spring Security 登录错误:HTTP 状态 404 - /j_spring_security_check

未调用 Spring Security j_spring_security_check