Spring boot + Spring security OAuth2简介

耿玄裳
2023-12-01

Spring boot + Spring security OAuth2简介

之前的文章简单介绍了Spring MVC + Spring security OAuth2的简单配置方法,主要以XML进行配置。这一篇将使用Spring boot做一个更全面的说明,以Java和yml来进行配置~

虽然配置方式与之前相比有些不同,但是运作流程还是一样,依然是Filter chain进行各种顾虑和验证操作~

1、验证服务器(AuthorizationServer)

OAuth2验证服务器的主要功能包括:

1、验证用户的username和password;

2、验证客户端的client_id和secret;

3、为通过验证的用户或者客户端颁发code或者token;

4、验证客户端提供的token是否合法有效;

5、根据有效的token提供对应的用户或者客户端详细信息;

  • 添加依赖
plugins {
    id 'java'
}

group 'com.chenlei'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.0.3.RELEASE'
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.0.3.RELEASE'
    compile group: 'org.springframework.security.oauth.boot', name: 'spring-security-oauth2-autoconfigure', version: '2.0.1.RELEASE'

    testCompile group: 'junit', name: 'junit', version: '4.12'
}

spring-security-oauth2-autoconfigure将为我们省去很多的配置事项

  • 启用AuthorizationServer
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

}

通过 @EnableAuthorizationServer 来启用AuthorizationServer的默认实现,以提供/oauth/token、/oauth/check_token、/oauth/authorize等endpoint

  • 配置User
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("admin")).authorities("IS_AUTHENTICATED_FULLY");
    }

}

在authorization_code模式下,通过Spring security来进行用户认证,SSO将通过此模式实现

  • 配置Client
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory().withClient("client-1")
            .secret(this.passwordEncoder.encode("client"))
            .authorizedGrantTypes("authorization_code")
            .scopes("read")
            .redirectUris("http://localhost:8081")
            .autoApprove(true)
            .and()
            .withClient("client-2")
            .secret(this.passwordEncoder.encode("client"))
            .authorizedGrantTypes("authorization_code")
            .scopes("read")
            .redirectUris("http://localhost:8082")
            .autoApprove(true)
            .and()
            .withClient("client-3")
            .secret(this.passwordEncoder.encode("client"))
            .authorizedGrantTypes("client_credentials")
            .scopes("rea.*")
            .and()
            .withClient("client-4")
            .secret(this.passwordEncoder.encode("client"))
            .authorizedGrantTypes("client_credentials")
            .scopes("read")
            .and()
            .withClient("client-5")
            .secret(this.passwordEncoder.encode("client"))
            .authorizedGrantTypes("client_credentials")
            .scopes("read");
}

这里先配置五个不同类型不同权限的客户端,客户端可能是资源服务器,也可能就是普通的应用程序

  • 配置UserInfo endpoint
@RestController
public class UserInfoController {

    @RequestMapping("/oauth/me")
    public Principal userInfo (Principal principal) {
        return principal;
    }

}

一个普通的REST API,为客户端提供用户详细信息以判断用户是否登陆、是否有权限访问资源

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.requestMatchers().antMatchers("/oauth/me")
                .and().authorizeRequests().anyRequest().access("#oauth2.hasScope('read')")
                .and().anonymous().disable();
    }

}

/oauth/me本身也是一种资源,只有提供合法token的client才能访问/oauth/me

  • 配置Spring security的默认登陆表单,所有请求都需要认证
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest().authenticated()  // 所有请求都需要授权
            .and().formLogin()  // 使用默认的登陆界面
            .and().anonymous().disable();
}

2、资源服务器——SSO客户端(authorization_code)

SSO的基本流程:

  • 启动SSO客户端,自动注册一个OAuth Filter chain,匹配登陆URL(/login)
  • 访问资源受保护的资源,FilterSecurityInterceptor拦截后跳转到资源服务器登陆URL(/login)
  • 登陆URL(/login)匹配到OAuth Filter chain使用OAuth2ClientAuthenticationProcessingFilter进行权限认证
  • OAuth2ClientAuthenticationProcessingFilter发现没有token,而且grantType为authorization_code,所以跳转到验证服务器的/oauth/authorize获取token
  • 浏览器访问验证服务器的/oauth/authorize,验证服务器拦截请求之后跳转到验证服务器到登陆URL(/login)
  • 用户完成登陆之后,携带code跳转回资源服务器
  • 资源服务器根据code再次访问验证服务器获得token
  • 然后根据token访问认证服务器/oauth/me获得用户信息,完成登陆流程

