У меня есть веб-приложение Spring MVC, которое использует Spring Security. Я хочу знать имя пользователя, вошедшего в систему в данный момент. Я использую приведенный ниже фрагмент кода. Это приемлемый способ?
Мне не нравится вызов статического метода внутри этого контроллера - это побеждает всю цель Spring, ИМХО. Есть ли способ настроить приложение, чтобы вместо этого вводился текущий SecurityContext или текущая проверка подлинности?
@RequestMapping(method = RequestMethod.GET)
public ModelAndView showResults(final HttpServletRequest request...) {
final String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
...
}




Для последнего написанного мной приложения Spring MVC я не вводил держатель SecurityContext, но у меня был базовый контроллер, с которым у меня было два служебных метода, связанных с этим ... isAuthenticated () и getUsername (). Внутри они выполняют описанный вами статический вызов метода.
По крайней мере, тогда это будет только один раз, если вам понадобится позже рефакторинг.
Вы можете использовать Spring AOP aproach. Например, если у вас есть служба, которой необходимо знать текущего участника. Вы можете ввести пользовательскую аннотацию, например @Principal, которая указывает на то, что эта служба должна зависеть от принципала.
public class SomeService {
private String principal;
@Principal
public setPrincipal(String principal){
this.principal=principal;
}
}
Затем в вашем совете, который, как мне кажется, необходимо расширить MethodBeforeAdvice, проверьте, что эта конкретная служба имеет аннотацию @Principal и введите имя участника или вместо этого установите для нее значение «АНОНИМНЫЙ».
Мне нужен доступ к принципалу внутри класса обслуживания, не могли бы вы опубликовать полный пример на github? Я не знаю Spring АОП, отсюда и просьба.
Единственная проблема заключается в том, что даже после аутентификации с помощью Spring Security компонент пользователя / участника не существует в контейнере, поэтому внедрение зависимостей будет затруднено. Перед тем, как использовать Spring Security, мы должны были создать bean-компонент с ограниченным сеансом, у которого был текущий участник, внедрить его в «AuthService», а затем внедрить этот сервис в большинство других сервисов в приложении. Таким образом, эти Службы просто вызовут authService.getCurrentUser (), чтобы получить объект. Если у вас есть место в вашем коде, где вы получаете ссылку на того же участника в сеансе, вы можете просто установить его как свойство для вашего bean-компонента с областью действия сеанса.
Я согласен с тем, что запрашивать SecurityContext для текущего пользователя воняет, это кажется очень не-Spring способом справиться с этой проблемой.
Я написал статический «вспомогательный» класс для решения этой проблемы; он грязный в том смысле, что это глобальный и статический метод, но я так понял, если мы изменим что-либо, связанное с безопасностью, по крайней мере, мне нужно будет изменить детали только в одном месте:
/**
* Returns the domain User object for the currently logged in user, or null
* if no User is logged in.
*
* @return User object for the currently logged in user, or null if no User
* is logged in.
*/
public static User getCurrentUser() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal()
if (principal instanceof MyUserDetails) return ((MyUserDetails) principal).getUser();
// principal object is either null or represents anonymous user -
// neither of which our domain User object can represent - so return null
return null;
}
/**
* Utility method to determine if the current user is logged in /
* authenticated.
* <p>
* Equivalent of calling:
* <p>
* <code>getCurrentUser() != null</code>
*
* @return if user is logged in
*/
public static boolean isLoggedIn() {
return getCurrentUser() != null;
}
он имеет длину, равную SecurityContextHolder.getContext (), и последний является потокобезопасным, поскольку сохраняет детали безопасности в threadLocal. Этот код не поддерживает состояние.
С тех пор, как был дан ответ на этот вопрос, в мире Spring многое изменилось. Spring упростил получение текущего пользователя в контроллере. Для других bean-компонентов Spring принял предложения автора и упростил внедрение SecurityContextHolder. Подробности в комментариях.
Это решение, к которому я пришел. Вместо использования SecurityContextHolder в моем контроллере я хочу внедрить что-то, что использует SecurityContextHolder под капотом, но абстрагирует этот одноэлементный класс из моего кода. Я не нашел другого способа сделать это, кроме разворачивания собственного интерфейса, например:
public interface SecurityContextFacade {
SecurityContext getContext();
void setContext(SecurityContext securityContext);
}
Теперь мой контроллер (или любой другой POJO) будет выглядеть так:
public class FooController {
private final SecurityContextFacade securityContextFacade;
public FooController(SecurityContextFacade securityContextFacade) {
this.securityContextFacade = securityContextFacade;
}
public void doSomething(){
SecurityContext context = securityContextFacade.getContext();
// do something w/ context
}
}
А поскольку интерфейс является точкой разделения, модульное тестирование не вызывает затруднений. В этом примере я использую Mockito:
public class FooControllerTest {
private FooController controller;
private SecurityContextFacade mockSecurityContextFacade;
private SecurityContext mockSecurityContext;
@Before
public void setUp() throws Exception {
mockSecurityContextFacade = mock(SecurityContextFacade.class);
mockSecurityContext = mock(SecurityContext.class);
stub(mockSecurityContextFacade.getContext()).toReturn(mockSecurityContext);
controller = new FooController(mockSecurityContextFacade);
}
@Test
public void testDoSomething() {
controller.doSomething();
verify(mockSecurityContextFacade).getContext();
}
}
Реализация интерфейса по умолчанию выглядит так:
public class SecurityContextHolderFacade implements SecurityContextFacade {
public SecurityContext getContext() {
return SecurityContextHolder.getContext();
}
public void setContext(SecurityContext securityContext) {
SecurityContextHolder.setContext(securityContext);
}
}
И, наконец, производственная конфигурация Spring выглядит так:
<bean id = "myController" class = "com.foo.FooController">
...
<constructor-arg index = "1">
<bean class = "com.foo.SecurityContextHolderFacade">
</constructor-arg>
</bean>
Кажется более чем немного глупым, что Spring, контейнер для внедрения зависимостей всего, не предоставил способ внедрить что-то подобное. Я так понимаю, SecurityContextHolder унаследован от acegi, но все же. Дело в том, что они так близки - если бы только у SecurityContextHolder был геттер для получения базового экземпляра SecurityContextHolderStrategy (который является интерфейсом), вы могли бы внедрить его. Фактически, я даже открыл выпуск Jira на этот счет.
И последнее - я только что существенно изменил ответ, который у меня был здесь раньше. Если вам интересно, проверьте историю, но, как мне заметил коллега, мой предыдущий ответ не работал бы в многопоточной среде. Базовый SecurityContextHolderStrategy, используемый SecurityContextHolder, по умолчанию является экземпляром ThreadLocalSecurityContextHolderStrategy, который хранит SecurityContext в ThreadLocal. Следовательно, не обязательно вводить SecurityContext непосредственно в компонент во время инициализации - его может потребоваться каждый раз извлекать из ThreadLocal в многопоточной среде, чтобы был извлечен правильный.
Мне нравится ваше решение - это умное использование поддержки фабричных методов в Spring. Тем не менее, это работает для вас, потому что объект контроллера привязан к веб-запросу. Если вы неправильно измените область действия bean-компонента контроллера, это может сломаться.
Предыдущие два комментария относятся к старому неправильному ответу, который я только что заменил.
Это все еще рекомендуемое решение в текущем выпуске Spring? Я не могу поверить, что ему нужно столько кода, чтобы получить только имя пользователя.
Если вы используете Spring Security 3.0.x, они реализовали мое предложение в проблеме JIRA. Я зарегистрировал jira.springsource.org/browse/SEC-1188, поэтому теперь вы можете вставить экземпляр SecurityContextHolderStrategy (из SecurityContextHolder) непосредственно в свой bean-компонент через стандартную конфигурацию Spring.
См. Ответ tsunade21. Spring 3 теперь позволяет использовать java.security.Principal в качестве аргумента метода в вашем контроллере.
Да, статика в целом плохая, но в этом случае статика - это наиболее безопасный код, который вы можете написать. Поскольку контекст безопасности связывает участника с текущим запущенным потоком, наиболее безопасный код будет обращаться к статике из потока как можно напрямую. Скрытие доступа за внедренным классом-оболочкой дает злоумышленнику больше точек для атаки. Им не понадобится доступ к коду (который им было бы трудно изменить, если бы jar был подписан), им просто нужен способ переопределить конфигурацию, что можно сделать во время выполнения или вставить некоторый XML в путь к классам. Даже использование инъекции аннотации в подписанный код можно переопределить с помощью внешнего XML. Такой XML может внедрить в работающую систему мошенника. Вероятно, поэтому Spring в данном случае делает что-то не похожее на Spring.
Я бы просто сделал это:
request.getRemoteUser();
Это может сработать, но ненадежно. Из javadoc: «Отправляется ли имя пользователя с каждым последующим запросом, зависит от браузера и типа аутентификации». -download-llnw.oracle.com/javaee/6/api/javax/servlet/http/…
На самом деле это действительный и очень простой способ получить удаленное имя пользователя в веб-приложении Spring Security. Стандартная цепочка фильтров включает SecurityContextHolderAwareRequestFilter, который обертывает запрос и реализует этот вызов, обращаясь к SecurityContextHolder.
Я получаю аутентифицированного пользователя HttpServletRequest.getUserPrincipal ();
Пример:
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.support.RequestContext;
import foo.Form;
@Controller
@RequestMapping(value = "/welcome")
public class IndexController {
@RequestMapping(method=RequestMethod.GET)
public String getCreateForm(Model model, HttpServletRequest request) {
if (request.getUserPrincipal() != null) {
String loginName = request.getUserPrincipal().getName();
System.out.println("loginName : " + loginName );
}
model.addAttribute("form", new Form());
return "welcome";
}
}
Мне нравится Ваше решение. Специалистам Spring: это хорошее и надежное решение?
Не лучшее решение. Вы получите null, если пользователь аутентифицирован анонимно (элементы http> anonymous в Spring Security XML). SecurityContextHolder или SecurityContextHolderStrategy - правильный путь.
Итак, я проверил, не равен ли null request.getUserPrincipal ()! = Null.
Нулевое значение в фильтре
Если вы используете Spring 3, самый простой способ:
@RequestMapping(method = RequestMethod.GET)
public ModelAndView showResults(final HttpServletRequest request, Principal principal) {
final String currentUser = principal.getName();
}
Попробуй это
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String userName = authentication.getName();
Вызов статического метода SecurityContextHolder.getContext () - это именно то, на что я жаловался в исходном вопросе. Вы ничего не ответили.
Однако это именно то, что рекомендует документация: static.springsource.org/spring-security/site/docs/3.0.x/… Итак, чего вы добиваетесь, избегая этого? Вы ищете сложное решение простой проблемы. В лучшем случае - вы получите такое же поведение. В худшем случае вы получите ошибку или дыру в безопасности.
@BobKerns Для тестирования было бы проще внедрить аутентификацию, а не помещать ее в локальный поток.
Лучшее решение, если вы используете Spring 3 и вам нужен аутентифицированный участник в вашем контроллере, - это сделать что-то вроде этого:
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
@Controller
public class KnoteController {
@RequestMapping(method = RequestMethod.GET)
public java.lang.String list(Model uiModel, UsernamePasswordAuthenticationToken authToken) {
if (authToken instanceof UsernamePasswordAuthenticationToken) {
user = (User) authToken.getPrincipal();
}
...
}
Почему вы выполняете проверку экземпляра UsernamePasswordAuthenticationToken, когда параметр уже имеет тип UsernamePasswordAuthenticationToken?
(authToken instanceof UsernamePasswordAuthenticationToken) является функциональным эквивалентом if (authToken! = null). Последний может быть немного чище, но в остальном разницы нет.
Чтобы он просто отображался на ваших JSP-страницах, вы можете использовать Spring Security Tag Lib:
http://static.springsource.org/spring-security/site/docs/3.0.x/reference/taglibs.html
Чтобы использовать любой из тегов, в вашем JSP должна быть объявлена библиотека тегов безопасности:
<%@ taglib prefix = "security" uri = "http://www.springframework.org/security/tags" %>
Затем на странице jsp сделайте что-то вроде этого:
<security:authorize access = "isAuthenticated()">
logged in as <security:authentication property = "principal.username" />
</security:authorize>
<security:authorize access = "! isAuthenticated()">
not logged in
</security:authorize>
ПРИМЕЧАНИЕ. Как упоминалось в комментариях @ SBerg413, вам необходимо добавить
use-expressions = "true"
к тегу "http" в конфигурации security.xml, чтобы это работало.
Похоже, это, вероятно, одобренный Spring Security способ!
Чтобы этот метод работал, вам необходимо добавить use-expression = "true" в тег http в конфигурации security.xml.
Спасибо @ SBerg413, я отредактирую свой ответ и добавлю ваше важное уточнение!
Мне нравится делиться своим способом поддержки информации о пользователях на странице freemarker. Все очень просто и отлично работает!
Вам просто нужно разместить повторный запрос аутентификации на default-target-url (страница после входа в форму)
Это мой метод Controler для этой страницы:
@RequestMapping(value = "/monitoring", method = RequestMethod.GET)
public ModelAndView getMonitoringPage(Model model, final HttpServletRequest request) {
showRequestLog("monitoring");
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String userName = authentication.getName();
//create a new session
HttpSession session = request.getSession(true);
session.setAttribute("username", userName);
return new ModelAndView(catalogPath + "monitoring");
}
А это мой ftl-код:
<@security.authorize ifAnyGranted = "ROLE_ADMIN, ROLE_USER">
<p style = "padding-right: 20px;">Logged in as ${username!"Anonymous" }</p>
</@security.authorize>
И все, имя пользователя будет отображаться на каждой странице после авторизации.
Спасибо за попытку ответить, но использование статического метода SecurityContextHolder.getContext () - это именно то, чего я хотел избежать, и это причина, по которой я задал этот вопрос в первую очередь.
В Spring 3+ у вас есть следующие возможности.
Опция 1 :
@RequestMapping(method = RequestMethod.GET)
public String currentUserNameByPrincipal(Principal principal) {
return principal.getName();
}
Вариант 2:
@RequestMapping(method = RequestMethod.GET)
public String currentUserNameByAuthentication(Authentication authentication) {
return authentication.getName();
}
Вариант 3:
@RequestMapping(method = RequestMethod.GET)
public String currentUserByHTTPRequest(HttpServletRequest request) {
return request.getUserPrincipal().getName();
}
Вариант 4: Необычный: Проверьте это для получения более подробной информации
public ModelAndView someRequestHandler(@ActiveUser User activeUser) {
...
}
Начиная с версии 3.2, spring-security-web поставляется с @CurrentUser, который работает как пользовательский @ActiveUser из вашей ссылки.
@MikePartridge, кажется, я не нашел, что вы говорите, ни одной ссылки ?? или подробнее ??
Моя ошибка - я неправильно понял javadoc Spring Security AuthenticationPrincipalArgumentResolver. Там показан пример упаковки @AuthenticationPrincipal с пользовательской аннотацией @CurrentUser. Начиная с версии 3.2 нам не нужно реализовывать настраиваемый преобразователь аргументов, как в связанном ответе. Этот другой ответ имеет более подробную информацию.
Я использую аннотацию @AuthenticationPrincipal в классах @Controller, а также в аннотированных классах @ControllerAdvicer. Бывший.:
@ControllerAdvice
public class ControllerAdvicer
{
private static final Logger LOGGER = LoggerFactory.getLogger(ControllerAdvicer.class);
@ModelAttribute("userActive")
public UserActive currentUser(@AuthenticationPrincipal UserActive currentUser)
{
return currentUser;
}
}
Где UserActive - это класс, который я использую для служб зарегистрированных пользователей, и расширяется от org.springframework.security.core.userdetails.User. Что-то типа:
public class UserActive extends org.springframework.security.core.userdetails.User
{
private final User user;
public UserActive(User user)
{
super(user.getUsername(), user.getPasswordHash(), user.getGrantedAuthorities());
this.user = user;
}
//More functions
}
Действительно просто.
Определите Principal как зависимость в методе вашего контроллера, и spring внедрит текущего аутентифицированного пользователя в ваш метод при вызове.
Если вы используете Spring Security ver> = 3.2, вы можете использовать аннотацию @AuthenticationPrincipal:
@RequestMapping(method = RequestMethod.GET)
public ModelAndView showResults(@AuthenticationPrincipal CustomUser currentUser, HttpServletRequest request) {
String currentUsername = currentUser.getUsername();
// ...
}
Здесь CustomUser - это настраиваемый объект, реализующий UserDetails, который возвращается настраиваемым UserDetailsService.
Дополнительную информацию можно найти в главе @AuthenticationPrincipal справочной документации Spring Security.
Почему у вас нет контроллера (безопасного контроллера) в качестве суперкласса, чтобы получить пользователя из SecurityContext и установить его как переменную экземпляра внутри этого класса? Таким образом, когда вы расширяете безопасный контроллер, весь ваш класс будет иметь доступ к принципалу User текущего контекста.