[英]What is the proper way to specify a xsd within a spring bean.xml
[英]When using Spring Security, what is the proper way to obtain current username (i.e. SecurityContext) information in a bean?
我有一个使用 Spring Security 的 Spring MVC Web 应用程序。 我想知道当前登录用户的用户名。 我正在使用下面给出的代码片段。 这是公认的方式吗?
我不喜欢在这个控制器中调用静态方法 - 这违背了 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 简化了在控制器中获取当前用户的过程。 对于其他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
有一个 getter 来获取底层SecurityContextHolderStrategy
实例(这是一个接口),您可以注入它。 事实上,我什至为此打开了一个 Jira 问题。
最后一件事 - 我刚刚大大改变了我之前在这里的答案。 如果您好奇,请查看历史记录,但正如一位同事向我指出的那样,我之前的答案在多线程环境中不起作用。 底层SecurityContextHolderStrategy
使用SecurityContextHolder
,默认情况下,的一个实例ThreadLocalSecurityContextHolderStrategy
,其存储SecurityContext
S IN一个ThreadLocal
。 因此,在初始化时将SecurityContext
直接注入 bean 不一定是一个好主意 - 在多线程环境中,每次可能需要从ThreadLocal
检索它,因此检索到正确的。
我同意必须查询当前用户的 SecurityContext 很糟糕,这似乎是处理这个问题的一种非常非 Spring 的方式。
我写了一个静态的“helper”类来处理这个问题; 它很脏,因为它是一种全局和静态方法,但我认为如果我们更改与安全相关的任何内容,至少我只需要在一个地方更改细节:
/**
* 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 在评论中提到的,您需要添加
使用表达式=“真”
到 security.xml 配置中的“http”标记以使其工作。
如果您使用的是 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
。
更多信息可以在 Spring Security 参考文档的@AuthenticationPrincipal章节中找到。
我通过 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) {
...
}
我只会这样做:
request.getRemoteUser();
是的,静态通常是不好的 - 通常,但在这种情况下,静态是您可以编写的最安全的代码。 由于安全上下文将 Principal 与当前运行的线程相关联,因此最安全的代码将尽可能直接地从线程访问静态。 将访问隐藏在注入的包装类后面,为攻击者提供了更多攻击点。 他们不需要访问代码(如果 jar 被签名,他们将很难更改代码),他们只需要一种覆盖配置的方法,这可以在运行时完成或将一些 XML 滑到类路径上。 即使在签名代码中使用注解注入也可以被外部 XML 覆盖。 这样的 XML 可能会向正在运行的系统注入一个流氓主体。 这可能就是为什么在这种情况下 Spring 会做一些与 Spring 不同的事情。
对于我写的最后一个 Spring MVC 应用程序,我没有注入 SecurityContext 持有者,但我确实有一个基本控制器,我有两个与此相关的实用程序方法...... isAuthenticated() & getUsername()。 在内部,他们执行您描述的静态方法调用。
至少,如果您以后需要重构,它只会出现一次。
您可以使用 Spring AOP 方法。 例如,如果您有一些服务,则需要知道当前的委托人。 您可以引入自定义注释,即 @Principal ,这表明该服务应该是主体相关的。
public class SomeService {
private String principal;
@Principal
public setPrincipal(String principal){
this.principal=principal;
}
}
然后在我认为需要扩展 MethodBeforeAdvice 的建议中,检查特定服务是否具有 @Principal 注释并注入主体名称,或者将其设置为“ANONYMOUS”。
唯一的问题是,即使在使用 Spring Security 进行身份验证后,容器中也不存在用户/主体 bean,因此依赖注入它会很困难。 在我们使用 Spring Security 之前,我们将创建一个具有当前 Principal 的 session-scoped bean,将它注入到“AuthService”中,然后将该 Service 注入到应用程序中的大多数其他服务中。 因此,这些服务将简单地调用 authService.getCurrentUser() 来获取对象。 如果您的代码中有一个地方可以在会话中获得对同一 Principal 的引用,则只需将其设置为会话范围 bean 上的属性即可。
如果您使用的是 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 将在调用时将当前经过身份验证的用户注入到您的方法中。
尝试这个
认证身份验证 = SecurityContextHolder.getContext().getAuthentication();
String userName = authentication.getName();
我喜欢在 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>
就是这样,用户名将在授权后出现在每个页面上。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.