SSO客户端配置:

server:
  port: 8081
  servlet:
    context-path: /client-1
security:
  oauth2:
    client:
      client-id: client-1
      client-secret: client
      access-token-uri: http://localhost:8080/server/oauth/token
      user-authorization-uri: http://localhost:8080/server/oauth/authorize
    resource:
      user-info-uri: http://localhost:8080/server/oauth/me
logging:
  level:
    org.springframework.web: DEBUG
    org.springframework.security: DEBUG
@Configuration
@EnableOAuth2Sso
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated();  // 所有请求都需要验证
    }

}

@EnableOAuth2Sso启用SSO客户端配置,即拦截/login的filter chain

3、资源服务器——客户端模式(client_credentials)

第三方应用访问资源服务器受保护资源时需要提供自身的token、资源服务器根据自己的client_id和secret登陆认证服务器并验证第三方应用token的token是否有效,从而确定是否在资源服务器保存第三方应用的认证信息

  • 资源服务器配置信息:
server:
  port: 8082
  servlet:
    context-path: /client-2
security:
  oauth2:
    client:
      client-id: client-2
      client-secret: client
      access-token-uri: http://localhost:8080/server/oauth/token
      user-authorization-uri: http://localhost:8080/server/oauth/authorize
    resource:
      user-info-uri: http://localhost:8080/server/oauth/me
    authorization:
      check-token-access: http://localhost:8080/server/oauth/check_token
logging:
  level:
    org.springframework.web: DEBUG
    org.springframework.security: DEBUG
  • 配置RemoteTokenServices验证token、配置资源访问所需权限
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
    remoteTokenServices.setCheckTokenEndpointUrl(this.getAuthorizationServerProperties().getCheckTokenAccess());
    remoteTokenServices.setClientId(this.oAuth2ClientProperties.getClientId());
    remoteTokenServices.setClientSecret(this.oAuth2ClientProperties.getClientSecret());
    resources.tokenServices(remoteTokenServices);
}
  • 配置所有资源访问均需要对应的scope
@Override
public void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest().access("#oauth2.hasScope('read')")
            .and().anonymous().disable();
}

4、访问资源服务器

第三方应用程序通过oAuth2RestTemplate访问资源服务器通过,oAuth2RestTemplate需要绑定client_id、secret以及认证服务器信息,从而获得对应的token(权限)

  • 客户端配置信息
server:
  port: 8083
  servlet:
    context-path: /client-3
security:
  oauth2:
    client:
      client-id: client-3
      client-secret: client
      access-token-uri: http://localhost:8080/server/oauth/token
      user-authorization-uri: http://localhost:8080/server/oauth/authorize
      grant-type: client_credentials
logging:
  level:
    org.springframework.web: DEBUG
    org.springframework.security: DEBUG
    com.chenlei: DEBUG
  • 配置oAuth2RestTemplate对应的Bean
@Configuration
public class AuthorizationClientConfig {

    @Bean
    @ConfigurationProperties(prefix = "security.oauth2.client")
    public ClientCredentialsResourceDetails getClientCredentialsResourceDetails() {
        return new ClientCredentialsResourceDetails();
    }

    @Bean
    public OAuth2RestOperations oAuth2RestOperations() {
        return new OAuth2RestTemplate(getClientCredentialsResourceDetails());
    }

}
  • 使用oAuth2RestTemplate访问资源服务器
@RestController
@RequestMapping("/resource")
public class ResourceController {

    private final Log logger = LogFactory.getLog(getClass());

    @Autowired
    private OAuth2RestOperations oAuth2RestTemplate;

    @RequestMapping("/")
    public String getResource() {
        logger.debug("I am client-3 and I will call client-4...");
        logger.debug("The client-4 say: " + oAuth2RestTemplate.getForObject("http://localhost:8084/client-4/resource/", String.class));
        logger.debug("The client-4 say: " + oAuth2RestTemplate.getForObject("http://localhost:8084/client-4/resource/web", String.class));
        logger.debug("The client-4 say: " + oAuth2RestTemplate.getForObject("http://localhost:8084/client-4/resource/method", String.class));
        logger.debug("The client-4 say: " + oAuth2RestTemplate.getForObject("http://localhost:8084/client-4/resource/cse/web", String.class));
        logger.debug("The client-4 say: " + oAuth2RestTemplate.getForObject("http://localhost:8084/client-4/resource/cse/method", String.class));
        return "I am client-3!!!";
    }

