增加OAuth2

This commit is contained in:
zhou-hao
2020-10-10 19:34:26 +08:00
parent 62b20b961b
commit 6d0a5a7fa2
44 changed files with 1230 additions and 72 deletions

View File

@@ -22,6 +22,8 @@ import reactor.core.publisher.Mono;
import java.io.Serializable;
import java.util.*;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
@@ -207,4 +209,13 @@ public interface Authentication extends Serializable {
*/
Authentication merge(Authentication source);
/**
* copy为新的权限信息
*
* @param permissionFilter 权限过滤
* @param dimension 维度过滤
* @return 新的权限信息
*/
Authentication copy(BiPredicate<Permission, String> permissionFilter,
Predicate<Dimension> dimension);
}

View File

@@ -153,7 +153,7 @@ public interface Permission extends Serializable {
* @see FieldFilterDataAccessConfig#getFields()
*/
default Optional<FieldFilterDataAccessConfig> findFieldFilter(String action) {
return findDataAccess(conf -> FieldFilterDataAccessConfig.class.isInstance(conf) && conf.getAction().equals(action));
return findDataAccess(conf -> conf instanceof FieldFilterDataAccessConfig && conf.getAction().equals(action));
}
/**
@@ -164,7 +164,7 @@ public interface Permission extends Serializable {
*/
default Set<String> findDenyFields(String action) {
return findFieldFilter(action)
.filter(conf -> DENY_FIELDS.equals(conf.getType()))
.filter(conf -> DENY_FIELDS.equals(conf.getType().getId()))
.map(FieldFilterDataAccessConfig::getFields)
.orElseGet(Collections::emptySet);
}
@@ -210,6 +210,8 @@ public interface Permission extends Serializable {
Permission copy();
Permission copy(Predicate<String> actionFilter,Predicate<DataAccessConfig> dataAccessFilter);
/**
* 数据权限查找判断逻辑接口
*

View File

@@ -6,10 +6,7 @@ import org.hswebframework.web.authorization.builder.DataAccessConfigBuilderFacto
import org.hswebframework.web.authorization.simple.builder.DataAccessConfigConverter;
import org.hswebframework.web.authorization.simple.builder.SimpleAuthenticationBuilderFactory;
import org.hswebframework.web.authorization.simple.builder.SimpleDataAccessConfigBuilderFactory;
import org.hswebframework.web.authorization.token.DefaultUserTokenManager;
import org.hswebframework.web.authorization.token.UserTokenAuthenticationSupplier;
import org.hswebframework.web.authorization.token.UserTokenReactiveAuthenticationSupplier;
import org.hswebframework.web.authorization.token.UserTokenManager;
import org.hswebframework.web.authorization.token.*;
import org.hswebframework.web.authorization.twofactor.TwoFactorValidatorManager;
import org.hswebframework.web.authorization.twofactor.defaults.DefaultTwoFactorValidatorManager;
import org.hswebframework.web.convert.CustomMessageConverter;

View File

@@ -23,7 +23,9 @@ import org.hswebframework.web.authorization.*;
import java.io.Serializable;
import java.util.*;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@Getter
@@ -40,9 +42,10 @@ public class SimpleAuthentication implements Authentication {
private Map<String, Serializable> attributes = new HashMap<>();
public static Authentication of(){
public static Authentication of() {
return new SimpleAuthentication();
}
@Override
@SuppressWarnings("unchecked")
public <T extends Serializable> Optional<T> getAttribute(String name) {
@@ -77,4 +80,19 @@ public class SimpleAuthentication implements Authentication {
}
return this;
}
@Override
public Authentication copy(BiPredicate<Permission, String> permissionFilter,
Predicate<Dimension> dimension) {
SimpleAuthentication authentication = new SimpleAuthentication();
authentication.setUser(user);
authentication.setDimensions(dimensions.stream().filter(dimension).collect(Collectors.toList()));
authentication.setPermissions(permissions
.stream()
.map(permission -> permission.copy(action -> permissionFilter.test(permission, action), conf -> true))
.filter(per -> !per.getActions().isEmpty())
.collect(Collectors.toList())
);
return authentication;
}
}

View File

@@ -5,6 +5,8 @@ import org.hswebframework.web.authorization.Permission;
import org.hswebframework.web.authorization.access.DataAccessConfig;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* @author zhouhao
@@ -42,16 +44,22 @@ public class SimplePermission implements Permission {
return dataAccesses;
}
public Permission copy() {
@Override
public Permission copy(Predicate<String> actionFilter,
Predicate<DataAccessConfig> dataAccessFilter) {
SimplePermission permission = new SimplePermission();
permission.setId(id);
permission.setName(name);
permission.setActions(new HashSet<>(getActions()));
permission.setDataAccesses(new HashSet<>(getDataAccesses()));
permission.setActions(getActions().stream().filter(actionFilter).collect(Collectors.toSet()));
permission.setDataAccesses(getDataAccesses().stream().filter(dataAccessFilter).collect(Collectors.toSet()));
if (options != null) {
permission.setOptions(new HashMap<>(options));
}
return permission;
}
public Permission copy() {
return copy(action -> true, conf -> true);
}
}

View File

@@ -15,4 +15,8 @@ public interface ParsedToken {
* @return 令牌类型
*/
String getType();
static ParsedToken of(String type, String token) {
return SimpleParsedToken.of(type, token);
}
}

View File

@@ -0,0 +1,32 @@
package org.hswebframework.web.authorization.token;
import lombok.AllArgsConstructor;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.authorization.ReactiveAuthenticationSupplier;
import org.hswebframework.web.context.ContextKey;
import org.hswebframework.web.context.ContextUtils;
import org.hswebframework.web.logger.ReactiveLogger;
import reactor.core.publisher.Mono;
@AllArgsConstructor
public class ReactiveTokenAuthenticationSupplier implements ReactiveAuthenticationSupplier {
private final TokenAuthenticationManager tokenManager;
@Override
public Mono<Authentication> get(String userId) {
return Mono.empty();
}
@Override
public Mono<Authentication> get() {
return ContextUtils.reactiveContext()
.flatMap(context ->
context.get(ContextKey.of(ParsedToken.class))
.map(t -> tokenManager.getByToken(t.getToken()))
.orElseGet(Mono::empty))
.flatMap(auth -> ReactiveLogger.mdc("userId", auth.getUser().getId())
.then(ReactiveLogger.mdc("username", auth.getUser().getName()))
.thenReturn(auth));
}
}

View File

@@ -0,0 +1,17 @@
package org.hswebframework.web.authorization.token;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor(staticName = "of")
public class SimpleParsedToken implements ParsedToken{
private String type;
private String token;
}

View File

@@ -31,4 +31,10 @@ public interface TokenAuthenticationManager {
*/
Mono<Void> putAuthentication(String token, Authentication auth, Duration ttl);
/**
* 删除token
* @param token token
* @return void
*/
Mono<Void> removeToken(String token);
}

View File

@@ -38,6 +38,13 @@ public class RedisTokenAuthenticationManager implements TokenAuthenticationManag
.get("token-auth:" + token);
}
@Override
public Mono<Void> removeToken(String token) {
return operations
.delete(token)
.then();
}
@Override
public Mono<Void> putAuthentication(String token, Authentication auth, Duration ttl) {
return ttl.isNegative()

View File

@@ -17,11 +17,8 @@
package org.hswebframework.web.authorization.basic.web;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.SneakyThrows;
import org.hswebframework.web.authorization.Authentication;
@@ -33,12 +30,10 @@ import org.hswebframework.web.authorization.events.AuthorizationFailedEvent;
import org.hswebframework.web.authorization.events.AuthorizationSuccessEvent;
import org.hswebframework.web.authorization.exception.AuthenticationException;
import org.hswebframework.web.authorization.exception.UnAuthorizedException;
import org.hswebframework.web.authorization.simple.CompositeReactiveAuthenticationManager;
import org.hswebframework.web.authorization.simple.PlainTextUsernamePasswordAuthenticationRequest;
import org.hswebframework.web.logging.AccessLogger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.repository.query.Param;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*;
@@ -71,7 +66,6 @@ public class AuthorizationController {
}
@PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation("用户名密码登录,json方式")
@Authorize(ignore = true)
@AccessLogger(ignore = true)
@Operation(summary = "登录",description = "必要参数:username,password.根据配置不同,其他参数也不同,如:验证码等.")

View File

@@ -1,13 +1,14 @@
package org.hswebframework.web.oauth2;
import lombok.Getter;
import org.hswebframework.web.exception.BusinessException;
@Getter
public class OAuth2Exception extends RuntimeException {
public class OAuth2Exception extends BusinessException {
private final ErrorType type;
public OAuth2Exception(ErrorType type) {
super(type.message());
super(type.message(), type.name(), type.code());
this.type = type;
}

View File

@@ -2,23 +2,27 @@ package org.hswebframework.web.oauth2.server;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.*;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class AccessToken extends OAuth2Response {
private static final long serialVersionUID = -6849794470754667710L;
@Schema(name="access_token")
@JsonProperty("access_token")
private String accessToken;
@Schema(name="expires_in")
@JsonProperty("expires_in")
private int expiresIn;
@Schema(name="refresh_token")
@JsonProperty("refresh_token")
private String refreshToken;
@Schema(name="expires_in")
@JsonProperty("expires_in")
private int expiresIn;
}

View File

@@ -7,7 +7,9 @@ public interface AccessTokenManager {
Mono<Authentication> getAuthenticationByToken(String accessToken);
Mono<AccessToken> createAccessToken(String clientId, Authentication authentication);
Mono<AccessToken> createAccessToken(String clientId,
Authentication authentication,
boolean singleton);
Mono<AccessToken> refreshAccessToken(String clientId, String refreshToken);

View File

@@ -1,15 +1,12 @@
package org.hswebframework.web.oauth2.server;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.oauth2.server.code.AuthorizationCodeGranter;
import reactor.core.publisher.Mono;
public interface OAuth2GrantService {
AuthorizationCodeGranter code();
AuthorizationCodeGranter authorizationCode();
ClientCredentialGranter clientCredential();
Mono<Authentication> grant(String scope, Authentication authentication);
}

View File

@@ -5,12 +5,13 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
@Getter
@Setter
public class OAuth2Response {
public class OAuth2Response implements Serializable {
@Hidden
private Map<String,Object> parameters;

View File

@@ -0,0 +1,77 @@
package org.hswebframework.web.oauth2.server;
import org.hswebframework.web.authorization.ReactiveAuthenticationHolder;
import org.hswebframework.web.authorization.basic.web.ReactiveUserTokenParser;
import org.hswebframework.web.oauth2.server.auth.ReactiveOAuth2AccessTokenParser;
import org.hswebframework.web.oauth2.server.code.AuthorizationCodeGranter;
import org.hswebframework.web.oauth2.server.code.DefaultAuthorizationCodeGranter;
import org.hswebframework.web.oauth2.server.impl.CompositeOAuth2GrantService;
import org.hswebframework.web.oauth2.server.impl.RedisAccessTokenManager;
import org.hswebframework.web.oauth2.server.web.OAuth2AuthorizeController;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
@Configuration(proxyBeanMethods = false)
public class OAuth2ServerAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ReactiveUserTokenParser.class)
static class ReactiveOAuth2AccessTokenParserConfiguration {
@Bean
@ConditionalOnBean(AccessTokenManager.class)
public ReactiveOAuth2AccessTokenParser reactiveOAuth2AccessTokenParser(AccessTokenManager accessTokenManager) {
ReactiveOAuth2AccessTokenParser parser = new ReactiveOAuth2AccessTokenParser(accessTokenManager);
ReactiveAuthenticationHolder.addSupplier(parser);
return parser;
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
static class ReactiveOAuth2ServerAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AccessTokenManager accessTokenManager(ReactiveRedisConnectionFactory redisConnectionFactory) {
return new RedisAccessTokenManager(redisConnectionFactory);
}
@Bean
@ConditionalOnMissingBean
public AuthorizationCodeGranter authorizationCodeGranter(AccessTokenManager tokenManager,
ReactiveRedisConnectionFactory redisConnectionFactory) {
return new DefaultAuthorizationCodeGranter(tokenManager, redisConnectionFactory);
}
@Bean
@ConditionalOnMissingBean
public OAuth2GrantService oAuth2GrantService(ObjectProvider<AuthorizationCodeGranter> codeProvider,
ObjectProvider<ClientCredentialGranter> credentialProvider) {
CompositeOAuth2GrantService grantService = new CompositeOAuth2GrantService();
grantService.setAuthorizationCodeGranter(codeProvider.getIfAvailable());
grantService.setClientCredentialGranter(credentialProvider.getIfAvailable());
return grantService;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(OAuth2ClientManager.class)
public OAuth2AuthorizeController oAuth2AuthorizeController(OAuth2GrantService grantService,
OAuth2ClientManager clientManager) {
return new OAuth2AuthorizeController(grantService, clientManager);
}
}
}

View File

@@ -0,0 +1,61 @@
package org.hswebframework.web.oauth2.server.auth;
import lombok.AllArgsConstructor;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.authorization.ReactiveAuthenticationSupplier;
import org.hswebframework.web.authorization.basic.web.ReactiveUserTokenParser;
import org.hswebframework.web.authorization.token.ParsedToken;
import org.hswebframework.web.context.ContextKey;
import org.hswebframework.web.context.ContextUtils;
import org.hswebframework.web.logger.ReactiveLogger;
import org.hswebframework.web.oauth2.server.AccessTokenManager;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@AllArgsConstructor
public class ReactiveOAuth2AccessTokenParser implements ReactiveUserTokenParser, ReactiveAuthenticationSupplier {
private final AccessTokenManager accessTokenManager;
@Override
public Mono<ParsedToken> parseToken(ServerWebExchange exchange) {
String token = exchange.getRequest().getQueryParams().getFirst("access_token");
if (StringUtils.isEmpty(token)) {
token = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (StringUtils.hasText(token)) {
String[] typeAndToken = token.split("[ ]");
if (typeAndToken.length == 2 && typeAndToken[0].equalsIgnoreCase("bearer")) {
token = typeAndToken[1];
}
}
}
if (StringUtils.hasText(token)) {
return Mono.just(ParsedToken.of("oauth2", token));
}
return Mono.empty();
}
@Override
public Mono<Authentication> get(String userId) {
return Mono.empty();
}
@Override
public Mono<Authentication> get() {
return ContextUtils.reactiveContext()
.flatMap(context ->
context.get(ContextKey.of(ParsedToken.class))
.filter(token -> "oauth2".equals(token.getType()))
.map(t -> accessTokenManager
.getAuthenticationByToken(t.getToken()))
.orElse(Mono.empty()))
.flatMap(auth -> ReactiveLogger.mdc("userId", auth.getUser().getId())
.then(ReactiveLogger.mdc("username", auth.getUser().getName()))
.thenReturn(auth));
}
}

View File

@@ -0,0 +1,26 @@
package org.hswebframework.web.oauth2.server.code;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hswebframework.web.authorization.Authentication;
import java.io.Serializable;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class AuthorizationCodeCache implements Serializable {
private static final long serialVersionUID = -6849794470754667710L;
private String clientId;
private String code;
private Authentication authentication;
private String scope;
}

View File

@@ -12,11 +12,6 @@ import reactor.core.publisher.Mono;
*/
public interface AuthorizationCodeGranter extends OAuth2Granter {
/**
* @return 申请授权码界面
*/
String getLoginUrl();
/**
* 申请授权码
*

View File

@@ -3,6 +3,7 @@ package org.hswebframework.web.oauth2.server.code;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hswebframework.web.oauth2.server.OAuth2Client;
import org.hswebframework.web.oauth2.server.OAuth2Request;
import org.hswebframework.web.oauth2.server.OAuth2Response;
@@ -11,6 +12,7 @@ import java.util.HashMap;
@Getter
@Setter
@ToString
public class AuthorizationCodeResponse extends OAuth2Response {
private String code;

View File

@@ -0,0 +1,105 @@
package org.hswebframework.web.oauth2.server.code;
import lombok.AllArgsConstructor;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.authorization.Permission;
import org.hswebframework.web.id.IDGenerator;
import org.hswebframework.web.oauth2.ErrorType;
import org.hswebframework.web.oauth2.OAuth2Exception;
import org.hswebframework.web.oauth2.server.AccessToken;
import org.hswebframework.web.oauth2.server.AccessTokenManager;
import org.hswebframework.web.oauth2.server.OAuth2Client;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.*;
import java.util.function.BiPredicate;
@AllArgsConstructor
public class DefaultAuthorizationCodeGranter implements AuthorizationCodeGranter {
private final AccessTokenManager accessTokenManager;
private final ReactiveRedisOperations<String, AuthorizationCodeCache> redis;
@SuppressWarnings("all")
public DefaultAuthorizationCodeGranter(AccessTokenManager accessTokenManager, ReactiveRedisConnectionFactory connectionFactory) {
this(accessTokenManager, new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext
.newSerializationContext()
.key((RedisSerializer) RedisSerializer.string())
.value(RedisSerializer.java())
.hashKey(RedisSerializer.string())
.hashValue(RedisSerializer.java())
.build()
));
}
@Override
public Mono<AuthorizationCodeResponse> requestCode(AuthorizationCodeRequest request) {
OAuth2Client client = request.getClient();
Authentication authentication = request.getAuthentication();
AuthorizationCodeCache codeCache = new AuthorizationCodeCache();
String code = IDGenerator.MD5.generate();
request.getParameter("scope").map(String::valueOf).ifPresent(codeCache::setScope);
codeCache.setCode(code);
codeCache.setClientId(client.getClientId());
codeCache.setAuthentication(authentication.copy(createPredicate(codeCache.getScope()), dimension -> true));
createPredicate(codeCache.getScope());
return redis
.opsForValue()
.set(getRedisKey(code), codeCache, Duration.ofMinutes(5))
.thenReturn(new AuthorizationCodeResponse(code));
}
static BiPredicate<Permission, String> createPredicate(String scopeStr) {
if (StringUtils.isEmpty(scopeStr)) {
return ((permission, s) -> false);
}
String[] scopes = scopeStr.split("[,]");
Map<String, Set<String>> actions = new HashMap<>();
for (String scope : scopes) {
String[] permissions = scope.split("[:]");
String per = permissions[0];
Set<String> acts = actions.computeIfAbsent(per, k -> new HashSet<>());
acts.addAll(Arrays.asList(permissions).subList(1, permissions.length));
}
return ((permission, action) -> Optional
.ofNullable(actions.get(permission.getId()))
.map(acts -> acts.contains(action))
.orElse(false));
}
private String getRedisKey(String code) {
return "oauth2-code:" + code;
}
@Override
public Mono<AccessToken> requestToken(AuthorizationCodeTokenRequest request) {
return Mono
.justOrEmpty(request.code())
.map(this::getRedisKey)
.flatMap(redis.opsForValue()::get)
.switchIfEmpty(Mono.error(() -> new OAuth2Exception(ErrorType.ILLEGAL_CODE)))
.flatMap(cache -> redis
.opsForValue()
.delete(getRedisKey(cache.getCode()))
.thenReturn(cache))
.flatMap(cache -> {
if (!request.getClient().getClientId().equals(cache.getClientId())) {
return Mono.error(new OAuth2Exception(ErrorType.ILLEGAL_CLIENT_ID));
}
return accessTokenManager.createAccessToken(cache.getClientId(), cache.getAuthentication(), false);
});
}
}

View File

@@ -0,0 +1,26 @@
package org.hswebframework.web.oauth2.server.impl;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.web.oauth2.server.ClientCredentialGranter;
import org.hswebframework.web.oauth2.server.OAuth2GrantService;
import org.hswebframework.web.oauth2.server.code.AuthorizationCodeGranter;
@Getter
@Setter
public class CompositeOAuth2GrantService implements OAuth2GrantService {
private AuthorizationCodeGranter authorizationCodeGranter;
private ClientCredentialGranter clientCredentialGranter;
@Override
public AuthorizationCodeGranter authorizationCode() {
return authorizationCodeGranter;
}
@Override
public ClientCredentialGranter clientCredential() {
return clientCredentialGranter;
}
}

View File

@@ -0,0 +1,37 @@
package org.hswebframework.web.oauth2.server.impl;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.oauth2.server.AccessToken;
import java.io.Serializable;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class RedisAccessToken implements Serializable {
private String clientId;
private String accessToken;
private String refreshToken;
private long createTime;
private Authentication authentication;
private boolean singleton;
public AccessToken toAccessToken(int expiresIn){
AccessToken token=new AccessToken();
token.setAccessToken(accessToken);
token.setRefreshToken(refreshToken);
token.setExpiresIn(expiresIn);
return token;
}
}

View File

@@ -0,0 +1,143 @@
package org.hswebframework.web.oauth2.server.impl;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.codec.digest.DigestUtils;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.oauth2.ErrorType;
import org.hswebframework.web.oauth2.OAuth2Exception;
import org.hswebframework.web.oauth2.server.AccessToken;
import org.hswebframework.web.oauth2.server.AccessTokenManager;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.UUID;
public class RedisAccessTokenManager implements AccessTokenManager {
private final ReactiveRedisOperations<String, RedisAccessToken> tokenRedis;
@Getter
@Setter
private int tokenExpireIn = 7200;//2小时
@Getter
@Setter
private int refreshExpireIn = 2592000; //30天
public RedisAccessTokenManager(ReactiveRedisOperations<String, RedisAccessToken> tokenRedis) {
this.tokenRedis = tokenRedis;
}
@SuppressWarnings("all")
public RedisAccessTokenManager(ReactiveRedisConnectionFactory connectionFactory) {
this(new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext
.newSerializationContext()
.key((RedisSerializer) RedisSerializer.string())
.value(RedisSerializer.java())
.hashKey(RedisSerializer.string())
.hashValue(RedisSerializer.java())
.build()
));
}
@Override
public Mono<Authentication> getAuthenticationByToken(String accessToken) {
return tokenRedis
.opsForValue()
.get(createTokenRedisKey(accessToken))
.map(RedisAccessToken::getAuthentication);
}
private String createTokenRedisKey(String token) {
return "oauth2-token:" + token;
}
private String createRefreshTokenRedisKey(String token) {
return "oauth2-refresh-token:" + token;
}
private String createSingletonTokenRedisKey(String clientId) {
return "oauth2-" + clientId + "-token";
}
private Mono<RedisAccessToken> doCreateAccessToken(String clientId, Authentication authentication, boolean singleton) {
String token = DigestUtils.md5Hex(UUID.randomUUID().toString());
String refresh = DigestUtils.md5Hex(UUID.randomUUID().toString());
RedisAccessToken accessToken = new RedisAccessToken(clientId, token, refresh, System.currentTimeMillis(), authentication, singleton);
return storeToken(accessToken).thenReturn(accessToken);
}
private Mono<Void> storeToken(RedisAccessToken token) {
return Mono
.zip(
tokenRedis.opsForValue().set(createTokenRedisKey(token.getAccessToken()), token, Duration.ofSeconds(tokenExpireIn)),
tokenRedis.opsForValue().set(createRefreshTokenRedisKey(token.getRefreshToken()), token, Duration.ofSeconds(refreshExpireIn))
).then();
}
private Mono<AccessToken> doCreateSingletonAccessToken(String clientId, Authentication authentication) {
String redisKey = createSingletonTokenRedisKey(clientId);
return tokenRedis
.opsForValue()
.get(redisKey)
.flatMap(token -> tokenRedis
.getExpire(redisKey)
.map(duration -> token.toAccessToken((int) (duration.toMillis() / 1000))))
.switchIfEmpty(Mono.defer(() -> doCreateAccessToken(clientId, authentication, true)
.flatMap(redisAccessToken -> tokenRedis
.opsForValue()
.set(redisKey, redisAccessToken, Duration.ofSeconds(tokenExpireIn))
.thenReturn(redisAccessToken.toAccessToken(tokenExpireIn))))
);
}
@Override
public Mono<AccessToken> createAccessToken(String clientId,
Authentication authentication,
boolean singleton) {
return singleton
? doCreateSingletonAccessToken(clientId, authentication)
: doCreateAccessToken(clientId, authentication, false).map(token -> token.toAccessToken(tokenExpireIn));
}
@Override
public Mono<AccessToken> refreshAccessToken(String clientId, String refreshToken) {
String redisKey = createRefreshTokenRedisKey(refreshToken);
return tokenRedis
.opsForValue()
.get(redisKey)
.switchIfEmpty(Mono.error(() -> new OAuth2Exception(ErrorType.EXPIRED_REFRESH_TOKEN)))
.flatMap(token -> {
if (!token.getClientId().equals(clientId)) {
return Mono.error(new OAuth2Exception(ErrorType.ILLEGAL_CLIENT_ID));
}
//生成新token
String accessToken = DigestUtils.md5Hex(UUID.randomUUID().toString());
token.setAccessToken(accessToken);
token.setCreateTime(System.currentTimeMillis());
return storeToken(token)
.as(result -> {
// 单例token
if (token.isSingleton()) {
return tokenRedis
.opsForValue()
.set(createSingletonTokenRedisKey(clientId), token, Duration.ofSeconds(tokenExpireIn))
.then(result);
}
return result;
})
.thenReturn(token.toAccessToken(tokenExpireIn));
});
}
}

View File

@@ -2,86 +2,102 @@ package org.hswebframework.web.oauth2.server.web;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.authorization.AuthenticationManager;
import org.hswebframework.web.authorization.annotation.Authorize;
import org.hswebframework.web.authorization.exception.UnAuthorizedException;
import org.hswebframework.web.authorization.token.TokenAuthenticationManager;
import org.hswebframework.web.oauth2.server.*;
import org.hswebframework.web.oauth2.ErrorType;
import org.hswebframework.web.oauth2.OAuth2Exception;
import org.hswebframework.web.oauth2.server.AccessToken;
import org.hswebframework.web.oauth2.server.OAuth2Client;
import org.hswebframework.web.oauth2.server.OAuth2ClientManager;
import org.hswebframework.web.oauth2.server.OAuth2GrantService;
import org.hswebframework.web.oauth2.server.code.AuthorizationCodeRequest;
import org.hswebframework.web.oauth2.server.code.AuthorizationCodeTokenRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/oauth2")
@AllArgsConstructor
@Tag(name = "OAuth2认证")
public class OAuth2AuthorizeController {
private final OAuth2GrantService oAuth2GrantService;
private final OAuth2ClientManager clientManager;
@PostMapping(value = "/authorize", params = "response_type=code")
@GetMapping(value = "/authorize", params = "response_type=code")
@Operation(summary = "申请授权码,并获取重定向地址", parameters = {
@Parameter(description = "client_id"),
@Parameter(description = "redirect_uri"),
@Parameter(description = "state")
@Parameter(name = "client_id", required = true),
@Parameter(name = "redirect_uri", required = true),
@Parameter(name = "state"),
@Parameter(name = "response_type", description = "固定值为code")
})
public Mono<String> authorizeByCode(@RequestBody Mono<Map<String, Object>> params) {
public Mono<String> authorizeByCode(ServerWebExchange exchange) {
Map<String, Object> param = new HashMap<>(exchange.getRequest().getQueryParams().toSingleValueMap());
return Authentication
.currentReactive()
.switchIfEmpty(Mono.error(UnAuthorizedException::new))
.flatMap(auth -> params
.flatMap(param -> this
.getOAuth2Client((String) param.get("client_id"))
.flatMap(client -> {
String redirectUri = (String) param.getOrDefault("redirect_uri", client.getRedirectUrl());
client.validateRedirectUri(redirectUri);
return oAuth2GrantService
.code()
.requestCode(new AuthorizationCodeRequest(client, auth, param))
.map(authorizationCodeResponse -> buildRedirect(redirectUri, authorizationCodeResponse.getParameters()));
})));
.flatMap(auth -> this
.getOAuth2Client((String) param.get("client_id"))
.switchIfEmpty(Mono.error(() -> new OAuth2Exception(ErrorType.ILLEGAL_CLIENT_ID)))
.flatMap(client -> {
String redirectUri = (String) param.getOrDefault("redirect_uri", client.getRedirectUrl());
client.validateRedirectUri(redirectUri);
return oAuth2GrantService
.authorizationCode()
.requestCode(new AuthorizationCodeRequest(client, auth, param))
.doOnNext(response -> {
Optional.ofNullable(param.get("state")).ifPresent(state -> response.with("state", state));
})
.map(response -> buildRedirect(redirectUri, response.getParameters()));
}));
}
@PostMapping(value = "/token", params = "grant_type=authorization_code")
@Operation(summary = "使用授权码申请token",parameters = {
@Parameter(description = "client_id"),
@Parameter(description = "client_secret"),
@Parameter(description = "code")
@GetMapping(value = "/token", params = "grant_type=authorization_code")
@Operation(summary = "使用授权码申请token", parameters = {
@Parameter(name = "client_id"),
@Parameter(name = "client_secret"),
@Parameter(name = "code"),
@Parameter(name = "grant_type", description = "固定值为authorization_code")
})
@Authorize(ignore = true)
public Mono<ResponseEntity<AccessToken>> requestTokenByCode(@RequestBody Mono<Map<String, Object>> params) {
public Mono<ResponseEntity<AccessToken>> requestTokenByCode(ServerWebExchange exchange) {
Map<String, String> params = exchange.getRequest().getQueryParams().toSingleValueMap();
return params
.flatMap(param -> this
.getOAuth2Client((String) param.get("client_id"))
.flatMap(client -> oAuth2GrantService
.code()
.requestToken(new AuthorizationCodeTokenRequest(client, param))))
return doRequestCode(new HashMap<>(params))
.map(ResponseEntity::ok);
}
private Mono<AccessToken> doRequestCode(Map<String, Object> param) {
return this
.getOAuth2Client((String) param.get("client_id"))
.switchIfEmpty(Mono.error(() -> new OAuth2Exception(ErrorType.ILLEGAL_CLIENT_ID)))
.flatMap(client -> oAuth2GrantService
.authorizationCode()
.requestToken(new AuthorizationCodeTokenRequest(client, param)));
}
@SneakyThrows
public static String urlEncode(String url) {
return URLEncoder.encode(url, "utf-8");
}
public String buildRedirect(String redirectUri, Map<String, Object> params) {
static String buildRedirect(String redirectUri, Map<String, Object> params) {
String paramsString = params.entrySet()
.stream()
.map(e -> e.getKey() + "=" + urlEncode(String.valueOf(e.getValue())))

View File

@@ -0,0 +1,3 @@
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.hswebframework.web.oauth2.server.OAuth2ServerAutoConfiguration

View File

@@ -0,0 +1,15 @@
package org.hswebframework.web.oauth2.server;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
public class RedisHelper {
public static LettuceConnectionFactory factory;
static {
factory = new LettuceConnectionFactory(new RedisStandaloneConfiguration("127.0.0.1"));
factory.afterPropertiesSet();
}
}

View File

@@ -0,0 +1,69 @@
package org.hswebframework.web.oauth2.server.code;
import org.hswebframework.web.authorization.Permission;
import org.hswebframework.web.authorization.simple.SimpleAuthentication;
import org.hswebframework.web.authorization.simple.SimplePermission;
import org.hswebframework.web.oauth2.server.OAuth2Client;
import org.hswebframework.web.oauth2.server.RedisHelper;
import org.hswebframework.web.oauth2.server.impl.RedisAccessTokenManager;
import org.junit.Test;
import reactor.test.StepVerifier;
import java.util.Collections;
import java.util.function.BiPredicate;
import static org.junit.Assert.*;
public class DefaultAuthorizationCodeGranterTest {
@Test
public void testPermission() {
BiPredicate<Permission, String> predicate = DefaultAuthorizationCodeGranter.createPredicate("user:info,device:query");
{
SimplePermission permission=new SimplePermission();
permission.setId("user");
permission.setActions(Collections.singleton("info"));
assertTrue(predicate.test(permission,"info"));
assertFalse(predicate.test(permission,"info2"));
}
{
SimplePermission permission=new SimplePermission();
permission.setId("device");
permission.setActions(Collections.singleton("query"));
assertTrue(predicate.test(permission,"query"));
assertFalse(predicate.test(permission,"query2"));
}
}
@Test
public void testRequestToken() {
DefaultAuthorizationCodeGranter codeGranter = new DefaultAuthorizationCodeGranter(
new RedisAccessTokenManager(RedisHelper.factory), RedisHelper.factory
);
OAuth2Client client = new OAuth2Client();
client.setClientId("test");
client.setClientSecret("test");
codeGranter
.requestCode(new AuthorizationCodeRequest(client, new SimpleAuthentication(), Collections.emptyMap()))
.doOnNext(System.out::println)
.flatMap(response -> codeGranter
.requestToken(new AuthorizationCodeTokenRequest(client, Collections.singletonMap("code", response.getCode()))))
.doOnNext(System.out::println)
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
}

View File

@@ -0,0 +1,45 @@
package org.hswebframework.web.oauth2.server.impl;
import org.hswebframework.web.authorization.simple.SimpleAuthentication;
import org.hswebframework.web.oauth2.server.RedisHelper;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.junit.Assert.*;
public class RedisAccessTokenManagerTest {
@Test
public void testCreateAccessToken() {
RedisAccessTokenManager tokenManager = new RedisAccessTokenManager(RedisHelper.factory);
SimpleAuthentication authentication = new SimpleAuthentication();
tokenManager.createAccessToken("test", authentication, false)
.doOnNext(System.out::println)
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
@Test
public void testCreateSingletonAccessToken() {
RedisAccessTokenManager tokenManager = new RedisAccessTokenManager(RedisHelper.factory);
SimpleAuthentication authentication = new SimpleAuthentication();
Flux
.concat(tokenManager
.createAccessToken("test", authentication, true),
tokenManager
.createAccessToken("test", authentication, true))
.doOnNext(System.out::println)
.as(StepVerifier::create)
.expectNextCount(2)
.verifyComplete();
}
}

View File

@@ -0,0 +1,25 @@
package org.hswebframework.web.oauth2.server.web;
import org.junit.Test;
import java.util.Collections;
import static org.junit.Assert.*;
public class OAuth2AuthorizeControllerTest {
@Test
public void testBuildRedirect() {
String url = OAuth2AuthorizeController.buildRedirect("http://hsweb.me/callback", Collections.singletonMap("code", "1234"));
assertEquals(url,"http://hsweb.me/callback?code=1234");
}
@Test
public void testBuildRedirectParam() {
String url = OAuth2AuthorizeController.buildRedirect("http://hsweb.me/callback?a=b", Collections.singletonMap("code", "1234"));
assertEquals(url,"http://hsweb.me/callback?a=b&code=1234");
}
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<logger name="io.netty" level="warn"/>
 
<logger name="io.lettuce" level="warn"/>
 <logger name="org.springframework" level="warn"/>
 
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hsweb-system-authorization</artifactId>
<groupId>org.hswebframework.web</groupId>
<version>4.0.8-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>hsweb-system-authorization-oauth2</artifactId>
<dependencies>
<dependency>
<groupId>org.hswebframework.web</groupId>
<artifactId>hsweb-commons-crud</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.hswebframework.web</groupId>
<artifactId>hsweb-authorization-oauth2</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hswebframework.web</groupId>
<artifactId>hsweb-authorization-api</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,37 @@
package org.hswebframework.web.oauth2.configuration;
import org.hswebframework.web.oauth2.server.OAuth2ClientManager;
import org.hswebframework.web.oauth2.service.InDBOAuth2ClientManager;
import org.hswebframework.web.oauth2.service.OAuth2ClientService;
import org.hswebframework.web.oauth2.web.WebFluxOAuth2ClientController;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class OAuth2ClientManagerAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
static class ReactiveOAuth2ClientManagerAutoConfiguration {
@Bean
public OAuth2ClientService oAuth2ClientService() {
return new OAuth2ClientService();
}
@Bean
@ConditionalOnMissingBean
public OAuth2ClientManager oAuth2ClientManager(OAuth2ClientService clientService) {
return new InDBOAuth2ClientManager(clientService);
}
@Bean
@ConditionalOnMissingBean
public WebFluxOAuth2ClientController webFluxOAuth2ClientController(OAuth2ClientService clientService){
return new WebFluxOAuth2ClientController(clientService);
}
}
}

View File

@@ -0,0 +1,84 @@
package org.hswebframework.web.oauth2.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec;
import org.hswebframework.web.api.crud.entity.GenericEntity;
import org.hswebframework.web.bean.ToString;
import org.hswebframework.web.crud.generator.Generators;
import org.hswebframework.web.oauth2.enums.OAuth2ClientState;
import org.hswebframework.web.oauth2.server.OAuth2Client;
import javax.persistence.Column;
import javax.persistence.Table;
import javax.validation.constraints.NotBlank;
@Table(name = "s_oauth2_client")
@Getter
@Setter
public class OAuth2ClientEntity extends GenericEntity<String> {
@Column(length = 1024)
@Schema(description = "Logo地址")
private String logoUrl;
@Column(length = 64, nullable = false)
@Schema(description = "客户端名称")
@NotBlank
private String name;
@Column(length = 128, nullable = false)
@Schema(description = "密钥")
@NotBlank
@ToString.Ignore
private String secret;
@Column(length = 64, nullable = false)
@Schema(description = "绑定用户ID")
@NotBlank
private String userId;
@Column(length = 1024, nullable = false)
@Schema(description = "回调地址")
@NotBlank
private String callbackUri;
@Column(length = 1024, nullable = false)
@Schema(description = "首页地址")
@NotBlank
private String homeUri;
@Column
@Schema(description = "说明")
private String description;
@Column(length = 32)
@EnumCodec
@ColumnType(javaType = String.class)
@DefaultValue("enabled")
@Schema(description = "状态")
private OAuth2ClientState state;
@Column(nullable = false)
@Schema(description = "创建时间")
@DefaultValue(generator = Generators.CURRENT_TIME)
private Long createTime;
public boolean enabled() {
return state == OAuth2ClientState.enabled;
}
public OAuth2Client toOAuth2Client() {
OAuth2Client client = new OAuth2Client();
client.setClientSecret(secret);
client.setClientId(getId());
client.setName(getName());
client.setRedirectUrl(callbackUri);
client.setDescription(description);
client.setUserId(userId);
return client;
}
}

View File

@@ -0,0 +1,20 @@
package org.hswebframework.web.oauth2.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.hswebframework.web.dict.EnumDict;
@Getter
@AllArgsConstructor
public enum OAuth2ClientState implements EnumDict<String> {
enabled("启用"),
disabled("禁用");
private final String text;
@Override
public String getValue() {
return name();
}
}

View File

@@ -0,0 +1,21 @@
package org.hswebframework.web.oauth2.service;
import lombok.AllArgsConstructor;
import org.hswebframework.web.oauth2.entity.OAuth2ClientEntity;
import org.hswebframework.web.oauth2.server.OAuth2Client;
import org.hswebframework.web.oauth2.server.OAuth2ClientManager;
import reactor.core.publisher.Mono;
@AllArgsConstructor
public class InDBOAuth2ClientManager implements OAuth2ClientManager {
private final OAuth2ClientService clientService;
@Override
public Mono<OAuth2Client> getClient(String clientId) {
return clientService
.findById(clientId)
.filter(OAuth2ClientEntity::enabled)
.map(OAuth2ClientEntity::toOAuth2Client);
}
}

View File

@@ -0,0 +1,12 @@
package org.hswebframework.web.oauth2.service;
import org.hswebframework.web.crud.service.GenericReactiveCacheSupportCrudService;
import org.hswebframework.web.oauth2.entity.OAuth2ClientEntity;
public class OAuth2ClientService extends GenericReactiveCacheSupportCrudService<OAuth2ClientEntity, String> {
@Override
public String getCacheName() {
return "oauth2-client";
}
}

View File

@@ -0,0 +1,26 @@
package org.hswebframework.web.oauth2.web;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.hswebframework.web.authorization.annotation.Resource;
import org.hswebframework.web.crud.service.ReactiveCrudService;
import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
import org.hswebframework.web.oauth2.entity.OAuth2ClientEntity;
import org.hswebframework.web.oauth2.service.OAuth2ClientService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/oauth2/client")
@AllArgsConstructor
@Resource(id = "oauth2-client", name = "OAuth2客户端管理")
@Tag(name = "OAuth2客户端管理")
public class WebFluxOAuth2ClientController implements ReactiveServiceCrudController<OAuth2ClientEntity, String> {
private final OAuth2ClientService oAuth2ClientService;
@Override
public ReactiveCrudService<OAuth2ClientEntity, String> getService() {
return oAuth2ClientService;
}
}

View File

@@ -0,0 +1,3 @@
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.hswebframework.web.oauth2.configuration.OAuth2ClientManagerAutoConfiguration

View File

@@ -0,0 +1,22 @@
package org.hswebframework.web.oauth2;
import org.hswebframework.web.authorization.simple.DefaultAuthorizationAutoConfiguration;
import org.hswebframework.web.crud.configuration.JdbcSqlExecutorConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcTransactionManagerAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication(exclude = {
//TransactionAutoConfiguration.class,
JdbcSqlExecutorConfiguration.class,
DataSourceAutoConfiguration.class
})
@ImportAutoConfiguration({
R2dbcTransactionManagerAutoConfiguration.class,
DefaultAuthorizationAutoConfiguration.class
})
public class ReactiveTestApplication {
}

View File

@@ -0,0 +1,24 @@
package org.hswebframework.web.oauth2.configuration;
import org.hswebframework.web.oauth2.ReactiveTestApplication;
import org.hswebframework.web.oauth2.server.OAuth2ClientManager;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ReactiveTestApplication.class)
public class OAuth2ClientManagerAutoConfigurationTest {
@Autowired
OAuth2ClientManager clientManager;
@Test
public void test(){
assertNotNull(clientManager);
}
}

View File

@@ -0,0 +1,46 @@
package org.hswebframework.web.oauth2.service;
import org.hswebframework.web.oauth2.ReactiveTestApplication;
import org.hswebframework.web.oauth2.entity.OAuth2ClientEntity;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ReactiveTestApplication.class)
public class OAuth2ClientServiceTest {
@Autowired
OAuth2ClientService clientService;
@Test
public void test() {
OAuth2ClientEntity clientEntity = new OAuth2ClientEntity();
clientEntity.setId("test");
clientEntity.setHomeUri("http://hsweb.me");
clientEntity.setCallbackUri("http://hsweb.me/callback");
clientEntity.setSecret("test");
clientEntity.setName("test");
clientEntity.setUserId("admin");
clientService.insert(Mono.just(clientEntity))
.as(StepVerifier::create)
.expectNext(1)
.verifyComplete();
clientService.findById("test")
.doOnNext(System.out::println)
.as(StepVerifier::create)
.expectNextMatches(client -> {
return client.getCreateTime() != null && client.getState() != null;
}).verifyComplete();
}
}

View File

@@ -0,0 +1,16 @@
logging:
level:
org.hswebframework: debug
org.springframework.transaction: debug
org.springframework.data.r2dbc.connectionfactory: debug
#spring:
# r2dbc:
spring:
aop:
proxy-target-class: true
hsweb:
authorize:
auto-parse: true
easyorm:
default-schema: PUBLIC
dialect: h2