【Spring Security OAuth】--- 使用JWT替换默认令牌


项目源码地址 https://github.com/nieandsun/security


1 JWT特点

在《从cookie/session和token的角度来认识一下spring security oauth》那篇文章里我简单提到过普通的token和JWT的概念,本篇不再过多叙述。相信通过前面几篇文章的介绍大家对普通的token(即spring security oauth使用的默认令牌)已经比较了解了,这篇文章来讲讲如何使用JWT替换默认令牌,并从源码角度来看一看JWT到底是怎么生成的。

在此之前先介绍一下JWT的几个特点:

(1)自包含 — ★ JWT里面包含信息
(2)可扩展 — 即JWT生成时会包含一些默认信息,我们可以再往JWT里加入一些额外的信息
(3)密签 — 在生成JWT时指定一个密钥,然后校验时再拿着这个密钥进行校验(看了源码后可以理解的更清楚)

我想大多数人应该并不一定明白这三个特点究竟讲的是什么。在下篇文章进行完源码解析后,我将会按照自己的理解再叙述一遍。


2 使用JWT替换默认令牌

2.1 代码开发

主要包括:

  • 配置TokenStore

TokenStore只负责token的存取,存到redis用到的TokenStore的实现类为RedisTokenStore,存到mysql用到的实现类为JdbcTokenStore。。。生成的token为JWT时要使用JwtTokenStore —》其实JwtTokenStore对对存、取token的操作就是啥也不做,因为jwt的自包含特性,服务端根本没必要去存储token。— 有兴趣的可以追踪一下源码

  • 配置JwtAccessTokenConverter — 真正生产JWT的类

JwtAccessTokenConverter 其实是一个TokenEnhancer。 通过阅读源码可知:TokenEnhancer是对生成的Token进行后续处理的(或者说是对Token进行增强的) 其实JwtAccessTokenConverter就是将默认生成的token做进一步处理使其成为一个JWT。

  • 将JwtTokenStore和JwtAccessTokenConverter设置到token的生成类中

具体代码如下:
(1)配置TokenStore和JwtAccessTokenConverter

package com.nrsc.security.app.config;

import com.nrsc.security.core.properties.NrscSecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

/**
 * @author : Sun Chuan
 * @date : 2019/10/24 13:56
 * Description: TokenStore的实现类有5个,可以在yml里通过配置指定使用RedisTokenStore还是JwtTokenStore
 */
@Configuration
public class TokenStoreConfig {

    /***
     * RedisTokenStore需要一个连接工厂,这里可以直接注入进来
     */
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    /***
     * 将RedisTokenStore注入到spring容器
     * 当yml配置文件里配置了nrsc.security.oauth2.tokenStore = redis时 ---> 下面的配置生效
     * @return
     */
    @Bean
    @ConditionalOnProperty(prefix = "nrsc.security.oauth2", name = "tokenStore", havingValue = "redis")
    public TokenStore redisTokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }

    /***
     * 当yml配置文件里配置了nrsc.security.oauth2.tokenStore = jwt或者根本就没配置该属性时 ---> 下面的配置生效
     */
    @Configuration
    @ConditionalOnProperty(prefix = "nrsc.security.oauth2", name = "tokenStore", havingValue = "jwt", matchIfMissing = true)
    public static class JwtConfig {

        @Autowired
        private NrscSecurityProperties securityProperties;

        /***
         * 配置JwtTokenStore ---> TokenStore只负责token的存储,不负责token的生成
         * @return
         */
        @Bean
        public TokenStore jwtTokenStore() {
            return new JwtTokenStore(jwtAccessTokenConverter());
        }

        /***
         * JwtAccessTokenConverter 其实就是一个TokenEnhancer
         * 通过阅读源码可知:TokenEnhancer是对生成的Token进行后续处理的(或者说增强),
         * 其实JwtAccessTokenConverter就是将默认生成的token做进一步处理使其成为一个JWT
         * @return
         */
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            converter.setSigningKey(securityProperties.getOauth2().getJwtSigningKey());
            return converter;
        }
    }
}

(2)将JwtTokenStore和JwtAccessTokenConverter设置到token的生成类中
注意:具体代码可看本篇文章对应的commit记录

扫描二维码关注公众号,回复: 8517277 查看本文章
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                //指定使用的TokenStore,tokenStore用来存取token,默认使用InMemoryTokenStore
                .tokenStore(tokenStore)
                //下面的配置主要用来指定"对正在进行授权的用户进行认证+校验"的类
                //在实现了AuthorizationServerConfigurerAdapter适配器类后,必须指定下面两项
                .authenticationManager(authenticationManager)
                .userDetailsService(NRSCDetailsService);
        //将JwtAccessTokenConverter设置到token的生成类中
        if (jwtAccessTokenConverter != null) {
            endpoints
                    .accessTokenConverter(jwtAccessTokenConverter);
        }
    }

2.2 测试

(1)获取token
在这里插入图片描述
(2)拿着token请求用户信息
在这里插入图片描述
注意: 通过JWT解析到的Authentication对象 (我代码里的/user/me1和/user/me2是获取Authentication对象)如下。其中principal为一个字符串,但是之前无论用户名+密码模式、短息登陆还是社交登陆过程中生成的Authentication对象里的principal都是一个UserDetails对象。— 》 这是一个很多的区别。

{
	"authorities": [
		{
			"authority": "admin"
		}
	],
	"details": {
		"remoteAddress": "127.0.0.1",
		"sessionId": null,
		"tokenValue": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzIwNzYxMDQsInVzZXJfbmFtZSI6InlveW8iLCJhdXRob3JpdGllcyI6WyJhZG1pbiJdLCJqdGkiOiJhODVkM2ZmNS1mNDZkLTQyNjUtYjExZi03NzBhZjZmMmUzYmYiLCJjbGllbnRfaWQiOiJucnNjIiwic2NvcGUiOlsiYWxsIl19.i0hJ5zomniZgSA6m4xkpsU6Sqj5YLHwLoJcFRffKA6E",
		"tokenType": "bearer",
		"decodedDetails": null
	},
	"authenticated": true,
	"userAuthentication": {
		"authorities": [
			{
				"authority": "admin"
			}
		],
		"details": null,
		"authenticated": true,
		"principal": "yoyo",
		"credentials": "N/A",
		"name": "yoyo"
	},
	"credentials": "",
	"oauth2Request": {
		"clientId": "nrsc",
		"scope": [
			"all"
		],
		"requestParameters": {
			"client_id": "nrsc"
		},
		"resourceIds": [],
		"authorities": [],
		"approved": true,
		"refresh": false,
		"redirectUri": null,
		"responseTypes": [],
		"extensions": {},
		"refreshTokenRequest": null,
		"grantType": null
	},
	"clientOnly": false,
	"principal": "yoyo",
	"name": "yoyo"
}

(3)正是由于通过JWT生成的Authentication对象里的principal为一个字符串而不是UserDetails对象的原因,请求下面这个接口将获取不到任何信息

 @GetMapping("/me3")
 public Object getCurrentUser3(@AuthenticationPrincipal UserDetails user) {
     //方式3,只获取User对象
     return user;
 }

测试结果如下:
在这里插入图片描述
(4)但是下面这样可以

    /***
     * JWT 情况下获取的principal是一个字符串
     * @param user
     * @return
     */
    @GetMapping("/me4")
    public Object getCurrentUser4(@AuthenticationPrincipal String user) {
        //方式3,只获取User对象
        return user;
    }

测试如下:
在这里插入图片描述

发布了189 篇原创文章 · 获赞 187 · 访问量 39万+

猜你喜欢

转载自blog.csdn.net/nrsc272420199/article/details/102750925