SpringBoot

SpringSecurity OAuth2 自定义令牌配置(JW

2019-09-28  本文已影响0人  lconcise

目录:

Spring Security允许我们自定义令牌配置,比如不同的client_id对应不同的令牌,令牌的有效时间,令牌的存储策略等;我们也可以使用JWT来替换默认的令牌。

自定义令牌配置

我们让认证服务器AuthorizationServerConfig继承AuthorizationServerConfigurerAdapter,并重写它的configure(ClientDetailsServiceConfigurer clients)方法:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private TokenEnhancer tokenEnhancer;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailService);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("test1")
                .secret(new BCryptPasswordEncoder().encode("test1111"))
                .accessTokenValiditySeconds(3600)
                .refreshTokenValiditySeconds(864000)
                .scopes("all", "a", "b", "c")
                .authorizedGrantTypes("password")
                .and()
                .withClient("test2")
                .secret(new BCryptPasswordEncoder().encode("test2222"))
                .accessTokenValiditySeconds(7200);
    }
}

认证服务器在继承了AuthorizationServerConfigurerAdapter适配器后,需要重写configure(AuthorizationServerEndpointsConfigurer endpoints)方法,指定 AuthenticationManager和UserDetailService。

创建一个新的配置类SecurityConfig,在里面注册我们需要的AuthenticationManagerBean:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

此外,重写configure(ClientDetailsServiceConfigurer clients)方法主要配置了:

  1. 定义两个client_id,及客户端可以通过不同的client_id来获取不同的令牌;
  2. client_id为test1的令牌有效时间为3600秒,client_id为test2的令牌有效时间为7200秒;
  3. client_id为test1的refresh_token(下面会介绍到)有效时间为864000秒,即10天,也就是说在这10天内都可以通过refresh_token来换取新的令牌;
  4. 在获取client_id为test1的令牌的时候,scope只能指定为all,a,b或c中的某个值,否则将获取失败;
  5. 只能通过密码模式(password)来获取client_id为test1的令牌,而test2则无限制。

启动项目后使用密码模式获取test1的令牌:


image.png

和前面介绍的那样,头部需要传入test1:test1111经过base64加密后的值:


image.png
返回结果:
{
    "access_token": "3f9864cc-dab5-420a-b129-560f409922d6",
    "token_type": "bearer",
    "expires_in": 3599,
    "scope": "all"
}

使用JWT替换默认令牌

使用JWT替换默认的令牌(默认令牌使用UUID生成)只需要指定TokenStore为JwtTokenStore即可。

创建一个JWTokenConfig配置类:

@Configuration
public class JWTokenConfig {

    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
        accessTokenConverter.setSigningKey("test_key"); // 签名密钥
        return accessTokenConverter;
    }
}

签名密钥为test_key。在配置类里配置好JwtTokenStore后,我们在认证服务器里指定它:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailService userDetailService;
    @Autowired
    private TokenStore jwtTokenStore;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {

        endpoints.authenticationManager(authenticationManager)
                .tokenStore(jwtTokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .userDetailsService(userDetailService);
    }

...
}

重启服务获取令牌,系统将返回如下格式令牌:

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Njg3ODkzNTIsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJhZG1pbiJdLCJqdGkiOiI2NzhlZTc5NC1lYzhhLTQxOTYtYmE2NS0xM2QwNjRiOGEyZWUiLCJjbGllbnRfaWQiOiJ0ZXN0MSIsInNjb3BlIjpbImFsbCJdfQ.ojuPewlJ_3hWRwOrlCwcHFZaNIX_78FQwj86Inw79_w",
    "token_type": "bearer",
    "expires_in": 3599,
    "scope": "all",
    "jti": "678ee794-ec8a-4196-ba65-13d064b8a2ee"
}

access_token中的内容复制到https://jwt.io/网站解析下:

image.png

拓展JWT

上面的Token解析得到的PAYLOAD内容为:

{
  "exp": 1568789352,
  "user_name": "user",
  "authorities": [
    "admin"
  ],
  "jti": "678ee794-ec8a-4196-ba65-13d064b8a2ee",
  "client_id": "test1",
  "scope": [
    "all"
  ]
}

如果想在JWT中添加一些额外的信息,我们需要实现TokenEnhancer(Token增强器):

public class JWTTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        Map<String, Object> info = new HashMap<>();
        info.put("message", "hello world");
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
        return oAuth2AccessToken;
    }
}

我们在Token中添加了message: hello world信息。然后在JWTokenConfig里注册该Bean:

@Configuration
public class JWTokenConfig {
    ......

    @Bean
    public TokenEnhancer tokenEnhancer() {
        return new JWTokenEnhancer();
    }
}

最后在认证服务器里配置该增强器:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailService userDetailService;
    @Autowired
    private TokenStore jwtTokenStore;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    @Autowired
    private TokenEnhancer tokenEnhancer;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> enhancers = new ArrayList<>();
        enhancers.add(tokenEnhancer);
        enhancers.add(jwtAccessTokenConverter);
        enhancerChain.setTokenEnhancers(enhancers);

        endpoints.authenticationManager(authenticationManager)
                .tokenStore(jwtTokenStore)
                .tokenEnhancer(enhancerChain)
                .accessTokenConverter(jwtAccessTokenConverter)
                .userDetailsService(userDetailService);
    }
