Spring Security5 Oauth2 Client认证

Spring Security5 Oauth2 Client认证

hb0730 86 2020-10-16

Spring Security5之Oauth2 Client认证

本项目采用spring-boot的方式:spring-boot版本为:2.3.3.RELEASE

源码地址: oauth2-login-multivariate

博客地址: https://blog.hb0730.com

1. 搭建授权服务器Authorization Server

Authorization Server采用的还是spring Security oauth提供的

提供端口为:8081

pom.xml

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

Config配置

  • AuthorizationServerConfiguration

主要提供了两个客户端 一个采用security5 Oauth2Client方式访问,一个通过传统的spring security Oauth SSO方式访问

采用的html方式进行登录,client 登录默认使用授权码方式,

Security5 Oauth2Login登录核心关注redirectUris重定向的路径问题

项目目录:

/**
 * 授权服务器配置
 *
 * @author bing_huang
 */
@Configuration
@EnableAuthorizationServer
@RequiredArgsConstructor
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    private final PasswordEncoder passwordEncoder;
    private final UserDetailsService userDetailsService;
    /**
     * 来支持 password grant type
     */
    private final AuthenticationManager authenticationManager;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 允许客户端认证ClientCredentialsTokenEndpointFilter
        security.allowFormAuthenticationForClients()
                .checkTokenAccess("isAuthenticated()")
                .tokenKeyAccess("permitAll()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 内存模式
        clients.inMemory()
                //客户端id
                .withClient("client1")
                // 客户端secret
                .secret(passwordEncoder.encode("secret"))
                //范围
                .scopes("all")
                //权限
                .authorities("read", "writer")
                //授权类型
                .authorizedGrantTypes("password", "refresh_token", "authorization_code")
                .redirectUris("http://localhost:8085/client1")

                .and()
                .withClient("client2")
                .secret(passwordEncoder.encode("secret"))
                .scopes("all")
                .authorities("read", "writer")
                .authorizedGrantTypes("password", "refresh_token", "authorization_code")
                .redirectUris("http://localhost:8086/login/oauth2/code/client2");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 允许get,post请求token端点
        endpoints.allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET)
                // token 存储服务
                .tokenServices(defaultTokenServices())
                //user 服务
                .userDetailsService(userDetailsService)
                //password grant type
                .authenticationManager(authenticationManager);
    }

    /**
     * <p>注意,自定义TokenServices的时候,需要设置@Primary,否则报错,</p>
     * 自定义的token
     * 认证的token是存到redis里的
     *
     * @return DefaultTokenServices
     */
    @Primary
    @Bean
    public DefaultTokenServices defaultTokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore());
        tokenServices.setSupportRefreshToken(true);
        return tokenServices;
    }

    /**
     * redis存储令牌
     *
     * @return token存储
     */
    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

}
  • webSecurityConfig
/**
 * web Security
 *
 * @author bing_huang
 */
@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(this.userDetailsService())
                .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().loginPage("/login")
                .and()
                .authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf().disable().cors();
        // 这里并没有禁用session,page 跳转使用的session
        ;

    }

    @Override
    @Bean
    protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(
                User.withUsername("user")
                        .password(passwordEncoder().encode("123456"))
                        .authorities("ROLE_USER").build());
        return userDetailsManager;
    }

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

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

Controller

主要就是返回视图

/**
 * @author bing_huang
 */
@Controller
@RequestMapping
public class LoginController {

    @GetMapping("/login")
    public String login() {
        return "login";
    }

}

Spring Security Oauth之SSO

项目目录:

端口: 8085

pom.xml

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-resource-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

Authorication-server多出一个spring-security-oauth2-autoconfigure

config

  • WebSecurityConfig

    /**
    * @author bing_huang
    */
    @Configuration
    @EnableOAuth2Sso
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
      @Override
      protected void configure(HttpSecurity http) throws Exception {
          http.logout().logoutSuccessUrl("http://localhost:8081/logout");
          http.authorizeRequests().anyRequest().authenticated();
          http.csrf().disable();
      }
    }
    

