spring-security-oauth2-authorization-server(四)Redis存储token实现

写在前面的话

因为SpringBoot3.x是目前最新的版本,整合spring-security-oauth2-authorization-server的资料很少,所以产生了这篇文章,主要为想尝试SpringBoot高版本,想整合最新的spring-security-oauth2-authorization-server的初学者,旨在为大家提供一个简单上手的参考,如果哪里写得不对或可以优化的还请大家踊跃评论指正。

前面几篇文章主要结合官网的描述实现了基于JDBC获取用户信息登录并存储token在数据库中。但是我们实际中使用较多的是将token存储在Redis,官方只提供了InMemory和JDBC两种模式,所以本篇文章主要想实现一个自定义的RedisService来存储和获取token。此中会遇到很多问题,会一一列出。
整个项目的配置还是复用的上一篇。

一、自定义RedisOAuth2AuthorizationService

1. 整合Redis

在pom中添加如下坐标,如果开启lettuce连接池需要引入common-pools否则会报错

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--  开启lettuce pool需要此依赖 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
spring:
  data:
    redis:
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 1
          max-wait: 30000
          # 开启连接池,需要引入common-pools
          enabled: true
      database: 15
      host: 192.168.0.51
      port: 6379
      password: lx90150..

2. 创建RedisOAuth2AuthorizationService

我们上一篇实现了基于JDBC的JdbcOAuth2AuthorizationService,点进去会看到是实现了OAuth2AuthorizationService接口,那我们也实现此接口。里面有用到RedisUtils工具类,比较简单就不贴了。

public class RedisOAuth2AuthorizationService implements OAuth2AuthorizationService {

    @Override
    public void save(OAuth2Authorization authorization) {

    }

    @Override
    public void remove(OAuth2Authorization authorization) {

    }

    @Override
    public OAuth2Authorization findById(String id) {
        return null;
    }

    @Override
    public OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType) {
        return null;
    }
}

然后我们一一去重写它。

2.1 save(OAuth2Authorization authorization)

之前我们认证服务器配置中,有配置RegisteredClientRepository Bean,里面我们设置了tokenSettings,里面包含token的一些信息,如:授权码过期时间,accessToken过期时间等,默认是5分钟,支持自定义。所以我们将RegisteredClientRepository引入进来供查询客户端信息。

    private final RegisteredClientRepository clientRepository;

    @Setter
    private ObjectMapper objectMapper = new ObjectMapper();

    public RedisOAuth2AuthorizationService(RegisteredClientRepository clientRepository) {
        this.clientRepository = clientRepository;
    }
	
	@Override
    public void save(OAuth2Authorization authorization) {
        // 获取客户端信息
        final String clientId = authorization.getRegisteredClientId();
        RegisteredClient registeredClient = clientRepository.findById(clientId);
        Assert.notNull(registeredClient, "客户端信息不可为空");
        Assert.notNull(registeredClient.getTokenSettings(), "token配置信息不可为空");

        // 获取授权码、AccessToken、RefreshToken过期时间,默认5分钟
        Duration codeLiveTime = registeredClient.getTokenSettings().getAuthorizationCodeTimeToLive();
        Duration accessTokenLiveTime = registeredClient.getTokenSettings().getAccessTokenTimeToLive();
        Duration refreshTokenLiveTime = registeredClient.getTokenSettings().getRefreshTokenTimeToLive();

        // 获取authorizationId
        final String authorizationId = authorization.getId();
        final String idToAuthorizationKey = SecurityConstant.getIdToAuthorizationKey(authorizationId);
        
        // write()是用objectMapper序列化,想以原样存储原样获取方便查看
        RedisUtils.setEx(idToAuthorizationKey, write(authorization), accessTokenLiveTime.getSeconds(), TimeUnit.SECONDS);

        //因为授权码、AccessToken、RefreshToken都是通过此service存储和获取,所以都需要存储
        Optional.ofNullable(authorization.getToken(OAuth2AuthorizationCode.class)).ifPresent(token -> {
            final String codeToAuthorizationKey = SecurityConstant.getCodeToAuthorization(token.getToken().getTokenValue());
            RedisUtils.setEx(codeToAuthorizationKey, authorizationId, codeLiveTime.getSeconds(), TimeUnit.SECONDS);
        });
        Optional.ofNullable(authorization.getAccessToken()).ifPresent(token -> {
            final String accessToAuthorization = SecurityConstant.getAccessToAuthorization(token.getToken().getTokenValue());
            RedisUtils.setEx(accessToAuthorization, authorizationId, accessTokenLiveTime.getSeconds(), TimeUnit.SECONDS);
        });
        Optional.ofNullable(authorization.getRefreshToken()).ifPresent(token -> {
            final String refreshToAuthorization = SecurityConstant.getRefreshToAuthorization(token.getToken().getTokenValue());
            RedisUtils.setEx(refreshToAuthorization, authorizationId, refreshTokenLiveTime.getSeconds(), TimeUnit.SECONDS);
        });
    }
  

    /**
     * 将OAuth2Authorization对象序列化成字符串
     *
     * @param data  OAuth2Authorization对象
     * @return OAuth2Authorization字符串
     */
    private String write(Object data) {
        try {
            return this.objectMapper.writeValueAsString(data);
        } catch (Exception ex) {
            throw new IllegalArgumentException(ex.getMessage(), ex);
        }
    }

