Spring Security OAuth专题学习-密码模式JWT实现

本文是Spring Security OAuth2学习系列文章中的第三篇;主要讲解密码模式下如何使用JWT管理Token。

关于密码模式非JWT的实现示例及Spring Security OAuth2的一些基础知识,请移步本博客文章清单进行查看。

1. JWT是什么

JWT是JSON Web Token的缩写。它与标准Token不一样的地方在于,在Token中它附加存储了很多额外的信息,如Token的失效时间,Token所拥有的权限等。

在了解为什么要有JWT及什么时候使用JWT前,我们先来看下非JWT模式下资源服务器如何校验Token的有效性;

  • 资源服务器收到带Token的请求;
  • 资源服务器将Token发送到授权服务器/oauth/check_token接口
  • 授权服务器校验Token有效性,并将Token的权限信息(如有权访问的资源清单、对资源清单能够进行的操作等)返回给资源服务器;
  • 资源服务器根据返回的信息进行处理:
    • Token无效,返回相应错误信息给调用方;
    • Token有效,但无权限访问想要访问的资源,返回相应错误给调用方;
    • Token有效,且有权限访问想要访问的资源,访问资源后返回成功给调用方;

注意到这种模式下,每次有请求时,资源服务器都要进行这样一个处理,这大大增加了资源服务器、授权服务器负担及网络开销!

那么有没有一种方式能够减少这个校验请求的次数呢?这就是JWT要解决的问题。

它的原理就是在Token中附带了权限校验需要的信息;那么在资源服务器中进行权限验证的时候就不需要访问授权服务器获取Token所对应的权限信息了。

但这又引入了另外一个问题,如何保证Token本身是有效的而不是被伪造的?JWT通过加密的方式来保证Token的有效性,可以选择多种加密方式,其中非对称方式使用的比较多:授权服务器通过私钥加密Token;然后对外暴露公钥的获取接口;资源服务器获取公钥后使用其来对Token进行解密。由于公钥基本不会变化,因此请求到的公钥资源服务器就可以缓存到本地服务中。

通过这样一系列处理,JWT校验Token时不需要再每次都请求授权服务器进行校验,相当于将校验过程从授权服务器中下沉到了资源服务器中。

通过https://jwt.io/可以解析某个Token,直接看到其中所包含的信息,如token:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidGVzdFJlc291cmNlIl0sInVzZXJfbmFtZSI6InRlc3QiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNTUwODU3NjA4LCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiYTg5MjdmNTItMDFlNi00NjFlLWJjNjEtOGNiZjYwODFjYmZlIiwiY2xpZW50X2lkIjoidGVzdCJ9.NXlLTSkLpajmYm6R66UYRQ81n6Z3J-wE4bTDf7sMqGU

解析到的信息为:

{
  "aud": [
    "testResource"
  ],
  "user_name": "test",
  "scope": [
    "read",
    "write"
  ],
  "exp": 1550857608,
  "authorities": [
    "ROLE_USER"
  ],
  "jti": "a8927f52-01e6-461e-bc61-8cbf6081cbfe",
  "client_id": "test"
}

如果想要向Token中增加更多自定义信息,可以通过TokenEnhancer接口实现。

2 实现

本节使用一个Spring Boot的示例来说明JWT的配置及使用。

2.1 MVN


    org.springframework.boot
    spring-boot-starter



    org.springframework.security.oauth
    spring-security-oauth2
    2.3.4.RELEASE



    org.springframework.boot
    spring-boot-starter-web
    2.1.2.RELEASE



    org.springframework.security
    spring-security-jwt
    1.0.9.RELEASE



    org.springframework.boot
    spring-boot-starter-security



    org.springframework.boot
    spring-boot-starter-test
    test

2.2 授权服务器

2.2.1 Security普通配置:

@Configuration
@EnableWebSecurity
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("test").password("test").roles("USER")
                .and().passwordEncoder(passwordEncoder());
    }

    @Bean
    public static NoOpPasswordEncoder passwordEncoder() {
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                    .anyRequest().authenticated()
                .and().formLogin().permitAll();
    }
}

2.2.2 授权服务器专用配置

