spring security_day5 开发基于app的认证框架

一. Spring Security OAuth简介

  上一章的开发l是以第三方应用角色为中心进行开发的,我们做的工作就是如何实现第三方应用,然后获取服务提供者的访问权限去访问服务提供者的一些受保护的资源。而在这一章,我们要使用spring security oauth基于服务提供者进行开发,学习如何向其他客户端提供令牌、并且可以验证这些令牌然后返回我们受保护的资源。
  服务提供商事实上包括两种角色:授权服务器和资源服务器,它们可以处于同一个应用程序中,当然多个资源服务器也可以共享同一个授权服务器。

服务提供者

  spring social oauth的主要流程是:第三方应用向认证服务器获取授权,认证服务器向应用发送token,第三方应用使用该token从资源服务器中获取数据。
  spring social oauth默认帮我们实现了四种传统的授权模式的认证逻辑,我们需要做的只是将我们自定义的认证逻辑添加到认证服务器中,使得我们的认证服务器可以支持用户名和密码、手机号和第三方登录的方式生成token。

二. 服务提供者的简单实现
@Configuration
@EnableAuthorizationServer      //该注解:表明当前是一个认证服务器
public class ImoocAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("imooc")    //第三方应用的标识:比如慕课微信助手,标识哪个第三方应用从服务提供者获取权限
                .secret(passwordEncoder().encode("imoocsecret"))
                .redirectUris("http://www.baidu.com")
                .authorizedGrantTypes("authorization_code","refresh_token")
                .scopes("all");
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

@Component
public class AuthUserDetailService implements UserDetailsService {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private PasswordEncoder passwordEncoder;
    // 服务提供者的哪个用户进行授权,比如微信中是哪个用户赋予第三方应用权限
    @Override
    public UserDetails loadUserByUsername(String username) {
        logger.info("用户{}请求授权", username);
        //注意:一定要包括"role_user"角色
        return  new User(username, passwordEncoder.encode("123456"), AuthorityUtils.createAuthorityList("ROLE_USER"));
    }
}
@Configuration
public class AppSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().and()
                .authorizeRequests().anyRequest().authenticated().and()
                .csrf().disable();
    }
}

  以上配置就完成了一个简单的认证服务器,我们可以测试一下根据传统的四种授权模式是否可以访问到我们想要的资源。
  1. 授权码模式:
  (1)获取授权码
    a :访问改地址,输入用户名和密码之后会跳往授权页面:http://localhost:8989/oauth/authorize?response_type=code&client_id=imooc&redirect_uri=http://www.baidu.com&scope=all

授权页面

     b:点击授权之后获取授权码

百度页面

  (2)根据授权码获取Token(可以使用谷歌插件Restlet Client工具):
  发送POST请求到:http://localhost:8989/oauth/token
    a. 请求头的设置:

请求头
Authorization参数的值

    b:请求体要根据Oauth2协议规定的参数进行添加

请求体

  认证服务器会返回我们需要的token。

返回的结果

  2. 密码模式:如果想使用这种模式,我们需要添加一些代码

1. AppSecurityConfig类中,需要覆盖一个方法,将AuthenticationManager对象交给容器管理
     @Override
     @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
2. ImoocAuthorizationServerConfig类中,需要将AuthenticationManager设置到AuthorizationServerEndpointsConfigurer中
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager)
                        .userDetailsService(userDetailsService);
    }
3. ImoocAuthorizationServerConfig类中,需要给该应用添加密码授权模式
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("imooc")
                .secret(passwordEncoder().encode("imoocsecret"))
                .redirectUris("http://www.baidu.com")
                .authorizedGrantTypes("authorization_code","password","refresh_token")
                .scopes("all");
    }
密码模式

  密码模式的请求头和授权码模式相同,参数根据OAuth2协议规定参数进行添加。
  在获取到对应的token之后,我们就可以使用该token从资源服务器中获取受保护的资源了,资源服务器的配置非常简单只需要加一个注解即可。

@Configuration
@EnableResourceServer
public class ImoocResourceServerConfig {
}

  然后我们就可以在请求头添加该token访问资源服务器中的资源。

访问资源
请求头中需要添加 :  Authorization=token_type access_token
三. SpringSecurityOauth核心源码解析
获取Token所需要的类的整体流程

  最好可以边看源码边借鉴上图,梳理类之间的关系,对 token的生成的流程有一个大概的认知,因为我们需要添加自定义的生成逻辑,我们需要知道可以从哪里入手,代码如下:

    TokenEndpoint类:
    @RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
    public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
    Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        // 作为受保护的资源,当程序运行到这里,说明已经完成了第三方应用的认证工作
        //principal包含了第三方应用的所有信息(比如慕课微信助手的clientid、clientSecret等等),说明了是哪个应用需要获取 tokne信息
        //parameters:获取token的请求所携带的参数

         //1. 首先根据 ClientDetailsService 获取第三方应用的所有信息,clientDetails中包括clientId,clientSecret,scope,grantType等等。
        String clientId = getClientId(principal);
        ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

        //2. 然后使用 DefaultOAuth2RequestFactory根据 请求参数和 clientDetail封装TokenRequest对象
        TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

        //3. 验证请求token中的scope是否有效,是否超过了该应用的授权范围
        if (authenticatedClient != null) {
            oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
        }

         //4. 如果是授权码模式,将请求参数中的scope设置为空,因为授权码模式 token中的scope已经在获取授权码时确定了,所以token的scope应该从code中获取
        if (isAuthCodeRequest(parameters)) {
            if (!tokenRequest.getScope().isEmpty()) {
                logger.debug("Clearing scope of incoming token request");
                tokenRequest.setScope(Collections.<String> emptySet());
            }
        }

        //5. 如果是刷新令牌的请求,新token中的scope应该和refresh_token中的一致
        if (isRefreshTokenRequest(parameters)) {
          tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
        }

        //6. 获取token
        OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
        if (token == null) {
            throw new UnsupportedGrantTypeException("Unsupported grant type");
        }
        return getResponse(token);
    }

  有上述代码可知,主要的逻辑都在第六步中,我们首先看下TokenGranter类,spring security oauth对该类的是这样描述的:

Interface for granters of access tokens. Various grant types are defined in the specification, and each of those has an implementation, leaving room for extensions to the specification as needed.

  大致的意意思:该类是一个授权接口,每种授权类型都应该实现该类定义自己的实现(加上refresh_token,目前包括五种授权类型,所以至少有五个实现类)。所以如果我们想要自定义我们的认证逻辑,需要实现该类。
  该类的作用将TokenRequest封装成我们想要的token对象。debug结果显示,,目前的确包括五种授权模式,由于密码模式比较简单,所以我们跟着密码模式的流程走一遍:

所有的TokenGranter

  spring security oauth使用装饰者模式,将这五个类封装到CompositeTokenGranter中,由它根据授权类型自动选择对应的TokenGranter

@Deprecated
public class CompositeTokenGranter implements TokenGranter {

    private final List<TokenGranter> tokenGranters;

    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
        for (TokenGranter granter : tokenGranters) {
            OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
            if (grant!=null) {
                return grant;
            }
        }
        return null;
    }
      ......
}  

  在调用ClientCredentialsTokenGranter的方法之前,会调用父类AbstractTokenGrantergrant()方法:

 1. 密码授权模式的方法:该方法没有做什么主要的工作,就是对父类返回token做了一层封装,因为某些情况下密码模式不应该返回refresh_token
    @Override
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
        OAuth2AccessToken token = super.grant(grantType, tokenRequest);
        if (token != null) {
            DefaultOAuth2AccessToken norefresh = new DefaultOAuth2AccessToken(token);
            if (!allowRefresh) {
                norefresh.setRefreshToken(null);
            }
            token = norefresh;
        }
        return token;
    }
  2. 父类的方法:使用 ClientDetailsService 对象获取 clientDetails 对象,然后和 tokenRequest 封装成token 对象
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
        String clientId = tokenRequest.getClientId();
        ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
        validateGrantType(grantType, client);

        return getAccessToken(client, tokenRequest);
    }

   3.  调用TokenServices 对象将 clientDetail 和 TokenRequest 封装成 结果token 对象
    protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
        return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
    }

    4. 首先使用 DefaultOAuth2RequestFactory 对象将 clientDetail 和 tokenRequest 封装成 OAuth2Authentication 对象
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, null);
    }

    createOAuth2Request方法如下,使用 TokenRequest 和 client 封装 OAuth2Request  对象,此时tokenRequest就包含了 client对象的一些细节
    public OAuth2Request createOAuth2Request(ClientDetails client, TokenRequest tokenRequest) {
        return tokenRequest.createOAuth2Request(client);
    }

    5. 然后调用 DefaultTokenServices 类的 createAccessToken() 方法根据 OAuth2Authentication创建结果 Token 对象
    @Transactional
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
        //1. 首先根据认证信息,从tokenStore中查询是否给该用户返回给Token, 这就是为什么我们测试密码模式和授权码模式时返回的token是相同的
        OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
        OAuth2RefreshToken refreshToken = null;
        if (existingAccessToken != null) {
             //(1)如果Token已过期,则将该token从 tokenStore中移除,然后重新创建 token
            if (existingAccessToken.isExpired()) {
                if (existingAccessToken.getRefreshToken() != null) {
                    refreshToken = existingAccessToken.getRefreshToken();
                    tokenStore.removeRefreshToken(refreshToken);
                }
                tokenStore.removeAccessToken(existingAccessToken);
            }
              //(2)重新存储一下 token,因为认证信息有可能改变
            else {
                tokenStore.storeAccessToken(existingAccessToken, authentication);
                return existingAccessToken;
            }
        }

        if (refreshToken == null) {
            refreshToken = createRefreshToken(authentication);
        }
                
        else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
            ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
            if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
                refreshToken = createRefreshToken(authentication);
            }
        }
          //2.  创建 token ,然后将 token和refresh_token存储起来
        OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
        tokenStore.storeAccessToken(accessToken, authentication);
        // In case it was modified
        refreshToken = accessToken.getRefreshToken();
        if (refreshToken != null) {
            tokenStore.storeRefreshToken(refreshToken, authentication);
        }
        return accessToken;
    }
6. 根据认证信息和 refresh_token 创建 Token 对象 
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
        //1.  创建 DefaultOAuth2AccessToken ,然后设置默认值
        DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
        int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
        if (validitySeconds > 0) {
            token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
        }
        token.setRefreshToken(refreshToken);
        token.setScope(authentication.getOAuth2Request().getScope());
        //2. 使用  token增强器 accessTokenEnhancer给 token添加附加信息,我们可以提供自定义 token增强器给token添加自定义的信息。
        return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
    }
四. 重构代码
image.png

  如果你指定了成功处理器,那么无论使用哪种方式进行认证,认证完成之后都会跳往成功处理器进行处理,所以我们只需要原来成功处理器中的代码修改成 向客户端返回 token 的逻辑就可以了。
  所以我们按照以上图片的流程对代码进行重构

1. 首先在认证服务器中做一些基本的配置,将成功处理器添加到流程中
@Configuration
@EnableResourceServer
public class ImoocResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.formLogin().successHandler(authenticationSuccessHandler).and()
                .authorizeRequests().anyRequest().authenticated().and()
                .csrf().disable();
    }
}
2. 成功处理器:根据之前源码分析流程构建 token 对象
@Component
public class ImoocSuccessHandler implements AuthenticationSuccessHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());
    @Autowired
    private ClientDetailsService clientDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private AuthorizationServerTokenServices authorizationServerTokenServices;
    @Autowired(required = false)
    private TokenEnhancer tokenEnhancer;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        // 1. BasicAuthenticationFilter类有获取头信息代码,可以借鉴
        String header = request.getHeader("Authorization");

        if (header == null || !header.startsWith("Basic ")) {
            throw new RuntimeException("请求的格式不正确");
        }

        String[] tokens = extractAndDecodeHeader(header, request);
        String clientId = tokens[0];
        String clientSecret = tokens[1];

        ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
        if(clientDetails == null){
            logger.info("{}对应的信息不存在!", clientId);
            throw new RuntimeException("无效的clientId");
        }
        if( !passwordEncoder.matches(clientSecret, clientDetails.getClientSecret())){
            throw new RuntimeException("无效的clientSecret");
        }
        // 根据之前分析流程获取最终结果
        TokenRequest tokenRequest = new TokenRequest(null, clientId,  clientDetails.getScope(), "custom");
        OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);

        OAuth2AccessToken accessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);

        accessToken =  tokenEnhancer == null? accessToken:tokenEnhancer.enhance(accessToken, oAuth2Authentication);

        logger.info("token为:{}", accessToken);
    }

    private String[] extractAndDecodeHeader (String header, HttpServletRequest request) throws
    UnsupportedEncodingException {
        
        byte[] base64Token = header.substring(6).getBytes("UTF-8");
        byte[] decoded;
        try {
            decoded = Base64.decode(base64Token);
        } catch (IllegalArgumentException e) {
            throw new BadCredentialsException(
                    "Failed to decode basic authentication token");
        }
        String token = new String(decoded, Charset.forName("UTF-8"));

        int delim = token.indexOf(":");

        if (delim == -1) {
            throw new BadCredentialsException("Invalid basic authentication token");
        }
        return new String[]{token.substring(0, delim), token.substring(delim + 1)};
    }
    }

  以上的配置就算完成了用户名和密码登录的基本授权流程,我们可以做一些测试:
   (1)发送请求获取 token, 注意请求头和之前相同。

