新增资源服务和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;
|
||||
|
||||
public class AbstractBusinessException extends RuntimeException{
|
||||
public class AbstractBusinessException extends RuntimeException {
|
||||
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.exception.AbstractBusinessException;
|
||||
|
||||
/**
|
||||
* 权限异常处理类
|
||||
* @author zhuyijun
|
||||
*/
|
||||
public class AuthRuntimeException extends AbstractBusinessException {
|
||||
public class AuthRuntimeException extends AbstractBusinessException{
|
||||
public AuthRuntimeException() {
|
||||
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)) {
|
||||
log.info("token过期");
|
||||
return getErrorMono(response, HttpCode.UNAUTHORIZED, "invalid_token");
|
||||
return getErrorMono(response, HttpCode.UNAUTHORIZED, "token失效");
|
||||
}
|
||||
if ("/user/login".equals(path)) {
|
||||
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()
|
||||
.antMatchers("/**")
|
||||
.authenticated()
|
||||
// .anyRequest().permitAll()
|
||||
// .access("#oauth2.hasAnyScope('all')")
|
||||
.anyRequest().permitAll()
|
||||
.and()
|
||||
.csrf().disable();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,19 +20,17 @@ public class OauthAccessTokenConverter extends DefaultAccessTokenConverter {
|
||||
private static final Logger log = LoggerFactory.getLogger(OauthAccessTokenConverter.class);
|
||||
|
||||
@Override
|
||||
public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
|
||||
return super.convertAccessToken(token, authentication);
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -2,4 +2,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
cn.zyjblogs.starter.oauth.config.OauthFeignInterceptorAutoConfiguration,\
|
||||
cn.zyjblogs.starter.oauth.resource.ResourceServerConfig,\
|
||||
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) {
|
||||
throw new RuntimeException("获取不到公私密钥");
|
||||
}
|
||||
DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
|
||||
OauthAccessTokenConverter accessTokenConverter = new OauthAccessTokenConverter();
|
||||
accessTokenConverter.setUserTokenConverter(oauthUserAuthenticationConverter);
|
||||
converter.setAccessTokenConverter(accessTokenConverter);
|
||||
return converter;
|
||||
|
@ -2,6 +2,8 @@ package cn.zyjblogs.oauth.config.security;
|
||||
|
||||
import cn.zyjblogs.starter.common.entity.constant.ContextKeyConstant;
|
||||
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.OAuth2AccessToken;
|
||||
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);
|
||||
if (user == null){
|
||||
this.logger.debug("用户不存在");
|
||||
throw new UsernameNotFoundException("用户不存在");
|
||||
throw new RuntimeException("用户不存在");
|
||||
}
|
||||
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.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.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
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.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
@ -17,15 +20,11 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Copyright (C), 2021, 北京同创永益科技发展有限公司
|
||||
*
|
||||
* @author zhuyijun
|
||||
* @version 3.0.0
|
||||
* @description
|
||||
* @date 2022/8/24 9:20
|
||||
*/
|
||||
@Component
|
||||
public class OauthUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
|
||||
public class OauthUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
|
||||
public OauthUserAuthenticationConverter(UserDetailsService userDetailsService){
|
||||
setUserDetailsService(userDetailsService);
|
||||
}
|
||||
@ -56,7 +55,6 @@ public class OauthUserAuthenticationConverter extends DefaultUserAuthenticationC
|
||||
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
|
||||
response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
|
||||
}
|
||||
|
||||
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();
|
||||
//使HttpSecurity接收以"/login/","/oauth/"开头请求, 配置HttpSecurity不阻止swagger页面
|
||||
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()
|
||||
//以下请求必须认证通过
|
||||
.antMatchers("/demo/**", "/oauth/**", "/login")
|
||||
.antMatchers( "/oauth/**", "/login")
|
||||
.authenticated()
|
||||
.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();
|
||||
queryWrapper.eq(UserPo::getUsername,s);
|
||||
UserPo userPo = userService.getBaseMapper().selectOne(queryWrapper);
|
||||
if (userPo == null){
|
||||
return null;
|
||||
}
|
||||
OauthUserDetails oauthUserDetails = new OauthUserDetails();
|
||||
BeanUtils.copyProperties(userPo, 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
|
||||
|
||||
---
|
||||
|
||||
spring:
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
application:
|
||||
name: zyjblogs-oauth
|
||||
cloud:
|
||||
|
@ -24,6 +24,7 @@ public class UserController {
|
||||
public UserPo findById(String id){
|
||||
log.info(BaseContext.getUserId());
|
||||
log.info(BaseContext.getUsername());
|
||||
log.info(BaseContext.getToken());
|
||||
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