1. 程式人生 > >spring security 整合 oauth2

spring security 整合 oauth2

前言

之前一篇部落格學過security的核心,這次整合一下oauth2,它也是市場上比較流行的介面驗證的一種方式了

引入pom

文中提及的整合oauth2的方式是建立在boot 的基礎上的.在引入的boot 和security的start之後,我們還需要引入oauth2,注意,它不是start,另外我們計劃將token儲存在redis中,所以我們還需要引入redis的start

  <!-- 將token儲存在redis中 -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
</dependencies>
<dependency>
  <groupId>org.springframework.security.oauth</groupId>
  <artifactId>spring-security-oauth2</artifactId>
  <version>2.3.3.RELEASE</version>
</dependency>

學習程式碼

security基本配置,主要是使用者許可權,以及設定oauth的認證路徑為所以角色都可訪問\

@Configuration
public class AppSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requestMatchers().anyRequest()
                .and()
                .authorizeRequests()
                .antMatchers("/oauth/*").permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        System.out.println("開始認證角色..............");
        //這裡設定的角色 系統會自動加上role_字首 既ROLE_ADMIN
        //inMemoryAuthentication 從記憶體中獲取
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("user_1").password(new BCryptPasswordEncoder().encode("12345678")).roles("client");
        //inMemoryAuthentication 從記憶體中獲取
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("user_2").password(new BCryptPasswordEncoder().encode("12345678")).roles("USER");
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        //oauth2的配置中需要這個bean
        return super.authenticationManagerBean();
    }
}

oauth2配置;如你所見oauth是建立在sercurity的基礎上的,所以資源伺服器和認證伺服器的configure與security基本的configure如出一轍;所謂資源伺服器配置就是值使用者請求資源時,哪些資源能被訪問,能被怎樣的許可權所訪問的配置,認證伺服器的配置主要是指採用哪種方式進行認證,認證的客戶端的賬號密碼的配置.關於認證方式有四種,這裡只提了兩種;

  • 授權碼模式(authorization code)

  • 簡化模式(implicit)

  • 密碼模式(resource owner password credentials)

  • 客戶端模式(client credentials)

@Configuration
public class Oauth2ServerConfig {
    private static  final String DEMO_RESOURCE_ID="order";

    /**
     * 資源伺服器配置
     */
    @Configuration
    @EnableResourceServer
    protected static class ResourceServerConfig extends ResourceServerConfigurerAdapter {

        public ResourceServerConfig() {
            super();
        }

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {

            resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                    .and()
                    .requestMatchers().anyRequest()
                    .and()
                    .anonymous()
                    .and()
                    .authorizeRequests()
                    //設定請求資源需要認證
                    .antMatchers("/getOrderInfo/**").authenticated();
        }
    }
    @Configuration
    @EnableAuthorizationServer
    protected  static  class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{

        @Autowired
        private PasswordEncoder passwordEncoder;
        @Autowired
        private RedisConnectionFactory redisConnectionFactory;
        @Autowired
        private AuthenticationManager authenticationManager;

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

        @Override
        /**
         * 配置authorizationServer安全認證的相關資訊,建立clientCredentialsTokenEndPointFilter核心過濾器
         */
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            security.allowFormAuthenticationForClients();
        }

        @Override
        /**
         * 配置oauth2的客戶端相關資訊
         */
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory().withClient("client1")
                    .resourceIds(DEMO_RESOURCE_ID)
                    //客戶端模式
                    .authorizedGrantTypes("client_credentials","refresh_token")
                    .scopes("select")
                    .authorities("ROLE_CLIENT")
                    //系統預設只接受加密的密碼
                    .secret(passwordEncoder.encode("123456"))
                    .and().withClient("cilent2")
                    .resourceIds(DEMO_RESOURCE_ID)
                    //password模式
                    .authorizedGrantTypes("password","refresh_token")
                    .scopes("select")
                    .authorities("client")
                    //a62d747e-91fb-42c5-8857-b47243179ecd
                    .secret(passwordEncoder.encode("123456"));
        }

        @Override
        /**
         * 配置AuthorizationServerEndpointsConfigurer眾多相關類,包括配置身份認證器,配置認證方式,TokenStore,TokenGranter,OAuth2RequestFactory
         */
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
                    .authenticationManager(authenticationManager);
        }

    }
}

兩個資源請求路徑的測試controller

@RestController
public class TestEndPoint {
    @GetMapping("/getProductInfo/{id}")
    public  String getProductInfo(@PathVariable String id){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        System.out.println(authentication.getDetails());
        System.out.println(authentication.getPrincipal());
        return  "product id:" +id;
    }

    @GetMapping("/getOrderInfo/{id}")
    public  String getOrderInfo(@PathVariable String id){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        System.out.println(authentication.getDetails());
        System.out.println(authentication.getPrincipal());
        return  "order id:" +id;
    }
}

核心說明

1.請求進入ClientCredentialsTokenEndpointFilter中的doFilter方法,跑到attemptAuthentication()開始進行認證

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
   ***********
   Authentication authResult;
   
      //開始認證
      authResult = attemptAuthentication(request, response);
      if (authResult == null) {
         // return immediately as subclass has indicated that it hasn't completed
         // authentication
         return;
      }
      sessionStrategy.onAuthentication(authResult, request, response);
   ******************
}

2.獲取使用者傳入的認證伺服器的client_id和client_secret 組裝成UsernamePasswordAuthenticationToken,傳入AuthenticationManager(預設是providerManager)進行認證.

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
      throws AuthenticationException, IOException, ServletException {

   **************************************

   String clientId = request.getParameter("client_id");
   String clientSecret = request.getParameter("client_secret");
  **************************************
   UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
         clientSecret);

   return this.getAuthenticationManager().authenticate(authRequest);

}

3.providerManager迴圈呼叫註冊的provider進行認證,一般用的是daoAutheticationProvider

public Authentication authenticate(Authentication authentication)
      throws AuthenticationException {
   Class<? extends Authentication> toTest = authentication.getClass();
   AuthenticationException lastException = null;
   Authentication result = null;
   boolean debug = logger.isDebugEnabled();

   for (AuthenticationProvider provider : getProviders()) {
      ***************************

      try {
         //呼叫provider進行認證
         result = provider.authenticate(authentication);

         if (result != null) {
            copyDetails(authentication, result);
            break;
         }
      }
     ******************************

   throw lastException;
}

4.daoAuthenticationProvider會呼叫userService獲取資料庫或記憶體中的使用者資訊,這裡我們是存在記憶體中的.

public Authentication authenticate(Authentication authentication)
      throws AuthenticationException {
   
   // Determine username
   String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
         : authentication.getName();

   boolean cacheWasUsed = true;
   //從快取中獲取使用者資訊
   UserDetails user = this.userCache.getUserFromCache(username);

   if (user == null) {
      cacheWasUsed = false;

      try {
        //準備呼叫userdetailService去獲取使用者資訊
         user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
      }
      
}
protected final UserDetails retrieveUser(String username,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
   prepareTimingAttackProtection();
   try {
     //呼叫userService去獲取使用者資訊(這裡是指認證伺服器的資訊)
      UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
      if (loadedUser == null) {
         throw new InternalAuthenticationServiceException(
               "UserDetailsService returned null, which is an interface contract violation");
      }
      return loadedUser;
   }
************************
}

5.provider拿到使用者輸入的伺服器的賬號密碼的token以及儲存在資料庫或者記憶體中的賬號密碼資訊之後便在additionalAuthenticationChecks中開始認證(主要是呼叫加密器進行密碼匹配),如果沒有丟擲異常則算成功

protected void additionalAuthenticationChecks(UserDetails userDetails,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
   if (authentication.getCredentials() == null) {
      logger.debug("Authentication failed: no credentials provided");

      throw new BadCredentialsException(messages.getMessage(
            "AbstractUserDetailsAuthenticationProvider.badCredentials",
            "Bad credentials"));
   }

   String presentedPassword = authentication.getCredentials().toString();

   if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
      logger.debug("Authentication failed: password does not match stored value");

      throw new BadCredentialsException(messages.getMessage(
            "AbstractUserDetailsAuthenticationProvider.badCredentials",
            "Bad credentials"));
   }
}

6.認證成功繼續往後走,provider將會將userDetail裡的許可權塞入token,返回給providermanager

public Authentication authenticate(Authentication authentication)
      throws AuthenticationException {
  
      try {
        //載入userDetail
         user = retrieveUser(username,
               (UsernamePasswordAuthenticationToken) authentication);
      }
     ***********************
   try {
     //賬號認證前檢查,是否可用,是否過期,是否鎖住
      preAuthenticationChecks.check(user);
     //賬號認證
      additionalAuthenticationChecks(user,
            (UsernamePasswordAuthenticationToken) authentication);
   }
   *************
   //賬號認證後檢查,是否過期
   postAuthenticationChecks.check(user);

   if (!cacheWasUsed) {
      this.userCache.putUserInCache(user);
   }

   Object principalToReturn = user;

   if (forcePrincipalAsString) {
      principalToReturn = user.getUsername();
   }
    //建立塞入許可權的token並返回給providerManager
   return createSuccessAuthentication(principalToReturn, authentication, user);
}

7.providerManager拿到token之後將會移除密碼,來保證系統安全,然後返回ClientCredentialsTokenEndpointFilter

try {
   //認證
   result = provider.authenticate(authentication);

   if (result != null) {
      //移除token的密碼
      copyDetails(authentication, result);
      break;
   }
}

8.認證成功後,進入successfulAuthetication方法

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
  *******************
   Authentication authResult;

   try {
      authResult = attemptAuthentication(request, response);
      if (authResult == null) {
         // return immediately as subclass has indicated that it hasn't completed
         // authentication
         return;
      }
      sessionStrategy.onAuthentication(authResult, request, response);
   }
  ****************
   successfulAuthentication(request, response, chain, authResult);
}
protected void successfulAuthentication(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain, Authentication authResult)
      throws IOException, ServletException {
***************************

   successHandler.onAuthenticationSuccess(request, response, authResult);
}

