Commit 3eb444be by tangyi

修复refresh token失败问题

parent 21f4f0c1
......@@ -21,17 +21,17 @@ public enum LoginTypeEnum {
/**
* 验证码登录
*/
SMS("SMS", "验证码登录", "/api/v1/mobile/token"),
SMS("SMS", "验证码登录", "/mobile/token"),
/**
* QQ登录
*/
QQ("QQ", "QQ登录", "/api/v1/mobile/token"),
QQ("QQ", "QQ登录", "/mobile/token"),
/**
* 微信登录
*/
WECHAT("WX", "微信登录", "/api/v1/wx/token");
WECHAT("WX", "微信登录", "/wx/token");
/**
* 类型
......
......@@ -29,12 +29,12 @@ public class SecurityConstant {
/**
* 手机登录URL
*/
public static final String MOBILE_TOKEN_URL = "/api/v1/mobile/token";
public static final String MOBILE_TOKEN_URL = "/mobile/token";
/**
* 微信登录URL
*/
public static final String WX_TOKEN_URL = "/api/v1/wx/token";
public static final String WX_TOKEN_URL = "/wx/token";
/**
* 租户编号请求头
......
......@@ -3,6 +3,7 @@ package com.github.tangyi.common.security.core;
import com.github.tangyi.common.security.mobile.MobileUser;
import com.github.tangyi.common.security.wx.WxUser;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
......@@ -11,7 +12,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
* @author tangyi
* @date 2019/5/28 21:05
*/
public interface CustomUserDetailsService {
public interface CustomUserDetailsService extends UserDetailsService {
/**
* 根据用户名和租户标识查询
......
......@@ -110,6 +110,8 @@ ignore:
- /health
- /metrics/**
- /loggers/**
- /mobile/token
- /wx/token
# 集群ID生成配置
cluster:
......
......@@ -117,6 +117,9 @@ ignore:
- /
- /error
- /favicon.ico
- /csrf
- /*swagger*/**
- /**/*swagger*/**
- /actuator/**
- /api/auth/**
- /api/user/**
......
......@@ -11,7 +11,7 @@ Auth Service
#### 账号密码+图片验证码登录
POST:`/api/auth/api/v1//token?grant_type=password&scope=read&username=admin&credential=lBTqrKS0kZixOFXeZ0HRng==&randomStr=86111562225179514&code=mf3f`
POST:`/api/auth/api/v1/token?grant_type=password&scope=read&username=admin&credential=lBTqrKS0kZixOFXeZ0HRng==&randomStr=86111562225179514&code=mf3f`
url参数:
```
......
package com.github.tangyi.auth.config;
import com.github.tangyi.auth.security.CustomTokenConverter;
import com.github.tangyi.auth.security.CustomTokenServices;
import com.github.tangyi.auth.security.CustomUserDetailsByNameServiceWrapper;
import com.github.tangyi.common.security.core.ClientDetailsServiceImpl;
import com.github.tangyi.common.security.core.CustomUserDetailsService;
import com.github.tangyi.common.security.exception.CustomOauthException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWKSet;
......@@ -15,7 +18,9 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.core.token.TokenService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
......@@ -29,10 +34,12 @@ import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import javax.sql.DataSource;
import java.security.KeyPair;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.Collections;
/**
......@@ -61,13 +68,16 @@ public class CustomAuthorizationServerConfigurer extends AuthorizationServerConf
*/
private final KeyProperties keyProperties;
private final CustomUserDetailsService userDetailsService;
@Autowired
public CustomAuthorizationServerConfigurer(RedisConnectionFactory redisConnectionFactory,
DataSource dataSource,
KeyProperties keyProperties) {
KeyProperties keyProperties, CustomUserDetailsService userDetailsService) {
this.redisConnectionFactory = redisConnectionFactory;
this.dataSource = dataSource;
this.keyProperties = keyProperties;
this.userDetailsService = userDetailsService;
}
/**
......@@ -81,6 +91,36 @@ public class CustomAuthorizationServerConfigurer extends AuthorizationServerConf
}
/**
* 防止刷新token是调用默认的loadUserByUsername,需要自定义tokenService
* @param endpoints endpoints
* @return CustomTokenServices
*/
private CustomTokenServices tokenService(AuthorizationServerEndpointsConfigurer endpoints) {
CustomTokenServices tokenServices = new CustomTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(true);
tokenServices.setAccessTokenValiditySeconds(-1);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(jwtTokenEnhancer());
addUserDetailsService(tokenServices, userDetailsService);
return tokenServices;
}
/**
* PreAuthenticatedAuthenticationProvider自定义userDetailsService
* @param tokenServices tokenServices
* @param userDetailsService userDetailsService
*/
private void addUserDetailsService(CustomTokenServices tokenServices, CustomUserDetailsService userDetailsService) {
if (userDetailsService != null) {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new CustomUserDetailsByNameServiceWrapper<>(userDetailsService));
tokenServices.setAuthenticationManager(new ProviderManager(Collections.singletonList(provider)));
}
}
/**
* 生成KeyPair
* @return KeyPair
*/
......@@ -141,6 +181,7 @@ public class CustomAuthorizationServerConfigurer extends AuthorizationServerConf
.authenticationManager(this.authenticationConfiguration.getAuthenticationManager())
// 将token存储到redis
.tokenStore(tokenStore())
.tokenServices(tokenService(endpoints))
// token增强
.tokenEnhancer(jwtTokenEnhancer())
// 异常转换
......
package com.github.tangyi.auth.security;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.*;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.common.exceptions.InvalidScopeException;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.*;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import java.util.Date;
import java.util.Set;
import java.util.UUID;
/**
* @author tangyi
* @date 2020-04-19 11:32
*/
public class CustomTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,
ConsumerTokenServices, InitializingBean {
private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.
private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.
private boolean supportRefreshToken = false;
private boolean reuseRefreshToken = true;
private TokenStore tokenStore;
private ClientDetailsService clientDetailsService;
private TokenEnhancer accessTokenEnhancer;
private AuthenticationManager authenticationManager;
/**
* Initialize these token services. If no random generator is set, one will be created.
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(tokenStore, "tokenStore must be set");
}
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
if (existingAccessToken != null) {
if (existingAccessToken.isExpired()) {
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
// The token store could remove the refresh token when the
// access token is removed, but we want to
// be sure...
tokenStore.removeRefreshToken(refreshToken);
}
tokenStore.removeAccessToken(existingAccessToken);
}
else {
// Re-store the access token in case the authentication has changed
tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
}
// Only create a new refresh token if there wasn't an existing one
// associated with an expired access token.
// Clients might be holding existing refresh tokens, so we re-use it in
// the case that the old access token
// expired.
if (refreshToken == null) {
refreshToken = createRefreshToken(authentication);
}
// But the refresh token itself might need to be re-issued if it has
// expired.
else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
}
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
// In case it was modified
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
@Transactional(noRollbackFor={InvalidTokenException.class, InvalidGrantException.class})
public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest)
throws AuthenticationException {
if (!supportRefreshToken) {
throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
}
OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue);
if (refreshToken == null) {
throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
}
OAuth2Authentication authentication = tokenStore.readAuthenticationForRefreshToken(refreshToken);
if (this.authenticationManager != null && !authentication.isClientOnly()) {
// The client has already been authenticated, but the user authentication might be old now, so give it a
// chance to re-authenticate.
Authentication user = new PreAuthenticatedAuthenticationToken(authentication.getUserAuthentication(), "", authentication.getAuthorities());
user = authenticationManager.authenticate(user);
Object details = authentication.getDetails();
authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user);
authentication.setDetails(details);
}
String clientId = authentication.getOAuth2Request().getClientId();
if (clientId == null || !clientId.equals(tokenRequest.getClientId())) {
throw new InvalidGrantException("Wrong client for this refresh token: " + refreshTokenValue);
}
// clear out any access tokens already associated with the refresh
// token.
tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);
if (isExpired(refreshToken)) {
tokenStore.removeRefreshToken(refreshToken);
throw new InvalidTokenException("Invalid refresh token (expired): " + refreshToken);
}
authentication = createRefreshedAuthentication(authentication, tokenRequest);
if (!reuseRefreshToken) {
tokenStore.removeRefreshToken(refreshToken);
refreshToken = createRefreshToken(authentication);
}
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
if (!reuseRefreshToken) {
tokenStore.storeRefreshToken(accessToken.getRefreshToken(), authentication);
}
return accessToken;
}
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
return tokenStore.getAccessToken(authentication);
}
/**
* Create a refreshed authentication.
*
* @param authentication The authentication.
* @param request The scope for the refreshed token.
* @return The refreshed authentication.
* @throws InvalidScopeException If the scope requested is invalid or wider than the original scope.
*/
private OAuth2Authentication createRefreshedAuthentication(OAuth2Authentication authentication, TokenRequest request) {
OAuth2Authentication narrowed = authentication;
Set<String> scope = request.getScope();
OAuth2Request clientAuth = authentication.getOAuth2Request().refresh(request);
if (scope != null && !scope.isEmpty()) {
Set<String> originalScope = clientAuth.getScope();
if (originalScope == null || !originalScope.containsAll(scope)) {
throw new InvalidScopeException("Unable to narrow the scope of the client authentication to " + scope
+ ".", originalScope);
}
else {
clientAuth = clientAuth.narrowScope(scope);
}
}
narrowed = new OAuth2Authentication(clientAuth, authentication.getUserAuthentication());
return narrowed;
}
protected boolean isExpired(OAuth2RefreshToken refreshToken) {
if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiringToken = (ExpiringOAuth2RefreshToken) refreshToken;
return expiringToken.getExpiration() == null
|| System.currentTimeMillis() > expiringToken.getExpiration().getTime();
}
return false;
}
public OAuth2AccessToken readAccessToken(String accessToken) {
return tokenStore.readAccessToken(accessToken);
}
public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException,
InvalidTokenException {
OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
if (accessToken == null) {
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
}
else if (accessToken.isExpired()) {
tokenStore.removeAccessToken(accessToken);
throw new InvalidTokenException("Access token expired: " + accessTokenValue);
}
OAuth2Authentication result = tokenStore.readAuthentication(accessToken);
if (result == null) {
// in case of race condition
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
}
if (clientDetailsService != null) {
String clientId = result.getOAuth2Request().getClientId();
try {
clientDetailsService.loadClientByClientId(clientId);
}
catch (ClientRegistrationException e) {
throw new InvalidTokenException("Client not valid: " + clientId, e);
}
}
return result;
}
public String getClientId(String tokenValue) {
OAuth2Authentication authentication = tokenStore.readAuthentication(tokenValue);
if (authentication == null) {
throw new InvalidTokenException("Invalid access token: " + tokenValue);
}
OAuth2Request clientAuth = authentication.getOAuth2Request();
if (clientAuth == null) {
throw new InvalidTokenException("Invalid access token (no client id): " + tokenValue);
}
return clientAuth.getClientId();
}
public boolean revokeToken(String tokenValue) {
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
if (accessToken == null) {
return false;
}
if (accessToken.getRefreshToken() != null) {
tokenStore.removeRefreshToken(accessToken.getRefreshToken());
}
tokenStore.removeAccessToken(accessToken);
return true;
}
private OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) {
if (!isSupportRefreshToken(authentication.getOAuth2Request())) {
return null;
}
int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request());
String value = UUID.randomUUID().toString();
if (validitySeconds > 0) {
return new DefaultExpiringOAuth2RefreshToken(value, new Date(System.currentTimeMillis()
+ (validitySeconds * 1000L)));
}
return new DefaultOAuth2RefreshToken(value);
}
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}
/**
* The access token validity period in seconds
*
* @param clientAuth the current authorization request
* @return the access token validity period in seconds
*/
protected int getAccessTokenValiditySeconds(OAuth2Request clientAuth) {
if (clientDetailsService != null) {
ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());
Integer validity = client.getAccessTokenValiditySeconds();
if (validity != null) {
return validity;
}
}
return accessTokenValiditySeconds;
}
/**
* The refresh token validity period in seconds
*
* @param clientAuth the current authorization request
* @return the refresh token validity period in seconds
*/
protected int getRefreshTokenValiditySeconds(OAuth2Request clientAuth) {
if (clientDetailsService != null) {
ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());
Integer validity = client.getRefreshTokenValiditySeconds();
if (validity != null) {
return validity;
}
}
return refreshTokenValiditySeconds;
}
/**
* Is a refresh token supported for this client (or the global setting if
* {@link #setClientDetailsService(ClientDetailsService) clientDetailsService} is not set.
*
* @param clientAuth the current authorization request
* @return boolean to indicate if refresh token is supported
*/
protected boolean isSupportRefreshToken(OAuth2Request clientAuth) {
if (clientDetailsService != null) {
ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());
return client.getAuthorizedGrantTypes().contains("refresh_token");
}
return this.supportRefreshToken;
}
/**
* An access token enhancer that will be applied to a new token before it is saved in the token store.
*
* @param accessTokenEnhancer the access token enhancer to set
*/
public void setTokenEnhancer(TokenEnhancer accessTokenEnhancer) {
this.accessTokenEnhancer = accessTokenEnhancer;
}
/**
* The validity (in seconds) of the refresh token. If less than or equal to zero then the tokens will be
* non-expiring.
*
* @param refreshTokenValiditySeconds The validity (in seconds) of the refresh token.
*/
public void setRefreshTokenValiditySeconds(int refreshTokenValiditySeconds) {
this.refreshTokenValiditySeconds = refreshTokenValiditySeconds;
}
/**
* The default validity (in seconds) of the access token. Zero or negative for non-expiring tokens. If a client
* details service is set the validity period will be read from the client, defaulting to this value if not defined
* by the client.
*
* @param accessTokenValiditySeconds The validity (in seconds) of the access token.
*/
public void setAccessTokenValiditySeconds(int accessTokenValiditySeconds) {
this.accessTokenValiditySeconds = accessTokenValiditySeconds;
}
/**
* Whether to support the refresh token.
*
* @param supportRefreshToken Whether to support the refresh token.
*/
public void setSupportRefreshToken(boolean supportRefreshToken) {
this.supportRefreshToken = supportRefreshToken;
}
/**
* Whether to reuse refresh tokens (until expired).
*
* @param reuseRefreshToken Whether to reuse refresh tokens (until expired).
*/
public void setReuseRefreshToken(boolean reuseRefreshToken) {
this.reuseRefreshToken = reuseRefreshToken;
}
/**
* The persistence strategy for token storage.
*
* @param tokenStore the store for access and refresh tokens.
*/
public void setTokenStore(TokenStore tokenStore) {
this.tokenStore = tokenStore;
}
/**
* An authentication manager that will be used (if provided) to check the user authentication when a token is
* refreshed.
*
* @param authenticationManager the authenticationManager to set
*/
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
/**
* The client details service to use for looking up clients (if necessary). Optional if the access token expiry is
* set globally via {@link #setAccessTokenValiditySeconds(int)}.
*
* @param clientDetailsService the client details service
*/
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
this.clientDetailsService = clientDetailsService;
}
}
package com.github.tangyi.auth.security;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.Assert;
/**
* @author tangyi
* @date 2020-04-19 11:32
*/
public class CustomUserDetailsByNameServiceWrapper<T extends Authentication> implements AuthenticationUserDetailsService<T>, InitializingBean {
private UserDetailsService userDetailsService = null;
public CustomUserDetailsByNameServiceWrapper() {
}
public CustomUserDetailsByNameServiceWrapper(UserDetailsService userDetailsService) {
Assert.notNull(userDetailsService, "userDetailsService cannot be null.");
this.userDetailsService = userDetailsService;
}
public void afterPropertiesSet() {
Assert.notNull(this.userDetailsService, "UserDetailsService must be set");
}
public UserDetails loadUserDetails(T authentication) throws UsernameNotFoundException {
return this.userDetailsService.loadUserByUsername(authentication.getName());
}
public void setUserDetailsService(UserDetailsService aUserDetailsService) {
this.userDetailsService = aUserDetailsService;
}
}
......@@ -15,6 +15,7 @@ import com.github.tangyi.common.core.utils.ResponseUtil;
import com.github.tangyi.common.security.core.CustomUserDetailsService;
import com.github.tangyi.common.security.core.GrantedAuthorityImpl;
import com.github.tangyi.common.security.mobile.MobileUser;
import com.github.tangyi.common.security.tenant.TenantContextHolder;
import com.github.tangyi.common.security.wx.WxUser;
import com.github.tangyi.user.api.dto.UserDto;
import com.github.tangyi.user.api.enums.IdentityType;
......@@ -48,6 +49,25 @@ public class CustomUserDetailsServiceImpl implements CustomUserDetailsService {
/**
* 加载用户信息
* @param username username
* @return UserDetails
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
long start = System.currentTimeMillis();
ResponseBean<UserVo> userVoResponseBean = userServiceClient.findUserByIdentifier(username, TenantContextHolder.getTenantCode());
if (!ResponseUtil.isSuccess(userVoResponseBean))
throw new ServiceException(GET_USER_INFO_FAIL + userVoResponseBean.getMsg());
UserVo userVo = userVoResponseBean.getData();
if (userVo == null)
throw new UsernameNotFoundException("user does not exist");
return new CustomUserDetails(username, userVo.getCredential(), CommonConstant.STATUS_NORMAL.equals(userVo.getStatus()), getAuthority(userVo), userVo.getTenantCode(), userVo.getId(), start, LoginTypeEnum.PWD);
}
/**
* 加载用户信息
*
* @param tenantCode 租户标识
* @param username 用户名
......
......@@ -26,7 +26,7 @@ public class ValidateTenantAspect {
private final UserServiceClient userServiceClient;
@Before("execution(* com.github.tangyi.auth.security.CustomUserDetailsServiceImpl.load*(..)) && args(tenantCode,..)")
@Before("execution(* com.github.tangyi.auth.security.CustomUserDetailsServiceImpl.*AndTenantCode(..)) && args(tenantCode,..)")
public void validateTenantCode(String tenantCode) throws TenantNotFoundException {
// 获取tenantCode
if (StringUtils.isBlank(tenantCode))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment