如何保存使用 OAuth 2 (Spring) 登录的用户?

Posted

技术标签:

【中文标题】如何保存使用 OAuth 2 (Spring) 登录的用户?【英文标题】:How do you save users who have logged in with OAuth 2 (Spring)? 【发布时间】:2018-07-13 21:12:15 【问题描述】:

我的主要目标是存储每个用户的客户端 ID,一旦他们使用谷歌登录。这个github repo 包含了我到目前为止所需要的大部分内容。关注的两个主要文件是OAuthSecurityConfig.java 和UserRestController.java。

当我导航到/user 时,主体包含我需要的有关用户的所有详细信息。因此我可以使用以下 sn-ps 来获取我需要的数据:

Authentication a = SecurityContextHolder.getContext().getAuthentication();
String clientId = ((OAuth2Authentication) a).getOAuth2Request().getClientId();

然后我可以将 clientId 存储在 repo 中

User user = new User(clientId);
userRepository.save(user);

这样做的问题是用户不必导航到/user。因此,无需注册即可导航到/score/user1

此 API 旨在成为未来 android 应用程序的后端,因此将 jquery 重定向到 /user 将不安全且无法正常工作。


我尝试过的事情:

尝试 1

我创建了以下类:

@Service
public class CustomUserDetailsService implements UserDetailsService 

private final UserRepository userRepository;

@Autowired
public CustomUserDetailsService(UserRepository userRepository) 
    this.userRepository = userRepository;


@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 
    User user = userRepository.findByUsername(username);
    if (user == null) 
        throw new UsernameNotFoundException(String.format("User %s does not exist!", username));
    
    return new UserRepositoryUserDetails(user);


并覆盖WebSecurityConfigurerAdapterwith:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception 
    auth.userDetailsService(customUserDetailsService);

当用户登录时,这两个被覆盖的方法都不会被调用(我用System.out.println检查过)


尝试 2

我尝试添加.userDetailsService(customUserDetailsService)

到:

@Override
protected void configure(HttpSecurity http) throws Exception 

    http
            // Starts authorizing configurations.
            .authorizeRequests()
            // Do not require auth for the "/" and "/index.html" URLs
            .antMatchers("/", "/**.html", "/**.js").permitAll()
            // Authenticate all remaining URLs.
            .anyRequest().fullyAuthenticated()
            .and()
            .userDetailsService(customUserDetailsService)
            // Setting the logout URL "/logout" - default logout URL.
            .logout()
            // After successful logout the application will redirect to "/" path.
            .logoutSuccessUrl("/")
            .permitAll()
            .and()
            // Setting the filter for the URL "/google/login".
            .addFilterAt(filter(), BasicAuthenticationFilter.class)
            .csrf()
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

这两种方法都没有被调用,我不觉得我离解决方案更近了。任何帮助将不胜感激。

【问题讨论】:

【参考方案1】:

您可以收听AuthenticationSuccessEvent。例如:

@Bean
ApplicationListener<AuthenticationSuccessEvent> doSomething() 
    return new ApplicationListener<AuthenticationSuccessEvent>() 
        @Override
        void onApplicationEvent(AuthenticationSuccessEvent event)
            OAuth2Authentication authentication = (OAuth2Authentication) event.authentication;
            // get required details from OAuth2Authentication instance and proceed further
        
    ;

【讨论】:

【参考方案2】:

这里的方法是提供一个自定义的 OidcUserService 并覆盖 loadUser() 方法,因为 Google 登录是基于 OpenId Connect 的。

首先定义一个模型类来保存提取的数据,如下所示:

public class GoogleUserInfo 

    private Map<String, Object> attributes;

    public GoogleUserInfo(Map<String, Object> attributes) 
        this.attributes = attributes;
    

    public String getId() 
        return (String) attributes.get("sub");
    

    public String getName() 
        return (String) attributes.get("name");
    

    public String getEmail() 
        return (String) attributes.get("email");
    

然后使用 loadUser() 方法创建自定义 OidcUserService,该方法首先调用提供的框架实现,然后添加您自己的逻辑来持久化您需要的用户数据,如下所示:

@Service
public class CustomOidcUserService extends OidcUserService 

    @Autowired
    private UserRepository userRepository; 

    @Override
    public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException 
        OidcUser oidcUser = super.loadUser(userRequest);

        try 
             return processOidcUser(userRequest, oidcUser);
         catch (Exception ex) 
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
        
    

     private OidcUser processOidcUser(OidcUserRequest userRequest, OidcUser oidcUser) 
        GoogleUserInfo googleUserInfo = new GoogleUserInfo(oidcUser.getAttributes());

        // see what other data from userRequest or oidcUser you need

        Optional<User> userOptional = userRepository.findByEmail(googleUserInfo.getEmail());
        if (!userOptional.isPresent()) 
            User user = new User();
            user.setEmail(googleUserInfo.getEmail());
            user.setName(googleUserInfo.getName());

           // set other needed data

            userRepository.save(user);
           

        return oidcUser;
    

并在安全配置类中注册自定义的OidcUserService:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter 

    @Autowired
    private CustomOidcUserService customOidcUserService;

    @Override
    public void configure(HttpSecurity http) throws Exception 

         http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .oauth2Login()
                     .userInfoEndpoint()
                        .oidcUserService(customOidcUserService);
    

模式详细解释可参考文档:

https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2login-advanced-oidc-user-service

【讨论】:

【参考方案3】:

如果其他人对此感到困惑,我的解决方案是创建一个自定义类,从 OAuth2ClientAuthenticationProcessingFilter 然后重写 successfulAuthentication 方法以获取用户身份验证详细信息并将其保存到我的数据库中。

示例(kotlin):

在您的 ssoFilter 方法(如果您按照本教程 https://spring.io/guides/tutorials/spring-boot-oauth2 操作)或您用于注册 ouath 客户端的 wharever 上,更改使用

val googleFilter = Auth2ClientAuthenticationProcessingFilter("/login/google");

为您的自定义类

val googleFilter = CustomAuthProcessingFilter("login/google")

当然还要声明 CustomAuthProcessingFilter 类

class CustomAuthProcessingFilter(defaultFilterProcessesUrl: String?)
    : OAuth2ClientAuthenticationProcessingFilter(defaultFilterProcessesUrl) 

    override fun successfulAuthentication(request: HttpServletRequest?, response: HttpServletResponse?, chain: FilterChain?, authResult: Authentication?) 
        super.successfulAuthentication(request, response, chain, authResult)
        // Check if user is authenticated.
        if (authResult === null || !authResult.isAuthenticated) 
            return
        

        // Use userDetails to grab the values you need like socialId, email, userName, etc...
        val userDetails: LinkedHashMap<*, *> = userAuthentication.details as LinkedHashMap<*, *>
    

【讨论】:

以上是关于如何保存使用 OAuth 2 (Spring) 登录的用户?的主要内容,如果未能解决你的问题,请参考以下文章

使用 OAuth2 实现 Spring RESTful Web 服务 - 将 id 令牌保存为 id 会话

如何重置 google oauth 2.0 授权?

okta oauth2 Spring security 所有受保护的页面重定向到登录

如何使用 redis 使用 spring-security-oauth2 持久化令牌

如何使用 spring-boot-starter-oauth2-client 执行刷新

Spring Security实现OAuth2.0授权服务 - 进阶版