9.認證成功之後,請求開始走如同我們一般請求的流程,進入dispatchServlet,dispatcher處理我們請求之後,通過/oauth/token對映到TokenEndPoint類

//這個mapping很關鍵

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {

   if (!(principal instanceof Authentication)) {
      throw new InsufficientAuthenticationException(
            "There is no client authentication. Try adding an appropriate authentication filter.");
   }
  //獲取伺服器賬號
   String clientId = getClientId(principal);
   //通過賬號獲取伺服器資訊,賬號,密碼,許可權,域等
   ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
   //獲取tokenRequest物件,主要包含了伺服器i資訊的賬號密碼 許可權,認證方式,以及請求引數
   TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

  *****************************
  //進入tokenGranter準備辦法accessToken
   OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
   if (token == null) {
      throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
   }

   return getResponse(token);

}

10.請求進入tokenGranter

public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

   if (!this.grantType.equals(grantType)) {
      return null;
   }
   
   String clientId = tokenRequest.getClientId();
   //獲取認證伺服器資訊
   ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
   validateGrantType(grantType, client);

   if (logger.isDebugEnabled()) {
      logger.debug("Getting access token for: " + clientId);
   }

   return getAccessToken(client, tokenRequest);

}
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
   return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}

11.實際頒發者是defaulTokenService,這個類負責重新整理,新增,移除accessToken等一切相關accessToken的相關操作

@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {

   OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
   OAuth2RefreshToken refreshToken = null;
   if (existingAccessToken != null) {
      if (existingAccessToken.isExpired()) {
         if (existingAccessToken.getRefreshToken() != null) {
            refreshToken = existingAccessToken.getRefreshToken();
            // The token store could remove the refresh token when the
            // access token is removed, but we want to
            // be sure...
            tokenStore.removeRefreshToken(refreshToken);
         }
         tokenStore.removeAccessToken(existingAccessToken);
      }
      else {
         // Re-store the access token in case the authentication has changed
         tokenStore.storeAccessToken(existingAccessToken, authentication);
         return existingAccessToken;
      }
   }
   if (refreshToken == null) {
      refreshToken = createRefreshToken(authentication);
   }
   // But the refresh token itself might need to be re-issued if it has
   // expired.
   else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
      ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
      if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
         refreshToken = createRefreshToken(authentication);
      }
   }
   //建立accessToken
   OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
   tokenStore.storeAccessToken(accessToken, authentication);
   ****************
   return accessToken;

}
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
  //我們收到的accessToken就是這個類序列化後的樣子
   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());

   return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}

12 返回dispatchServlet中的doFilter,認證流程結束

後記

有空再分析一下資源請求的原始碼,寫部落格還是很費時間的..................