如何在分布式环境中搭建单点登录系统| 第二篇:基于Oauth2.0开发SSO核心代码

什么是SSO

单点登录系统主要解决统一认证授权的问题,在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。
想要完成单点登录的效果,必须先统一管理用户信息,其他应用系统必须配合完成改造和对接。

什么场景下,需要用到SSO?

较大型的企业,往往存在多套应用系统。各个应用系统都是在企业发展的某个阶段,因业务发展的需求,开发研制而成。每套系统都会有一套自己的用户体系,需要终端用户注册、登录后才能使用。
随着企业的发展,用到的系统随之增多,用户在操作不同的系统时,需要多次登录,而且每个系统的账号都不一样,这对于用户来说很不方便。
于是,设计一套统一的登录认证系统,避免不必要的反复登录。减轻用户操作负担,提高效率,在企业的发展进程中,显得越来越重要。

什么是OAuth 2.0

OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容。
OAuth 2.0是OAuth协议的下一版本,但不向后兼容OAuth 1.0。 OAuth 2.0关注客户端开发者的简易性,同时为Web应用,桌面应用和手机,和起居室设备提供专门的认证流程。

为什么需要OAuth 2.0?

上文提到,为了实现SSO,各个应用系统需要配合对接单点登录系统。OAuth2.0提供了一套简单,通用,可扩展的开放认证授权协议。既可以实现企业内部系统的对接,也可以实现企业与第三方外部系统的认证互通。
鉴于OAuth2.0的开放特性,只要遵守OAuth2.0协议,可以大幅降低系统间对接开发的沟通调试成本。

什么是Spring Security OAuth

Spring Security对OAuth的实现,借助SpringSecurity框架在认证授权方面既有的优势,可以让开发者简易地使用OAuth协议。

image.png

Spring Security OAuth存在的问题

  • 鉴于OAuth协议本身就有较多的概念(4种角色,4种授权模式),使用Spring Security OAuth就需要定义和管理OAuth相关的Bean。
  • 另一个比较大的问题,在于Spring Security OAuth默认基于Session实现,这在微服务场景下并不适用。这也是本系列文章要解决的核心问题。

代码实现

Springsecurity基础配置

  • 声明认证管理器AuthenticationManager,认证阶段的用户身份鉴别使用自定义的UserDetailsService
  • 采用BCryptPasswordEncoder对登录密码进行加密及编码,BCryptPasswordEncoder基于随机盐+密钥对密码进行加密,并通过SHA-256算法进行编码
  • 自定义认证逻辑过滤器,满足登录请求参数个性化,多样化需求
  • 自定义token解析过滤器,解决微服务无状态场景下,Spring Security OAuth无法在Session中获取用户认证信息的问题
package com.codeiy.auth.oauth.config;

import com.codeiy.auth.oauth.filter.AuthenticationProcessingFilter;
import com.codeiy.auth.oauth.filter.TokenAuthenticationFilter;
import com.codeiy.auth.oauth.handler.OAuthFailureHandler;
import com.codeiy.auth.oauth.handler.OAuthSuccessHandler;
import com.codeiy.auth.oauth.handler.SsoLogoutSuccessHandler;
import com.codeiy.auth.oauth.service.UserDetailsServiceImpl;
import com.codeiy.core.constant.AuthConstants;
import com.codeiy.user.client.UserClient;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.Filter;

/**
 * SpringSecurity基础配置
 * 声明认证管理器{@link AuthenticationManager},认证阶段的用户身份鉴别使用自定义的{@link UserDetailsService}
 * 采用{@link BCryptPasswordEncoder}对登录密码进行加密及编码,{@link BCryptPasswordEncoder}基于随机盐+密钥对密码进行加密,并通过SHA-256算法进行编码
 *
 * @author free@codeiy.com
 */
@Primary
@Order(90)
@Configuration(proxyBeanMethods = false)
@RequiredArgsConstructor
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    private final UserClient userClient;
    /**
     * 基于随机盐+密钥对密码进行加密,并通过SHA-256算法进行编码
     */
    PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    /**
     * 自定义身份认证服务
     */
    @Override
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetailsServiceImpl userDetailsService = new UserDetailsServiceImpl();
        userDetailsService.setUserClient(userClient);
        userDetailsService.setPasswordEncoder(passwordEncoder);
        return userDetailsService;
    }

    /**
     * 1. 禁用csrf
     * 2. 请求会话采用无状态方式
     * 3. 自定义登录登出路径
     * 4. 自定义登录请求参数{@link UsernamePasswordAuthenticationFilter}
     *
     * @param http http请求安全相关配置
     */
    @Override
    @SneakyThrows
    protected void configure(HttpSecurity http) {
        Filter loginFilter = loginFilter();
        Filter tokenAuthenticationFilter = tokenAuthenticationFilter();
        http.formLogin()
                .loginProcessingUrl(AuthConstants.LOGIN_PROCESSING_URL).permitAll()
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().logout().logoutSuccessHandler(new SsoLogoutSuccessHandler())
                .and().authorizeRequests()
                .antMatchers(AuthConstants.LOGOUT_URL).permitAll()
                .anyRequest().authenticated()
                // 登录请求参数除了用户名,密码之外,还有验证码等其他参数,通过过滤器自定义认证逻辑
                .and().addFilterBefore(loginFilter, UsernamePasswordAuthenticationFilter.class)
                // 解决微服务架构无状态请求场景下,如何识别当前请求所属用户,并绑定到SpringSecurity上下文
                .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

    }

    /**
     * 认证管理器
     */
    @Bean
    @Override
    @SneakyThrows
    public AuthenticationManager authenticationManagerBean() {
        return super.authenticationManagerBean();
    }

    /**
     * 登录密码编码工具
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return passwordEncoder;
    }

    /**
     * 自定义登录认证过滤器,满足登录请求参数个性化,多样化需求
     */
    private Filter loginFilter() throws Exception {
        AuthenticationProcessingFilter loginFilter = new AuthenticationProcessingFilter();
        loginFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(AuthConstants.LOGIN_PROCESSING_URL, AuthConstants.METHOD_POST));
        loginFilter.setAuthenticationSuccessHandler(new OAuthSuccessHandler());
        loginFilter.setAuthenticationFailureHandler(new OAuthFailureHandler());
        loginFilter.setAuthenticationManager(authenticationManager());
        return loginFilter;
    }

    /**
     * token解析过滤器,解决微服务无状态场景下,Spring Security OAuth无法在Session中获取用户认证信息的问题
     */
    private Filter tokenAuthenticationFilter() {
        TokenAuthenticationFilter tokenAuthenticationFilter = new TokenAuthenticationFilter();
        return tokenAuthenticationFilter;
    }
}


OAuth认证服务器配置

  • 通过注解@EnableAuthorizationServer声明认证服务器
  • 基于ClientDetailsServiceImpl自定义应用系统信息管理
  • 基于Redis存储OAuth协议的AccessToken
  • 声明OAuth2.0简化模式AccessToken生成规则
  • 自定义Auth2.0协议确认授权以及认证请求链接
package com.codeiy.auth.oauth.config;

import com.codeiy.auth.oauth.service.AuthenticationCodeServiceImpl;
import com.codeiy.auth.oauth.service.ClientDetailsServiceImpl;
import com.codeiy.core.constant.AuthConstants;
import com.codeiy.user.client.AppClient;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.implicit.ImplicitTokenGranter;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

/**
 * 通过注解{@code @EnableAuthorizationServer}声明认证服务器
 * 基于{@link ClientDetailsServiceImpl}自定义应用系统信息管理
 * 基于Redis存储OAuth协议的AccessToken
 * 声明OAuth2.0简化模式AccessToken生成规则
 * 自定义Auth2.0协议确认授权以及认证请求链接
 */
@RequiredArgsConstructor
@EnableAuthorizationServer
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    private final AppClient appClient;
    private final AuthenticationManager authenticationManager;
    private final RedisConnectionFactory redisConnectionFactory;
    private final PasswordEncoder passwordEncoder;

    private ClientDetailsServiceImpl clientDetailsService;
    private RedisTokenStore redisTokenStore;
    private AuthorizationServerTokenServices tokenServices;
    private OAuth2RequestFactory oAuth2RequestFactory;
    private ImplicitTokenGranter implicitTokenGranter;
    private AuthorizationCodeServices authorizationCodeServices;

    /**
     * 基于Redis存储OAuth协议的AccessToken
     */
    @Bean
    public TokenStore tokenStore() {
        if (redisTokenStore == null) {
            redisTokenStore = new RedisTokenStore(redisConnectionFactory);
            redisTokenStore.setPrefix(AuthConstants.OAUTH_PREFIX);
        }
        return redisTokenStore;
    }

    /**
     * 令牌权限范围配置,使用默认的{@link DefaultOAuth2RequestFactory},可匹配到用户的角色。
     */
    @Bean
    public OAuth2RequestFactory oAuth2RequestFactory() {
        if (oAuth2RequestFactory == null) {
            oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetailsService());
        }
        return oAuth2RequestFactory;
    }

    /**
     * OAuth2.0简化模式AccessToken生成规则
     */
    @Bean
    public ImplicitTokenGranter implicitTokenGranter() {
        if (implicitTokenGranter == null) {
            implicitTokenGranter = new ImplicitTokenGranter(tokenServices(), clientDetailsService(), oAuth2RequestFactory());
        }
        return implicitTokenGranter;
    }

    /**
     * OAuth2.0授权码模式,自定义授权码的存储方式
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        if (authorizationCodeServices == null) {
            authorizationCodeServices = new AuthenticationCodeServiceImpl(redisConnectionFactory);
        }
        return authorizationCodeServices;
    }

    /**
     * 自定义应用系统信息管理
     */
    private ClientDetailsService clientDetailsService() {
        if (clientDetailsService == null) {
            clientDetailsService = new ClientDetailsServiceImpl();
            clientDetailsService.setPasswordEncoder(passwordEncoder);
            clientDetailsService.setAppClient(appClient);
        }
        return clientDetailsService;
    }

    /**
     * 声明AccessToken管理服务
     */
    private AuthorizationServerTokenServices tokenServices() {
        if (tokenServices == null) {
            tokenServices = new DefaultTokenServices();
        }
        return tokenServices;
    }

    /**
     * 配置应用系统管理,基于{@link ClientDetailsServiceImpl}自定义应用系统信息管理
     */
    @Override
    @SneakyThrows
    public void configure(ClientDetailsServiceConfigurer clients) {
        clients.withClientDetails(clientDetailsService());
    }

    /**
     * 配置获取AccessToken请求的访问权限
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
        oauthServer.tokenKeyAccess("permitAll()").allowFormAuthenticationForClients().checkTokenAccess("permitAll()");
    }

    /**
     * 自定义Auth2.0协议确认授权以及认证请求链接
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                .authenticationManager(authenticationManager)
                .authorizationCodeServices(authorizationCodeServices())
                .tokenServices(tokenServices())
                .allowedTokenEndpointRequestMethods(HttpMethod.POST)
                .pathMapping("/oauth/confirm_access", "/oauth/confirmAccess")
                .pathMapping("/oauth/authorize", "/oauth/customAuthorize")
        ;
    }

}

确认授权核心代码

package com.codeiy.auth.oauth.endpoint;

import cn.hutool.core.util.StrUtil;
import com.codeiy.core.util.R;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.*;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.endpoint.DefaultRedirectResolver;
import org.springframework.security.oauth2.provider.endpoint.RedirectResolver;
import org.springframework.security.oauth2.provider.implicit.ImplicitTokenGranter;
import org.springframework.security.oauth2.provider.implicit.ImplicitTokenRequest;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestValidator;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.util.*;

/**
 * OAuth授权自定义端点
 */
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/oauth")
public class AuthEndpoint {
    private final ClientDetailsService clientDetailsService;
    private final ImplicitTokenGranter implicitTokenGranter;
    private final AuthorizationCodeServices authorizationCodeServices;
    private final OAuth2RequestFactory oAuth2RequestFactory;

    private RedirectResolver redirectResolver = new DefaultRedirectResolver();
    private OAuth2RequestValidator oauth2RequestValidator = new DefaultOAuth2RequestValidator();


    /**
     * 授权确认页面
     *
     * @return
     */
    @GetMapping("/confirmAccess")
    public R confirmAccess(HttpServletRequest request) {
        String clientId = request.getParameter("clientId");
        if (StrUtil.isBlank(clientId)) {
            return R.failed("clientId不能为空");
        }
        ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
        if (clientDetails == null) {
            return R.failed("根据clientId查无数据");
        }
        return R.ok(clientDetails);
    }