...
}

重启项目,再次获取令牌,系统返回:

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU2ODc4OTkzNSwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiZDlmM2M4NGItMWNlNy00YWJmLWE1ZjUtODlkMTVmMTA2YTRhIiwiY2xpZW50X2lkIjoidGVzdDEifQ.WjIvH4dpaF8oKXh1qWuSP5o4slgpG9fAWzGTBUrwlA4",
    "token_type": "bearer",
    "expires_in": 3599,
    "scope": "all",
    "message": "hello world",
    "jti": "d9f3c84b-1ce7-4abf-a5f5-89d15f106a4a"
}

可以看到,在返回的JSON内容里已经多了我们添加的message信息,此外将access_token复制到jwt.io网站解析,内容如下:

{
  "user_name": "user",
  "scope": [
    "all"
  ],
  "exp": 1568789935,
  "message": "hello world",
  "authorities": [
    "admin"
  ],
  "jti": "d9f3c84b-1ce7-4abf-a5f5-89d15f106a4a",
  "client_id": "test1"
}

解析后的JWT也包含了我们添加的message信息。

Java中解析JWT

要在Java代码中解析JWT,需要添加如下依赖:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

编写接口index

    @GetMapping("/index")
    public Object index(HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        String token = StringUtils.substringAfter(header, "bearer ");

        return Jwts.parser().setSigningKey("test_key".getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();
    }

signkey需要和JwtAccessTokenConverter中指定的签名密钥一致。重启项目,获取令牌后访问/index,输出内容如下:

{
    "user_name": "user",
    "scope": [
        "all"
    ],
    "exp": 1568790296,
    "message": "hello world",
    "authorities": [
        "admin"
    ],
    "jti": "4da297b1-9c12-4251-bf18-93bbc35d25bd",
    "client_id": "test1"
}

刷新令牌

令牌过期后我们可以使用refresh_token来从系统中换取一个新的可用令牌。但是从前面的例子可以看到,在认证成功后返回的JSON信息里并没有包含refresh_token,要让系统返回refresh_token,需要在认证服务器自定义配置里添加如下配置:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    ......

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("test1")
                .secret(new BCryptPasswordEncoder().encode("test1111"))
                .authorizedGrantTypes("password", "refresh_token")
                .accessTokenValiditySeconds(3600)
                .refreshTokenValiditySeconds(864000)
                .scopes("all", "a", "b", "c")
            .and()
                .withClient("test2")
                .secret(new BCryptPasswordEncoder().encode("test2222"))
                .accessTokenValiditySeconds(7200);
    }
}

授权方式需要加上refresh_token,除了四种标准的OAuth2获取令牌方式外,Spring Security OAuth2内部把refresh_token当作一种拓展的获取令牌方式。

通过上面的配置,使用test1这个client_id获取令牌时将返回refresh_token,refresh_token的有效期为10天,即10天之内都可以用它换取新的可用令牌。

重启项目,认证成功后,系统返回如:

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU2ODc5MDY4OCwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiMGMwZjg3N2ItMmNkYy00NmI2LWJkYTktNThhYmMzMmNkZDQ3IiwiY2xpZW50X2lkIjoidGVzdDEifQ.RmZExfQmbPgCo2UR8HChaTxRoUzkmKB2r2h7quSAUrw",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6IjBjMGY4NzdiLTJjZGMtNDZiNi1iZGE5LTU4YWJjMzJjZGQ0NyIsImV4cCI6MTU2OTY1MTA4OCwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiNDJmNTQyMjQtNDJiZS00ZWE2LTg5ODktOGE3ZTFhNjkzMjA5IiwiY2xpZW50X2lkIjoidGVzdDEifQ.1MLmQYoh--ExRuuf0_glHApPnTyCBi9UbZoZM3-76Ds",
    "expires_in": 3599,
    "scope": "all",
    "message": "hello world",
    "jti": "0c0f877b-2cdc-46b6-bda9-58abc32cdd47"
}

假设现在access_token过期了,我们用refresh_token去换取新的令牌。使用postman发送如下请求:


image.png
image.png
{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU2ODc5MDgxMSwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiMjdkOTNkMWQtNzczNi00ODQzLThjOTQtZWI1ZjdkMzIyMWJlIiwiY2xpZW50X2lkIjoidGVzdDEifQ.733DihtA3G3GkfFT82Bu-Z3FYuWHWxX8l_A0hON3XO8",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6IjI3ZDkzZDFkLTc3MzYtNDg0My04Yzk0LWViNWY3ZDMyMjFiZSIsImV4cCI6MTU2OTY1MTA4OCwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiNDJmNTQyMjQtNDJiZS00ZWE2LTg5ODktOGE3ZTFhNjkzMjA5IiwiY2xpZW50X2lkIjoidGVzdDEifQ.61o51OwW7twZ3tDRzEu__ho0bwwpIgwDPytkpnF00u8",
    "expires_in": 3599,
    "scope": "all",
    "message": "hello world",
    "jti": "27d93d1d-7736-4843-8c94-eb5f7d3221be"
}

源码地址:https://github.com/lbshold/springboot/tree/master/Spring-Security-OAuth2-JWT

上一篇下一篇

猜你喜欢

热点阅读