Form login с дополнительным полем

280
21 сентября 2017, 14:41

Мне нужно добавить в форму авторизации третье поле, но не соображу, как научить Spring Security с ним работать. У меня пользователи лежат в базе, но кроме имени и пароля имеют ещё один признак, вроде домена. Смутно догадываюсь, что проблема состоит из двух частей:

  1. как-то объяснить Spring Security, что этот третий параметр надо из формы принимать
  2. как-то объяснить Spring Security использовать параметр при поиске в БД

Уже почти неделю бьюсь. На два раза перечитал Spring Security Reference и с головой закопался в javadoc по API. Но или там этого нет, или у меня знаний и опыта не хватает для понимания.

Возможно ли это? Если да, то как такое сделать?

Answer 1

Документация к Spring Security уже много лет держит третье место в моём личном рейтинге злых и бесполезных документов. Когда я столкнулся с подобной задачей, пришлось лезть в исходный код Spring Security, чтобы разобраться. И до сих пор не уверен, что всё сделал правильно.

Для реализации вашей задачи потребуется как минимум переопределить WebAuthenticationDetailsSource, WebAuthenticationDetails, AuthenticationProvider и UserDetailsService. Чтобы сэкономить время и силы, наследовать два последних будем от DaoAuthenticationProvider и JdbcDaoImpl, соответственно.

RealmAuthenticationDetailsSource.java

@Component("authenticationDetailSource")
public class RealmAuthenticationDetailsSource extends WebAuthenticationDetailsSource {
    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new RealmAuthenticationDetails(context);
    }
}

RealmAuthenticationDetails.java

public class RealmAuthenticationDetails extends WebAuthenticationDetails {
    private final String realm;
    public RealmAuthenticationDetails(HttpServletRequest context) {
        super(context);
        String realm = context.getParameter("realm");
        this.realm = realm != null ? realm : "";
    }
    public String getRealm() {
        return realm;
    }
}

RealmAuthenticationProvider.java

@Service("authenticationProvider")
public class RealmAuthenticationProvider extends DaoAuthenticationProvider {
    @Autowired
    private RealmedUserDetailsService userDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @PostConstruct
    private void initialize() {
        setUserDetailsService(userDetailsService);
        setPasswordEncoder(passwordEncoder);
    }
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        RealmAuthenticationDetails details = (RealmAuthenticationDetails) authentication.getDetails();
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();
        String realm = details.getRealm();
        User user = (User) userDetailsService.loadUserByUsernameAndRealm(username, realm);
        if (user == null) {
            throw new BadCredentialsException("Username not found");
        }
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new BadCredentialsException("Wrong password");
        }
        return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
    }
}

RealmedUserDetailsService.java

