SpringSecurity OAuth2 自定义令牌配置(JWT)

目录:

  • 自定义令牌配置
  • 使用JWT替换默认令牌
  • 扩展JWT
  • JAVA中解析JWT
  • 刷新令牌

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,490评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,581评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,830评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,957评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,974评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,754评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,464评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,847评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,995评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,137评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,819评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,482评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,023评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,149评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,409评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,086评论 2 355

推荐阅读更多精彩内容

  • 今天的文章介绍一种适用于restful+json的API认证方法,这个方法是基于jwt,并且加入了一些从oauth...
    茶客furu声阅读 22,303评论 9 49
  • Spring OAuth2.0 提供者实现原理: Spring OAuth2.0提供者实际上分为: 授权服务 Au...
    EnchIGo阅读 1,231评论 0 5
  • OAuth2.0 是关于授权的开放网络标准,它允许用户已第三方应用获取该用户在某一网站的私密资源,而无需提供用户名...
    baiyi阅读 10,206评论 4 25
  • Spring security OAuth2 深入解析 一、OAuth2 概要 1.1.OAuth2基本流程 话不...
    CatalpaFlat阅读 3,991评论 0 16
  • 天光欲破,拂晓将至。 白影越过层层黑山,迅速穿过葱茏山林,飒飒声响伴随着溪水的潺潺,樱花落下倒是无声,然而锐利枪尖...
    xiwhdhfksbs阅读 445评论 0 0