При использовании Spring Security, каков правильный способ получить текущее имя пользователя (т. е.SecurityContext) информация в компоненте?
-
05-07-2019 - |
Вопрос
У меня есть веб-приложение Spring MVC, которое использует Spring Security.Я хочу знать имя пользователя, вошедшего в систему в данный момент.Я использую фрагмент кода, приведенный ниже.Это общепринятый способ?
Мне не нравится, когда внутри этого контроллера вызывается статический метод - это сводит на нет всю цель Spring, ИМХО.Есть ли способ настроить приложение так, чтобы вместо него вводился текущий SecurityContext или текущая аутентификация?
@RequestMapping(method = RequestMethod.GET)
public ModelAndView showResults(final HttpServletRequest request...) {
final String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
...
}
Решение
Если вы используете Spring 3 , самый простой способ:
@RequestMapping(method = RequestMethod.GET)
public ModelAndView showResults(final HttpServletRequest request, Principal principal) {
final String currentUser = principal.getName();
}
Другие советы
С тех пор, как на этот вопрос был дан ответ, многое изменилось в мире Spring. Spring упростил получение текущего пользователя в контроллере. Для других бинов 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
непосредственно в bean-компонент во время инициализации - его, возможно, придется извлекать из ThreadLocal
каждый раз, в мульти многопоточная среда, поэтому получается правильная.
Я согласен с тем, что при запросе 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;
}
Чтобы он просто отображался на ваших страницах JSP, вы можете использовать тег безопасности Spring Lib:
http://static.springsource.org/spring-security/site/docs/3.0.x/reference/taglibs.html
Чтобы использовать любой из тегов, у вас должен быть taglib безопасности, объявленный в вашем 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-выражения="истина"
к тегу "http" в конфигурации security.xml, чтобы это сработало.
Если вы используете 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.
Я получаю аутентифицированного пользователя с помощью 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 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) {
...
}
Да, как правило, статика - это плохо, но в этом случае статика - самый безопасный код, который вы можете написать. Поскольку контекст безопасности связывает принципала с текущим выполняющимся потоком, наиболее безопасный код будет обращаться к статическому потоку из потока как можно напрямую. Скрытие доступа за внедренным классом-оболочкой дает атакующему больше очков для атаки. Им не понадобится доступ к коду (который им будет сложно изменить, если jar был подписан), им просто нужен способ переопределить конфигурацию, что можно сделать во время выполнения или вставить какой-то XML-файл в путь к классам. Даже использование внедрения аннотаций в подписанном коде может быть заменено внешним XML. Такой XML может внедрить в работающую систему мошеннического принципала. Вероятно, поэтому Spring делает что-то не похожее на Spring в этом случае.
Я бы просто сделал это:
request.getRemoteUser();
Для последнего написанного мной приложения Spring MVC я не вставлял держатель SecurityContext, но у меня был базовый контроллер, у меня было два служебных метода, связанных с этим ... isAuthenticated () & amp; GetUserName (). Внутренне они выполняют статический вызов метода, который вы описали.
По крайней мере, тогда это будет только в одном месте, если вам понадобится последующий рефакторинг.
Вы можете использовать подход Spring AOP. Например, если у вас есть какой-то сервис, он должен знать текущий принципал. Вы можете ввести пользовательскую аннотацию, т. Е. @Principal, которая указывает, что эта служба должна зависеть от принципала.
public class SomeService {
private String principal;
@Principal
public setPrincipal(String principal){
this.principal=principal;
}
}
Затем в своем совете, который, как мне кажется, необходимо расширить MethodBeforeAdvice, проверьте, что у конкретного сервиса есть аннотация @Principal, и введите имя Принципала или установите вместо него значение «ANONYMOUS».
Единственная проблема заключается в том, что даже после аутентификации в Spring Security пользовательский / основной бин не существует в контейнере, поэтому внедрение через зависимости будет затруднено. Перед тем, как использовать Spring Security, мы создадим bean-объект в области сеанса с текущим принципалом и вставим его в «AuthService». а затем внедрить эту службу в большинство других служб в приложении. Таким образом, эти службы просто вызовут authService.getCurrentUser () для получения объекта. Если в вашем коде есть место, где вы получаете ссылку на того же Принципала в сеансе, вы можете просто установить его в качестве свойства для вашего bean-объекта в области сеанса.
Попробуйте это
Аутентификация аутентификации = . SecurityContextHolder.getContext () getAuthentication ();
Строка userName = authentication.getName ();
Лучшее решение, если вы используете 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();
}
...
}
Я использую аннотацию @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 вызовет текущего аутентифицированного пользователя в вашем методе при вызове.
Я хотел бы поделиться своим способом поддержки пользовательских данных на странице бесплатного маркера. Все очень просто и работает отлично!
Вам просто нужно разместить запрос на аутентификацию в default-target-url
(страница после входа в систему)
Это мой метод Контролера для этой страницы:
@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>
И все, имя пользователя будет появляться на каждой странице после авторизации.