发送请求获取token
token数据

  (2)根据 token 获取资源

返回结果
五. Token相关

  1. TokenStore配置:默认情况下,服务提供者的 token 是存储在内存中的,当服务重启时,发出去的 token 将变得无效,因为应用无法在内存中获取 token对应的认证信息,所以可以将 token保存到持久化的介质中,以保证当某些特殊情况机器导致重启时发出去的 token 依然有效。

1. TokenStore配置类
@Configuration
public class TokenStoreConfig {
    @Bean
    public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory){
        return new RedisTokenStore(redisConnectionFactory);
    }
}
2. 通过授权服务器中的配置, 将 tokenStore加到 入口类中
@Configuration
@EnableAuthorizationServer    
public class ImoocAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private TokenStore tokenStore;
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints
                    .tokenStore(tokenStore)
                    ......
    }
}

  2. 使用 JWT 替换 token
   JWT ( JSON WEB TOKEN ),主要用作交换信息和授权,主要包含三个部分:Header(包括所使用的 token 类型和所使用的签名算法)、Payload(主要数据,jwt 存储数据的部分),Signature(签名算法,判断数据的有效性(是否发生被恶意篡改、丢失等改变))。
   spring security oauth中的token中包含的数据是没有任何业务含义的,它的作用主要是保证了自身在持久化介质中的唯一性,可以根据 token从介质中可以获取到用户认证的信息,它的初始化方式是 DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());,而JWT不需要依赖任何持久化介质,用户的身份信息是存储在 token中的,所以它也不存在由于服务重启导致token变得无效的问题。

@Configuration
public class TokenStoreConfig {
    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("imooc");  //使用密签加密和解密 token
        return jwtAccessTokenConverter;
    }
    /**
     *  添加附加信息
     * */
    @Bean
    public TokenEnhancer tokenEnhancer(){
        return new TokenEnhancer() {
            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
                Map<String, Object> info = new HashMap<>();
                info.put("键", "自定义数据");
                ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
                return accessToken;
            }
        };
    }
}

@Configuration
@EnableAuthorizationServer  
public class ImoocAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    @Autowired
    private TokenEnhancer tokenEnhancer;
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
            List<TokenEnhancer> chain = new ArrayList<>();
            chain.add(tokenEnhancer);
            chain.add(jwtAccessTokenConverter);
            tokenEnhancerChain.setTokenEnhancers(chain);
            endpoints
                    .tokenStore(tokenStore)
                    .tokenEnhancer(tokenEnhancerChain)
                    ......
    }
}

  将 OauthToken 替换为 JWT的配置就完成了,我们可以做一些测试:

  (1)获取 token:有以下结果可知,的确已经替换成功,不再是32位的UUID字符串,而是包含三个部分的 JWT,每个部分由"." 隔开。

获取 token

  (2)访问资源:将生成的 token 添加到请求头中,获取到了受保护的资源

测试结果

  如果想要查看我们生成的 JWT 包含了什么信息,可以点击该网址查看。

jwt 内容

   SpringSecurity没有给我们提供可以获取 JWT 中自定义的数据的途径,所以我们需要自定义接口来解析 JWT获取我们想要的数据。

1.添加依赖
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>
2. 解析 token
    @GetMapping("/me")
    public Object getMe(HttpServletRequest request){
        String authorization = request.getHeader("Authorization");
        String token = StringUtils.substringAfter(authorization, "bearer ");
        Claims claims = Jwts.parser().setSigningKey("imooc".getBytes(Charset.forName("UTF-8"))).parseClaimsJws(token).getBody();
        return claims;
    }
