欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

spring security 整合 oauth2

程序员文章站 2022-06-13 15:46:46
...

前言

之前一篇博客学过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);

}

spring security 整合 oauth2

spring security 整合 oauth2

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,认证流程结束

 

后记

有空再分析一下资源请求的源码,写博客还是很费时间的..................