• Spring Security 自定义用户信息端点与多种登录方式共存


    相关文章:

    1. OAuth2的定义和运行流程
    2. Spring Security OAuth实现Gitee快捷登录
    3. Spring Security OAuth实现GitHub快捷登录
    4. Spring Security的过滤器链机制
    5. Spring Security OAuth Client配置加载源码分析
    6. Spring Security内置过滤器详解
    7. 为什么加载了两个OAuth2AuthorizationRequestRedirectFilter分析
    8. Spring Security 自定义授权服务器实践
    9. Spring Security 自定义资源服务器实践

    前言

    我们之前对接第三方OAuth2快捷登录,只要通过配置文件即可实现对接,但是总有一些第三方登录会返回各种各样的格式,导致默认的OAuth2无法使用。

    自定义扩展

    为了能够自定义扩展,我们重新创建项目,命名为spring-security-resource-server-customspring-security-oauth2-client-custom
    spring-security-resource-server-custom:修改/userinfo,将返回信息包装一下,返回code等属性
    spring-security-oauth2-client-custom:自定义获取userInfo的逻辑

    spring-security-resource-server-custom

    @Data
    public class Result {
    
        private int code = 0;
    
        private Object data;
    
        private String msg;
    
        public static Result ok(Object data) {
            Result result = new Result();
            result.data = data;
            return result;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    定义了一个Result包装类,这是框架常有的返回结果包装类。

    @RestController
    public class UserInfoController {
    
        @GetMapping("/userinfo")
        public Result getUserInfo() {
            UserInfoRes userInfoRes = new UserInfoRes();
            userInfoRes.setUsername("阿提说说");
            return Result.ok(userInfoRes);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    spring-security-oauth2-client-custom

    1、自定义实现OAuth2User接口

    由于/userinfo返回的用户信息格式改变,原来的DefaultOAuth2User已经不能使用,我们需要自定义OAuth2User实现

    public class CustomOAuth2User implements OAuth2User {
    
        private final Set<GrantedAuthority> authorities;
    
        private final Map<String, Object> attributes;
    
        private final String nameAttributeKey;
    
        //用户信息所在的属性名
        public static final String DATA_KEY = "data";
    
        public CustomOAuth2User(Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes, String nameAttributeKey) {
            this.authorities = new LinkedHashSet<>(authorities);
            this.attributes = attributes;
            this.nameAttributeKey = nameAttributeKey;
        }
    
    
        @Override
        public Map<String, Object> getAttributes() {
            //从原有返回格式中提取出data,原{"code"0,"data":{"username":"阿提说说"},"msg":null}
            return (Map<String, Object>) attributes.get(DATA_KEY);
        }
    
        //获取权限信息
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return this.authorities;
        }
    
        //获取指定nameKey的值
        @Override
        public String getName() {
            return this.getAttribute(this.nameAttributeKey).toString();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    2、自定义OAuth2UserService接口实现

    OAuth2UserService 负责请求用户信息,由于我们请求用户信息接口的方式并没有变,依旧是使用access_token从资源服务器获取用户信息,因此大部分逻辑可以使用DefaultOAuth2UserService的逻辑,只需要改变方法的OAuth2User对象。
    如果获取用户信息的方式不一样,也可以在loadUser中进行修改,但是方法的CustomOAuth2User必须包含authoritiesattributesnameAttributeKey3个属性。

    public class CustomOAuth2UserService implements OAuth2UserService {
        private static final String MISSING_USER_INFO_URI_ERROR_CODE = "missing_user_info_uri";
    
        private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute";
    
        private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
    
        private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE = new ParameterizedTypeReference<Map<String, Object>>() {
        };
    
        private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new OAuth2UserRequestEntityConverter();
    
        private RestOperations restOperations;
    
        public CustomOAuth2UserService() {
            RestTemplate restTemplate = new RestTemplate();
            restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
            this.restOperations = restTemplate;
        }
    
        @Override
        public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
            Assert.notNull(userRequest, "userRequest cannot be null");
            if (!StringUtils
                    .hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
                OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_INFO_URI_ERROR_CODE,
                        "Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: "
                                + userRequest.getClientRegistration().getRegistrationId(),
                        null);
                throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
            }
            String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint()
                    .getUserNameAttributeName();
            if (!StringUtils.hasText(userNameAttributeName)) {
                OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
                        "Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "
                                + userRequest.getClientRegistration().getRegistrationId(),
                        null);
                throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
            }
            RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
            ResponseEntity<Map<String, Object>> response = getResponse(userRequest, request);
            Map<String, Object> userAttributes = response.getBody();
            Set<GrantedAuthority> authorities = new LinkedHashSet<>();
            authorities.add(new OAuth2UserAuthority(userAttributes));
            OAuth2AccessToken token = userRequest.getAccessToken();
            for (String authority : token.getScopes()) {
                authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
            }
            //更换为自定义的OAuth2User实现
            return new CustomOAuth2User(authorities, userAttributes, userNameAttributeName);
        }
    
        private ResponseEntity<Map<String, Object>> getResponse(OAuth2UserRequest userRequest, RequestEntity<?> request) {
            try {
                return this.restOperations.exchange(request, PARAMETERIZED_RESPONSE_TYPE);
            }
            catch (OAuth2AuthorizationException ex) {
                OAuth2Error oauth2Error = ex.getError();
                StringBuilder errorDetails = new StringBuilder();
                errorDetails.append("Error details: [");
                errorDetails.append("UserInfo Uri: ")
                        .append(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri());
                errorDetails.append(", Error Code: ").append(oauth2Error.getErrorCode());
                if (oauth2Error.getDescription() != null) {
                    errorDetails.append(", Error Description: ").append(oauth2Error.getDescription());
                }
                errorDetails.append("]");
                oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
                        "An error occurred while attempting to retrieve the UserInfo Resource: " + errorDetails.toString(),
                        null);
                throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
            }
            catch (UnknownContentTypeException ex) {
                String errorMessage = "An error occurred while attempting to retrieve the UserInfo Resource from '"
                        + userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri()
                        + "': response contains invalid content type '" + ex.getContentType().toString() + "'. "
                        + "The UserInfo Response should return a JSON object (content type 'application/json') "
                        + "that contains a collection of name and value pairs of the claims about the authenticated End-User. "
                        + "Please ensure the UserInfo Uri in UserInfoEndpoint for Client Registration '"
                        + userRequest.getClientRegistration().getRegistrationId() + "' conforms to the UserInfo Endpoint, "
                        + "as defined in OpenID Connect 1.0: 'https://openid.net/specs/openid-connect-core-1_0.html#UserInfo'";
                OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, errorMessage, null);
                throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
            }
            catch (RestClientException ex) {
                OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
                        "An error occurred while attempting to retrieve the UserInfo Resource: " + ex.getMessage(), null);
                throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
            }
        }
    
        /**
         * Sets the {@link Converter} used for converting the {@link OAuth2UserRequest} to a
         * {@link RequestEntity} representation of the UserInfo Request.
         * @param requestEntityConverter the {@link Converter} used for converting to a
         * {@link RequestEntity} representation of the UserInfo Request
         * @since 5.1
         */
        public final void setRequestEntityConverter(Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter) {
            Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
            this.requestEntityConverter = requestEntityConverter;
        }
    
        /**
         * Sets the {@link RestOperations} used when requesting the UserInfo resource.
         *
         * 

    * NOTE: At a minimum, the supplied {@code restOperations} must be configured * with the following: *

      *
    1. {@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}
    2. *
    * @param restOperations the {@link RestOperations} used when requesting the UserInfo * resource * @since 5.1 */
    public final void setRestOperations(RestOperations restOperations) { Assert.notNull(restOperations, "restOperations cannot be null"); this.restOperations = restOperations; } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122

    3、配置自定义OAuth2UserService实现

    创建一个@Configuration注解的类,用来生成SecurityFilterChain Bean

        @Bean
        SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
            //自定义用户信息获取实现
            http.oauth2Login(oauth2 -> oauth2.userInfoEndpoint(userInfo -> userInfo.userService(new CustomOAuth2UserService())));
            http.oauth2Client();
            return http.build();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    测试一下,当我们点击Customize,正常跳转,并显示了Hello,阿提说说,说明成功了。
    image.png

    💡当再使用Gitee、GitHub登录的时候,不能登录了,这是怎么回事?
    原因是上面这种配置方式,把其他的OAuth2登录都给覆盖了,所有获取用户信息的逻辑都会使用 CustomOAuth2UserService,但这几个第三方登录的接口返回格式又不一样了,因此这种配置方式违背了我们的初衷。

    4、多方登录共存

    创建一个用于保存多个登录实现的类CompositeOAuth2UserService,同样实现OAuth2UserService接口。

    @Configuration
    public class OAuth2LoginConfig {
        //无法共存
    //    @Bean
    //    SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
    //        http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
    //        //自定义用户信息获取实现
    //        http.oauth2Login(oauth2 -> oauth2.userInfoEndpoint(userInfo -> userInfo.userService(new CustomOAuth2UserService())));
    //        http.oauth2Client();
    //        return http.build();
    //    }
    
    
        //多方登录共存的方式
        @Bean
        SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
            http.oauth2Login(oauth2 -> oauth2.userInfoEndpoint(userInfo -> userInfo.userService(CustomUserService())));
            http.oauth2Client();
            return http.build();
        }
    
        private OAuth2UserService<OAuth2UserRequest, OAuth2User> CustomUserService() {
            //自定义的OAuth2客户端id
            final String CUSTOM = "customize";
            final CompositeOAuth2UserService compositeOAuth2UserService = new CompositeOAuth2UserService();
            //这里可以把所有自定义的实现都初始化进去
            compositeOAuth2UserService.getUserServiceMap().put(CUSTOM, new CustomOAuth2UserService());
            return compositeOAuth2UserService;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    多个三方登录共存主要实现类

    /**
     * 多个三方登录共存
     */
    public class CompositeOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    
        //重点,registrationId -> OAuth2UserService实现
        private Map<String, OAuth2UserService> userServiceMap = new ConcurrentHashMap<>();
    
        //默认OAuth2UserService实现
        private static final String DEFAULT_KEY = "default_key";
    
        public CompositeOAuth2UserService() {
            //初始化一个默认值
            userServiceMap.put(DEFAULT_KEY, new DefaultOAuth2UserService());
        }
    
        @Override
        public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
            ClientRegistration clientRegistration = userRequest.getClientRegistration();
            //根据注册客户端id获取对于的OAuth2UserService实现
            OAuth2UserService service = userServiceMap.get(clientRegistration.getRegistrationId());
            //没有获取到自定义的,使用默认实现
            if (service == null) {
                service = userServiceMap.get(DEFAULT_KEY);
            }
            //调用loadUser
            return service.loadUser(userRequest);
        }
    
        public Map<String, OAuth2UserService> getUserServiceMap() {
            return this.userServiceMap;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    至此,我们的自定义用户信息端点扩展完成了,并且支持多种登录方式共存。

    总结

    通过上述的扩展方式,在接入其他第三方登录,并且不能使用默认OAuth2UserService时,只需创建CustomOAuth2UserCustomOAuth2UserService两个类,并将CustomOAuth2UserService 加入SecurityFilterChain中即可。

    💡思考一下,Spring Security OAuth2 默认是支持GitHub、Google等方式登录的,那么我们是不是也可以按照他的方式,把微信、QQ等集成进去?后面我们将再进行探讨,请关注后期文章。

  • 相关阅读:
    【FPGA教程案例73】基础操作3——基于FPGA的Vivado功耗估计
    Verilog中 高位与低位
    springboot 如何引用外部配置文件(spring.config.location)
    面向对象【递归方法】
    2022年5月17日刷题
    matlab使用hampel滤波,去除异常值
    2022.12.2Treats for the Cows POJ - 3186(区间dp
    从精装到智装,下一波浪潮浮现,听听智能家居的大咖们怎么说?
    WebGL层次模型——单节点模型
    服务器端编程/数据库驱动程序/RESTful API:介绍
  • 原文地址:https://blog.csdn.net/weixin_40972073/article/details/126456539