附上SecurityConstant定义的常量

public class SecurityConstant {

    /**
     * redisToken前缀
     */
    public final static String CACHE_TOKEN_PREFIX = "ADP-SSO:";


    /**
     * authorization_id key
     */
    public static final String ID_TO_AUTHORIZATION = CACHE_TOKEN_PREFIX + "id_to_authorization:";
    
    /**
     * authorization_code key
     */
    public static final String CODE_TO_AUTHORIZATION = CACHE_TOKEN_PREFIX + "code_to_authorization:";

    /**
     * access_token key
     */
    public static final String ACCESS_TO_AUTHORIZATION = CACHE_TOKEN_PREFIX + "access_to_authorization:";

    /**
     * refresh_token key
     */
    public static final String REFRESH_TO_AUTHORIZATION = CACHE_TOKEN_PREFIX + "refresh_to_authorization:";

    private static final MessageDigest DIGEST;

    static {
        try {
            DIGEST = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    protected static String generateKey(String rawKey) {
        byte[] bytes = DIGEST.digest(rawKey.getBytes(StandardCharsets.UTF_8));
        return String.format("%032x", new BigInteger(1, bytes));
    }

    public static String getIdToAuthorizationKey(String authorizationId) {
        return SecurityConstant.ID_TO_AUTHORIZATION + authorizationId;
    }

    public static String getCodeToAuthorization(String code) {
        return SecurityConstant.CODE_TO_AUTHORIZATION + generateKey(code);
    }

    public static String getAccessToAuthorization(String accessToken) {
        return SecurityConstant.ACCESS_TO_AUTHORIZATION + generateKey(accessToken);
    }

    public static String getRefreshToAuthorization(String refreshToken) {
        return SecurityConstant.REFRESH_TO_AUTHORIZATION + generateKey(refreshToken);
    }
}

2.2 OAuth2Authorization findById(String id)

提供根据authorizationId查询OAuth2Authorization

    @Override
    public OAuth2Authorization findById(String id) {
        return Optional.ofNullable(RedisUtils.get(SecurityConstant.getIdToAuthorizationKey(id))).map(this::parse).orElse(null);
    }

	/**
     * 将redis里的OAuth2Authorization反序列化成OAuth2Authorization对象
     * 
     * @param data  OAuth2Authorization序列化串
     * @return OAuth2Authorization
     */
    private OAuth2Authorization parse(String data) {
        try {
            return this.objectMapper.readValue(data, new TypeReference<>() {
            });
        } catch (Exception ex) {
            throw new IllegalArgumentException(ex.getMessage(), ex);
        }
    }

2.3 OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType)

根据token查询OAuth2Authorization

	@Override
    public OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType) {
        Assert.notBlank(token, "token不能为空");
        if (tokenType == null) {
            return Optional.ofNullable(RedisUtils.get(SecurityConstant.getCodeToAuthorization(token)))
                    .or(() -> Optional.ofNullable(RedisUtils.get(SecurityConstant.getAccessToAuthorization(token))))
                    .or(() -> Optional.ofNullable(RedisUtils.get(SecurityConstant.getRefreshToAuthorization(token))))
                    .map(this::findById).orElse(null);
        } else if (OAuth2ParameterNames.CODE.equals(tokenType.getValue())) {
            return Optional.ofNullable(RedisUtils.get(SecurityConstant.getCodeToAuthorization(token))).map(this::findById).orElse(null);
        } else if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) {
            return Optional.ofNullable(RedisUtils.get(SecurityConstant.getAccessToAuthorization(token))).map(this::findById).orElse(null);
        } else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
            return Optional.ofNullable(RedisUtils.get(SecurityConstant.getRefreshToAuthorization(token))).map(this::findById).orElse(null);
        }
        return null;
    }

2.4 remove(OAuth2Authorization authorization)

    @Override
    public void remove(OAuth2Authorization authorization) {
        List<String> keysToRemove = new ArrayList<>();
        keysToRemove.add(SecurityConstant.getIdToAuthorizationKey(authorization.getId()));
        RedisUtils.delete(keysToRemove);
    }

2.5 测试并处理一系列问题

2.5.1 反序列化时间错误

访问http://192.168.0.10:8080/oauth2/authorize?response_type=code&scope=user&client_id=oauth2-client&state=ok&redirect_uri=https://www.baidu.com登录后报错:

Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: org.springframework.security.oauth2.server.authorization.OAuth2Authorization["attributes"]->java.util.Collections$UnmodifiableMap["java.security.Principal"]->org.springframework.security.authentication.UsernamePasswordAuthenticationToken["principal"]->com.roshine.authorization.domain.AccAccountDO["gmtCreate"])

看着像是无法反序列化attributes下principal的gmtCreate时间字段,那我们把JavaTimeModule配好再试试。

public class ObjectMapperUtils {

	public static ObjectMapper objectMapper() {
		ObjectMapper objectMapper = new ObjectMapper();
		objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
		objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
		objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);

		JavaTimeModule javaTimeModule = new JavaTimeModule();
		// 添加序列化
		javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN)));
		javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN)));
		javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN)));
		// 添加反序列化
		javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN)));
		javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN)));
		javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN)));
		objectMapper.registerModules(javaTimeModule);
		return objectMapper;
	}
}

然后把RedisOAuth2AuthorizationService中的ObjectMapper改成ObjectMapperUtils.objectMapper()再测试试试

    @Setter
    private ObjectMapper objectMapper = ObjectMapperUtils.objectMapper();
2.5.2 点击授权按钮重定向到/oauth2/authorize 400

成功来到授权页,继续下面的操作,发现又报错:
在这里插入图片描述
不知道什么原因,大概率是我们刚写的RedisOAuth2AuthorizationService有问题,debug看下情况,发现:点击授权按钮后来到了findByToken方法,找tokenType == state的信息,我们没有定义返回了null。
在这里插入图片描述
那就把它补上再试试。常量类SecurityConstant补充:

    /**
     * state key
     */
    public static final String STATE_TO_AUTHORIZATION = CACHE_TOKEN_PREFIX + "state_to_authorization:";

    public static String getStateToAuthorization(String state) {
        return SecurityConstant.STATE_TO_AUTHORIZATION + generateKey(state);
    }

save(OAuth2Authorization authorization)下追加state存储

        // 点击授权按钮,会根据state查询authorization,所以state也要存起来
        Optional.ofNullable(authorization.getAttribute(OAuth2ParameterNames.STATE)).ifPresent(token -> {
            final String stateToAuthorizationKey = SecurityConstant.getStateToAuthorization((String) token);
            RedisUtils.setEx(stateToAuthorizationKey, authorizationId, stateLiveTime.getSeconds(), TimeUnit.SECONDS);
        });

OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType)下追加state查询OAuth2Authorization的方法

else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
            return Optional.ofNullable(RedisUtils.get(SecurityConstant.getStateToAuthorization(token))).map(this::findById).orElse(null);
        } 
2.5.3 反序列问题 (重要)

再测试成功通过授权,页面报错500了,有错误信息就接着往下看。错误信息是反序列化问题,这个很重要也很麻烦

Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `org.springframework.security.oauth2.core.AuthorizationGrantType` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"{"id":"70362044-b300-4845-a6ce-dc9a90e724d7","registeredClientId":"oauth2-client","principalName":"admin","authorizationGrantType":{"value":"authorization_code"},"authorizedScopes":[],"attributes":{"java.security.Principal":{"authorities":[{"authority":"user"}],"details":{"remoteAddress":"192.168.0.10","sessionId":"40EA09B3FBC4DB6BCC6A5D9EFC8ED54E"},"authenticated":true,"principal":{"id":1,"createBy":1,"createByName":"roshine","gmtCreate":"2022-08-20 17:42:08","modifyBy":1,"modifyByName":"roshin"[truncated 1088 chars]; line: 1, column: 133] (through reference chain: org.springframework.security.oauth2.server.authorization.OAuth2Authorization["authorizationGrantType"])