@Configuration
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
    @Resource
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 配置客户端信息
        clients.inMemory()
                .withClient("test")
                .resourceIds("testResource")
                .authorizedGrantTypes("password")
                .authorities("ROLE_CLIENT")
                .scopes("read", "write")
                .secret("secret")
                .redirectUris("http://localhost:8080");
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()") // 如果不配置,那么未授权的Token获取请求将无法成功获取Token
                .allowFormAuthenticationForClients();
    }

    @Bean
    public DefaultTokenServices tokenServices() {
        // 配置TokenServices,实际项目中可以使用其它的JdbcClientTokenServices等进行持久化
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(getTokenStore());      // 配置Token存储方式
        tokenServices.setTokenEnhancer(tokenConverter());  // 配置Token转换器 

        return tokenServices;
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .tokenServices(tokenServices());
    }

    public TokenStore getTokenStore() {
        return new JwtTokenStore(tokenConverter());
    }

    public JwtAccessTokenConverter tokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        // 配置Token加密Key,注意要与客户端保持一致; 也可以配置成RSA方式;
        converter.setSigningKey("7Ub2aV");
        return converter;
    }
}

2.3 资源服务器

@Configuration
public class OAuth2Configurer extends ResourceServerConfigurerAdapter {
    @Bean
    public ResourceServerTokenServices tokenServices() {
        // 与授权服务器保持一致
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(getTokenStore());
        tokenServices.setTokenEnhancer(tokenConverter());

        return tokenServices;
    }

    public TokenStore getTokenStore() {
        return new JwtTokenStore(tokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter tokenConverter() {
        // 与服务器端保持一致,注意这里需要将TokenConverter注入到容器中去,
        // 否则会报错:InvalidTokenException, Cannot convert access token to JSON
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("7Ub2aV");
        return converter;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    	// 配置访问的资源
        resources.resourceId("testResource")
                .tokenServices(tokenServices());
        super.configure(resources);
    }
}

2.4 测试

2.4.1 curl测试

  • 获取Token:

    curl -X POST -d grant_type=password -d username=test -d password=test http://test:secret@localhost:8080/oauth/token
    // 或者
    curl -X POST -d id=test -d client_id=test -d client_secret=secret -d scope=read -d grant_type=password -d username=test -d password=test http://localhost:8080/oauth/token
    

    返回结果:

    {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidGVzdFJlc291cmNlIl0sInVzZXJfbmFtZSI6InRlc3QiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNTUwODU3NjA4LCJhdXRob3JpdGllcyI6WyJST0x
         FX1VTRVIiXSwianRpIjoiYTg5MjdmNTItMDFlNi00NjFlLWJjNjEtOGNiZjYwODFjYmZlIiwiY2xpZW50X2lkIjoidGVzdCJ9.NXlLTSkLpajmYm6R66UYRQ81n6Z3J-wE4bTDf7sMqGU","token_type":"bearer","expires_in":43200,"scope":"
         read write","jti":"a8927f52-01e6-461e-bc61-8cbf6081cbfe"}
    
  • 访问资源:

    curl -X GET -H "Authorization:BearereyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidGVzdFJlc291cmNlIl0sInVzZXJfbmFtZSI6InRlc3QiLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU1MDg2Nzc2NCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6ImExYmJmMzE1LWFlMzYtNGY0Zi04MmEwLTQxMzkyZTU5NDAyNyIsImNsaWVudF9pZCI6InRlc3QifQ.RENWXpzBPoPH8S_WRaArFHpVs2XOBHi-TN2YMQNm13M" http://localhost:8081/test
    

2.4.2 Java代码测试

ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails();
details.setId("testResource");
details.setClientId("test");
details.setClientSecret("secret");
details.setScope(Arrays.asList("read", "write"));
details.setGrantType("password");
details.setAccessTokenUri("http://localhost:8080/oauth/token");
details.setUsername("test");
details.setPassword("test");
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(details);

AccessTokenProviderChain provider = new AccessTokenProviderChain(Collections.singletonList(new ResourceOwnerPasswordAccessTokenProvider()));
restTemplate.setAccessTokenProvider(provider);

ResponseEntity<String> responseEntity = restTemplate.getForEntity(
        URI.create("http://localhost:8080/test"), String.class);
System.out.println(responseEntity);

3 JWT模式总结

  • 资源服务器与授权服务器要配置同样的TokenService、TokenConverter、TokenStore;
  • 资源服务器与授权服务器加解密配置要保持一致,如都使用对称方式或者非对称方式进行加密;对称方式下两边密钥需要保持一致。
  • 特别注意在资源服务器中TokenConverter需要注入到容器中去,否则在解析Token时会报错;

下一文中将研究授权码模式的实现。

你可能感兴趣的:(Spring,Spring,Security,OAuth2应用示例)