新增资源服务和token,配置上下文

This commit is contained in:
zhuyijun 2022-09-17 23:41:37 +08:00
parent 1a0675643b
commit 832a972466
35 changed files with 714 additions and 1641 deletions

View File

@ -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;
/**
* 异常码

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -42,11 +42,9 @@ public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
.authorizeRequests()
.antMatchers("/**")
.authenticated()
// .anyRequest().permitAll()
// .access("#oauth2.hasAnyScope('all')")
.anyRequest().permitAll()
.and()
.csrf().disable();
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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, ".", "", "");
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
//比较前端传入的密码明文和数据库中加密的密码是否相等

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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()
//允许表单登录

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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");
}
}

View File

@ -3,7 +3,10 @@ spring:
active: test
---
spring:
main:
allow-bean-definition-overriding: true
application:
name: zyjblogs-oauth
cloud:

View File

@ -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);
}

View File

@ -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

View File

@ -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>