    @RequestMapping("/call/2")
    public String callClient_2() {
        logger.debug("I am client-3 and I will call client-2...");
        logger.debug("The client-2 say: " + oAuth2RestTemplate.getForObject("http://localhost:8082/client-2/resource/", String.class));
        return "I am client-3!!!";
    }

}

5、扩展或者自定义鉴权表达式

OAuth默认的OAuth2SecurityExpressionMethods提供一些常用的鉴权方法,特殊情况下我们需要扩展自己的鉴权方法,或者直接调用自己的外部方法。

Spring security提供两种方式配置权限拦截,一种是拦截URL并配置访问权限,称之为WebSecurity,另一种是在方法上加注解,控制方法访问权限,称之为MethodSecurity

  • 配置WebSecurity
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {

        RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
        remoteTokenServices.setCheckTokenEndpointUrl(this.getAuthorizationServerProperties().getCheckTokenAccess());
        remoteTokenServices.setClientId(this.oAuth2ClientProperties.getClientId());
        remoteTokenServices.setClientSecret(this.oAuth2ClientProperties.getClientSecret());

        ExtendOAuth2WebSecurityExpressionHandler extendOAuth2WebSecurityExpressionHandler = new ExtendOAuth2WebSecurityExpressionHandler();
        // 往ExpressionHandler中设置ApplicationContext,否则在鉴权时无法通过@调用CustomSecurityExpression,比如:@cse.permitAll(authentication, 'customSecurityExpression')
        extendOAuth2WebSecurityExpressionHandler.setApplicationContext(this.applicationContext);

        resources.tokenServices(remoteTokenServices).expressionHandler(extendOAuth2WebSecurityExpressionHandler);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/resource/web/**").access("#oauth2.hasMatchingScope('read')")  // 使用扩展的表达式鉴权
                .antMatchers("/resource/cse/web/**").access("@cse.permitAll(authentication, 'customSecurityExpression')")  // 使用自定义表达式鉴权
                .and().anonymous().disable();
    }
  • 配置MethodSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class GlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    @Autowired
    private ApplicationContext applicationContext;

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        ExtendOAuth2MethodSecurityExpressionHandler extendOAuth2MethodSecurityExpressionHandler = new ExtendOAuth2MethodSecurityExpressionHandler();
        // 往ExpressionHandler中设置ApplicationContext,否则在鉴权时无法通过@调用CustomSecurityExpression,比如:@cse.permitAll(authentication, 'customSecurityExpression')
        extendOAuth2MethodSecurityExpressionHandler.setApplicationContext(this.applicationContext);

        return extendOAuth2MethodSecurityExpressionHandler;
    }

}

6、启用JWT

Json web token是认证服务器使用私钥将用户详细信息签名加密参数的token,客户端可以访问认证服务器提供的endpoint获得对应的公钥,然后用公钥将jwt解密,从而获得对应的用户信息,省去了客户端频繁用token换取用户详细信息的过程,从而提高效率,减轻认证服务器的负担。

  • 生成密钥库文件,文件中包含一组公钥和私钥
LiondeMBP:tmp lion$ keytool -genkeypair -alias jwt -keyalg RSA -dname "CN=jwt, L=Wuhan, S=Hubei, C=CN" -keypass awesomePass -keystore jwt.jks -storepass awesomePass
  • 配置密钥对提取密码并配置AuthorizationServer支持JWT
server:
  port: 8080
  servlet:
    context-path: /server
logging:
  level:
    org.springframework.web: DEBUG
    org.springframework.security: DEBUG
    org.springframework.boot.autoconfigure: DEBUG
keystore:
  password: awesomePass
@Bean
public TokenStore tokenStore() {
    return new JwtTokenStore(jwtTokenEnhancer());
}

@Bean
public JwtAccessTokenConverter jwtTokenEnhancer() {
    String password = this.environment.getProperty("keystore.password");
    KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), password.toCharArray());
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
    return converter;
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints.tokenStore(this.tokenStore())
            .tokenEnhancer(jwtTokenEnhancer())
            .userApprovalHandler(this.userApprovalHandler());
}

7、事例源码

https://github.com/chenlein/springboot-oauth

 类似资料: