Spring Security 3 Custom Authentication

Posted on: September 13th, 2011 by Spade No Comments »

Редкое web приложение не имеет админ. части. В этой статье мы рассмотрим как настроить защищенный доступ к бэк-енду системы на Spring Framework 3. Для простоты будем полагать, что вся система закрыта, то есть администраторская часть лежит на отдельном домене/субдомене и не пересекается урл с front-end. Скачиваем необходимые библиотеки spring-security-…-3.0.5.RELEASE.jar (они идут отдельно) и приступаем…

В web.xml закрываем все пути:

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

В файле основного контекста подключаем файл для конфигурации секьюрити:

<import resource="security.xml" />

В этом файле будет такое содержимое:

    <http use-expressions="true" access-denied-page="/error403.jsp">
        <intercept-url pattern="/favicon.ico" filters="none"/>
        <intercept-url pattern="/images/**" filters="none"/>
        <intercept-url pattern="/js/**" filters="none"/>
        <intercept-url pattern="/resources/**" filters="none"/>
        <intercept-url pattern="/login.jsp" access="isAnonymous()"/>
        <intercept-url pattern="/**" access="isAuthenticated()"/>
 
        <form-login login-page="/login.jsp"
                    authentication-failure-url="/login.jsp?login_error=1"
                    default-target-url="/index.jsp"/>
        <http-basic/>
        <logout logout-url="/logout" invalidate-session="true" logout-success-url="/"/>
        <remember-me/>
    </http>

Тег intercept-url задает соответствие между адресом и политикой доступа. Вся статика отдается без фильтров – скрипты, картинки, таблицы стилей. Хотя можно закрыть. Открыли мы для того, чтоб страница логина могла отображаться используя общий стиль оформления системы. Дальше мы закрываем все адреса, кроме страницы логина. Тегом form-login мы настраиваем поведение при различных исходах попытки входа в систему – ошибки, страницы последующего перехода. Следующий тег конфигурирует выход из системы – logout-урл, куда перенаправлять пользователя после выхода и разрушать ли сессию (со всеми данными внутри). remember-me позволяет воплотить функции «запоминания пользователя» – с помощью кук. С помощью аттрибута data-source-ref можно принудить хранить информацию для логина на сервере – считается более надежным вариантом:

<remember-me data-source-ref="dataSource"/>

В этом случае в базе должна присутствовать таблица с именем persistent_logins с определенной структурой (на самостоятельное изучение).

Далее самое важное – Spring многое позволяет сделать из коробки, особо не заморачиваясь, но тогда нужно играть по их правилам. Сам процесс входа в систему при поиске пользователей с паролями будет лезть в определенные таблицы с определенной структурой в вашей базе. А что делать если база уже есть, и менять структуру таблиц пользовательских данных не желательно? Тогда Spring предоставит вам возможность имплементировать собственный механизм авторизации – используя нужны вам запросы.

    <authentication-manager>
        <authentication-provider user-service-ref="userDetailsServiceImpl">
            <password-encoder hash="md5"/>
        </authentication-provider>
    </authentication-manager>
 
    <beans:bean id="userDetailsServiceImpl"
               class="com.acestime.services.auth.AdminDetailsServiceImpl">
        <beans:property name="dataSource" ref="dataSource"/>
    </beans:bean>

Мы указываем класс-провайдер для аутентификации, и хеш-провайдер для пароля (если только ваш заказчик не хочет по какой-то странной причине хранить пароли в базе в открытом виде). И дальше пишем свой класс – он должен будет вернуть тип UserDetails – информацию о пользователе, которая будет хранится в сессии. Набор полей там очень беден – имя, пароль, и служебная информация. Если мы хотим при входе пользователя сразу запоминать еще и First name, Last name, E-mail и т.д. – можем расширить класс org.springframework.security.core.userdetails.User этими полями и возвращать его (главное потом привести к этому типу):

public class UserAuthInfo extends User{
 
    private String email;
    private String firstName;
    private String lastName;
 
    public UserAuthInfo(String username, String password, boolean enabled,
                        boolean accountNonExpired, boolean credentialsNonExpired,
                        boolean accountNonLocked, Collection authorities,
                        String email, String firstName, String lastName) {
 
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
        this.email = email;
        this.firstName = firstName;
        this.lastName = lastName;
    }
 
    public String getEmail() {
        return email;
    }
 
    public String getFirstName() {
        return firstName;
    }
 
    public String getLastName() {
        return lastName;
    }
}

Сам класс аутентификации выглядит так:

public class AdminDetailsServiceImpl extends JdbcDaoImpl {
 
    private static final String USERS_BY_USERNAME_QUERY =
            "select admin_name, admin_password, valid, firstname, lastname, email " +
            "from admins " +
            "where admin_name = ?";
 
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
 
        List users = loadUsersList(username);
 
        if (users.size() == 0) {
            logger.debug("Query returned no results for user '" + username + "'");
 
            throw new UsernameNotFoundException(
                    messages.getMessage("JdbcDaoImpl.notFound", new Object[]{username}, "Username {0} not found"), username);
        }
 
        return users.get(0);
    }
 
    protected List loadUsersList(String username) {
        return getJdbcTemplate().query(USERS_BY_USERNAME_QUERY, new String[] {username}, new RowMapper() {
            public UserAuthInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
                String username = rs.getString(1);
                String password = rs.getString(2);
                boolean enabled = rs.getBoolean(3);
                String firstName = rs.getString(4);
                String lastName = rs.getString(5);
                String email = rs.getString(6);
                return new UserAuthInfo(username, password, enabled, true, true, true, AuthorityUtils.NO_AUTHORITIES, firstName, lastName, email);
            }
        });
    }
}

Класс наследуем от JdbcDaoImpl, и перегружаем только нужный нам метод: loadUserByUsername(String username). В строковой константе пишем запрос актуальный для нашей собственной структуры базы данных. Достаем все нужные поля, и упаковываем в созданый ранее класс-обертку UserAuthInfo. Компилируем и запускаем приложение. Всем спасибо, ждем вопросов и предложений.

Leave a Reply