Jackson反序列化时需要对象的无参构造器,但是org.springframework.security.oauth2.core.AuthorizationGrantType又没有为我们提供,那就只能自定义反序列化器。

网上有这样一篇文章,介绍了两种方式自定义反序列器值得一看《Jackson 解决没有无参构造函数的反序列化问题》,官方也给我们定义了几个Module可以看看。
在这里插入图片描述

public class AuthorizationGrantTypeDeserializer extends StdDeserializer<AuthorizationGrantType> {

	@Serial
	private static final long serialVersionUID = 2884780317780523184L;
	public final ObjectMapper objectMapper = new ObjectMapper();

	public AuthorizationGrantTypeDeserializer() {
		super(AuthorizationGrantType.class);
	}

	@Override
	public AuthorizationGrantType deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
		Map<String, String> map = objectMapper.readValue(p, new TypeReference<>() {});
		return new AuthorizationGrantType(map.values().stream().findFirst().orElse(null));
	}

}
2.5.4 AuthorizationGrantTypeMixin
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
    isGetterVisibility = JsonAutoDetect.Visibility.NONE)
public abstract class AuthorizationGrantTypeMixin {

    @JsonCreator
    AuthorizationGrantTypeMixin(@JsonProperty("value") String value) {
    }
}
2.5.5 OAuth2AccessTokenMixin
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
    isGetterVisibility = JsonAutoDetect.Visibility.NONE)
public abstract class OAuth2AccessTokenMixin {

    @JsonCreator
    OAuth2AccessTokenMixin(@JsonProperty("tokenType") OAuth2AccessToken.TokenType tokenType,
                           @JsonProperty("tokenValue") String tokenValue,
                           @JsonProperty("issuedAt") Instant issuedAt,
                           @JsonProperty("expiresAt") Instant expiresAt,
                           @JsonProperty("scopes") Set<String> scopes) {
    }
}
2.5.6 OAuth2AuthorizationCodeMixin
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
    isGetterVisibility = JsonAutoDetect.Visibility.NONE)
public abstract class OAuth2AuthorizationCodeMixin {

    @JsonCreator
    OAuth2AuthorizationCodeMixin(@JsonProperty("tokenValue") String tokenValue,
                                 @JsonProperty("issuedAt") Instant issuedAt,
                                 @JsonProperty("expiresAt") Instant expiresAt) {
    }
}
2.5.7 OAuth2RefreshTokenMixin
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
    isGetterVisibility = JsonAutoDetect.Visibility.NONE)
public abstract class OAuth2RefreshTokenMixin {

    @JsonCreator
    OAuth2RefreshTokenMixin(@JsonProperty("tokenValue") String tokenValue,
                            @JsonProperty("issuedAt") Instant issuedAt,
                            @JsonProperty("expiresAt") Instant expiresAt) {
    }
}
2.5.8 TokenMixin
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
    isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
public abstract class TokenMixin {

    @JsonCreator
    TokenMixin(@JsonProperty("token") OAuth2Token token,
               @JsonProperty("metadata") Map<String, Object> metadata) {
    }
}
2.5.9 TokenTypeMixin
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
    isGetterVisibility = JsonAutoDetect.Visibility.NONE)
public abstract class TokenTypeMixin {

    @JsonCreator
    TokenTypeMixin(@JsonProperty("value") String value) {
    }
}
2.5.10 OAuth2AuthorizationMixin

这里比较重要,OAuth2Authorization内部对象很复杂,通过简单的@JsonCreator已经无法实现,所以需要自定义OAuth2AuthorizationDeserializer

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonDeserialize(using = OAuth2AuthorizationDeserializer.class)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
        isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
public abstract class OAuth2AuthorizationMixin {

}
public class OAuth2AuthorizationDeserializer extends JsonDeserializer<OAuth2Authorization> {

    private final RegisteredClientRepository registeredClientRepository;

    private static final TypeReference<Set<String>> SET_TYPE_REFERENCE = new TypeReference<>() {
    };

    private static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeReference<>() {
    };

    private static final TypeReference<AuthorizationGrantType> GRANT_TYPE_TYPE_REFERENCE = new TypeReference<>() {
    };

    public OAuth2AuthorizationDeserializer(RegisteredClientRepository registeredClientRepository) {
        this.registeredClientRepository = registeredClientRepository;
    }

    @Override
    public OAuth2Authorization deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
        ObjectMapper mapper = (ObjectMapper) jp.getCodec();
        JsonNode jsonNode = mapper.readTree(jp);

        Set<String> authorizedScopes = mapper.convertValue(jsonNode.get("authorizedScopes"), SET_TYPE_REFERENCE);
        Map<String, Object> attributes = mapper.convertValue(jsonNode.get("attributes"), MAP_TYPE_REFERENCE);
        Map<String, Object> tokens = mapper.convertValue(jsonNode.get("tokens"), MAP_TYPE_REFERENCE);
        AuthorizationGrantType grantType = mapper.convertValue(jsonNode.get("authorizationGrantType"), GRANT_TYPE_TYPE_REFERENCE);

        String id = readJsonNode(jsonNode, "id").asText();
        String registeredClientId = readJsonNode(jsonNode, "registeredClientId").asText();
        String principalName = readJsonNode(jsonNode, "principalName").asText();

        RegisteredClient registeredClient = registeredClientRepository.findById(registeredClientId);
        Assert.notNull(registeredClient, "Registered client must not be null");

        Builder builder = withRegisteredClient(registeredClient)
            .id(id)
            .principalName(principalName)
            .authorizationGrantType(grantType)
            .authorizedScopes(authorizedScopes)
            .attributes(map -> map.putAll(attributes));

        Optional.ofNullable(tokens.get(OAuth2AuthorizationCode.class.getName())).ifPresent(
            token -> addToken((Token) token, builder));
        Optional.ofNullable(tokens.get(OAuth2AccessToken.class.getName())).ifPresent(
            token -> addToken((Token) token, builder));
        Optional.ofNullable(tokens.get(OAuth2RefreshToken.class.getName())).ifPresent(
            token -> addToken((Token) token, builder));
        Optional.ofNullable(tokens.get(OidcIdToken.class.getName())).ifPresent(
            token -> addToken((Token) token, builder));

        return builder.build();
    }

    public void addToken(Token<OAuth2Token> token, Builder builder) {
        builder.token(token.getToken(), map -> map.putAll(token.getMetadata()));
    }

    private JsonNode readJsonNode(JsonNode jsonNode, String field) {
        return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
    }
}

然后加入到RedisOAuth2AuthorizationService构造器中。

    public RedisOAuth2AuthorizationService(RegisteredClientRepository clientRepository) {
        this.clientRepository = clientRepository;
        objectMapper.registerModule(new OAuth2AuthorizationModule());
    }