yaml配置

server:
  port: 8085
  servlet:
    session:
      cookie:
        # Possible CSRF detected - state parameter was required but no state could be
        name: resource1
security:
  oauth2:
    client:
      client-id: client1
      client-secret: secret
      access-token-uri: http://localhost:8081/oauth/token
      user-authorization-uri: http://localhost:8081/oauth/authorize
      pre-established-redirect-uri: http://localhost:8085/client1
      use-current-uri: false
    resource:
      token-info-uri: http://localhost:8081/oauth/check_token
    sso:
      login-path: /client1

logging:
  level:
    root: WARN
    org.springframework.web: DEBUG
    org.springframework.security: DEBUG

说明:

  1. session,应该使用的同一个路径导致回冲掉前一个缓存信息,所以重命名cookie,异常信息 Possible CSRF detected - state parameter was required but no state could be
  2. 认证后响应路径问题,默认路径为/login,这里自定义路径

访问

当使用浏览器访问http://localhost:8085/ 时会自动跳转到 http://localhost:8081

然后输入用户名密码user/123456 登录授权,值得我们注意的是地址栏的变化

授权码模式登录的,授权完成后又会跳回 http://localhost:8085

这里就已经完成了sso登录,sso登录采用的是client授权码方式

如果我们使用 security5 推荐的方式呢

security-client

目录:

端口: 8086

config配置

  • WebSecurityConfig

    /**
     * @author bing_huang
     */
    @EnableWebSecurity
    @Configuration
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .csrf().disable()
                    .cors();
    
            http.oauth2Login();
            http.oauth2Client();
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    

yaml配置

server:
  port: 8086
  servlet:
    session:
      cookie:
        name: resource2
spring:
  security:
    oauth2:
      client:
        registration:
          client2:
            provider: client2
            client-id: client2
            client-secret: secret
            authorization-grant-type: authorization_code
            redirectUri: "{baseUrl}/login/oauth2/code/{registrationId}"
        provider:
          client2:
            authorization-uri: http://localhost:8081/oauth/authorize
            token-uri: http://localhost:8081/oauth/token

logging:
  level:
    root: WARN
    org.springframework.web: DEBUG
    org.springframework.security: DEBUG

访问

配置完成后启动项目访问 http://localhost:8086

地址栏: http://localhost:8081/oauth/authorize?response_type=code&client_id=client2&state=ym9ynGfFaLlMdFyJC1sBkakT9C2ttaUVwcAcdJt1BIY%3D&redirect_uri=http://localhost:8086/login/oauth2/code/client2

授权登录

发现有一个必要配置UserInfoEndpoint user_info_uri,按照sso方式我们发现还差一个http://localhost:8081/oauth/check_token,修改yaml

server:
  port: 8086
  servlet:
    session:
      cookie:
        name: resource2
spring:
  security:
    oauth2:
      client:
        registration:
          client2:
            provider: client2
            client-id: client2
            client-secret: secret
            authorization-grant-type: authorization_code
            redirectUri: "{baseUrl}/login/oauth2/code/{registrationId}"
        provider:
          client2:
            authorization-uri: http://localhost:8081/oauth/authorize
            token-uri: http://localhost:8081/oauth/token
            user-info-uri: http://localhost:8081/oauth/check_token

logging:
  level:
    root: WARN
    org.springframework.web: DEBUG
    org.springframework.security: DEBUG

再一次访问,发现还有有问题

还有一个user-name-attribute

server:
  port: 8086
  servlet:
    session:
      cookie:
        name: resource2
spring:
  security:
    oauth2:
      client:
        registration:
          client2:
            provider: client2
            client-id: client2
            client-secret: secret
            authorization-grant-type: authorization_code
            redirectUri: "{baseUrl}/login/oauth2/code/{registrationId}"
        provider:
          client2:
            authorization-uri: http://localhost:8081/oauth/authorize
            token-uri: http://localhost:8081/oauth/token
            user-info-uri: http://localhost:8081/oauth/check_token
            user-name-attribute: name

logging:
  level:
    root: WARN
    org.springframework.web: DEBUG
    org.springframework.security: DEBUG

访问发现是未经授权401

security5 client 是如何获取access_token的,是通过那个filter换取access_token

  1. 查看错误日志发现

发现其中的类: DefaultOAuth2UserService ,OAuth2LoginAuthenticationProvider ,OAuth2LoginAuthenticationFilter
OAuth2AuthorizationRequestRedirectFilter,

  1. 查看 DefaultOAuth2UserService#loadUser, OAuth2UserRequestEntityConverter使用的是BearerAuth,而只有是资源服务时如SSo EnableResourceServer启用的是资源服务以及securiy5 Resourcesspringsecurity5之resourceserver资源服务器,才是通过BearerAuthen认证其余都是Basic认证

补充

  • BearerAuth -----> BearerTokenAuthenticationFilter resource-server
  • BasicAuth ------> BasicAuthenticationFilter security-web (任何)

Security5 resources server

springsecurity5之resourceserver资源服务器

新增一个controller 用于 user-info-uri

/**
 * 为了支持oauth2Login(), 为何不用oauth/check_token ,
 * /oauth/check_token 是basic认证
 *
 * @author bing_huang
 */
@RestController
public class UserController {
    @RequestMapping("/user/me")
    public Map<String, Object> user(Principal principal) {
        // 为何转为map
        // 当security5 Client请求过来时流程
        // Oauth2LoginAuthenticationFilter#attemptAuthentication 184
        // OAuth2LoginAuthenticationProvider#authenticate 110
        // DefaultOAuth2Service#loadUser
        // DefaultOAuth2User
        BearerTokenAuthentication authentication = (BearerTokenAuthentication) SecurityContextHolder.getContext().getAuthentication();
        Map<String, Object> map = new HashMap<>();
        map.put("authorities", authentication.getAuthorities());
        map.put("details", authentication.getDetails());
        map.put("authenticated", authentication.isAuthenticated());
        map.put("principal", authentication.getPrincipal());
        map.put("credentials", authentication.getCredentials());
        map.put("token", authentication.getToken());
        map.put("tokenAttributes", authentication.getTokenAttributes());
        map.put("name", authentication.getTokenAttributes().get("user_name"));
        return map;
    }
}

最终

Security5 client

server:
  port: 8086
  servlet:
    session:
      cookie:
        name: resource2
spring:
  security:
    oauth2:
      client:
        registration:
          client2:
            provider: client2
            client-id: client2
            client-secret: secret
            authorization-grant-type: authorization_code
            redirectUri: "{baseUrl}/login/oauth2/code/{registrationId}"
        provider:
          client2:
            authorization-uri: http://localhost:8081/oauth/authorize
            token-uri: http://localhost:8081/oauth/token
#            user-info-uri: http://localhost:8081/oauth/check_token
            # 为何不用/oauth/check_token ,原因是/oauth/check_token是basic认证
            # DefaultOAuth2UserService#loadUser
            # Bearer 认证 Security5 resources BearerTokenAuthenticationFilter
            # Basic 认证 Security Basic BasicAuthenticationFilter
            user-info-uri: http://localhost:8082/user/me
            userInfoAuthenticationMethod: post
            user-name-attribute: name

logging:
  level:
    root: WARN
    org.springframework.web: DEBUG
    org.springframework.security: DEBUG

注意

  • user-name-attribute 对应在资源服务user-info-uriname
    具体可以查看源码OAuth2LoginAuthenticationFilter#attemptAuthentication第195行

Security5 Client5主要filter