首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Spring Security 自定义用户信息端点与多种登录方式共存

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

作者头像
阿提说说
发布2022-11-18 16:40:44
发布2022-11-18 16:40:44
1.4K0
举报
文章被收录于专栏:Java技术进阶Java技术进阶

前言

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

自定义扩展

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

spring-security-resource-server-custom

代码语言:javascript
复制
@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;
    }
}

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

代码语言:javascript
复制
@RestController
public class UserInfoController {

    @GetMapping("/userinfo")
    public Result getUserInfo() {
        UserInfoRes userInfoRes = new UserInfoRes();
        userInfoRes.setUsername("阿提说说");
        return Result.ok(userInfoRes);
    }
}

spring-security-oauth2-client-custom

1、自定义实现OAuth2User接口

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

代码语言:javascript
复制
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();
    }

}

2、自定义OAuth2UserService接口实现

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

代码语言:javascript
复制
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.
     *
     * <p>
     * <b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured
     * with the following:
     * <ol>
     * <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}</li>
     * </ol>
     * @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;
    }
}

3、配置自定义OAuth2UserService实现

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

代码语言:javascript
复制
    @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();
    }

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

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

4、多方登录共存

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

代码语言:javascript
复制
@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;
    }
}

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

代码语言:javascript
复制
/**
 * 多个三方登录共存
 */
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;
    }
}

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

总结

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-08-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 自定义扩展
    • spring-security-resource-server-custom
    • spring-security-oauth2-client-custom
      • 1、自定义实现OAuth2User接口
      • 2、自定义OAuth2UserService接口实现
      • 3、配置自定义OAuth2UserService实现
      • 4、多方登录共存
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档