Java Spring Boot 2.3和Spring Security 5-在一个模式中支持UserDetailsService和OAuth2

Java Spring Boot 2.3和Spring Security 5-在一个模式中支持UserDetailsService和OAuth2,java,spring-boot,spring-mvc,spring-security,spring-security-oauth2,Java,Spring Boot,Spring Mvc,Spring Security,Spring Security Oauth2,我正在使用SpringBoot2.3.x、SpringSecurity5和Thymeleaf构建一个JavaWebApp,运行在Java11上 该应用程序需要支持某种类型的用户帐户。作为起点,我遵循了John Thompson(又称Spring框架大师)在其“课程”中使用的方法。John的方法使用Spring数据JPA和HTTP基本身份验证,其中我实现了Spring接口UserDetailsService,并允许应用程序在HTTP基本身份验证期间根据需要从数据库加载用户凭据(用户名、密码、角色、

我正在使用SpringBoot2.3.x、SpringSecurity5和Thymeleaf构建一个JavaWebApp,运行在Java11上

该应用程序需要支持某种类型的用户帐户。作为起点,我遵循了John Thompson(又称Spring框架大师)在其“课程”中使用的方法。John的方法使用Spring数据JPA和HTTP基本身份验证,其中我实现了Spring接口
UserDetailsService
,并允许应用程序在HTTP基本身份验证期间根据需要从数据库加载用户凭据(用户名、密码、角色、权限)。这一切都很好。因为我将每个用户的角色/权限存储在数据库中,所以我拥有完全的控制权,可以将它们与Spring安全方法级别的注释一起使用,如:
@PreAuthorize(“hasAuthority('user.details.read')”)
。同样,这一切都很好

从课程材料来看,John方法的问题在于,我仅限于HTTP Basic和存储/管理所有用户密码

昨天,我试用了SpringSecurity5的OAuth2.0特性,即“使用Facebook登录”。我使用了中的一些代码开始。单独来说,我的应用程序可以很好地将用户验证为Facebook成员不幸的是,这提供了另一种
@AuthenticationPrincipal
对象,其中包含仅与Facebook相关的角色和权限。

问题 我现在有两种断开连接的用户类型:

  • HTTP基本身份验证用户,其凭据、角色和权限由我管理。他们的角色/授权适合我的应用程序
  • OAuth2身份验证用户,其凭据、角色和权限不在我的控制范围内。他们的角色/权限与我的申请无关
  • 我想要的最终状态是:

    • 我的应用程序数据库存储分配给每个用户的角色/权限
    • 应用程序将支持通过HTTP Basic或OAuth2(最初到Facebook)进行身份验证,但我的应用程序数据库将提供角色/权限
    • 每个用户的“唯一标识符”将是他们的电子邮件地址(FacebookOAuth2将此作为一个属性提供),因此我希望这可以用于关联HTTP Basic和OAuth2身份验证对象
    • 用户可以为其帐户设置HTTP Basic和OAuth2,如果是这样,则可以使用任一方法登录。无论哪种方式,他们在我的应用程序中仍然具有相同的角色/权限
    总而言之:我只想让Facebook OAuth2确认“这是一个活跃的Facebook用户,其电子邮件地址为xyz@example.com'然后将他们的Facebook帐户与我的应用程序中的用户帐户链接。

    接下来呢? Spring在为每个单独用例(HTTP Basic和OAuth2)的组件提供“合理默认值”方面做得很好。我怀疑我需要覆盖和/或禁用这些组件的一些行为,以获得我想要的。我只是不知道从哪里开始

    迄今为止的代码 我在下面提供了一些我正在工作的代码示例。如上所述,与
    UserDetailsService
    相关的部分以及为该服务提供服务的JPA实体已经运行良好。我的问题本质上是“如何将OAuth2合并到已经工作的内容中?”

    My
    WebSecurityConfigureAdapter
    实现类

    我的登录表单的控制器,支持“使用Facebook登录”或HTTP Basic

    My
    UserDetailsService
    实现类(用于HTTP基本身份验证)

    我的
    UserRepository
    定义

    schema.sql
    -在Spring Security中创建OAuth2类用于持久令牌存储的表

    CREATE TABLE oauth2_authorized_client (
        client_registration_id varchar(100) NOT NULL,
        principal_name varchar(200) NOT NULL,
        access_token_type varchar(100) NOT NULL,
        access_token_value blob NOT NULL,
        access_token_issued_at timestamp NOT NULL,
        access_token_expires_at timestamp NOT NULL,
        access_token_scopes varchar(1000) DEFAULT NULL,
        refresh_token_value blob DEFAULT NULL,
        refresh_token_issued_at timestamp DEFAULT NULL,
        created_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
        PRIMARY KEY (client_registration_id, principal_name)
    );
    
    当我使用自己的Facebook帐户通过Facebook OAuth2登录时,my
    LoginFormController
    的日志输出如下所示:

    [INFO ] LoginFormController - USER AUTHENTICATED WITH OAUTH2
    [INFO ] LoginFormController - auth token 'authorized client registration id': [facebook]
    [INFO ] LoginFormController - auth token 'name': [10139295061993788]
    [INFO ] LoginFormController - oauth2User 'name': [10139295061993788]
    [INFO ] LoginFormController - oauth2User 'id' attribute value: [10139295061993788]
    [INFO ] LoginFormController - oauth2User 'name' attribute value: [Jim Tough]
    [INFO ] LoginFormController - oauth2User 'email' attribute value: [jim@jimtough.com]
    
    我还看到了我的另一个侦听器类的日志输出:

    [INFO ] AuthenticationEventLogger - principal type: OAuth2LoginAuthenticationToken | authorities: [ROLE_USER, SCOPE_email, SCOPE_public_profile]
    
    权限
    ROLE\u USER、SCOPE\u email、SCOPE\u public\u profile
    在我的应用程序上下文中毫无意义



    我相信您正在寻找的是
    授权权威映射器

    您注册了一个bean,它将您的权限映射到要使用的角色

    @EnableWebSecurity
    public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .oauth2Login(oauth2 -> oauth2
                    .userInfoEndpoint(userInfo -> userInfo
                        .userAuthoritiesMapper(this.userAuthoritiesMapper())
                        ...
                    )
                );
        }
    
        private GrantedAuthoritiesMapper userAuthoritiesMapper() {
            return (authorities) -> {
                Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
    
                authorities.forEach(authority -> {
                    if (OidcUserAuthority.class.isInstance(authority)) {
                        OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;
    
                        OidcIdToken idToken = oidcUserAuthority.getIdToken();
                        OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
    
                        // Map the claims found in idToken and/or userInfo
                        // to one or more GrantedAuthority's and add it to mappedAuthorities
    
                    } else if (OAuth2UserAuthority.class.isInstance(authority)) {
                        OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
    
                        Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
    
                        // Map the attributes found in userAttributes
                        // to one or more GrantedAuthority's and add it to mappedAuthorities
    
                    }
                });
    
                return mappedAuthorities;
            };
        }
    }
    

    您可以阅读更多信息。

    谢谢@Toerktumlare!我来看看你推荐的。如果这行得通,我会回来接受你的回答。
    @Setter
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    @Entity
    public class User implements UserDetails, CredentialsContainer {
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Integer id;
    
        private String username;
        private String password;
    
        @Singular
        @ManyToMany(cascade = {CascadeType.MERGE}, fetch = FetchType.EAGER)
        @JoinTable(name = "user_role",
            joinColumns = {@JoinColumn(name = "USER_ID", referencedColumnName = "ID")},
            inverseJoinColumns = {@JoinColumn(name = "ROLE_ID", referencedColumnName = "ID")})
        private Set<Role> roles;
    
        @Transient
        public Set<GrantedAuthority> getAuthorities() {
            return this.roles.stream()
                    .map(Role::getAuthorities)
                    .flatMap(Set::stream)
                    .map(authority -> {
                        return new SimpleGrantedAuthority(authority.getPermission());
                    })
                    .collect(Collectors.toSet());
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return this.accountNonExpired;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return this.accountNonLocked;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return this.credentialsNonExpired;
        }
    
        @Override
        public boolean isEnabled() {
            return this.enabled;
        }
    
        @Builder.Default
        private Boolean accountNonExpired = true;
    
        @Builder.Default
        private Boolean accountNonLocked = true;
    
        @Builder.Default
        private Boolean credentialsNonExpired = true;
    
        @Builder.Default
        private Boolean enabled = true;
    
        @Override
        public void eraseCredentials() {
            this.password = null;
        }
    
        @CreationTimestamp
        @Column(updatable = false)
        private Timestamp createdDate;
    
        @UpdateTimestamp
        private Timestamp lastModifiedDate;
    
    }
    
    CREATE TABLE oauth2_authorized_client (
        client_registration_id varchar(100) NOT NULL,
        principal_name varchar(200) NOT NULL,
        access_token_type varchar(100) NOT NULL,
        access_token_value blob NOT NULL,
        access_token_issued_at timestamp NOT NULL,
        access_token_expires_at timestamp NOT NULL,
        access_token_scopes varchar(1000) DEFAULT NULL,
        refresh_token_value blob DEFAULT NULL,
        refresh_token_issued_at timestamp DEFAULT NULL,
        created_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
        PRIMARY KEY (client_registration_id, principal_name)
    );
    
    [INFO ] LoginFormController - USER AUTHENTICATED WITH OAUTH2
    [INFO ] LoginFormController - auth token 'authorized client registration id': [facebook]
    [INFO ] LoginFormController - auth token 'name': [10139295061993788]
    [INFO ] LoginFormController - oauth2User 'name': [10139295061993788]
    [INFO ] LoginFormController - oauth2User 'id' attribute value: [10139295061993788]
    [INFO ] LoginFormController - oauth2User 'name' attribute value: [Jim Tough]
    [INFO ] LoginFormController - oauth2User 'email' attribute value: [jim@jimtough.com]
    
    [INFO ] AuthenticationEventLogger - principal type: OAuth2LoginAuthenticationToken | authorities: [ROLE_USER, SCOPE_email, SCOPE_public_profile]
    
    @EnableWebSecurity
    public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .oauth2Login(oauth2 -> oauth2
                    .userInfoEndpoint(userInfo -> userInfo
                        .userAuthoritiesMapper(this.userAuthoritiesMapper())
                        ...
                    )
                );
        }
    
        private GrantedAuthoritiesMapper userAuthoritiesMapper() {
            return (authorities) -> {
                Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
    
                authorities.forEach(authority -> {
                    if (OidcUserAuthority.class.isInstance(authority)) {
                        OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;
    
                        OidcIdToken idToken = oidcUserAuthority.getIdToken();
                        OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
    
                        // Map the claims found in idToken and/or userInfo
                        // to one or more GrantedAuthority's and add it to mappedAuthorities
    
                    } else if (OAuth2UserAuthority.class.isInstance(authority)) {
                        OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
    
                        Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
    
                        // Map the attributes found in userAttributes
                        // to one or more GrantedAuthority's and add it to mappedAuthorities
    
                    }
                });
    
                return mappedAuthorities;
            };
        }
    }
    
    @EnableWebSecurity
    public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .oauth2Login(withDefaults());
        }
    
        @Bean
        public GrantedAuthoritiesMapper userAuthoritiesMapper() {
            ...
        }
    }