2.5.11 OAuth2AuthorizationDeserializer内部对象没有无参构造器错误
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Class com.roshine.authorization.deserializer.OAuth2AuthorizationDeserializer has no default (no arg) constructor
 at [Source: (String)"{"@class":"org.springframework.security.oauth2.server.authorization.OAuth2Authorization","id":"56b7e0b2-0366-49a6-899f-17bc3f0fe79f","registeredClientId":"oauth2-client","principalName":"admin","authorizationGrantType":{"@class":"org.springframework.security.oauth2.core.AuthorizationGrantType","value":"authorization_code"},"authorizedScopes":["java.util.Collections$UnmodifiableSet",[]],"tokens":{"@class":"java.util.Collections$UnmodifiableMap"},"attributes":{"@class":"java.util.Collections$Unmod"[truncated 2130 chars]; line: 1, column: 1]

说我们自定义的OAuth2AuthorizationDeserializer没有无参构造器,那尝试提供一个无参构造,就变成这样

private RegisteredClientRepository registeredClientRepository;

    private static final TypeReference<Set<String>> SET_TYPE_REFERENCE = new TypeReference<>() {
    };

    private static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeReference<>() {
    };

    private static final TypeReference<AuthorizationGrantType> GRANT_TYPE_TYPE_REFERENCE = new TypeReference<>() {
    };

    public OAuth2AuthorizationDeserializer() {}

    public OAuth2AuthorizationDeserializer(RegisteredClientRepository registeredClientRepository) {
        this.registeredClientRepository = registeredClientRepository;
    }
2.5.12 Collections$UnmodifiableSet is not in the allowlist.

此时没有再提示:OAuth2AuthorizationDeserializer has no default (no arg) constructor错误了,有了新的错误,接着往下。

Caused by: java.lang.IllegalArgumentException: The class with java.util.Collections$UnmodifiableSet and name of java.util.Collections$UnmodifiableSet is not in the allowlist. If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See https://github.com/spring-projects/spring-security/issues/4370 for details

我们发现就是一些简单数据类型的反序列化失败,翻看github的issue,也翻看了一些文章说试着找找类加载器里有什么module,都加进来再试试。

    public RedisOAuth2AuthorizationService(RegisteredClientRepository clientRepository) {
        this.clientRepository = clientRepository;
        objectMapper.registerModule(new OAuth2AuthorizationModule());
        ClassLoader classLoader = this.getClass().getClassLoader();
        List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
        objectMapper.registerModules(securityModules);
    }
2.5.13 java.util.HashSet is not in the allowlist

发现可以了,没有再提示UnmodifiableSet is not in the allowlist,提示了以下错误,也是常见数据类型反序列化失败,翻看源码发现给我们提供了一个OAuth2AuthorizationServerJackson2Module,里面就包含了我们需要的类型。

Caused by: java.lang.IllegalArgumentException: The class with java.util.HashSet and name of java.util.HashSet is not in the allowlist. If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See https://github.com/spring-projects/spring-security/issues/4370 for details

在这里插入图片描述
加进来就变成了这样

    public RedisOAuth2AuthorizationService(RegisteredClientRepository clientRepository) {
        this.clientRepository = clientRepository;
        objectMapper.registerModule(new OAuth2AuthorizationModule());
        ClassLoader classLoader = this.getClass().getClassLoader();
        List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
        objectMapper.registerModules(securityModules);
        objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
    }

很好,没有反序列化的报错了,但是又出现了新的报错。

Caused by: java.lang.NullPointerException: Cannot invoke "org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository.findById(String)" because "this.registeredClientRepository" is null

这一看就是我们上面加的OAuth2AuthorizationDeserializer加的无参构造器引起的,实例化对象的时候默认调用了无参构造器,导致我们的registeredClientRepository为null,所以不能添加无参构造器,但是不添加又会报OAuth2AuthorizationDeserializer内部对象没有无参构造器错误。查阅一些文章得到了解决方法。
这种方式为什么能解决这个问题还不是很清楚,希望有大佬看到这里能解解惑。
先把OAuth2AuthorizationDeserializer无参构造去掉,然后RedisOAuth2AuthorizationService构造器中添加AutowireCapableBeanFactory beanFactory。

    public RedisOAuth2AuthorizationService(RegisteredClientRepository clientRepository, AutowireCapableBeanFactory beanFactory) {
        this.clientRepository = clientRepository;
        objectMapper.registerModule(new OAuth2AuthorizationModule());
        ClassLoader classLoader = this.getClass().getClassLoader();
        List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
        objectMapper.registerModules(securityModules);
        objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
        objectMapper.setHandlerInstantiator(new SpringHandlerInstantiator(beanFactory));
    }
    @Bean
    OAuth2AuthorizationService auth2AuthorizationService(RegisteredClientRepository registeredClientRepository, AutowireCapableBeanFactory beanFactory) {
        return new RedisOAuth2AuthorizationService(registeredClientRepository, beanFactory);
    }

2.6 最终达到目的

最终成功获取到授权码,然后就可以去拿我们的token了。
https://www.baidu.com/?code=DMtKb2nwpgNJy1a8DPzq68FqXFmnnePsb56nMJGEmMYGcW1Q5Y4zKgdNVLISfbiOGdV-LpRnt1W_WB6wwEElBwFGyBRfjxD5vDIYY4JpfGfOQjk7ezg-o3YaDBetPFiW&state=ok
在这里插入图片描述
这篇文章写的很长,本次实现断断续续持续了很长时间看起来可能有点乱希望可以耐心看。但是这是整套开发的必经过程。
如果发现有哪里不对或者能优化的还望不吝赐教。

2.7 源码地址

gitee源码地址

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值