    /**
     * 自定义确认授权
     *
     * @return
     */
    @RequestMapping("/customAuthorize")
    public R customAuthorize(HttpServletRequest request, @RequestParam Map<String, String> parameters) {
        AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(parameters);
        Set<String> responseTypes = authorizationRequest.getResponseTypes();
        if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
            throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
        }
        if (authorizationRequest.getClientId() == null) {
            throw new InvalidClientException("A client id must be provided");
        }

        Authentication principal = SecurityContextHolder.getContext().getAuthentication();
        if (principal == null || !principal.isAuthenticated()) {
            throw new InsufficientAuthenticationException(
                    "User must be authenticated with Spring Security before authorization can be completed.");
        }

        ClientDetails client = clientDetailsService.loadClientByClientId(authorizationRequest.getClientId());

        // The resolved redirect URI is either the redirect_uri from the parameters or the one from
        // clientDetails. Either way we need to store it on the AuthorizationRequest.
        String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
        String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
        if (!StringUtils.hasText(resolvedRedirect)) {
            throw new RedirectMismatchException(
                    "A redirectUri must be either supplied or preconfigured in the ClientDetails");
        }
        authorizationRequest.setRedirectUri(resolvedRedirect);

        // We intentionally only validate the parameters requested by the client (ignoring any data that may have
        // been added to the request by the manager).
        oauth2RequestValidator.validateScope(authorizationRequest, client);

        authorizationRequest.setApproved("true".equals(parameters.get("user_oauth_approval")));

        // Validation is all done, so we can check for auto approval...
        if (authorizationRequest.isApproved()) {
            if (responseTypes.contains("token")) {
                ModelAndView mv = getImplicitGrantResponse(authorizationRequest);
            }
            if (responseTypes.contains("code")) {
                String url = getSuccessfulRedirect(authorizationRequest, generateCode(authorizationRequest, principal));
                log.info(url);
            }
        }


