【发布时间】:2018-04-02 08:43:59
【问题描述】:
我已经为 Spring Security 配置了一个自定义的 UserDetailsService 和 UserDetails 实现对象,它们从 JPA 存储中检索用户数据。但是,JPA 管理的 User 对象随后被存储在 HTTP 会话中。这会导致几个问题:
JPA 管理的对象应该在每个会话中存在一个,但是这个 User 对象被传递给用户发起的所有请求,当访问惰性属性时可能会导致错误,等等。我尝试过通过在从 UserDetailsService 请求时尝试刷新用户对象来解决此问题,但这似乎并不完全可靠,并且不能防止在某些上下文中发生异常(例如,当会话从服务器上的序列化副本恢复时启动)。
更新数据库时,存储在会话中的 User 对象不会更改以反映更新,在某些情况下会导致返回过时的信息。
我想简单地将用户 ID 存储在会话中,然后根据每个请求将其解析为实际的用户对象。我该怎么做?
我的 UserDetailsService 如下所示:
@Service
public class JPAUserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService
{
public static final Map<User.UserType,List<SimpleGrantedAuthority>> AUTHORITIES;
static
{
AUTHORITIES = new HashMap<> ();
AUTHORITIES.put (User.UserType.RESTRICTED, Arrays.asList (
new SimpleGrantedAuthority ("PUBLIC"),
new SimpleGrantedAuthority ("USER_PROFILE")));
AUTHORITIES.put (User.UserType.NORMAL, Arrays.asList (
new SimpleGrantedAuthority ("PUBLIC"),
new SimpleGrantedAuthority ("USER_PROFILE"),
new SimpleGrantedAuthority ("REGISTERED")));
AUTHORITIES.put (User.UserType.ADMIN, Arrays.asList (
new SimpleGrantedAuthority ("PUBLIC"),
new SimpleGrantedAuthority ("USER_PROFILE"),
new SimpleGrantedAuthority ("REGISTERED"),
new SimpleGrantedAuthority ("ADMIN")));
}
public class UserWrapper implements UserDetails
{
User user;
public UserWrapper (User user)
{
super ();
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities ()
{
return AUTHORITIES.get(user.getType ());
}
@Override
public String getPassword ()
{
return user.getPassword ();
}
@Override
public String getUsername ()
{
// the users canonical username (they may have multiple) is their most recent email address:
return user.getEmailAddress ().stream ()
.sorted ((addr1, addr2) -> addr2.getValidFrom ().compareTo (addr1.getValidFrom ()))
.findFirst ()
.map (UserEmailAddress::getAddress)
.orElseGet (() -> user.getId ().toString ());
}
@Override
public boolean isAccountNonExpired ()
{
return true;
}
@Override
public boolean isAccountNonLocked ()
{
return ! user.isAccountLocked ();
}
@Override
public boolean isCredentialsNonExpired ()
{
return true;
}
@Override
public boolean isEnabled ()
{
return true;
}
public User getUser ()
{
User thisUser = user;
if (!jpaContext.getEntityManagerByManagedType (User.class).contains (thisUser))
{
// user object needs refreshing for the current session
thisUser = users.get (thisUser.getId ()).orElseThrow (
() -> new ConcurrencyFailureException ("User deleted from database during operation"));
user = thisUser;
}
return thisUser;
}
}
private JpaContext jpaContext;
private UserRepository users;
@Autowired
public JPAUserDetailsService (UserRepository users, JpaContext jpaContext)
{
this.users = users;
this.jpaContext = jpaContext;
}
@Override
public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException
{
return users.findByCurrentEmailAddress (username)
.map (UserWrapper::new)
.orElseThrow (() -> new UsernameNotFoundException (username));
}
public UserDetails wrap (User u)
{
return new UserWrapper(u);
}
}
更新
写下 Spring 用于处理身份验证以响应下面的评论的过程,我想我应该将我的笔记记录到这里,因为它可能会帮助其他人弄清楚我缺少什么。我对我试图修改的过程的理解是这样的:
- 当有人登录我的站点时,登录表单提交被 Spring Security 过滤器拦截,并调用我的
UserDetailsService实现(一个常规的、单例范围的 Spring bean)来获取对应于的UserDetails对象输入的用户名。检查密码,如果它匹配登录过滤器,则创建UsernamePassworkAuthenticationToken并在当前请求的安全上下文中调用SecurityContext.setAuthentication。 HTTP 安全上下文实现最终将令牌存储在 HTTP 会话对象中。
对此的明显解决方案不起作用。更改UserDetails 对象以存储User 对象的ID 字段并按需获取它会失败,因为HTTP 会话要求放置在其中的所有对象都是Serializable(实际上可以在不通知的情况下任意序列化和反序列化它们,例如在服务器重新配置期间或负载平衡系统中两个节点之间的迁移期间),并且获取User 的能力取决于对Spring 配置的UserRepository 实例的引用,我没有合理的方法来获取静态上下文:我只能在可以控制可用实例数据的情况下创建一个有效的UserDetails,这样才能确保正确的UserRepository可用。
这表明有一个可能的替代解决方案,这可能比找到一种方法让 Spring Security 按需重新创建 UserDetails 对象更通用:有没有一种方法可以在 HTTP 会话中存储对象,这样当它们由容器反序列化,与 Spring 服务 bean 的关联(例如我的 UserRepository 实例)可以在反序列化期间重新连接吗?
【问题讨论】:
-
“我想简单地将用户 ID 存储在会话中,然后根据每个请求将其解析为实际的用户对象”,这就是所谓的请求范围,所以我想想如果你将
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)添加到你的服务中,它就可以解决问题。 -
@zakariaamine - 这不会为每个请求创建一个 UserDetailsService 实例,而不是一个 UserDetails 对象吗?
-
会的。 UserDetails 不是 bean,所以不会受到影响。
-
...但我需要更改其行为的是 UserDetails,而不是 UserDetailsService。
-
抱歉,我没有在 UserDetails 上看到
@Autowired,是的,您也可以在UserDetails上使用它
标签: spring session spring-security