新增资源服务和token,配置上下文
This commit is contained in:
parent
1a0675643b
commit
832a972466
@ -2,7 +2,7 @@ package cn.zyjblogs.starter.common.exception;
|
|||||||
|
|
||||||
import cn.zyjblogs.starter.common.entity.response.HttpCode;
|
import cn.zyjblogs.starter.common.entity.response.HttpCode;
|
||||||
|
|
||||||
public class AbstractBusinessException extends RuntimeException{
|
public class AbstractBusinessException extends RuntimeException {
|
||||||
private static final long serialVersionUID = -6583471361241853199L;
|
private static final long serialVersionUID = -6583471361241853199L;
|
||||||
/**
|
/**
|
||||||
* 异常码
|
* 异常码
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
package cn.zyjblogs.starter.oauth.exception;
|
package cn.zyjblogs.starter.common.exception;
|
||||||
|
|
||||||
import cn.zyjblogs.starter.common.entity.response.HttpCode;
|
import cn.zyjblogs.starter.common.entity.response.HttpCode;
|
||||||
import cn.zyjblogs.starter.common.exception.AbstractBusinessException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* 权限异常处理类
|
||||||
* @author zhuyijun
|
* @author zhuyijun
|
||||||
*/
|
*/
|
||||||
public class AuthRuntimeException extends AbstractBusinessException {
|
public class AuthRuntimeException extends AbstractBusinessException{
|
||||||
public AuthRuntimeException() {
|
public AuthRuntimeException() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
@ -0,0 +1,267 @@
|
|||||||
|
package cn.zyjblogs.starter.common.utils.jwt;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.*;
|
||||||
|
import io.jsonwebtoken.impl.DefaultClaims;
|
||||||
|
import io.jsonwebtoken.impl.DefaultClock;
|
||||||
|
import io.jsonwebtoken.impl.DefaultHeader;
|
||||||
|
import io.jsonwebtoken.impl.DefaultJws;
|
||||||
|
import io.jsonwebtoken.impl.DefaultJwsHeader;
|
||||||
|
import io.jsonwebtoken.impl.DefaultJwt;
|
||||||
|
import io.jsonwebtoken.impl.DefaultJwtParser;
|
||||||
|
import io.jsonwebtoken.impl.TextCodec;
|
||||||
|
import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver;
|
||||||
|
import io.jsonwebtoken.impl.crypto.JwtSignatureValidator;
|
||||||
|
import io.jsonwebtoken.lang.Assert;
|
||||||
|
import io.jsonwebtoken.lang.Objects;
|
||||||
|
import io.jsonwebtoken.lang.Strings;
|
||||||
|
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.security.Key;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author zhuyijun
|
||||||
|
*/
|
||||||
|
public class JwtParsers extends DefaultJwtParser {
|
||||||
|
|
||||||
|
private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
|
||||||
|
private final long allowedClockSkewMillis = 0;
|
||||||
|
private final CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver();
|
||||||
|
private final Clock clock = DefaultClock.INSTANCE;
|
||||||
|
private final boolean checkExpired;
|
||||||
|
private byte[] keyBytes;
|
||||||
|
private Key key;
|
||||||
|
private SigningKeyResolver signingKeyResolver;
|
||||||
|
|
||||||
|
public JwtParsers(boolean checkExpired) {
|
||||||
|
this.checkExpired = checkExpired;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException {
|
||||||
|
Assert.hasText(jwt, "JWT String argument cannot be null or empty.");
|
||||||
|
|
||||||
|
String base64UrlEncodedHeader = null;
|
||||||
|
String base64UrlEncodedPayload = null;
|
||||||
|
String base64UrlEncodedDigest = null;
|
||||||
|
|
||||||
|
int delimiterCount = 0;
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder(128);
|
||||||
|
|
||||||
|
for (char c : jwt.toCharArray()) {
|
||||||
|
|
||||||
|
if (c == SEPARATOR_CHAR) {
|
||||||
|
|
||||||
|
CharSequence tokenSeq = Strings.clean(sb);
|
||||||
|
String token = tokenSeq != null ? tokenSeq.toString() : null;
|
||||||
|
|
||||||
|
if (delimiterCount == 0) {
|
||||||
|
base64UrlEncodedHeader = token;
|
||||||
|
} else if (delimiterCount == 1) {
|
||||||
|
base64UrlEncodedPayload = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
delimiterCount++;
|
||||||
|
sb.setLength(0);
|
||||||
|
} else {
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delimiterCount != 2) {
|
||||||
|
String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount;
|
||||||
|
throw new MalformedJwtException(msg);
|
||||||
|
}
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
base64UrlEncodedDigest = sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (base64UrlEncodedPayload == null) {
|
||||||
|
throw new MalformedJwtException("JWT string '" + jwt + "' is missing a body/payload.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============== Header =================
|
||||||
|
Header header = null;
|
||||||
|
|
||||||
|
CompressionCodec compressionCodec = null;
|
||||||
|
|
||||||
|
if (base64UrlEncodedHeader != null) {
|
||||||
|
String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader);
|
||||||
|
Map<String, Object> m = readValue(origValue);
|
||||||
|
|
||||||
|
if (base64UrlEncodedDigest != null) {
|
||||||
|
header = new DefaultJwsHeader(m);
|
||||||
|
} else {
|
||||||
|
header = new DefaultHeader(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
compressionCodec = compressionCodecResolver.resolveCompressionCodec(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============== Body =================
|
||||||
|
String payload;
|
||||||
|
if (compressionCodec != null) {
|
||||||
|
byte[] decompressed = compressionCodec.decompress(TextCodec.BASE64URL.decode(base64UrlEncodedPayload));
|
||||||
|
payload = new String(decompressed, Strings.UTF_8);
|
||||||
|
} else {
|
||||||
|
payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
Claims claims = null;
|
||||||
|
|
||||||
|
if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it:
|
||||||
|
Map<String, Object> claimsMap = readValue(payload);
|
||||||
|
claims = new DefaultClaims(claimsMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============== Signature =================
|
||||||
|
if (base64UrlEncodedDigest != null) { //it is signed - validate the signature
|
||||||
|
|
||||||
|
JwsHeader jwsHeader = (JwsHeader) header;
|
||||||
|
|
||||||
|
SignatureAlgorithm algorithm = null;
|
||||||
|
|
||||||
|
if (header != null) {
|
||||||
|
String alg = jwsHeader.getAlgorithm();
|
||||||
|
if (Strings.hasText(alg)) {
|
||||||
|
algorithm = SignatureAlgorithm.forName(alg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (algorithm == null || algorithm == SignatureAlgorithm.NONE) {
|
||||||
|
//it is plaintext, but it has a signature. This is invalid:
|
||||||
|
String msg = "JWT string has a digest/signature, but the header does not reference a valid signature " +
|
||||||
|
"algorithm.";
|
||||||
|
throw new MalformedJwtException(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key != null && keyBytes != null) {
|
||||||
|
throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either.");
|
||||||
|
} else if ((key != null || keyBytes != null) && signingKeyResolver != null) {
|
||||||
|
String object = key != null ? "a key object" : "key bytes";
|
||||||
|
throw new IllegalStateException("A signing key resolver and " + object + " cannot both be specified. Choose either.");
|
||||||
|
}
|
||||||
|
|
||||||
|
//digitally signed, let's assert the signature:
|
||||||
|
Key key = this.key;
|
||||||
|
|
||||||
|
if (key == null) { //fall back to keyBytes
|
||||||
|
|
||||||
|
byte[] keyBytes = this.keyBytes;
|
||||||
|
|
||||||
|
if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver
|
||||||
|
if (claims != null) {
|
||||||
|
key = signingKeyResolver.resolveSigningKey(jwsHeader, claims);
|
||||||
|
} else {
|
||||||
|
key = signingKeyResolver.resolveSigningKey(jwsHeader, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Objects.isEmpty(keyBytes)) {
|
||||||
|
|
||||||
|
Assert.isTrue(algorithm.isHmac(),
|
||||||
|
"Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance.");
|
||||||
|
|
||||||
|
key = new SecretKeySpec(keyBytes, algorithm.getJcaName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed.");
|
||||||
|
|
||||||
|
//re-create the jwt part without the signature. This is what needs to be signed for verification:
|
||||||
|
String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload;
|
||||||
|
|
||||||
|
JwtSignatureValidator validator;
|
||||||
|
try {
|
||||||
|
validator = createSignatureValidator(algorithm, key);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
String algName = algorithm.getValue();
|
||||||
|
String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " +
|
||||||
|
"algorithm, but the specified signing key of type " + key.getClass().getName() +
|
||||||
|
" may not be used to validate " + algName + " signatures. Because the specified " +
|
||||||
|
"signing key reflects a specific and expected algorithm, and the JWT does not reflect " +
|
||||||
|
"this algorithm, it is likely that the JWT was not expected and therefore should not be " +
|
||||||
|
"trusted. Another possibility is that the parser was configured with the incorrect " +
|
||||||
|
"signing key, but this cannot be assumed for security reasons.";
|
||||||
|
throw new UnsupportedJwtException(msg, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validator.isValid(jwtWithoutSignature, base64UrlEncodedDigest)) {
|
||||||
|
String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " +
|
||||||
|
"asserted and should not be trusted.";
|
||||||
|
throw new SignatureException(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean allowSkew = this.allowedClockSkewMillis > 0;
|
||||||
|
|
||||||
|
//since 0.3:
|
||||||
|
if (claims != null) {
|
||||||
|
|
||||||
|
SimpleDateFormat sdf;
|
||||||
|
|
||||||
|
final Date now = this.clock.now();
|
||||||
|
long nowTime = now.getTime();
|
||||||
|
|
||||||
|
Date exp = claims.getExpiration();
|
||||||
|
if (exp != null && checkExpired) {
|
||||||
|
|
||||||
|
long maxTime = nowTime - this.allowedClockSkewMillis;
|
||||||
|
Date max = allowSkew ? new Date(maxTime) : now;
|
||||||
|
if (max.after(exp)) {
|
||||||
|
sdf = new SimpleDateFormat(ISO_8601_FORMAT);
|
||||||
|
String expVal = sdf.format(exp);
|
||||||
|
String nowVal = sdf.format(now);
|
||||||
|
|
||||||
|
long differenceMillis = maxTime - exp.getTime();
|
||||||
|
|
||||||
|
String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + ", a difference of " +
|
||||||
|
differenceMillis + " milliseconds. Allowed clock skew: " +
|
||||||
|
this.allowedClockSkewMillis + " milliseconds.";
|
||||||
|
throw new ExpiredJwtException(header, claims, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Date nbf = claims.getNotBefore();
|
||||||
|
if (nbf != null) {
|
||||||
|
|
||||||
|
long minTime = nowTime + this.allowedClockSkewMillis;
|
||||||
|
Date min = allowSkew ? new Date(minTime) : now;
|
||||||
|
if (min.before(nbf)) {
|
||||||
|
sdf = new SimpleDateFormat(ISO_8601_FORMAT);
|
||||||
|
String nbfVal = sdf.format(nbf);
|
||||||
|
String nowVal = sdf.format(now);
|
||||||
|
|
||||||
|
long differenceMillis = nbf.getTime() - minTime;
|
||||||
|
|
||||||
|
String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal +
|
||||||
|
", a difference of " +
|
||||||
|
differenceMillis + " milliseconds. Allowed clock skew: " +
|
||||||
|
this.allowedClockSkewMillis + " milliseconds.";
|
||||||
|
throw new PrematureJwtException(header, claims, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//validateExpectedClaims(header, claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object body = claims != null ? claims : payload;
|
||||||
|
|
||||||
|
if (base64UrlEncodedDigest != null) {
|
||||||
|
return new DefaultJws<Object>((JwsHeader) header, body, base64UrlEncodedDigest);
|
||||||
|
} else {
|
||||||
|
return new DefaultJwt<Object>(header, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JwtParser setSigningKey(Key key) {
|
||||||
|
Assert.notNull(key, "signing key cannot be null.");
|
||||||
|
this.key = key;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
@ -68,7 +68,7 @@ public class AuthFilter implements GlobalFilter {
|
|||||||
}
|
}
|
||||||
if (isExpired(token)) {
|
if (isExpired(token)) {
|
||||||
log.info("token过期");
|
log.info("token过期");
|
||||||
return getErrorMono(response, HttpCode.UNAUTHORIZED, "invalid_token");
|
return getErrorMono(response, HttpCode.UNAUTHORIZED, "token失效");
|
||||||
}
|
}
|
||||||
if ("/user/login".equals(path)) {
|
if ("/user/login".equals(path)) {
|
||||||
return chain.filter(exchange);
|
return chain.filter(exchange);
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
package cn.zyjblogs.starter.oauth.config;
|
||||||
|
|
||||||
|
import cn.zyjblogs.starter.common.entity.constant.ContextKeyConstant;
|
||||||
|
import cn.zyjblogs.starter.common.entity.constant.HttpHeaderConstant;
|
||||||
|
import cn.zyjblogs.starter.common.entity.context.BaseContext;
|
||||||
|
import cn.zyjblogs.starter.common.entity.dto.ContextDto;
|
||||||
|
import cn.zyjblogs.starter.common.entity.response.HttpCode;
|
||||||
|
import cn.zyjblogs.starter.common.exception.AuthRuntimeException;
|
||||||
|
import cn.zyjblogs.starter.common.utils.jwt.JwtParsers;
|
||||||
|
import cn.zyjblogs.starter.common.utils.rsa.RsaUtils;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class JwtTokenHandlerInterceptor extends HandlerInterceptorAdapter {
|
||||||
|
|
||||||
|
private boolean checkInner;
|
||||||
|
|
||||||
|
public JwtTokenHandlerInterceptor(boolean checkInner) {
|
||||||
|
this.checkInner = checkInner;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||||
|
String authorization = request.getHeader(HttpHeaderConstant.AUTHORIZATION);
|
||||||
|
//token为空直接放行
|
||||||
|
if (StringUtils.isEmpty(authorization)) {
|
||||||
|
BaseContext.set(ContextDto.builder().build());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.preHandle(request, response, handler);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package cn.zyjblogs.starter.oauth.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author zhuyijun
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@Order(100)
|
||||||
|
public class OauthInterceptorAutoConfiguration implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 忽略的请求,该请求不校验token
|
||||||
|
*/
|
||||||
|
private List<String> ignoreUrl = List.of("/user/login");
|
||||||
|
|
||||||
|
@Value("${zyjblogs.config.check-inner:true}")
|
||||||
|
private boolean checkInner;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
registry.addInterceptor(new JwtTokenHandlerInterceptor(checkInner))
|
||||||
|
.addPathPatterns("/**")
|
||||||
|
.excludePathPatterns(ignoreUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||||
|
converters.add(0, this.mappingJackson2HttpMessageConverter());
|
||||||
|
}
|
||||||
|
|
||||||
|
private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
|
||||||
|
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
return new MappingJackson2HttpMessageConverter(mapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -42,11 +42,9 @@ public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
|
|||||||
.authorizeRequests()
|
.authorizeRequests()
|
||||||
.antMatchers("/**")
|
.antMatchers("/**")
|
||||||
.authenticated()
|
.authenticated()
|
||||||
// .anyRequest().permitAll()
|
.anyRequest().permitAll()
|
||||||
// .access("#oauth2.hasAnyScope('all')")
|
|
||||||
.and()
|
.and()
|
||||||
.csrf().disable();
|
.csrf().disable();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,19 +20,17 @@ public class OauthAccessTokenConverter extends DefaultAccessTokenConverter {
|
|||||||
private static final Logger log = LoggerFactory.getLogger(OauthAccessTokenConverter.class);
|
private static final Logger log = LoggerFactory.getLogger(OauthAccessTokenConverter.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
|
public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) {
|
||||||
return super.convertAccessToken(token, authentication);
|
String userId = (String) map.get(ContextKeyConstant.USER_ID_KEY);
|
||||||
|
String username = (String) map.get(ContextKeyConstant.USERNAME_KEY);
|
||||||
|
BaseContext.set(ContextDto.builder().userId(userId).username(username).token(value).build());
|
||||||
|
return super.extractAccessToken(value, map);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
|
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
|
||||||
OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
|
OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
|
||||||
oAuth2Authentication.setDetails(map);
|
oAuth2Authentication.setDetails(map);
|
||||||
String userId = (String) map.get(ContextKeyConstant.USER_ID_KEY);
|
|
||||||
String username = (String) map.get(ContextKeyConstant.USERNAME_KEY);
|
|
||||||
BaseContext.set(ContextDto.builder().userId(userId).username(username).token("").build());
|
|
||||||
log.info("进入");
|
|
||||||
return oAuth2Authentication;
|
return oAuth2Authentication;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,4 +2,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
|||||||
cn.zyjblogs.starter.oauth.config.OauthFeignInterceptorAutoConfiguration,\
|
cn.zyjblogs.starter.oauth.config.OauthFeignInterceptorAutoConfiguration,\
|
||||||
cn.zyjblogs.starter.oauth.resource.ResourceServerConfig,\
|
cn.zyjblogs.starter.oauth.resource.ResourceServerConfig,\
|
||||||
cn.zyjblogs.starter.oauth.security.TokenConfig,\
|
cn.zyjblogs.starter.oauth.security.TokenConfig,\
|
||||||
cn.zyjblogs.starter.oauth.security.OauthAccessTokenConverter
|
cn.zyjblogs.starter.oauth.security.OauthAccessTokenConverter,\
|
||||||
|
cn.zyjblogs.starter.oauth.config.OauthInterceptorAutoConfiguration
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
package cn.zyjblogs.oauth.config.redis;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
|
||||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author zhuyijun
|
|
||||||
* @version 3.0.0
|
|
||||||
* @description redis配置
|
|
||||||
* @date 2022/8/17 17:58
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class RedisConfig {
|
|
||||||
@Bean
|
|
||||||
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
|
|
||||||
StringRedisSerializer keySerializer = new StringRedisSerializer();
|
|
||||||
Jackson2JsonRedisSerializer<Object> valSerializer = new Jackson2JsonRedisSerializer(Object.class);
|
|
||||||
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate();
|
|
||||||
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
|
||||||
redisTemplate.setKeySerializer(keySerializer);
|
|
||||||
redisTemplate.setValueSerializer(valSerializer);
|
|
||||||
redisTemplate.setHashKeySerializer(valSerializer);
|
|
||||||
redisTemplate.setHashValueSerializer(valSerializer);
|
|
||||||
redisTemplate.afterPropertiesSet();
|
|
||||||
return redisTemplate;
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,9 +0,0 @@
|
|||||||
package cn.zyjblogs.oauth.config.redis.lock;
|
|
||||||
|
|
||||||
public abstract class AbstractLockExecutor<T> implements LockExecutor<T> {
|
|
||||||
|
|
||||||
protected T obtainLockInstance(boolean locked, T lockInstance) {
|
|
||||||
return locked ? lockInstance : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package cn.zyjblogs.oauth.config.redis.lock;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class DefaultLockKeyBuilder implements LockKeyBuilder {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String buildKey(Collection<String> definitionKeys) {
|
|
||||||
if (CollectionUtils.isEmpty(definitionKeys)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return StringUtils.collectionToDelimitedString(definitionKeys, ".", "", "");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
package cn.zyjblogs.oauth.config.redis.lock;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copyright (C), 2021, 北京同创永益科技发展有限公司
|
|
||||||
*
|
|
||||||
* @author zhuyijun
|
|
||||||
* @version 3.0.0
|
|
||||||
* @description 分布式锁核心处理器
|
|
||||||
* @date 2022/5/23 10:26
|
|
||||||
*/
|
|
||||||
public interface LockExecutor<T> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加锁
|
|
||||||
*
|
|
||||||
* @param lockKey 锁标识
|
|
||||||
* @param lockValue 锁值
|
|
||||||
* @param expire 锁有效时间
|
|
||||||
* @param acquireTimeout 获取锁超时时间
|
|
||||||
* @return 锁信息
|
|
||||||
*/
|
|
||||||
T acquire(String lockKey, String lockValue, long expire, long acquireTimeout);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解锁
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* 为何解锁需要校验lockValue
|
|
||||||
* 客户端A加锁,一段时间之后客户端A解锁,在执行releaseLock之前,锁突然过期了。
|
|
||||||
* 此时客户端B尝试加锁成功,然后客户端A再执行releaseLock方法,则将客户端B的锁给解除了。
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @param key 加锁key
|
|
||||||
* @param value 加锁value
|
|
||||||
* @param lockInstance 锁实例
|
|
||||||
* @return 是否释放成功
|
|
||||||
*/
|
|
||||||
boolean releaseLock(String key, String value, T lockInstance);
|
|
||||||
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
package cn.zyjblogs.oauth.config.redis.lock;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
public interface LockKeyBuilder {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建key
|
|
||||||
*
|
|
||||||
* @param definitionKeys 定义
|
|
||||||
* @return key
|
|
||||||
*/
|
|
||||||
String buildKey(Collection<String> definitionKeys);
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
package cn.zyjblogs.oauth.config.redis.lock;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copyright (C), 2021, 北京同创永益科技发展有限公司
|
|
||||||
*
|
|
||||||
* @author zhuyijun
|
|
||||||
* @version 3.0.0
|
|
||||||
* @description
|
|
||||||
* @date 2022/5/23 11:01
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Builder
|
|
||||||
public class RedisLock {
|
|
||||||
/**
|
|
||||||
* 锁名称
|
|
||||||
*/
|
|
||||||
private String lockKey;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 锁值
|
|
||||||
*/
|
|
||||||
private String lockValue;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 过期时间
|
|
||||||
*/
|
|
||||||
private Long expire;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取锁超时时间
|
|
||||||
*/
|
|
||||||
private Long acquireTimeout;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取锁次数
|
|
||||||
*/
|
|
||||||
private int acquireCount;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 锁实例
|
|
||||||
*/
|
|
||||||
private Object lockInstance;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 锁执行器
|
|
||||||
*/
|
|
||||||
private LockExecutor lockExecutor;
|
|
||||||
|
|
||||||
/***
|
|
||||||
* 解锁
|
|
||||||
* @author zhuyijun
|
|
||||||
* @Description
|
|
||||||
* @date 15:10
|
|
||||||
*/
|
|
||||||
public void unLock() {
|
|
||||||
if (lockExecutor != null && lockInstance != null && !StringUtils.isEmpty(lockKey)) {
|
|
||||||
lockExecutor.releaseLock(lockKey, lockValue, lockInstance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,128 +0,0 @@
|
|||||||
package cn.zyjblogs.oauth.config.redis.lock;
|
|
||||||
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copyright (C), 2021, 北京同创永益科技发展有限公司
|
|
||||||
*
|
|
||||||
* @author zhuyijun
|
|
||||||
* @version 3.0.0
|
|
||||||
* @description
|
|
||||||
* @date 2022/5/23 11:02
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class RedisLockTemplate {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private LockExecutor<String> RedisTemplateLockExecutor;
|
|
||||||
/**
|
|
||||||
* 默认失效时间
|
|
||||||
*/
|
|
||||||
@Value("${hatech.redis.lock-expire:3000}")
|
|
||||||
private Long defaultExpire;
|
|
||||||
|
|
||||||
@Value("${hatech.redis.lock-timeout:30000}")
|
|
||||||
private Long defaultAcquireTimeout;
|
|
||||||
/**
|
|
||||||
* 获取锁失败时重试时间间隔 单位:毫秒
|
|
||||||
*/
|
|
||||||
@Value("${hatech.redis.lock-retry:100}")
|
|
||||||
private Long retryInterval;
|
|
||||||
|
|
||||||
@Value("${hatech.redis.lock-prefix:lock}")
|
|
||||||
private String prefix;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取锁(推荐不推荐)
|
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* @param expire
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public RedisLock lock(String key, long expire) {
|
|
||||||
return tryLock(key, expire, 0, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 尝试获取锁方法(推荐)
|
|
||||||
*
|
|
||||||
* @param key 锁key 同一个key只能被一个客户端持有
|
|
||||||
* @param expire 过期时间(ms) 防止死锁
|
|
||||||
* @param acquireTimeout 尝试获取锁超时时间(ms)
|
|
||||||
* @return 加锁成功返回锁信息 失败返回null
|
|
||||||
*/
|
|
||||||
public RedisLock tryLock(String key, long expire, long acquireTimeout) {
|
|
||||||
return tryLock(key, expire, acquireTimeout, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加锁方法
|
|
||||||
*
|
|
||||||
* @param key 锁key 同一个key只能被一个客户端持有
|
|
||||||
* @param expire 过期时间(ms) 防止死锁
|
|
||||||
* @param acquireTimeout 尝试获取锁超时时间(ms)
|
|
||||||
* @param executor 执行器
|
|
||||||
* @return 加锁成功返回锁信息 失败返回null
|
|
||||||
*/
|
|
||||||
public RedisLock tryLock(String key, long expire, long acquireTimeout, LockExecutor executor) {
|
|
||||||
key = prefix + key;
|
|
||||||
LockExecutor lockExecutor = obtainExecutor(executor);
|
|
||||||
expire = expire < 0 ? defaultExpire : expire;
|
|
||||||
long currentAcquireTimeout = acquireTimeout < 0 ? defaultAcquireTimeout : acquireTimeout;
|
|
||||||
int acquireCount = 0;
|
|
||||||
String value = IdWorker.get32UUID();
|
|
||||||
long start = System.currentTimeMillis();
|
|
||||||
try {
|
|
||||||
do {
|
|
||||||
acquireCount++;
|
|
||||||
Object lockInstance = lockExecutor.acquire(key, value, expire, acquireTimeout);
|
|
||||||
if (null != lockInstance) {
|
|
||||||
return RedisLock.builder()
|
|
||||||
.lockKey(key)
|
|
||||||
.lockValue(value)
|
|
||||||
.expire(expire)
|
|
||||||
.acquireTimeout(currentAcquireTimeout)
|
|
||||||
.acquireCount(acquireCount)
|
|
||||||
.lockInstance(lockInstance)
|
|
||||||
.lockExecutor(lockExecutor)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
TimeUnit.MILLISECONDS.sleep(retryInterval);
|
|
||||||
} while (System.currentTimeMillis() - start < currentAcquireTimeout);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
log.error("lock error", e);
|
|
||||||
throw new RuntimeException("加锁失败");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected LockExecutor obtainExecutor(LockExecutor lockExecutor) {
|
|
||||||
if (null == lockExecutor) {
|
|
||||||
return RedisTemplateLockExecutor;
|
|
||||||
}
|
|
||||||
return lockExecutor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/***
|
|
||||||
* 解锁
|
|
||||||
* @param redisLock
|
|
||||||
* @author zhuyijun
|
|
||||||
* @Description
|
|
||||||
* @date 11:29
|
|
||||||
*/
|
|
||||||
public boolean unLock(RedisLock redisLock) {
|
|
||||||
if (null == redisLock) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return redisLock.getLockExecutor().releaseLock(redisLock.getLockKey(), redisLock.getLockValue(),
|
|
||||||
redisLock.getLockInstance());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
package cn.zyjblogs.oauth.config.redis.lock;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
|
||||||
import org.springframework.data.redis.core.script.RedisScript;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class RedisTemplateLockExecutor extends AbstractLockExecutor<String> {
|
|
||||||
|
|
||||||
private static final RedisScript<String> SCRIPT_LOCK = new DefaultRedisScript<>("return redis.call('set',KEYS[1]," +
|
|
||||||
"ARGV[1],'NX','PX',ARGV[2])", String.class);
|
|
||||||
private static final RedisScript<String> SCRIPT_UNLOCK = new DefaultRedisScript<>("if redis.call('get',KEYS[1]) " +
|
|
||||||
"== ARGV[1] then return tostring(redis.call('del', KEYS[1])==1) else return 'false' end", String.class);
|
|
||||||
private static final String LOCK_SUCCESS = "OK";
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private RedisTemplate<String, String> redisTemplate;
|
|
||||||
|
|
||||||
/***
|
|
||||||
* 枷锁
|
|
||||||
* @param lockKey
|
|
||||||
* @param lockValue
|
|
||||||
* @param expire
|
|
||||||
* @param acquireTimeout
|
|
||||||
* @author zhuyijun
|
|
||||||
* @Description
|
|
||||||
* @date 10:26
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String acquire(String lockKey, String lockValue, long expire, long acquireTimeout) {
|
|
||||||
String lock = redisTemplate.execute(SCRIPT_LOCK,
|
|
||||||
redisTemplate.getStringSerializer(),
|
|
||||||
redisTemplate.getStringSerializer(),
|
|
||||||
Collections.singletonList(lockKey),
|
|
||||||
lockValue, String.valueOf(expire));
|
|
||||||
final boolean locked = LOCK_SUCCESS.equals(lock);
|
|
||||||
return obtainLockInstance(locked, lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
/***
|
|
||||||
* 解锁
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
* @param lockInstance
|
|
||||||
* @author zhuyijun
|
|
||||||
* @Description
|
|
||||||
* @date 10:26
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean releaseLock(String key, String value, String lockInstance) {
|
|
||||||
String releaseResult = redisTemplate.execute(SCRIPT_UNLOCK,
|
|
||||||
redisTemplate.getStringSerializer(),
|
|
||||||
redisTemplate.getStringSerializer(),
|
|
||||||
Collections.singletonList(key), value);
|
|
||||||
return Boolean.parseBoolean(releaseResult);
|
|
||||||
}
|
|
||||||
}
|
|
@ -47,7 +47,7 @@ public class JwtTokenConfig {
|
|||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
throw new RuntimeException("获取不到公私密钥");
|
throw new RuntimeException("获取不到公私密钥");
|
||||||
}
|
}
|
||||||
DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
|
OauthAccessTokenConverter accessTokenConverter = new OauthAccessTokenConverter();
|
||||||
accessTokenConverter.setUserTokenConverter(oauthUserAuthenticationConverter);
|
accessTokenConverter.setUserTokenConverter(oauthUserAuthenticationConverter);
|
||||||
converter.setAccessTokenConverter(accessTokenConverter);
|
converter.setAccessTokenConverter(accessTokenConverter);
|
||||||
return converter;
|
return converter;
|
||||||
|
@ -2,6 +2,8 @@ package cn.zyjblogs.oauth.config.security;
|
|||||||
|
|
||||||
import cn.zyjblogs.starter.common.entity.constant.ContextKeyConstant;
|
import cn.zyjblogs.starter.common.entity.constant.ContextKeyConstant;
|
||||||
import cn.zyjblogs.oauth.server.user.po.OauthUserDetails;
|
import cn.zyjblogs.oauth.server.user.po.OauthUserDetails;
|
||||||
|
import cn.zyjblogs.starter.common.entity.context.BaseContext;
|
||||||
|
import cn.zyjblogs.starter.common.entity.dto.ContextDto;
|
||||||
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
|
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
|
||||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||||
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
package cn.zyjblogs.oauth.config.security;
|
||||||
|
|
||||||
|
import cn.zyjblogs.starter.common.entity.constant.ContextKeyConstant;
|
||||||
|
import cn.zyjblogs.starter.common.entity.context.BaseContext;
|
||||||
|
import cn.zyjblogs.starter.common.entity.dto.ContextDto;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||||
|
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
||||||
|
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author zhuyijun
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class OauthAccessTokenConverter extends DefaultAccessTokenConverter {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(OauthAccessTokenConverter.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) {
|
||||||
|
String userId = (String) map.get(ContextKeyConstant.USER_ID_KEY);
|
||||||
|
String username = (String) map.get(ContextKeyConstant.USERNAME_KEY);
|
||||||
|
BaseContext.set(ContextDto.builder().userId(userId).username(username).token(value).build());
|
||||||
|
return super.extractAccessToken(value, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
|
||||||
|
OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
|
||||||
|
oAuth2Authentication.setDetails(map);
|
||||||
|
return oAuth2Authentication;
|
||||||
|
}
|
||||||
|
}
|
@ -50,7 +50,7 @@ public class OauthAuthenticationProvider extends DaoAuthenticationProvider {
|
|||||||
UserDetails user = userDetailsService.loadUserByUsername(username);
|
UserDetails user = userDetailsService.loadUserByUsername(username);
|
||||||
if (user == null){
|
if (user == null){
|
||||||
this.logger.debug("用户不存在");
|
this.logger.debug("用户不存在");
|
||||||
throw new UsernameNotFoundException("用户不存在");
|
throw new RuntimeException("用户不存在");
|
||||||
}
|
}
|
||||||
OauthUserDetails userDetails = (OauthUserDetails) user;
|
OauthUserDetails userDetails = (OauthUserDetails) user;
|
||||||
//比较前端传入的密码明文和数据库中加密的密码是否相等
|
//比较前端传入的密码明文和数据库中加密的密码是否相等
|
||||||
|
@ -2,11 +2,14 @@ package cn.zyjblogs.oauth.config.security;
|
|||||||
|
|
||||||
import cn.zyjblogs.starter.common.entity.constant.ContextKeyConstant;
|
import cn.zyjblogs.starter.common.entity.constant.ContextKeyConstant;
|
||||||
import cn.zyjblogs.oauth.server.user.po.OauthUserDetails;
|
import cn.zyjblogs.oauth.server.user.po.OauthUserDetails;
|
||||||
|
import cn.zyjblogs.starter.common.entity.context.BaseContext;
|
||||||
|
import cn.zyjblogs.starter.common.entity.dto.ContextDto;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.AuthorityUtils;
|
import org.springframework.security.core.authority.AuthorityUtils;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
|
||||||
import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;
|
import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
@ -17,15 +20,11 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright (C), 2021, 北京同创永益科技发展有限公司
|
|
||||||
*
|
*
|
||||||
* @author zhuyijun
|
* @author zhuyijun
|
||||||
* @version 3.0.0
|
|
||||||
* @description
|
|
||||||
* @date 2022/8/24 9:20
|
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class OauthUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
|
public class OauthUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
|
||||||
public OauthUserAuthenticationConverter(UserDetailsService userDetailsService){
|
public OauthUserAuthenticationConverter(UserDetailsService userDetailsService){
|
||||||
setUserDetailsService(userDetailsService);
|
setUserDetailsService(userDetailsService);
|
||||||
}
|
}
|
||||||
@ -56,7 +55,6 @@ public class OauthUserAuthenticationConverter extends DefaultUserAuthenticationC
|
|||||||
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
|
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
|
||||||
response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
|
response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
package cn.zyjblogs.oauth.config.security;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
|
||||||
|
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
|
||||||
|
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
|
||||||
|
import org.springframework.security.oauth2.provider.token.TokenStore;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableResourceServer
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RefreshScope
|
||||||
|
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
|
||||||
|
@Value("${spring.application.name}")
|
||||||
|
private String resourceId;
|
||||||
|
private final TokenStore tokenStore;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
|
||||||
|
resources.resourceId(resourceId)
|
||||||
|
// 验证令牌的服务
|
||||||
|
.tokenStore(tokenStore)
|
||||||
|
.stateless(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(HttpSecurity http) throws Exception {
|
||||||
|
http.sessionManagement()
|
||||||
|
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||||
|
.and()
|
||||||
|
.authorizeRequests()
|
||||||
|
.antMatchers("/user/login")
|
||||||
|
.permitAll()
|
||||||
|
.and()
|
||||||
|
.csrf().disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -61,10 +61,10 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
|
|||||||
http.csrf().disable();
|
http.csrf().disable();
|
||||||
//使HttpSecurity接收以"/login/","/oauth/"开头请求, 配置HttpSecurity不阻止swagger页面
|
//使HttpSecurity接收以"/login/","/oauth/"开头请求, 配置HttpSecurity不阻止swagger页面
|
||||||
http.authorizeRequests()
|
http.authorizeRequests()
|
||||||
.antMatchers("/webjars/**", "/swagger-ui.html/**", "/swagger-resources/**", "/v2/api-docs/**")
|
.antMatchers("/user/login","/webjars/**", "/swagger-ui.html/**", "/swagger-resources/**", "/v2/api-docs/**")
|
||||||
.permitAll()
|
.permitAll()
|
||||||
//以下请求必须认证通过
|
//以下请求必须认证通过
|
||||||
.antMatchers("/demo/**", "/oauth/**", "/login")
|
.antMatchers( "/oauth/**", "/login")
|
||||||
.authenticated()
|
.authenticated()
|
||||||
.and()
|
.and()
|
||||||
//允许表单登录
|
//允许表单登录
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
package cn.zyjblogs.oauth.server.user.controller;
|
||||||
|
|
||||||
|
import cn.zyjblogs.oauth.server.user.dto.UserLoginDto;
|
||||||
|
import cn.zyjblogs.oauth.server.user.service.LoginService;
|
||||||
|
import cn.zyjblogs.oauth.server.user.vo.OAuth2AccessTokenVo;
|
||||||
|
import cn.zyjblogs.starter.common.entity.response.ResponseObject;
|
||||||
|
import cn.zyjblogs.starter.common.entity.response.ResponseResult;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author zhuyijun
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("/user")
|
||||||
|
public class LoginController {
|
||||||
|
private final LoginService loginService;
|
||||||
|
|
||||||
|
@ApiOperation(value = "用户登录", notes = "用户登录")
|
||||||
|
@PostMapping("/login")
|
||||||
|
public ResponseObject<OAuth2AccessTokenVo> login(@RequestBody @Validated UserLoginDto userLoginDto) {
|
||||||
|
return ResponseResult.success(loginService.login(userLoginDto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "用户注销", notes = "用户注销")
|
||||||
|
@PostMapping("/logout")
|
||||||
|
public void logout() {
|
||||||
|
loginService.logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package cn.zyjblogs.oauth.server.user.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author zhuyijun
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class UserLoginDto implements Serializable {
|
||||||
|
/**
|
||||||
|
* 账号 (手机号 邮箱 用户名)
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
/**
|
||||||
|
* 密码
|
||||||
|
*/
|
||||||
|
private String password;
|
||||||
|
/**
|
||||||
|
* 登录类型
|
||||||
|
*/
|
||||||
|
private Integer type;
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package cn.zyjblogs.oauth.server.user.service;
|
||||||
|
|
||||||
|
import cn.zyjblogs.oauth.server.user.dto.UserLoginDto;
|
||||||
|
import cn.zyjblogs.oauth.server.user.vo.OAuth2AccessTokenVo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录
|
||||||
|
* @author zhuyijun
|
||||||
|
*/
|
||||||
|
public interface LoginService {
|
||||||
|
/**
|
||||||
|
* 登录接口
|
||||||
|
* @param userLoginDto
|
||||||
|
* @author zhuyijun
|
||||||
|
* @date 2022/9/17 下午5:11
|
||||||
|
* @return cn.zyjblogs.oauth.server.user.vo.OAuth2AccessTokenVo
|
||||||
|
*/
|
||||||
|
OAuth2AccessTokenVo login(UserLoginDto userLoginDto);
|
||||||
|
/**
|
||||||
|
* 退出
|
||||||
|
* @param
|
||||||
|
* @author zhuyijun
|
||||||
|
* @date 2022/9/17 下午5:53
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
void logout();
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
package cn.zyjblogs.oauth.server.user.service.impl;
|
||||||
|
|
||||||
|
import cn.zyjblogs.oauth.server.user.dto.UserLoginDto;
|
||||||
|
import cn.zyjblogs.oauth.server.user.service.LoginService;
|
||||||
|
import cn.zyjblogs.oauth.server.user.vo.OAuth2AccessTokenVo;
|
||||||
|
import cn.zyjblogs.starter.common.entity.constant.HttpHeaderConstant;
|
||||||
|
import cn.zyjblogs.starter.common.entity.context.BaseContext;
|
||||||
|
import cn.zyjblogs.starter.common.entity.dto.ContextDto;
|
||||||
|
import cn.zyjblogs.starter.common.entity.response.HttpCode;
|
||||||
|
import cn.zyjblogs.starter.common.exception.AuthRuntimeException;
|
||||||
|
import cn.zyjblogs.starter.common.utils.bean.BeanUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
|
||||||
|
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||||
|
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration;
|
||||||
|
import org.springframework.security.oauth2.provider.ClientDetails;
|
||||||
|
import org.springframework.security.oauth2.provider.ClientDetailsService;
|
||||||
|
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
|
||||||
|
import org.springframework.security.oauth2.provider.TokenGranter;
|
||||||
|
import org.springframework.security.oauth2.provider.TokenRequest;
|
||||||
|
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
|
||||||
|
import org.springframework.security.oauth2.provider.token.TokenStore;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author zhuyijun
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class LoginServiceImpl implements LoginService {
|
||||||
|
private final TokenGranter tokenGranter;
|
||||||
|
private final ClientDetailsService clientDetails;
|
||||||
|
private final OAuth2RequestFactory oAuth2RequestFactory;
|
||||||
|
private final TokenStore tokenStore;
|
||||||
|
public LoginServiceImpl(AuthorizationServerEndpointsConfiguration authorizationServerEndpointsConfiguration,
|
||||||
|
ClientDetailsService clientDetails,
|
||||||
|
TokenStore tokenStore){
|
||||||
|
this.tokenGranter = authorizationServerEndpointsConfiguration.getEndpointsConfigurer().getTokenGranter();
|
||||||
|
this.clientDetails = clientDetails;
|
||||||
|
this.oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetails);
|
||||||
|
this.tokenStore = tokenStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Value("${spring.application.name}")
|
||||||
|
private String clientId;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OAuth2AccessTokenVo login(UserLoginDto userLoginDto) {
|
||||||
|
|
||||||
|
Map<String, String> parameters = BeanUtils.map(userLoginDto, Map.class);
|
||||||
|
parameters.put("grant_type", "password");
|
||||||
|
ClientDetails authenticatedClient = clientDetails.loadClientByClientId(clientId);
|
||||||
|
TokenRequest tokenRequest = oAuth2RequestFactory.createTokenRequest(parameters, authenticatedClient);
|
||||||
|
OAuth2AccessToken token = tokenGranter.grant(tokenRequest.getGrantType(), tokenRequest);
|
||||||
|
if (token == null) {
|
||||||
|
throw new AuthRuntimeException(HttpCode.INTERNAL_SERVER_ERROR, "客户端获取token失败");
|
||||||
|
}
|
||||||
|
return transferToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout(){
|
||||||
|
String token = BaseContext.getToken();
|
||||||
|
if (StringUtils.isEmpty(token)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
token = token.replace(HttpHeaderConstant.AUTHORIZATION_TYPE, "").trim();
|
||||||
|
OAuth2AccessToken oAuth2AccessToken = new DefaultOAuth2AccessToken(token);
|
||||||
|
tokenStore.removeAccessToken(oAuth2AccessToken);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 处理token
|
||||||
|
* @param token
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private OAuth2AccessTokenVo transferToken(OAuth2AccessToken token){
|
||||||
|
OAuth2AccessTokenVo oAuth2AccessTokenVo = new OAuth2AccessTokenVo(
|
||||||
|
token.getValue(),
|
||||||
|
token.getTokenType(),
|
||||||
|
token.getRefreshToken().getValue(),
|
||||||
|
token.getExpiresIn(),
|
||||||
|
token.getScope(),
|
||||||
|
token.getAdditionalInformation());
|
||||||
|
BaseContext.set(ContextDto.builder()
|
||||||
|
.userId(oAuth2AccessTokenVo.getUserId())
|
||||||
|
.username(oAuth2AccessTokenVo.getUsername())
|
||||||
|
.token(oAuth2AccessTokenVo.getValue())
|
||||||
|
.build());
|
||||||
|
return oAuth2AccessTokenVo;
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,9 @@ public class OauthUserDetailsServiceImpl implements UserDetailsService {
|
|||||||
LambdaQueryWrapper<UserPo> queryWrapper = Wrappers.lambdaQuery();
|
LambdaQueryWrapper<UserPo> queryWrapper = Wrappers.lambdaQuery();
|
||||||
queryWrapper.eq(UserPo::getUsername,s);
|
queryWrapper.eq(UserPo::getUsername,s);
|
||||||
UserPo userPo = userService.getBaseMapper().selectOne(queryWrapper);
|
UserPo userPo = userService.getBaseMapper().selectOne(queryWrapper);
|
||||||
|
if (userPo == null){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
OauthUserDetails oauthUserDetails = new OauthUserDetails();
|
OauthUserDetails oauthUserDetails = new OauthUserDetails();
|
||||||
BeanUtils.copyProperties(userPo, oauthUserDetails);
|
BeanUtils.copyProperties(userPo, oauthUserDetails);
|
||||||
return oauthUserDetails;
|
return oauthUserDetails;
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
package cn.zyjblogs.oauth.server.user.vo;
|
||||||
|
|
||||||
|
import cn.zyjblogs.starter.common.entity.constant.ContextKeyConstant;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author zhuyijun
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class OAuth2AccessTokenVo implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "access token", dataType = "String", example = "abc.efg.hjk")
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "token类型", dataType = "String", example = "bearer")
|
||||||
|
private String token_type;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "刷新 token", dataType = "String", example = "abc.efg.hjk")
|
||||||
|
private String refresh_token;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "token过期剩余时间(秒)", dataType = "Integer", example = "43199")
|
||||||
|
private Integer expires_in;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "token作用域", dataType = "String", example = "server")
|
||||||
|
private String scope;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "token唯一标识", dataType = "String", example = "18b82fad-0b9b-401f-a3ab-1b3592c7f267")
|
||||||
|
private String jti;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "用户id", dataType = "String", example = "0997eb541e59cc2d704a0f29708710fe")
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "用户账号", dataType = "String", example = "hangman")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
|
||||||
|
public OAuth2AccessTokenVo(String value, String tokenType, String refreshToken, int expiresIn, Set<String> scope, Map<String, Object> addition) {
|
||||||
|
this.value = value;
|
||||||
|
this.token_type = tokenType;
|
||||||
|
this.refresh_token = refreshToken;
|
||||||
|
this.expires_in = expiresIn;
|
||||||
|
this.scope = String.join(" ", scope);
|
||||||
|
this.userId = (String) addition.get(ContextKeyConstant.USER_ID_KEY);
|
||||||
|
this.username = (String) addition.get(ContextKeyConstant.USERNAME_KEY);
|
||||||
|
this.jti = (String) addition.get("jti");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -3,7 +3,10 @@ spring:
|
|||||||
active: test
|
active: test
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
|
main:
|
||||||
|
allow-bean-definition-overriding: true
|
||||||
application:
|
application:
|
||||||
name: zyjblogs-oauth
|
name: zyjblogs-oauth
|
||||||
cloud:
|
cloud:
|
||||||
|
@ -24,6 +24,7 @@ public class UserController {
|
|||||||
public UserPo findById(String id){
|
public UserPo findById(String id){
|
||||||
log.info(BaseContext.getUserId());
|
log.info(BaseContext.getUserId());
|
||||||
log.info(BaseContext.getUsername());
|
log.info(BaseContext.getUsername());
|
||||||
|
log.info(BaseContext.getToken());
|
||||||
return userService.getById(id);
|
return userService.getById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
#Generated by Maven
|
|
||||||
#Mon Jul 25 15:29:06 CST 2022
|
|
||||||
groupId=cn.zyjblogs.starter
|
|
||||||
artifactId=zyjblogs-web-spring-boot-starter
|
|
||||||
version=1.0-SNAPSHOT
|
|
@ -1,59 +0,0 @@
|
|||||||
<?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>zyjblogs-parent</artifactId>
|
|
||||||
<groupId>cn.zyjblogs</groupId>
|
|
||||||
<version>1.0-SNAPSHOT</version>
|
|
||||||
</parent>
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
|
|
||||||
<artifactId>zyjblogs-web-spring-boot-starter</artifactId>
|
|
||||||
<groupId>cn.zyjblogs.starter</groupId>
|
|
||||||
<version>1.0-SNAPSHOT</version>
|
|
||||||
|
|
||||||
<properties>
|
|
||||||
<maven.compiler.source>11</maven.compiler.source>
|
|
||||||
<maven.compiler.target>11</maven.compiler.target>
|
|
||||||
</properties>
|
|
||||||
<dependencies>
|
|
||||||
<!-- Spring 集成 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.xiaoymin</groupId>
|
|
||||||
<artifactId>knife4j-spring-boot-starter</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.zyjblogs.starter</groupId>
|
|
||||||
<artifactId>zyjblogs-common-spring-boot-starter</artifactId>
|
|
||||||
<version>${zyjblogs.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.cloud</groupId>
|
|
||||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
|
||||||
<configuration>
|
|
||||||
<skip>true</skip>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
</project>
|
|
Loading…
Reference in New Issue
Block a user