当使用Spring Security时,在bean中获取当前用户名(即SecurityContext)信息的正确方法是什么?
我有一个使用Spring Security的Spring MVC web应用程序。 我想知道当前login用户的用户名。 我正在使用下面给出的代码片段。 这是被接受的方式吗?
我不喜欢有一个调用这个控制器内的静态方法 – 这是挫败了Spring的全部目的,恕我直言。 有没有一种方法来configuration应用程序,而不是注入当前的SecurityContext或当前的身份validation?
@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简化了控制器中的当前用户。 对于其他bean,Spring采用了作者的build议,简化了“SecurityContextHolder”的注入。 更多细节在评论。
这是我最终解决的解决scheme。 而不是在我的控制器中使用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 } }
而且,由于接口是一个解耦点,所以unit testing很简单。 在这个例子中,我使用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); } }
最后,生产Springconfiguration看起来像这样:
<bean id="myController" class="com.foo.FooController"> ... <constructor-arg index="1"> <bean class="com.foo.SecurityContextHolderFacade"> </constructor-arg> </bean>
Spring似乎有点愚蠢,Spring是一切dependency injection的容器,并没有提供一种类似的注入方式。 我明白SecurityContextHolder
是从acegiinheritance,但仍然。 事情是,他们是如此接近 – 如果只有SecurityContextHolder
有一个getter来获取底层SecurityContextHolderStrategy
实例(这是一个接口),你可以注入。 事实上,我甚至为此打开了一个Jira问题 。
最后一件事 – 我刚刚大大改变了我以前在这里的答案。 如果您好奇,请检查历史logging,但正如同事指出的那样,我以前的回答在multithreading环境下不起作用。 SecurityContextHolderStrategy
使用的底层SecurityContextHolderStrategy
默认情况下是一个ThreadLocalSecurityContextHolderStrategy
实例,它将SecurityContext
存储在一个ThreadLocal
。 因此,在初始化的时候将SecurityContext
直接注入到bean中并不一定是个好主意 – 它可能需要在multithreading环境中每次从ThreadLocal
检索,所以才能检索到正确的。
我同意,不得不为当前用户查询SecurityContext,似乎是一个非Spring的方式来处理这个问题。
我写了一个静态的“助手”类来处理这个问题。 这是一个全球性和静态的方法,但是我认为如果我们改变与安全相关的任何东西,至less我只需要在一个地方改变细节:
/** * 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 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的评论中所述,您需要添加
使用expression式=“真”
到security.xmlconfiguration文件中的“http”标签来运行。
我通过HttpServletRequest.getUserPrincipal()获得身份validation用户;
例:
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 Security ver> = 3.2,则可以使用@AuthenticationPrincipal
注释:
@RequestMapping(method = RequestMethod.GET) public ModelAndView showResults(@AuthenticationPrincipal CustomUser currentUser, HttpServletRequest request) { String currentUsername = currentUser.getUsername(); // ... }
在这里, CustomUser
是一个自定义对象,实现由UserDetailsService
自定义返回的UserDetailsService
。
更多的信息可以在Spring Security参考文档的@AuthenticationPrincipal章节find。
在春季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) { ... }
是的,静态一般是不好的 – 一般来说,但在这种情况下,静态是您可以编写的最安全的代码。 由于安全上下文将Principal与当前正在运行的线程相关联,因此最安全的代码将尽可能直接从线程访问静态。 隐藏被注入的包装类后面的访问提供了攻击者更多的攻击点。 他们不需要访问代码(如果jar被签名,他们将很难改变),他们只需要一种方法来覆盖configuration,这可以在运行时完成,或者将一些XML放到classpath中。 即使在签名代码中使用注释注入也会被外部XML覆盖。 这样的XML可以将运行系统注入stream氓主体。 这可能就是为什么Spring在这种情况下做了一些像Spring一样的事情。
我只是这样做:
request.getRemoteUser();
对于我写的最后一个Spring MVC应用程序,我没有注入SecurityContext持有者,但是我有一个基本控制器,我有两个与此相关的实用程序方法:isAuthenticated()和getUsername()。 他们在内部执行您所描述的静态方法调用。
至less那么只有一次,如果你需要以后重构。
你可以使用Spring AOP aproach。 例如,如果你有一些服务,那就需要知道当前的委托人。 你可以引入自定义注释,例如@Principal,它表示这个服务应该是主体依赖的。
public class SomeService { private String principal; @Principal public setPrincipal(String principal){ this.principal=principal; } }
然后在您的build议,我认为需要扩展MethodBeforeAdvice,检查该特定的服务有@Principal注释并注入主体名称,或将其设置为“匿名”。
唯一的问题是,即使在使用Spring Security进行身份validation之后,用户/主体bean也不存在于容器中,所以dependency injection是非常困难的。 在使用Spring Security之前,我们将创build一个具有当前Principal的会话范围的bean,将其注入到“AuthService”中,然后将该Service注入到应用程序中的大多数其他服务中。 所以这些服务只需调用authService.getCurrentUser()来获取对象。 如果你在你的代码中有一个地方,你可以在会话中获得对同一个委托人的引用,你可以简单地将它设置为会话作用域bean上的一个属性。
如果您使用Spring 3并且需要在您的控制器中使用身份validation的主体,那么最好的解决scheme就是这样做:
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(); } ... }
尝试这个
Authentication authentication = SecurityContextHolder.getContext()。getAuthentication();
String userName = authentication.getName();
我在@Controller
类中使用@AuthenticationPrincipal
注释以及@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
是我用于logging用户服务的类,并从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会在调用时在您的方法中注入当前已authentication的用户。
这是一个老问题,但我们仍然有不同的方式来获取用户名在2016年7月使用Spring 4,引导,安全,OAuth2和Angular2请纠正我,如果我错了,因为我正在寻找简单的解决scheme
@RequestMapping("/user") public Principal user(Principal principal) { if (principal != null) { System.out.println("#### Sample Debug Message for token = " + ((OAuth2AuthenticationDetails) ((OAuth2Authentication) principal).getDetails()) .getTokenValue()); String name = ( ((Map)((Map.Entry)((LinkedHashMap)((UsernamePasswordAuthenticationToken)((OAuth2Authentication)principal).getUserAuthentication()).getDetails()).entrySet().toArray()[2]).getValue()).entrySet().toArray()[6]).toString(); System.out.println("##### Debug user name = " + name); } else { System.out.println("##### found principal = null"); } return principal; }
我喜欢在freemarker页面上分享我的支持用户详细信息的方式。 一切都非常简单,完美的工作!
你只需要将身份validation重新请求置于default-target-url
(form-login之后的页面)这是我的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>
就是这样,授权后每个页面上都会出现用户名。