@Service("userDetailsService")
public class RealmedUserDetailsService extends JdbcDaoImpl {
    private final boolean enableGroups = true;
    private final boolean enableAuthorities = false;
    private final String rolePrefix = "ROLE_";
    private final String usersByUsernameQuery
      = "select login, password, enabled from employee where login = ?";
    private final String usersByUsernameAndRealmQuery
      = "select e.login, e.password, e.enabled from groups_employee as m2m "
      + "inner join employee as e on e.id = m2m.members_id "
      + "inner join groups as g on g.id = m2m.groups_id "
      + "where e.login = ? and g.company_realm = ?";
    private final String authoritiesByUsernameQuery
      = "select e.login, g.authority_name from groups_employee as m2m "
      + "inner join employee as e on e.id = m2m.members_id "
      + "inner join groups as g on g.id = m2m.groups_id "
      + "where e.login = ?";
    private final String authoritiesByUsernameAndRealmQuery
      = "select e.login, g.authority_name from groups_employee as m2m "
      + "inner join employee as e on e.id = m2m.members_id "
      + "inner join groups as g on g.id = m2m.groups_id "
      + "where e.login = ? and g.company_realm = ?";
    private final String groupAuthoritiesByUsernameQuery
      = "select e.login, g.name, g.authority_name from groups_employee as m2m "
      + "inner join employee as e on e.id = m2m.members_id "
      + "inner join groups as g on g.id = m2m.groups_id "
      + "where e.login = ?";
    private final String groupAuthoritiesByUsernameAndRealmQuery
      = "select e.login, g.name, g.authority_name from groups_employee as m2m "
      + "inner join employee as e on e.id = m2m.members_id "
      + "inner join groups as g on g.id = m2m.groups_id "
      + "where e.login = ? and g.company_realm = ?";
    @Autowired
    private DataSource dataSource;
    protected void initialize() {
        setEnableGroups(enableGroups);
        setEnableAuthorities(enableAuthorities);
        setRolePrefix(rolePrefix);
        setUsersByUsernameQuery(usersByUsernameQuery);
        setAuthoritiesByUsernameQuery(authoritiesByUsernameQuery);
        setGroupAuthoritiesByUsernameQuery(groupAuthoritiesByUsernameQuery);
        setDataSource(dataSource);
    }
    public UserDetails loadUserByUsernameAndRealm(String username, String realm) {
        List<UserDetails> users = loadUsersByUsernameAndRealm(username, realm);
        if (users.size() == 0) {
            throw new UsernameNotFoundException("Username not found");
        }
        UserDetails user = users.get(0);
        List<GrantedAuthority> authorities = loadGroupAuthoritiesForRealm(user.getUsername(), realm);
        return createUserDetails(username, user, authorities);
    }
    protected List<UserDetails> loadUsersByUsernameAndRealm(String username, String realm) {
        return getJdbcTemplate().query(usersByUsernameAndRealmQuery, new String[] { username, realm },
          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);
              boolean accountNonExpired = true;
              boolean credentialsNonExpired = true;
              boolean accountNonLocked = true;
              return new User(username, password, enabled, accountNonExpired, credentialsNonExpired,
                accountNonLocked, AuthorityUtils.NO_AUTHORITIES);
            }
          }
        );
    }
    protected List<GrantedAuthority> loadGroupAuthoritiesForRealm(String username, String realm) {
        return getJdbcTemplate().query(groupAuthoritiesByUsernameAndRealmQuery, new String[] { username, realm },
          new RowMapper<GrantedAuthority>() {
            public GrantedAuthority mapRow(ResultSet rs, int rowNum) throws SQLException {
              String roleName = getRolePrefix() + rs.getString(3);
              return new SimpleGrantedAuthority(roleName);
            }
          }
        );
    }
}

security-context.xml

<beans:beans xmlns="http://www.springframework.org/schema/security"
             xmlns:beans="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security.xsd">
    <http use-expressions="true" request-matcher="regex">
        <intercept-url pattern="/admin/.*" access="hasRole('ROLE_ADMIN')" />
        <form-login
            login-page="/auth/login"
            authentication-failure-url="/auth/login?fail"
            default-target-url="/"
            login-processing-url="/auth/login"
            authentication-details-source-ref="authenticationDetailSource" />
        <logout
            logout-url="/auth/logout"
            logout-success-url="/" />
    </http>
    <global-method-security pre-post-annotations="enabled"/>
    <authentication-manager>
        <authentication-provider ref="authenticationProvider" />
    </authentication-manager>
    <beans:bean name="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
</beans:beans>

Скорее всего, у вас другая структура БД, запросы надо подредактировать под себя. Все параметры UserDetailsService можно вынести в конфигурацию, если вам так будет удобнее. И рекомендую организовать кэширование UserDetails, чтобы не дёргать БД на каждом запросе.

READ ALSO
Thinking in Java. Помогите понять задание: part14.project

Thinking in Java. Помогите понять задание: part14.project

ПроектНапишите систему, использующую динамические заместители для реализации транзакций: заместитель закрепляет транзакцию, если опосредованный...

284
Как узнать индекс кнопки при клике

Как узнать индекс кнопки при клике

Есть массив из кнопок

195
Проблема путей пакетов в RMI

Проблема путей пакетов в RMI

Представим, что у нас есть два приложенияОни имеют разные пакеты

207