六. 使用 JWT 实现 SSO

单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的

应用交互

  使用三个系统:应用A、应用B、认证服务器,进行模拟单点登录,当任何一个应用经过认证服务器的认证和授权之后,其他的应用都无需进行认证直接可以向认证服务器请求授权,并根据所授予的权限访问资源服务器中的资源。
  所以我们需要搭建三个项目, APP1、APP2、APPServer
  (1)首先创建三个项目的父项目:sso-demo,所导的依赖和之前的父项目相同
  (2)创建认证服务器

1. 添加依赖,注意这三个项目的依赖相同
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
    </dependencies>

2. 服务端的一些配置
@Configuration
public class SsoServerConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().and()
                .authorizeRequests().anyRequest().authenticated()
                .and().csrf().disable();
    }
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new PasswordEncoder() {
            public String encode(CharSequence charSequence) {
                return charSequence.toString();
            }

            public boolean matches(CharSequence charSequence, String s) {
                return StringUtils.equals(charSequence, s);
            }
        };
    }
}
3. 认证服务器类的一些配置
@Configuration
@EnableAuthorizationServer
public class SsoAuthrizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("app1")
                .secret("appsecret1")
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .redirectUris("http://localhost:9091/app1/login")
                .scopes("all")
                .autoApprove(true)      //自动授权
              .and()
                .withClient("app2")
                .secret("appsecret2")
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .redirectUris("http://localhost:9092/app2/login")
                .scopes("all")
                .autoApprove(true);
    }
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore())
                  .accessTokenConverter(jwtAccessTokenConverter());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("isAuthenticated()"); //表示用户经过身份认证之后,才可以访问 tokenkey
    }
    // JWT 的一些配置
    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("imooc");
        return jwtAccessTokenConverter;
    }
}

4. 端口和系统用户的一些配置
server.port=9090
server.servlet.context-path=/server
spring.security.user.password=123456

  (2)创建客户端应用(两个客户端的配置基本相同)

1. 客户端应用的配置类
@SpringBootApplication
@EnableOAuth2Sso    //开启单点登录功能
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

2. 客户端应用的配置文件
server.port=9092
server.servlet.context-path=/app2

security.oauth2.client.client-secret=appsecret2
security.oauth2.client.client-id=app2

#用户认证的地址
security.oauth2.client.user-authorization-uri=http://localhost:9090/server/oauth/authorize
#用户获取token的地址
security.oauth2.client.access-token-uri=http://localhost:9090/server/oauth/token
#由于配置了 tokenKey是需要已认证的用户才可以访问,所以配置用户获取tokenkey的地址
security.oauth2.resource.jwt.key-uri=http://localhost:9090/server/oauth/token_key

3. 测试使用的前台页面
    <h1>客户端2</h1>
    <a href="http://localhost:9091/app1/sso1demo.html">跳转到应用1</a>

  由于我们设置的是自动授权,所以不会显示授权页面(可以选择确认授权或者拒绝授权),默认的授权页面是由WhitelabelApprovalEndpoint类提供的,如果想要自定义授权页面,只需要仿照该类自定义即可。

1. controller类
@RestController
@SessionAttributes("authorizationRequest")
public class GrantPage{
    @Autowired
    private ObjectMapper objectMapper;
    @RequestMapping("/oauth/confirm_access")
    public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request)  {
        // model中包含了授权请求中的所有信息,包括 回调地址、授权范围 Scope等等,你可以自定义将某些信息显示到页面中
        //比如提示用 当前哪个应用正在请求授权、授权范围等等
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("/base_grant.html");
        return modelAndView;
    }
}

2. 前台页面
<h2>自定义授权页面</h2>
//注意两个表单提交地址是相同的 /oauth/authorize,由于这里我存在相对路径问题所以我写成了绝对路径
// 判断用户是否授权是根据user_oauth_approval的值来判断的,具体逻辑在`AuthorizationEndpoint`类中,想看的可以看下
<form method="post" action="http://localhost:9090/server/oauth/authorize">
    <input name="user_oauth_approval" value="true" type="hidden"/>
    <button class="btn" type="submit"> 同意授权</button>
</form>

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

推荐阅读更多精彩内容