Spring OAuth2 资源服务器从数据库加载同步用户
Spring OAuth2 resource server load synchronized user from database
初始情况的简要说明:让我们假设一个Spring基于RESTfulAPI的引导作为OAuth2资源服务器。资源服务器使用 Spring Security 5 和常用方法配置。外部授权服务器将用户信息(例如电子邮件、名字、姓氏)传递为 JWT 声明,在客户端进行身份验证时收到。授权服务器持有的通用用户信息由资源服务器特定的用户信息(例如业务角色、域 UID)扩展。域用户由来自两个数据源的信息组成:
- 授权服务器用户数据
- 资源服务器(应用程序)用户数据
在资源服务器的数据库中创建从未通过JWT 在资源服务器上进行身份验证的新用户。资源服务器的数据库相应地为每个使用资源服务器API的用户包含一个用户实体,由授权服务器同步的信息和资源服务器业务逻辑补充的信息组成。
public class User {
// Information synchronized from Authorization Server
private String subject;
private String preferredUsername;
private String firstName;
private String lastName;
private String email;
// Information added by Resource Server's business logic
private UUID id;
private String businessRole;
}
域用户的同步是通过侦听AuthenticationSuccessEvent 并在数据库中创建用户或在必要时更新用户来执行的。总而言之,对于由主题声明标识的每个 OAuth2 用户,数据库中都有一个域配置文件,其中包含域特定的附加信息。
这个article也清楚的描述了这个用户数据的同步和分发
现在开始实际问题:虽然 OAuth2 范围控制应用程序拥有哪些授权,但它也必须控制用户拥有哪些授权。用户授权是域特定的,并记录在资源服务器的数据库中。例如,用户应该只能删除他创建的评论。此类访问控制无法通过 OAuth2 范围进行控制。顺便说一句,我说的是 Spring 方法安全性 使用 @PreAuthorize
或 @PostAuthorize
.
@GetMapping
@PreAuthorize("...")
public void func(@AuthenticationPrincipal JWT jwt) {
}
例如,可以通过基于属性的访问控制 (ABAC) 来控制此类访问。但是,这假设当前 @AuthenticationPrincipal
不是 JWT
,因为它是 Spring 资源服务器的标准,而是特定于域的 User
配置文件的实例。有没有办法将 JWT
转换为 User
配置文件,可能使用 UserDetailsService
?
SpringSecurity 5 OAuth2 资源服务器是否有推荐的方法来从基于 JWT 的数据库加载信息,更准确地说是 User
?
我通常最终得到的是 JWT 转换器和创建自定义会话对象 (AbstractAuthenticationToken)
@Autowired
private final UserService userService;
@Override
public void configure(HttpSecurity http) {
http
.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(new JwtConverter(userService))
... other security config ...
JWT 转换器:
public class JwtConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final UserService userService;
public JwtConverter(UserService userService) {
this.userManager = userService;
}
@Override
public AbstractAuthenticationToken convert(@NotNull final Jwt jwt) {
// Here is where I usually lazy-create or sync users instead of AuthenticationSuccessEvent
User user = userService.getByEmail(jwt.getClaim("email"));
Collection<? extends GrantedAuthority> authorities = translateAuthorities(jwt);
return new CustomSession(user, jwt, authorities);
}
// Translate from your jwt as seen fit (I use a roles claim)
private static Collection<? extends GrantedAuthority> translateAuthorities(final Jwt jwt) {
Collection<String> userRoles = jwt.getClaimAsStringList("roles");
if (userRoles != null)
return userRoles
.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
.collect(Collectors.toSet());
return Collections.emptySet();
}
}
CustomSession 粗略示例:
public class CustomSession extends AbstractAuthenticationToken {
final private User user;
final private Jwt jwt;
public CustomSession(User user, Jwt jwt, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.user = user;
this.jwt = jwt;
this.setAuthenticated(true);
}
@Override
public Object getPrincipal() {
return getUser();
}
@Override
public Object getCredentials() {
return getJwt();
}
public Jwt getJwt() {
return jwt;
}
public User getUser() {
return user;
}
//*********************************************************************
//* Static Helpers
//*********************************************************************
public static Optional<CustomSession> GetSession() {
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.map(s -> s instanceof CustomSession ? (CustomSession) s : null);
}
public static Optional<Jwt> GetJwt() {
return GetSession().map(CustomSession::getJwt);
}
public static Optional<User> GetUser() {
return GetSession().map(CustomSession::getUser);
}
public static Optional<String> GetUserId() {
return GetUser().map(User::getId);
}
public static Optional<String> GetUserEmail() {
return GetUser().map(User::getEmail);
}
}
在我的例子中,我在 userService.getByEmail()
后面使用了 JPA,所以需要注意的是用户对象是 'detached',所以你不应该使用这个对象进行用户更新。
另一个选项是扩展 BearerTokenAuthenticationToken
并提供构造函数来设置权限(而不是 CustomSession)。
====
吃完午饭回来,完全忘了提及权限检查。我介绍一个自定义权限评估器(一个简单的例子让你开始):
public class CustomPermissionEvaluator implements PermissionEvaluator {
private SecurityService securityService;
public CustomPermissionEvaluator(SecurityService securityService) {
this.securityService = securityService;
}
@Override
public boolean hasPermission(Authentication auth, Object resourceId, Object action) {
if ((auth == null)
|| !(resourceId instanceof String) || StringUtils.isBlank((String) resourceId)
|| !(action instanceof String) || StringUtils.isBlank((String) action)){
return false;
}
try {
return securityService.hasAccess((String) resourceId, (String) action);
} catch(Exception e){
return false;
}
}
@Override
public boolean hasPermission(Authentication auth, Serializable resourceId, String component, Object action) {
if ((auth == null)
|| !(resourceId instanceof String) || StringUtils.isBlank((String) resourceId)
|| StringUtils.isBlank(component)
|| !(action instanceof String) || StringUtils.isBlank((String) action)) {
return false;
}
try {
return securityService.hasAccess((String) resourceId, component, (String) action);
} catch(Exception e){
return false;
}
}
}
配置方式:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Autowired SecurityService securityService;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator(securityService));
return expressionHandler;
}
}
然后利用 @PreAuthorize("hasPersmission(...)"")
注释,例如:
// Can I Create {SubDomain} on resourceId
@PreAuthorize("hasPermission(#resourceId, 'SubDomain', 'CREATE')")
void example1(int resourceId) {...}
// Can I Update resource
@PreAuthorize("hasPermission(#resourceId, 'UPDATE')")
可能不完全是您要找的,而是另一种(我认为是干净的)解决方案。
初始情况的简要说明:让我们假设一个Spring基于RESTfulAPI的引导作为OAuth2资源服务器。资源服务器使用 Spring Security 5 和常用方法配置。外部授权服务器将用户信息(例如电子邮件、名字、姓氏)传递为 JWT 声明,在客户端进行身份验证时收到。授权服务器持有的通用用户信息由资源服务器特定的用户信息(例如业务角色、域 UID)扩展。域用户由来自两个数据源的信息组成:
- 授权服务器用户数据
- 资源服务器(应用程序)用户数据
在资源服务器的数据库中创建从未通过JWT 在资源服务器上进行身份验证的新用户。资源服务器的数据库相应地为每个使用资源服务器API的用户包含一个用户实体,由授权服务器同步的信息和资源服务器业务逻辑补充的信息组成。
public class User {
// Information synchronized from Authorization Server
private String subject;
private String preferredUsername;
private String firstName;
private String lastName;
private String email;
// Information added by Resource Server's business logic
private UUID id;
private String businessRole;
}
域用户的同步是通过侦听AuthenticationSuccessEvent 并在数据库中创建用户或在必要时更新用户来执行的。总而言之,对于由主题声明标识的每个 OAuth2 用户,数据库中都有一个域配置文件,其中包含域特定的附加信息。
这个article也清楚的描述了这个用户数据的同步和分发
现在开始实际问题:虽然 OAuth2 范围控制应用程序拥有哪些授权,但它也必须控制用户拥有哪些授权。用户授权是域特定的,并记录在资源服务器的数据库中。例如,用户应该只能删除他创建的评论。此类访问控制无法通过 OAuth2 范围进行控制。顺便说一句,我说的是 Spring 方法安全性 使用 @PreAuthorize
或 @PostAuthorize
.
@GetMapping
@PreAuthorize("...")
public void func(@AuthenticationPrincipal JWT jwt) {
}
例如,可以通过基于属性的访问控制 (ABAC) 来控制此类访问。但是,这假设当前 @AuthenticationPrincipal
不是 JWT
,因为它是 Spring 资源服务器的标准,而是特定于域的 User
配置文件的实例。有没有办法将 JWT
转换为 User
配置文件,可能使用 UserDetailsService
?
SpringSecurity 5 OAuth2 资源服务器是否有推荐的方法来从基于 JWT 的数据库加载信息,更准确地说是 User
?
我通常最终得到的是 JWT 转换器和创建自定义会话对象 (AbstractAuthenticationToken)
@Autowired
private final UserService userService;
@Override
public void configure(HttpSecurity http) {
http
.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(new JwtConverter(userService))
... other security config ...
JWT 转换器:
public class JwtConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final UserService userService;
public JwtConverter(UserService userService) {
this.userManager = userService;
}
@Override
public AbstractAuthenticationToken convert(@NotNull final Jwt jwt) {
// Here is where I usually lazy-create or sync users instead of AuthenticationSuccessEvent
User user = userService.getByEmail(jwt.getClaim("email"));
Collection<? extends GrantedAuthority> authorities = translateAuthorities(jwt);
return new CustomSession(user, jwt, authorities);
}
// Translate from your jwt as seen fit (I use a roles claim)
private static Collection<? extends GrantedAuthority> translateAuthorities(final Jwt jwt) {
Collection<String> userRoles = jwt.getClaimAsStringList("roles");
if (userRoles != null)
return userRoles
.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
.collect(Collectors.toSet());
return Collections.emptySet();
}
}
CustomSession 粗略示例:
public class CustomSession extends AbstractAuthenticationToken {
final private User user;
final private Jwt jwt;
public CustomSession(User user, Jwt jwt, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.user = user;
this.jwt = jwt;
this.setAuthenticated(true);
}
@Override
public Object getPrincipal() {
return getUser();
}
@Override
public Object getCredentials() {
return getJwt();
}
public Jwt getJwt() {
return jwt;
}
public User getUser() {
return user;
}
//*********************************************************************
//* Static Helpers
//*********************************************************************
public static Optional<CustomSession> GetSession() {
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.map(s -> s instanceof CustomSession ? (CustomSession) s : null);
}
public static Optional<Jwt> GetJwt() {
return GetSession().map(CustomSession::getJwt);
}
public static Optional<User> GetUser() {
return GetSession().map(CustomSession::getUser);
}
public static Optional<String> GetUserId() {
return GetUser().map(User::getId);
}
public static Optional<String> GetUserEmail() {
return GetUser().map(User::getEmail);
}
}
在我的例子中,我在 userService.getByEmail()
后面使用了 JPA,所以需要注意的是用户对象是 'detached',所以你不应该使用这个对象进行用户更新。
另一个选项是扩展 BearerTokenAuthenticationToken
并提供构造函数来设置权限(而不是 CustomSession)。
====
吃完午饭回来,完全忘了提及权限检查。我介绍一个自定义权限评估器(一个简单的例子让你开始):
public class CustomPermissionEvaluator implements PermissionEvaluator {
private SecurityService securityService;
public CustomPermissionEvaluator(SecurityService securityService) {
this.securityService = securityService;
}
@Override
public boolean hasPermission(Authentication auth, Object resourceId, Object action) {
if ((auth == null)
|| !(resourceId instanceof String) || StringUtils.isBlank((String) resourceId)
|| !(action instanceof String) || StringUtils.isBlank((String) action)){
return false;
}
try {
return securityService.hasAccess((String) resourceId, (String) action);
} catch(Exception e){
return false;
}
}
@Override
public boolean hasPermission(Authentication auth, Serializable resourceId, String component, Object action) {
if ((auth == null)
|| !(resourceId instanceof String) || StringUtils.isBlank((String) resourceId)
|| StringUtils.isBlank(component)
|| !(action instanceof String) || StringUtils.isBlank((String) action)) {
return false;
}
try {
return securityService.hasAccess((String) resourceId, component, (String) action);
} catch(Exception e){
return false;
}
}
}
配置方式:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Autowired SecurityService securityService;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator(securityService));
return expressionHandler;
}
}
然后利用 @PreAuthorize("hasPersmission(...)"")
注释,例如:
// Can I Create {SubDomain} on resourceId
@PreAuthorize("hasPermission(#resourceId, 'SubDomain', 'CREATE')")
void example1(int resourceId) {...}
// Can I Update resource
@PreAuthorize("hasPermission(#resourceId, 'UPDATE')")
可能不完全是您要找的,而是另一种(我认为是干净的)解决方案。