如何在身份验证期间或之后设置自定义主体对象?
How to set a custom principal object during or after authentication?
我已经更改了用户在我的后端进行身份验证的方式。从现在开始,我将从 Firebase 接收 JWT 令牌,然后在我的 Spring 启动服务器上进行验证。
到目前为止一切正常,但有一个变化我不太满意,主体对象现在是 org.springframework.security.oauth2.jwt.Jwt
而不是 AppUserEntity
,用户模型, 和以前一样。
// Note: "authentication" is a JwtAuthenticationToken
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Jwt jwt = (Jwt) authentication.getPrincipal();
所以,经过一些阅读和调试后,我发现 BearerTokenAuthenticationFilter
基本上像这样设置 Authentication
对象:
// BearerTokenAuthenticationFilter.java
AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request);
// Note: authenticationResult is our JwtAuthenticationToken
Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authenticationResult);
SecurityContextHolder.setContext(context);
正如我们所见,另一方面,它来自 authenticationManager
,它是 org.springframework.security.authentication.ProviderManager
等等。兔子洞变深了。
我没有找到任何可以让我以某种方式替换 Authentication
的东西。
那么计划是什么?
由于 Firebase 现在负责用户身份验证,因此可以在我的后端不知道的情况下创建用户。我不知道这是否是最好的方法,但我打算在发现尚不存在的用户的有效 JWT 令牌后,在我的数据库中简单地创建一个用户记录。
此外,我的很多业务逻辑目前都依赖于作为用户实体业务对象的主体。我可以更改此代码,但这是一项乏味的工作,谁不想回顾几行遗留代码?
This is working fine so far but there's one change which I am not too happy about and it's that the principal-object is now a org.springframework.security.oauth2.jwt.Jwt and not a AppUserEntity, the user-model, like before.
在我的应用程序中,我通过滚动我自己的 JwtAuthenticationFilter
而不是使用 BearerTokenAuthenticationFilter
来规避此问题,然后将我的 User
实体设置为 principal
Authentication
对象。但是,在我的例子中,这几乎从 JWT 声明中构造了一个 User
,这可能是一种不好的做法:SonarLint 提示使用 DTO 来减轻有人使用受损的 JWT 令牌将任意数据注入其用户记录的风险.我不知道这是否是一个大问题 - 如果您不信任您的 JWT,那么您还有其他问题,恕我直言。
I don't know if this is the best way to do it but I intend to simply create a user record in my database once I discover a valid JWT-token of a user which does not exist yet.
请记住,您的应用程序应以无状态方式验证 JWT,仅通过验证其签名。您不应该在每次验证它们时都访问数据库。因此,如果您使用像
这样的方法调用来创建用户记录会更好
void foo(@AuthenticationPrincipal final Jwt jwt) {
// only invoke next line if reading JWT claims is not enough
final User user = userService.findOrCreateByJwt(jwt);
// TODO method logic
}
一旦您需要保留对涉及该用户的数据库的更改。
我做的和 Julian Echkard 有点不同。
在我的 WebSecurityConfigurerAdapter
中,我设置 Customizer
如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2ResourceServer()
.jwt(new JwtResourceServerCustomizer(this.customAuthenticationProvider));
}
customAuthenticationProvider
是一个 JwtResourceServerCustomizer
,我是这样实现的:
public class JwtResourceServerCustomizer implements Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer> {
private final JwtAuthenticationProvider customAuthenticationProvider;
public JwtResourceServerCustomizer(JwtAuthenticationProvider customAuthenticationProvider) {
this.customAuthenticationProvider = customAuthenticationProvider;
}
@Override
public void customize(OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer jwtConfigurer) {
String key = UUID.randomUUID().toString();
AnonymousAuthenticationProvider anonymousAuthenticationProvider = new AnonymousAuthenticationProvider(key);
ProviderManager providerManager = new ProviderManager(this.customAuthenticationProvider, anonymousAuthenticationProvider);
jwtConfigurer.authenticationManager(providerManager);
}
}
我正在这样配置 NimbusJwtDecoder
:
@Component
public class JwtConfig {
@Bean
public JwtDecoder jwtDecoder() {
String jwkUri = "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com";
return NimbusJwtDecoder.withJwkSetUri(jwkUri)
.build();
}
}
最后,我们需要一个自定义 AuthenticationProvider
,它将 return 我们想要的 Authentication
对象:
@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
private final JwtDecoder jwtDecoder;
@Autowired
public JwtAuthenticationProvider(JwtDecoder jwtDecoder) {
this.jwtDecoder = jwtDecoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
BearerTokenAuthenticationToken token = (BearerTokenAuthenticationToken) authentication;
Jwt jwt;
try {
jwt = this.jwtDecoder.decode(token.getToken());
} catch (JwtValidationException ex) {
return null;
}
List<GrantedAuthority> authorities = new ArrayList<>();
if (jwt.hasClaim("roles")) {
List<String> rolesClaim = jwt.getClaim("roles");
List<RoleEntity.RoleType> collect = rolesClaim
.stream()
.map(RoleEntity.RoleType::valueOf)
.collect(Collectors.toList());
for (RoleEntity.RoleType role : collect) {
authorities.add(new SimpleGrantedAuthority(role.toString()));
}
}
return new JwtAuthenticationToken(jwt, authorities);
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(BearerTokenAuthenticationToken.class);
}
}
自
SecurityContextHolder.setContext(context);
不适用于
request.getUserPrincipal();
您可以创建自定义 class 扩展 HttpServletRequestWrapper
import java.security.Principal;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class UserPrincipalHttpServletRequest extends HttpServletRequestWrapper {
private final Principal principal;
public UserPrincipalHttpServletRequest(HttpServletRequest request, Principal principal) {
super(request);
this.principal = principal;
}
@Override
public Principal getUserPrincipal() {
return principal;
}
}
然后在您的过滤器中执行如下操作:
protected void doFilterInternal(HttpServletRequest request){
. . .
// create user details, roles are required
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(new SimpleGrantedAuthority("SOME ROLE"));
UserDetails userDetails = new User("SOME USERNAME", "SOME PASSWORD", authorities);
// Create an authentication token
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// follow the filter chain, using the new wrapped UserPrincipalHtppServletRequest
chain.doFilter(new UserPrincipalHttpServletRequest(request, usernamePasswordAuthenticationToken), response);
// all filters coming up, will be able to run request.getUserPrincipal()
}
我已经更改了用户在我的后端进行身份验证的方式。从现在开始,我将从 Firebase 接收 JWT 令牌,然后在我的 Spring 启动服务器上进行验证。
到目前为止一切正常,但有一个变化我不太满意,主体对象现在是 org.springframework.security.oauth2.jwt.Jwt
而不是 AppUserEntity
,用户模型, 和以前一样。
// Note: "authentication" is a JwtAuthenticationToken
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Jwt jwt = (Jwt) authentication.getPrincipal();
所以,经过一些阅读和调试后,我发现 BearerTokenAuthenticationFilter
基本上像这样设置 Authentication
对象:
// BearerTokenAuthenticationFilter.java
AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request);
// Note: authenticationResult is our JwtAuthenticationToken
Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authenticationResult);
SecurityContextHolder.setContext(context);
正如我们所见,另一方面,它来自 authenticationManager
,它是 org.springframework.security.authentication.ProviderManager
等等。兔子洞变深了。
我没有找到任何可以让我以某种方式替换 Authentication
的东西。
那么计划是什么?
由于 Firebase 现在负责用户身份验证,因此可以在我的后端不知道的情况下创建用户。我不知道这是否是最好的方法,但我打算在发现尚不存在的用户的有效 JWT 令牌后,在我的数据库中简单地创建一个用户记录。
此外,我的很多业务逻辑目前都依赖于作为用户实体业务对象的主体。我可以更改此代码,但这是一项乏味的工作,谁不想回顾几行遗留代码?
This is working fine so far but there's one change which I am not too happy about and it's that the principal-object is now a org.springframework.security.oauth2.jwt.Jwt and not a AppUserEntity, the user-model, like before.
在我的应用程序中,我通过滚动我自己的 JwtAuthenticationFilter
而不是使用 BearerTokenAuthenticationFilter
来规避此问题,然后将我的 User
实体设置为 principal
Authentication
对象。但是,在我的例子中,这几乎从 JWT 声明中构造了一个 User
,这可能是一种不好的做法:SonarLint 提示使用 DTO 来减轻有人使用受损的 JWT 令牌将任意数据注入其用户记录的风险.我不知道这是否是一个大问题 - 如果您不信任您的 JWT,那么您还有其他问题,恕我直言。
I don't know if this is the best way to do it but I intend to simply create a user record in my database once I discover a valid JWT-token of a user which does not exist yet.
请记住,您的应用程序应以无状态方式验证 JWT,仅通过验证其签名。您不应该在每次验证它们时都访问数据库。因此,如果您使用像
这样的方法调用来创建用户记录会更好void foo(@AuthenticationPrincipal final Jwt jwt) {
// only invoke next line if reading JWT claims is not enough
final User user = userService.findOrCreateByJwt(jwt);
// TODO method logic
}
一旦您需要保留对涉及该用户的数据库的更改。
我做的和 Julian Echkard 有点不同。
在我的 WebSecurityConfigurerAdapter
中,我设置 Customizer
如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2ResourceServer()
.jwt(new JwtResourceServerCustomizer(this.customAuthenticationProvider));
}
customAuthenticationProvider
是一个 JwtResourceServerCustomizer
,我是这样实现的:
public class JwtResourceServerCustomizer implements Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer> {
private final JwtAuthenticationProvider customAuthenticationProvider;
public JwtResourceServerCustomizer(JwtAuthenticationProvider customAuthenticationProvider) {
this.customAuthenticationProvider = customAuthenticationProvider;
}
@Override
public void customize(OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer jwtConfigurer) {
String key = UUID.randomUUID().toString();
AnonymousAuthenticationProvider anonymousAuthenticationProvider = new AnonymousAuthenticationProvider(key);
ProviderManager providerManager = new ProviderManager(this.customAuthenticationProvider, anonymousAuthenticationProvider);
jwtConfigurer.authenticationManager(providerManager);
}
}
我正在这样配置 NimbusJwtDecoder
:
@Component
public class JwtConfig {
@Bean
public JwtDecoder jwtDecoder() {
String jwkUri = "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com";
return NimbusJwtDecoder.withJwkSetUri(jwkUri)
.build();
}
}
最后,我们需要一个自定义 AuthenticationProvider
,它将 return 我们想要的 Authentication
对象:
@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
private final JwtDecoder jwtDecoder;
@Autowired
public JwtAuthenticationProvider(JwtDecoder jwtDecoder) {
this.jwtDecoder = jwtDecoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
BearerTokenAuthenticationToken token = (BearerTokenAuthenticationToken) authentication;
Jwt jwt;
try {
jwt = this.jwtDecoder.decode(token.getToken());
} catch (JwtValidationException ex) {
return null;
}
List<GrantedAuthority> authorities = new ArrayList<>();
if (jwt.hasClaim("roles")) {
List<String> rolesClaim = jwt.getClaim("roles");
List<RoleEntity.RoleType> collect = rolesClaim
.stream()
.map(RoleEntity.RoleType::valueOf)
.collect(Collectors.toList());
for (RoleEntity.RoleType role : collect) {
authorities.add(new SimpleGrantedAuthority(role.toString()));
}
}
return new JwtAuthenticationToken(jwt, authorities);
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(BearerTokenAuthenticationToken.class);
}
}
自
SecurityContextHolder.setContext(context);
不适用于
request.getUserPrincipal();
您可以创建自定义 class 扩展 HttpServletRequestWrapper
import java.security.Principal;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class UserPrincipalHttpServletRequest extends HttpServletRequestWrapper {
private final Principal principal;
public UserPrincipalHttpServletRequest(HttpServletRequest request, Principal principal) {
super(request);
this.principal = principal;
}
@Override
public Principal getUserPrincipal() {
return principal;
}
}
然后在您的过滤器中执行如下操作:
protected void doFilterInternal(HttpServletRequest request){
. . .
// create user details, roles are required
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(new SimpleGrantedAuthority("SOME ROLE"));
UserDetails userDetails = new User("SOME USERNAME", "SOME PASSWORD", authorities);
// Create an authentication token
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// follow the filter chain, using the new wrapped UserPrincipalHtppServletRequest
chain.doFilter(new UserPrincipalHttpServletRequest(request, usernamePasswordAuthenticationToken), response);
// all filters coming up, will be able to run request.getUserPrincipal()
}