How to configure spring http security for a rest api which needs to validate a JWT token generated by a third party and users don't have passwords?

huangapple go评论142阅读模式
英文:

How to configure spring http security for a rest api which needs to validate a JWT token generated by a third party and users don't have passwords?

问题

我想保护一个由Angular应用程序调用的Spring Boot REST API。没有其他用户或应用程序应该能够访问这个API。

我无法控制这个Angular应用程序,但我被告知实际上是在Angular应用程序上对用户进行身份验证,然后在验证用户后设置并发送JWT。由于Angular应用程序和我的API部署在同一个域上,我确实能够访问作为请求的Cookie发送的JWT。我还能够成功地从中提取用户名。

现在,我必须使用JWT用户名和与之关联的角色进行请求的认证和授权,以限制对某些端点的访问。为了做到这一点,我已经被赋予了许多包含所需信息的数据库视图的访问权限,但我没有获取任何密码信息的途径。我无法控制这些视图或数据库中的其他任何内容。显然,如果令牌中的用户名在这些视图中找不到,用户将未经授权。角色也是同样的情况。

我的问题是如何配置WebSecurityConfigurerAdapterUserDetails

无论我尝试什么,要么立即收到401未经授权的错误,要么当从控制器代码调用SecurityContextHolder.getContext().getAuthentication().getPrincipal()时,返回的是"anonymousUser"而不是UserDetails对象,尽管它被正确设置了。

这只是一个猜测,但可能返回401是因为我UserDetails类中重写的getPassword()方法返回一个空字符串(return ""),但由于我在任何时候都无法访问类似用户密码的内容,它应该返回什么呢?

WebSecurityConfigurerAdapter:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
    // ... 这里是配置的其他部分
}

UserDetails:

public class MyUserDetails implements UserDetails
{
    private static final long serialVersionUID = 7371936004280236913L;

    private final MyUserDto MyUser;

    public MyUserDetails(MyUserDto MyUser)
    {
        this.MyUser = MyUser;
    }

    // ... 这里是 UserDetails 的其他实现方法

    @Override
    public String getPassword()
    { return ""; } // 这里应该返回什么呢?

    // ... 其他方法
}
英文:

I want to secure a Spring Boot REST API which is consumed by an Angular app. No user and no other app should have access to this API.

I have no control over this angular app, but I have been told that the actual authentication of the users is being done on the Angular app, which sets and sends a JWT after authenticating it's users. Since the angular app and my API are deployed on the same domain, I am indeed able to access the JWT which is sent as a cookie with the request. I am also able to successfully extract the username from it.

I must now authenticate and authorize the request using the JWT username and the roles associated with it to restrict access to certain endpoints. In order to do this, I have been given access to several database views which contain the required information, however I don't get any sort of password information. I have no control over the views or anything else in that database. Obviously, the user is unauthorized if the username from the token is not found in these views. Same goes for the roles.

My question is how do I configure WebSecurityConfigurerAdapter and UserDetails?

Whatever I try, I either instantly get 401 unauthorized or SecurityContextHolder.getContext().getAuthentication().getPrincipal() returns "anonymousUser" instead of the UserDetails object when called from the controller code, even though it is being set correctly.

This is just a guess, but maybe it returns 401 because the overriden getPassword() method in my UserDetails class returns an empty string (return "";), but what should it return since I don't, at any point, have access to anything resembling a user's password?

WebSecurityConfigurerAdapter:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Autowired
    private JwtUserDetailsService jwtUserDetailsService;

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        httpSecurity.cors().and().csrf().disable()
                     // I will eventually filter multiple endpoints by roles
                     // but at this point I just want it to work 
                    .authorizeRequests().anyRequest().authenticated().and() // I am guessing it fails here because of the password issue?
                    .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

UserDetails:

public class MyUserDetails implements UserDetails
{
    private static final long serialVersionUID = 7371936004280236913L;

    private final MyUserDto MyUser;

    public MyUserDetails(MyUserDto MyUser)
    {
        this.MyUser = MyUser;
    }

    public MyUserDto getMyUser()
    { return MyUser; }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities()
    {
        List<MyUserAuthority> authorities = new ArrayList<MyUserAuthority>();
        for(MyRoleDto userRole : MyUser.getRoles())
            authorities.add(new MyUserAuthority(userRole));
        return authorities;
    }

    @Override
    public String getPassword()
    { return ""; } // What should this return?

    @Override
    public String getUsername()
    { return this.MyUser.getUserId(); }

    @Override
    public boolean isAccountNonExpired()
    { return true; }

    @Override
    public boolean isAccountNonLocked()
    { return true; }

    @Override
    public boolean isCredentialsNonExpired()
    { return true; }

    @Override
    public boolean isEnabled()
    { return true; }
}

答案1

得分: 2

好的,以下是您要翻译的内容:

鉴于有这么多答案,正如适用于像Java/Spring Boot这样受欢迎且有着完善文档的编程语言和框架,我决定将用户名直接用作密码。

@Override
public String getPassword() {
    // 我在使用 BCryptPasswordEncoder,但您可能需要根据自己的情况进行更改。
    return new BCryptPasswordEncoder().encode(this.getUsername()); 
}

请记住,您需要对其进行编码,因为密码提供程序希望它被编码。否则,提供程序将报告密码不匹配的错误。

我不知道这是否是“最佳实践”,但它是有效的。

英文:

Well, since there are so many answers, as befits a programming language and framework as popular and as well documented as java/spring boot, I've decided to just use the username as the password.

@Override
public String getPassword()
{
    // I am using BCryptPasswordEncoder but you may 
    // have to change to whatever you're using.
    return new BCryptPasswordEncoder().encode(this.getUsername()); 
}

Keep in mind that you have to encode it, as the password provider expects it to be encoded. Otherwise, the provider will say that the password does not match.

I have no idea if this is "best practice" but it works.

答案2

得分: 2

你的问题与是否设置用户密码无关。

正如你所指出的,用户已在前端经过身份验证,可能是通过与OAuth2客户端交互实现的。因此,OAuth2客户端将向你的前端提供一个JWT令牌。

在每次HTTP交互中,前端都会将此JWT令牌发送到后端。

后端有两个责任:

  • 一方面,验证收到的令牌是否有效(过期、是否具有有效签名(如果数字签名)、来自受信任的来源、具有正确的受众等)。
  • 另一方面,你需要识别与收到的JWT令牌相对应的用户。

对于这第二个责任,你需要某个属性,该属性在所有在JWT令牌中定义的属性中能够唯一标识用户。

这个属性必须是用来从数据库中提取用户信息的属性。

在Spring Security的情况下,你需要从你的UserDetailsService实现的loadByUserName方法中返回UserDetails接口的某个实现。

如果如你所述,你能够获得一个username,只需创建一个UserDetailsService实现,该实现使用该信息查询数据库中的视图以获取用户信息,并定义一个实现UserDetails的类,不必关注密码,你不需要密码,也不需要再次对用户进行身份验证。

这个UserDetailsService实现将在你的Spring Security配置中发挥作用。

你的用户是预验证的,你需要预验证的内容。在你的WebSecurityConfig类中包含以下内容:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken> wrapper = new UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken>(userDetailsService);
  PreAuthenticatedAuthenticationProvider preAuthProvider = new PreAuthenticatedAuthenticationProvider();
  preAuthProvider.setPreAuthenticatedUserDetailsService(wrapper);
  auth.authenticationProvider(preAuthProvider);
}

如果你的JwtRequestFilter是一个OncePerRequestFilter,就像你所说的一样,一旦你验证了JWT令牌并获得了唯一的用户标识符,你需要将此信息提供给Spring Security的AuthenticationManager,以便获取对适当的UserDetails实现的引用,并将此引用存储在SecurityContext中,类似于:

PreAuthenticatedAuthenticationToken authRequest = new PreAuthenticatedAuthenticationToken(username, jwtToken);
Authentication authResult = authenticationManager.authenticate(authRequest); // 将调用你的UserDetailsService
SecurityContextHolder.getContext().setAuthentication(authResult);

你可以修改此过滤器,使其扩展AbstractPreAuthenticatedProcessingFilter。这将简化身份验证工作流程。你只需返回从JWT令牌派生的username,Spring Security将处理其余部分:

@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
  String jwtToken = resolveToken(httpServletRequest);
  String username = getUsernameFromToken(token); // 参考此部分获取用户名
  return username;
}

@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
  return "-";
}

你不需要配置任何类型的PasswordEncoder。你的其余配置看起来都很好。

英文:

Your problem have nothing to do with setting or not the user password.

As you indicated, the user has been authenticated in the frontend, possibly by interacting with an OAuth2 client. As a result, the OAuth2 client will provide your frontend a JWT token.

In every HTTP interaction, the frontend will send this JWT token to your backend.

The backend have two responsibilities:

  • On one hand, verify that the received token is valid (expiration, with a valid signature if it is digitally signed, from a trusted origin, with the right audience, etcetera).
  • On the other hand, you need to identify the user which corresponds to the received JWT token.

For this second responsibility you need some attribute that uniquely identifies at the user among all those defined in the JWT token.

This attribute must be the one used to fetch the user information from your database.

In the case of Spring Security, you need to return some implementation of the UserDetails interface from the loadByUserName method in your UserDetailsService implementation.

If, as you indicated, you are able to obtain an username, just create an UserDetailsService implementation that use that information to query your views in the database to fetch the user information, and define some class that implements UserDetails, and do not pay attention on the password, you do not need one, you do not need to authenticate the user again.

This UserDetailsService implementation will be of relevance in your Spring Security configuration.

Your user is preauthenticated and you need preauthentication stuff. Include the following in your WebSecurityConfig class:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  UserDetailsByNameServiceWrapper&lt;PreAuthenticatedAuthenticationToken&gt; wrapper = new UserDetailsByNameServiceWrapper&lt;PreAuthenticatedAuthenticationToken&gt;(userDetailsService);
  PreAuthenticatedAuthenticationProvider preAuthProvider = new PreAuthenticatedAuthenticationProvider();
  preAuthProvider.setPreAuthenticatedUserDetailsService(wrapper);
  auth.authenticationProvider(preAuthProvider);
}

If your JwtRequestFilter is a OncePerRequestFilter as you indicated, once you validate the JWT token and you obtain the unique user identifier, you need to provide this information to the Spring Security AuthenticationManager in order to obtain a reference to the appropriate UserDetails implementation, and store this reference in the SecurityContext, something like:

PreAuthenticatedAuthenticationToken authRequest = new PreAuthenticatedAuthenticationToken(username, jwtToken);
Authentication authResult = authenticationManager.authenticate(authRequest); // Will call your UserDetailsService
SecurityContextHolder.getContext().setAuthentication(authResult);

You can modify this filter and make it extend AbstractPreAuthenticatedProcessingFilter. This will simplify the authentication workflow. You only need to return the username derived from the JWT token and Spring Security will do the rest:

@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
  String jwtToken = resolveToken(httpServletRequest);
  String username = getUsernameFromToken(token); // Take the idea
  return username;
}

@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
  return &quot;-&quot;;
}

You do not need to configure any kind of PasswordEncoder. The rest of your configuration looks fine.

答案3

得分: 1

我认为 user1969903 已经回答了这个问题的解决方案。然而,我建议你在这个解决方案中加入一点点盐。

@Override
public String getPassword()
{
    // 我正在使用 BCryptPasswordEncoder,但你可能需要根据你实际使用的加密方式进行修改。
    return new BCryptPasswordEncoder().encode(this.getUsername() + someString); 
}

someString 可以是你在环境中配置的某个值,也可以是用户配置数据的一部分,比如姓氏、邮箱、邮政编码等。这样,即使有人获取了令牌,也不容易解码。

英文:

I think user1969903 has answered the solution to the problem. However, I would recommend you to add a pinch of salt to the solution.

@Override
public String getPassword()
{
    // I am using BCryptPasswordEncoder but you may 
    // have to change to whatever you&#39;re using.
    return new BCryptPasswordEncoder().encode(this.getUsername() + someString); 
}

someString can be something you can have it configured on your environment ot may be something part of user profile data, like last name, email, zipcode, etc. This way, if someone happens to get the token, it is not easy for them to decode it.

huangapple
  • 本文由 发表于 2020年9月21日 21:20:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/63993145.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定