Використовуючи Spring Security, який правильний спосіб отримати поточну інформацію про ім'я користувача (тобто SecurityContext) в квасолі?


288

У мене є веб-додаток Spring MVC, який використовує Spring Security. Я хочу знати ім’я користувача, який зараз увійшов користувач. Я використовую фрагмент коду, наведений нижче. Це прийнятий спосіб?

Мені не подобається, що виклик статичного методу всередині цього контролера - це перемагає цілі Spring, IMHO. Чи є спосіб налаштувати додаток так, щоб замість нього вводили поточний SecurityContext або поточну автентифікацію?

  @RequestMapping(method = RequestMethod.GET)
  public ModelAndView showResults(final HttpServletRequest request...) {
    final String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
    ...
  }

Чому ви не маєте контролера (захищеного контролера) як суперкласу, отримуйте користувача з SecurityContext і встановлюйте його як змінну екземпляра всередині цього класу? Таким чином, коли ви розширюєте захищений контролер, весь ваш клас матиме доступ до головного користувачу поточного контексту.
Дехан де Кроос

Відповіді:


259

Якщо ви використовуєте Spring 3 , найпростіший спосіб:

 @RequestMapping(method = RequestMethod.GET)   
 public ModelAndView showResults(final HttpServletRequest request, Principal principal) {

     final String currentUser = principal.getName();

 }

69

У весняному світі багато що змінилося з того часу, коли на це питання було дано відповідь. 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 Spring виглядає приблизно так:

<bean id="myController" class="com.foo.FooController">
     ...
  <constructor-arg index="1">
    <bean class="com.foo.SecurityContextHolderFacade">
  </constructor-arg>
</bean>

Здається більш ніж трохи нерозумно, що Spring, контейнер для ін'єкцій залежностей від усіх речей, не запропонував способу ввести щось подібне. Я розумію, що SecurityContextHolderвін успадкував від ацегі, але все ж. Річ у тім, що вони настільки близькі - якби тільки SecurityContextHolderдістав, щоб отримати базовий SecurityContextHolderStrategyекземпляр (який є інтерфейсом), ви могли б ввести це. Насправді я навіть відкрив питання про Джиру для цього.

Останнє - я лише суттєво змінив відповідь, яку мав тут раніше. Перевірте історію, якщо вам цікаво, але, як зазначив мені колега, моя попередня відповідь не працювала б у багатопотоковому середовищі. За замовчуванням SecurityContextHolderStrategyвикористовується базовий SecurityContextHolderпримірник ThreadLocalSecurityContextHolderStrategy, який зберігає SecurityContexts в a ThreadLocal. Тому не обов’язково добре вводити SecurityContextбезпосередньо в квасолі під час ініціалізації - можливо, її потрібно буде отримувати ThreadLocalкожен раз у багатопотоковому середовищі, щоб отримати правильний.


1
Мені подобається ваше рішення - це розумне використання підтримки заводу-методу навесні. Однак, це працює для вас, оскільки об’єкт контролера підходить до веб-запиту. Якщо ви неправильно змінили область використання контролера, це порушиться.
Пол Морі

2
Попередні два коментарі стосуються старої, неправильної відповіді, яку я щойно замінив.
Скотт Бейл

12
Це все-таки рекомендоване рішення з поточним випуском Spring? Я не можу повірити, що йому потрібно так багато коду, щоб отримати лише ім'я користувача.
Ta Sas

6
Якщо ви використовуєте Spring Security 3.0.x, вони реалізували мою пропозицію у випуску JIRA, у якому я зареєструвався jira.springsource.org/browse/SEC-1188, тож тепер ви можете ввести екземпляр SecurityContextHolderStrategy (із SecurityContextHolder) безпосередньо у ваш боб через стандартний Конфігурація весни.
Скотт Бейл

4
Будь ласка, дивіться відповідь tsunade21. Весна 3 тепер дозволяє використовувати java.security.Principal як аргумент методу у вашому контролері
Патрік

22

Я погоджуюся з тим, що потребуючи запитів у SecurityContext для поточного користувача, смердить, здається, дуже не весняним способом вирішення цієї проблеми.

Я написав статичний клас «помічник» для вирішення цієї проблеми; брудно тим, що це глобальний і статичний метод, але я зрозумів, що таким чином, якщо ми змінимо щось, що стосується безпеки, принаймні мені доведеться лише змінити деталі в одному місці:

/**
* 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;
}

22
він до тих пір, поки SecurityContextHolder.getContext () є, і останній є безпечним для потоків, оскільки він зберігає деталі безпеки в потоці Local. Цей код не підтримує стан.
мат б

22

Щоб відобразити його просто на своїх сторінках JSP, ви можете використовувати Spring Spring 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 = "вірно"

до тегу "http" у конфігурації security.xml, щоб це працювало.


Це здається, що це, мабуть, затверджений Весняною безпекою спосіб!
Нік Спейпк

3
Щоб цей метод працював, вам потрібно додати тег http у тезі http у конфігурації security.xml.
SBerg413

Дякую @ SBerg413, я відредагую свою відповідь та додаю важливе уточнення!
Бред Паркс

14

Якщо ви використовуєте 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.


13

Я отримую аутентифікованого користувача 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: це безпечне, хороше рішення?
marioosh

Не вдале рішення. Ви отримаєте, nullякщо користувач має анонімну автентифікацію ( http> anonymousелементи у Spring Security XML). SecurityContextHolderабо SecurityContextHolderStrategyналежним чином.
Nowaker

1
Так що я перевірив, якщо не нульовий request.getUserPrincipal ()! = Null.
digz6666


9

Навесні 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) {
  ...
}

1
Починаючи з 3.2, постачається Spring-security-web, @CurrentUserякий працює як звичайний @ActiveUserз вашого посилання.
Майк Партрідж

@MikePartridge, я, здається, не знаходжу те, що ви говорите, жодного посилання ?? чи більше інформації ??
azerafati

1
Моя помилка - я неправильно зрозумів Spring Security AuthenticationPrincipalArgumentResolver javadoc. Наведений приклад обгортання @AuthenticationPrincipalіз власною @CurrentUserанотацією. Починаючи з 3.2, нам не потрібно реалізовувати власну розв’язувачу аргументів, як у пов'язаній відповіді. У цій іншій відповіді є детальніше.
Майк Партрідж

5

Так, статика взагалі погана - загалом, але в цьому випадку статика - це найбезпечніший код, який можна написати. Оскільки контекст безпеки асоціює Principal з поточним потоковим потоком, найбезпечніший код отримає доступ до статики з потоку якомога прямо. Приховування доступу за класом обгортки, який вводиться, забезпечує зловмиснику більше очок для атаки. Їм не знадобиться доступ до коду (який би важко змінився, якщо jar був підписаний), їм просто потрібен спосіб переосмислити конфігурацію, що можна зробити під час виконання або просунути якийсь XML на класний шлях. Навіть використання ін'єкції анотацій у підписаному коді було б переносимим із зовнішнім XML. Такий XML може вводити працюючу систему із шахрайським принципом.


5

Я просто зробив би це:

request.getRemoteUser();

1
Це може спрацювати, але не надійно. Від javadoc: "Чи надсилається ім'я користувача з кожним наступним запитом, залежить від браузера та типу аутентифікації." - скачати
llnw.oracle.com/javaee/

3
Це фактично дійсний і дуже простий спосіб отримати віддалене ім’я користувача у веб-програмі Spring Security. Стандартна ланцюжок фільтрів включає в себе SecurityContextHolderAwareRequestFilterзапит і реалізує цей виклик шляхом доступу до SecurityContextHolder.
Шон овець

4

Для останньої програми MVC про весну, яку я написав, я не ввів власника SecurityContext, але у мене був базовий контролер, що у мене були два утилітні методи, пов’язані з цим ... isAuthentication () & getUsername (). Всередині вони роблять описаний вами статичний метод.

Принаймні тоді це лише один раз, якщо вам потрібно згодом рефактор.


3

Ви можете використати весняний підхід AOP. Наприклад, якщо у вас є якась послуга, вона повинна знати поточного основного. Ви можете ввести спеціальну анотацію, тобто @Principal, яка вказує на те, що ця Служба повинна залежати від принципу.

public class SomeService {
    private String principal;
    @Principal
    public setPrincipal(String principal){
        this.principal=principal;
    }
}

Тоді у вашій раді, яку, на мою думку, потрібно поширити на MethodBeforeAdvice, перевірте, чи конкретна служба має @Principal анотацію та вводить прізвище Principal, або встановіть замість нього «ANONYMOUS».


Мені потрібно звернутися до Principal всередині класу обслуговування. Чи можете ви опублікувати повний приклад на github? Я не знаю весняного АОП, звідси запит.
Ракеш Вагела

2

Єдина проблема полягає в тому, що навіть після автентифікації з Spring Security, користувач / основний боб не існує в контейнері, тому ввести залежність буде важко. Перш ніж ми використали Spring Security, ми створили бин, що охоплює сеанс, який мав поточний Principal, ввести його в "AuthService", а потім ввести цю Службу в більшість інших служб Додатку. Таким чином, ці Служби просто зателефонували authService.getCurrentUser (), щоб отримати об'єкт. Якщо у вашому коді є місце, де ви отримуєте посилання на того самого Principal під час сеансу, ви можете просто встановити його як властивість у вашому сеансі.


1

Спробуйте це

Автентифікація автентифікації = SecurityContextHolder.getContext (). GetAuthentication ();
Рядок userName = authentication.getName ();


3
Виклик SecurityContextHolder.getContext () статичного методу - це саме те, на що я скаржився в оригінальному запитанні. Ви нічого не відповіли.
Скотт Бейл

2
Однак саме це рекомендує документація: static.springsource.org/spring-security/site/docs/3.0.x/… Тож що ви робите, уникаючи цього? Ви шукаєте складне рішення простої проблеми. У кращому випадку - ви отримуєте таку саму поведінку. У гіршому випадку ви отримаєте помилку або захисну дірку.
Боб Кернс

2
@BobKerns Для тестування зрозуміліше мати можливість ввести автентифікацію на відміну від того, щоб ставити її на локальний потік.

1

Найкраще рішення, якщо ви використовуєте 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();
            }
            ...

    }

1
Чому ви робите цей екземпляр UsernamePasswordAuthenticationToken перевіряйте, коли параметр вже типу типу UsernamePasswordAuthenticationToken?
Скотт Бейл

(authToken instanceof UsernamePasswordAuthenticationToken) є функціональним еквівалентом if (authToken! = null). Останні можуть бути трохи чистішими, але в іншому випадку різниці немає.
Марк

1

Я використовую @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
}

Дійсно легко.


0

Визначте Principalяк залежність у вашому методі контролера, і пружина буде вводити поточного аутентифікованого користувача у ваш метод при виклику.


-2

Мені подобається ділитися способом підтримки деталей користувачів на сторінці freemarker. Все дуже просто і працює чудово!

Вам просто потрібно розмістити запит на автентифікацію на 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> 

І це все, ім’я користувача з'явиться на кожній сторінці після авторизації.


Дякую за спробу відповісти, але використання статичного методу SecurityContextHolder.getContext () - це саме те, чого я хотів уникнути, і причиною я задав це питання в першу чергу.
Скотт Бейл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.