        String clientId = request.getParameter("clientId");
        if (StrUtil.isBlank(clientId)) {
            return R.failed("clientId不能为空");
        }
        ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
        if (clientDetails == null) {
            return R.failed("根据clientId查无数据");
        }
        return R.ok(clientDetails);
    }


    private String getSuccessfulRedirect(AuthorizationRequest authorizationRequest, String authorizationCode) {

        if (authorizationCode == null) {
            throw new IllegalStateException("No authorization code found in the current request scope.");
        }

        Map<String, String> query = new LinkedHashMap<String, String>();
        query.put("code", authorizationCode);

        String state = authorizationRequest.getState();
        if (state != null) {
            query.put("state", state);
        }

        return append(authorizationRequest.getRedirectUri(), query, null, false);
    }


    private String generateCode(AuthorizationRequest authorizationRequest, Authentication authentication)
            throws AuthenticationException {

        try {

            OAuth2Request storedOAuth2Request = oAuth2RequestFactory.createOAuth2Request(authorizationRequest);

            OAuth2Authentication combinedAuth = new OAuth2Authentication(storedOAuth2Request, authentication);
            String code = authorizationCodeServices.createAuthorizationCode(combinedAuth);

            return code;

        } catch (OAuth2Exception e) {

            if (authorizationRequest.getState() != null) {
                e.addAdditionalInformation("state", authorizationRequest.getState());
            }

            throw e;

        }
    }

    private ModelAndView getImplicitGrantResponse(AuthorizationRequest authorizationRequest) {
        try {
            TokenRequest tokenRequest = oAuth2RequestFactory.createTokenRequest(authorizationRequest, "implicit");
            OAuth2Request storedOAuth2Request = oAuth2RequestFactory.createOAuth2Request(authorizationRequest);
            OAuth2AccessToken accessToken = getAccessTokenForImplicitGrant(tokenRequest, storedOAuth2Request);
            if (accessToken == null) {
                throw new UnsupportedResponseTypeException("Unsupported response type: token");
            }
            return new ModelAndView(new RedirectView(appendAccessToken(authorizationRequest, accessToken), false, true,
                    false));
        } catch (OAuth2Exception e) {
            return new ModelAndView(new RedirectView(getUnsuccessfulRedirect(authorizationRequest, e, true), false,
                    true, false));
        }
    }

    private String appendAccessToken(AuthorizationRequest authorizationRequest, OAuth2AccessToken accessToken) {

        Map<String, Object> vars = new LinkedHashMap<String, Object>();
        Map<String, String> keys = new HashMap<String, String>();

        if (accessToken == null) {
            throw new InvalidRequestException("An implicit grant could not be made");
        }

        vars.put("access_token", accessToken.getValue());
        vars.put("token_type", accessToken.getTokenType());
        String state = authorizationRequest.getState();

        if (state != null) {
            vars.put("state", state);
        }
        Date expiration = accessToken.getExpiration();
        if (expiration != null) {
            long expires_in = (expiration.getTime() - System.currentTimeMillis()) / 1000;
            vars.put("expires_in", expires_in);
        }
        String originalScope = authorizationRequest.getRequestParameters().get(OAuth2Utils.SCOPE);
        if (originalScope == null || !OAuth2Utils.parseParameterList(originalScope).equals(accessToken.getScope())) {
            vars.put("scope", OAuth2Utils.formatParameterList(accessToken.getScope()));
        }
        Map<String, Object> additionalInformation = accessToken.getAdditionalInformation();
        for (String key : additionalInformation.keySet()) {
            Object value = additionalInformation.get(key);
            if (value != null) {
                keys.put("extra_" + key, key);
                vars.put("extra_" + key, value);
            }
        }
        // Do not include the refresh token (even if there is one)
        return append(authorizationRequest.getRedirectUri(), vars, keys, true);
    }

    private String getUnsuccessfulRedirect(AuthorizationRequest authorizationRequest, OAuth2Exception failure,
                                           boolean fragment) {

        if (authorizationRequest == null || authorizationRequest.getRedirectUri() == null) {
            // we have no redirect for the user. very sad.
            throw new UnapprovedClientAuthenticationException("Authorization failure, and no redirect URI.", failure);
        }

        Map<String, String> query = new LinkedHashMap<String, String>();

        query.put("error", failure.getOAuth2ErrorCode());
        query.put("error_description", failure.getMessage());

        if (authorizationRequest.getState() != null) {
            query.put("state", authorizationRequest.getState());
        }

        if (failure.getAdditionalInformation() != null) {
            for (Map.Entry<String, String> additionalInfo : failure.getAdditionalInformation().entrySet()) {
                query.put(additionalInfo.getKey(), additionalInfo.getValue());
            }
        }

        return append(authorizationRequest.getRedirectUri(), null, query, fragment);

    }

    private String append(String base, Map<String, ?> query, Map<String, String> keys, boolean fragment) {

        UriComponentsBuilder template = UriComponentsBuilder.newInstance();
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base);
        URI redirectUri;
        try {
            // assume it's encoded to start with (if it came in over the wire)
            redirectUri = builder.build(true).toUri();
        } catch (Exception e) {
            // ... but allow client registrations to contain hard-coded non-encoded values
            redirectUri = builder.build().toUri();
            builder = UriComponentsBuilder.fromUri(redirectUri);
        }
        template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost())
                .userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath());

        if (fragment) {
            StringBuilder values = new StringBuilder();
            if (redirectUri.getFragment() != null) {
                String append = redirectUri.getFragment();
                values.append(append);
            }
            for (String key : query.keySet()) {
                if (values.length() > 0) {
                    values.append("&");
                }
                String name = key;
                if (keys != null && keys.containsKey(key)) {
                    name = keys.get(key);
                }
                values.append(name + "={" + key + "}");
            }
            if (values.length() > 0) {
                template.fragment(values.toString());
            }
            UriComponents encoded = template.build().expand(query).encode();
            builder.fragment(encoded.getFragment());
        } else {
            for (String key : query.keySet()) {
                String name = key;
                if (keys != null && keys.containsKey(key)) {
                    name = keys.get(key);
                }
                template.queryParam(name, "{" + key + "}");
            }
            template.fragment(redirectUri.getFragment());
            UriComponents encoded = template.build().expand(query).encode();
            builder.query(encoded.getQuery());
        }

        return builder.build().toUriString();

    }

    private synchronized OAuth2AccessToken getAccessTokenForImplicitGrant(TokenRequest tokenRequest,
                                                                          OAuth2Request storedOAuth2Request) {
        return implicitTokenGranter.grant("implicit", new ImplicitTokenRequest(tokenRequest, storedOAuth2Request));
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容