diff --git a/common/src/main/java/org/apache/seata/common/ConfigurationKeys.java b/common/src/main/java/org/apache/seata/common/ConfigurationKeys.java index ff8436b6dc9..a46c31358ef 100644 --- a/common/src/main/java/org/apache/seata/common/ConfigurationKeys.java +++ b/common/src/main/java/org/apache/seata/common/ConfigurationKeys.java @@ -68,6 +68,11 @@ public interface ConfigurationKeys { */ String SEATA_PREFIX = SEATA_FILE_ROOT_CONFIG + "."; + /** + * The constant SECURITY_PREFIX + */ + String SECURITY_PREFIX = "security."; + /** * The constant SERVICE_PREFIX. */ @@ -1014,6 +1019,31 @@ public interface ConfigurationKeys { */ String SERVER_APPLICATION_DATA_SIZE_CHECK = SERVER_PREFIX + "applicationDataLimitCheck"; + /** + * The constant SECURITY_USERNAME; + */ + String SECURITY_USERNME = SECURITY_PREFIX + "username"; + + /** + * The constant SECURITY_PASSWORD; + */ + String SECURITY_PASSWORD = SECURITY_PREFIX + "password"; + + /** + * The constant SECURITY_SECRET_KEY; + */ + String SECURITY_SECRET_KEY = SECURITY_PREFIX + "secretKey"; + + /** + * The constant SECURITY_ACCESS_TOKEN_VALID_TIME; + */ + String SECURITY_ACCESS_TOKEN_VALID_TIME = SECURITY_PREFIX + "accessTokenValidityInMilliseconds"; + + /** + * The constant SECURITY_REFRESH_TOKEN_VALID_TIME; + */ + String SECURITY_REFRESH_TOKEN_VALID_TIME = SECURITY_PREFIX + "refreshTokenValidityInMilliseconds"; + /** * The constant ROCKET_MQ_MSG_TIMEOUT */ diff --git a/common/src/main/java/org/apache/seata/common/result/Code.java b/common/src/main/java/org/apache/seata/common/result/Code.java index 18756e3f0b0..abce5fc4271 100644 --- a/common/src/main/java/org/apache/seata/common/result/Code.java +++ b/common/src/main/java/org/apache/seata/common/result/Code.java @@ -16,12 +16,19 @@ */ package org.apache.seata.common.result; - +/** + * The Code for the response of message + * + */ public enum Code { /** * response success */ SUCCESS("200", "ok"), + /** + * the custom error + */ + ACCESS_TOKEN_NEAR_EXPIRATION("200", "Access token is near expiration"), /** * server error */ @@ -29,17 +36,29 @@ public enum Code { /** * the custom error */ - LOGIN_FAILED("401", "Login failed"); + LOGIN_FAILED("401", "Login failed"), + /** + * the custom error + */ + CHECK_TOKEN_FAILED("401", "Check token failed"), + /** + * the custom error + */ + ACCESS_TOKEN_EXPIRED("401", "Access token expired"), + /** + * the custom error + */ + REFRESH_TOKEN_EXPIRED("401", "Refresh token expired"); /** * The Code. */ - public String code; + private String code; /** * The Msg. */ - public String msg; + private String msg; private Code(String code, String msg) { this.code = code; @@ -98,4 +117,3 @@ public static String getErrorMsg(String code) { return null; } } - diff --git a/common/src/main/java/org/apache/seata/common/result/SingleResult.java b/common/src/main/java/org/apache/seata/common/result/SingleResult.java index 97394053fab..82218984aa0 100644 --- a/common/src/main/java/org/apache/seata/common/result/SingleResult.java +++ b/common/src/main/java/org/apache/seata/common/result/SingleResult.java @@ -18,11 +18,10 @@ import java.io.Serializable; - /** * The single result */ -public class SingleResult extends Result implements Serializable { +public class SingleResult extends Result implements Serializable { private static final long serialVersionUID = 77612626624298767L; /** @@ -33,22 +32,18 @@ public class SingleResult extends Result implements Serializable { public SingleResult(String code, String message) { super(code, message); } + public SingleResult(Code code) { + super(code.getCode(), code.getMsg()); + } public SingleResult(String code, String message, T data) { super(code, message); this.data = data; } - public static SingleResult failure(String code, String msg) { - return new SingleResult<>(code, msg); - } - - public static SingleResult failure(Code errorCode) { - return new SingleResult(errorCode.getCode(), errorCode.getMsg()); - } - - public static SingleResult success(T data) { - return new SingleResult<>(SUCCESS_CODE, SUCCESS_MSG,data); + public SingleResult(Code code, T data) { + super(code.getCode(), code.getMsg()); + this.data = data; } public T getData() { diff --git a/console/src/main/java/org/apache/seata/console/config/WebSecurityConfig.java b/console/src/main/java/org/apache/seata/console/config/ConsoleSecurityConfig.java similarity index 81% rename from console/src/main/java/org/apache/seata/console/config/WebSecurityConfig.java rename to console/src/main/java/org/apache/seata/console/config/ConsoleSecurityConfig.java index 7c76cc3e504..380e52a98e1 100644 --- a/console/src/main/java/org/apache/seata/console/config/WebSecurityConfig.java +++ b/console/src/main/java/org/apache/seata/console/config/ConsoleSecurityConfig.java @@ -17,18 +17,18 @@ package org.apache.seata.console.config; import org.apache.seata.common.util.StringUtils; -import org.apache.seata.console.filter.JwtAuthenticationTokenFilter; +import org.apache.seata.console.filter.ConsoleAuthenticationTokenFilter; import org.apache.seata.console.security.CustomUserDetailsServiceImpl; import org.apache.seata.console.security.JwtAuthenticationEntryPoint; import org.apache.seata.console.utils.JwtTokenUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.core.env.Environment; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.BeanIds; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -44,19 +44,29 @@ * */ @Configuration(proxyBeanMethods = false) -@EnableGlobalMethodSecurity(prePostEnabled = true) -public class WebSecurityConfig extends WebSecurityConfigurerAdapter { +@Order(2) +public class ConsoleSecurityConfig extends WebSecurityConfigurerAdapter { /** * The constant AUTHORIZATION_HEADER. */ public static final String AUTHORIZATION_HEADER = "Authorization"; + /** + * The constant REFRESH_TOKEN. + */ + public static final String REFRESH_TOKEN = "refresh_token"; + /** * The constant AUTHORIZATION_TOKEN. */ public static final String AUTHORIZATION_TOKEN = "access_token"; + /** + * The constant ACCESS_TOKEN_NEAR_EXPIRATION. + */ + public static final String ACCESS_TOKEN_NEAR_EXPIRATION = "Access_token_near_expiration"; + /** * The constant SECURITY_IGNORE_URLS_SPILT_CHAR. */ @@ -68,18 +78,21 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { public static final String TOKEN_PREFIX = "Bearer "; @Autowired + @Qualifier("consoleUserDetailsService") private CustomUserDetailsServiceImpl userDetailsService; @Autowired + @Qualifier("consoleJwtAuthenticationEntryPoint") private JwtAuthenticationEntryPoint unauthorizedHandler; @Autowired + @Qualifier("consoleJwtTokenUtils") private JwtTokenUtils tokenProvider; @Autowired private Environment env; - @Bean(name = BeanIds.AUTHENTICATION_MANAGER) + @Bean("consoleAuthenticationManager") @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); @@ -92,7 +105,7 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { @Override public void configure(WebSecurity web) { - String ignoreURLs = env.getProperty("seata.security.ignore.urls", "/**"); + String ignoreURLs = env.getProperty("console.ignore.urls", "/**"); for (String ignoreURL : ignoreURLs.trim().split(SECURITY_IGNORE_URLS_SPILT_CHAR)) { web.ignoring().antMatchers(ignoreURL.trim()); } @@ -110,9 +123,9 @@ protected void configure(HttpSecurity http) throws Exception { csrf.ignoringAntMatchers(csrfIgnoreUrls.trim().split(SECURITY_IGNORE_URLS_SPILT_CHAR)); } csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); - // don't disable csrf, jwt may be implemented based on cookies - http.addFilterBefore(new JwtAuthenticationTokenFilter(tokenProvider), - UsernamePasswordAuthenticationFilter.class); + // don't disable csrf, jwt may be implemented based on cookies + http.antMatcher("/api/v1/**").addFilterBefore(new ConsoleAuthenticationTokenFilter(tokenProvider), + UsernamePasswordAuthenticationFilter.class); // disable cache http.headers().cacheControl(); @@ -123,7 +136,6 @@ protected void configure(HttpSecurity http) throws Exception { * * @return the password encoder */ - @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } diff --git a/console/src/main/java/org/apache/seata/console/controller/AuthController.java b/console/src/main/java/org/apache/seata/console/controller/AuthController.java index 7f43218c4c8..c4683818619 100644 --- a/console/src/main/java/org/apache/seata/console/controller/AuthController.java +++ b/console/src/main/java/org/apache/seata/console/controller/AuthController.java @@ -16,14 +16,13 @@ */ package org.apache.seata.console.controller; -import javax.servlet.http.HttpServletResponse; - import org.apache.seata.common.result.Code; -import org.apache.seata.console.config.WebSecurityConfig; import org.apache.seata.common.result.SingleResult; +import org.apache.seata.console.config.ConsoleSecurityConfig; import org.apache.seata.console.security.User; import org.apache.seata.console.utils.JwtTokenUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -33,6 +32,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import javax.servlet.http.HttpServletResponse; /** * auth user @@ -42,8 +42,11 @@ @RequestMapping("/api/v1/auth") public class AuthController { @Autowired + @Qualifier("consoleJwtTokenUtils") private JwtTokenUtils jwtTokenUtils; + @Autowired + @Qualifier("consoleAuthenticationManager") private AuthenticationManager authenticationManager; /** @@ -57,7 +60,7 @@ public class AuthController { @PostMapping("/login") public SingleResult login(HttpServletResponse response, @RequestBody User user) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( - user.getUsername(), user.getPassword()); + user.getUsername(), user.getPassword()); try { //AuthenticationManager(default ProviderManager) #authenticate check Authentication @@ -65,15 +68,16 @@ public SingleResult login(HttpServletResponse response, @RequestBody Use //bind authentication to securityContext SecurityContextHolder.getContext().setAuthentication(authentication); //create token - String token = jwtTokenUtils.createToken(authentication); + String accessToken = jwtTokenUtils.createAccessToken(authentication); + String refreshToken = jwtTokenUtils.createRefreshToken(authentication); - String authHeader = WebSecurityConfig.TOKEN_PREFIX + token; + String authHeader = ConsoleSecurityConfig.TOKEN_PREFIX + accessToken; //put token into http header - response.addHeader(WebSecurityConfig.AUTHORIZATION_HEADER, authHeader); - - return SingleResult.success(authHeader); + response.addHeader(ConsoleSecurityConfig.AUTHORIZATION_HEADER, authHeader); + response.addHeader(ConsoleSecurityConfig.REFRESH_TOKEN, refreshToken); + return new SingleResult<>(Code.SUCCESS, authHeader); } catch (BadCredentialsException authentication) { - return SingleResult.failure(Code.LOGIN_FAILED); + return new SingleResult<>(Code.LOGIN_FAILED); } } } diff --git a/console/src/main/java/org/apache/seata/console/controller/OverviewController.java b/console/src/main/java/org/apache/seata/console/controller/OverviewController.java index 6bbfc31239a..48a4e4aece9 100644 --- a/console/src/main/java/org/apache/seata/console/controller/OverviewController.java +++ b/console/src/main/java/org/apache/seata/console/controller/OverviewController.java @@ -16,16 +16,15 @@ */ package org.apache.seata.console.controller; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - - +import org.apache.seata.common.result.Code; import org.apache.seata.common.result.SingleResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * Overview @@ -50,7 +49,6 @@ public SingleResult getData() { hashMap.put("id", count); result.add(hashMap); } - - return SingleResult.success(result); + return new SingleResult<>(Code.SUCCESS, result); } } diff --git a/console/src/main/java/org/apache/seata/console/filter/ConsoleAuthenticationTokenFilter.java b/console/src/main/java/org/apache/seata/console/filter/ConsoleAuthenticationTokenFilter.java new file mode 100644 index 00000000000..c00b1921f1a --- /dev/null +++ b/console/src/main/java/org/apache/seata/console/filter/ConsoleAuthenticationTokenFilter.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.console.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.seata.common.result.Code; +import org.apache.seata.common.result.SingleResult; +import org.apache.seata.console.config.ConsoleSecurityConfig; +import org.apache.seata.console.utils.JwtTokenUtils; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * jwt auth token filter + * + */ +public class ConsoleAuthenticationTokenFilter extends OncePerRequestFilter { + + private JwtTokenUtils tokenProvider; + + /** + * Instantiates a new Jwt authentication token filter. + * + * @param tokenProvider the token provider + */ + public ConsoleAuthenticationTokenFilter(JwtTokenUtils tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { + String accessToken = resolveAccessToken(request); + String refreshToken = resolveRefreshToken(request); + SingleResult result = new SingleResult(Code.CHECK_TOKEN_FAILED); + ObjectMapper objectMapper = new ObjectMapper(); + if (accessToken != null) { + result = this.tokenProvider.validateAccessToken(accessToken); + if (result.getMessage().equals(Code.ACCESS_TOKEN_NEAR_EXPIRATION.getMsg())) { + //access token is near expiration + response.addHeader(ConsoleSecurityConfig.ACCESS_TOKEN_NEAR_EXPIRATION, "true"); + } + } else if (refreshToken != null) { + result = this.tokenProvider.validateRefreshToken(refreshToken); + if (result.getCode().equals(Code.SUCCESS.getCode())) { + //create access token + String newAccessToken = this.tokenProvider.createAccessToken((UsernamePasswordAuthenticationToken)result.getData()); + + String authHeader = ConsoleSecurityConfig.TOKEN_PREFIX + newAccessToken; + //put token into http header + response.addHeader(ConsoleSecurityConfig.AUTHORIZATION_HEADER, authHeader); + } + } + if (result.getCode().equals(Code.SUCCESS.getCode())) { + /** + * get auth info + */ + Authentication authentication = (UsernamePasswordAuthenticationToken)result.getData(); + /** + * save user info to securityContext + */ + SecurityContextHolder.getContext().setAuthentication(authentication); + chain.doFilter(request, response); + } else { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write(objectMapper.writeValueAsString(result)); + } + } + + /** + * Get access token from header + */ + private String resolveAccessToken(HttpServletRequest request) { + String bearerToken = request.getHeader(ConsoleSecurityConfig.AUTHORIZATION_HEADER); + if (bearerToken != null && bearerToken.startsWith(ConsoleSecurityConfig.TOKEN_PREFIX)) { + String accessToken = bearerToken.substring(ConsoleSecurityConfig.TOKEN_PREFIX.length()); + return StringUtils.hasText(accessToken) ? accessToken : null; + } + String accessToken = request.getParameter(ConsoleSecurityConfig.AUTHORIZATION_TOKEN); + if (StringUtils.hasText(accessToken)) { + return accessToken; + } + return null; + } + + /** + * Get refresh token from header + */ + private String resolveRefreshToken(HttpServletRequest request) { + String refreshToken = request.getHeader(ConsoleSecurityConfig.REFRESH_TOKEN); + return StringUtils.hasText(refreshToken) ? refreshToken : null; + } +} + diff --git a/console/src/main/java/org/apache/seata/console/filter/JwtAuthenticationTokenFilter.java b/console/src/main/java/org/apache/seata/console/filter/JwtAuthenticationTokenFilter.java deleted file mode 100644 index 38e651b65ca..00000000000 --- a/console/src/main/java/org/apache/seata/console/filter/JwtAuthenticationTokenFilter.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.seata.console.filter; - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.seata.console.config.WebSecurityConfig; -import org.apache.seata.console.utils.JwtTokenUtils; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.util.StringUtils; -import org.springframework.web.filter.OncePerRequestFilter; - -/** - * jwt auth token filter - * - */ -public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { - - private JwtTokenUtils tokenProvider; - - /** - * Instantiates a new Jwt authentication token filter. - * - * @param tokenProvider the token provider - */ - public JwtAuthenticationTokenFilter(JwtTokenUtils tokenProvider) { - this.tokenProvider = tokenProvider; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws IOException, ServletException { - String jwt = resolveToken(request); - - if (jwt != null && !"".equals(jwt.trim()) && SecurityContextHolder.getContext().getAuthentication() == null) { - if (this.tokenProvider.validateToken(jwt)) { - /** - * get auth info - */ - Authentication authentication = this.tokenProvider.getAuthentication(jwt); - /** - * save user info to securityContext - */ - SecurityContextHolder.getContext().setAuthentication(authentication); - } - } - - chain.doFilter(request, response); - } - - /** - * Get token from header - */ - private String resolveToken(HttpServletRequest request) { - String bearerToken = request.getHeader(WebSecurityConfig.AUTHORIZATION_HEADER); - if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(WebSecurityConfig.TOKEN_PREFIX)) { - return bearerToken.substring(WebSecurityConfig.TOKEN_PREFIX.length()); - } - String jwt = request.getParameter(WebSecurityConfig.AUTHORIZATION_TOKEN); - if (StringUtils.hasText(jwt)) { - return jwt; - } - return null; - } -} - diff --git a/console/src/main/java/org/apache/seata/console/security/CustomAuthenticationProvider.java b/console/src/main/java/org/apache/seata/console/security/CustomAuthenticationProvider.java deleted file mode 100644 index 45ff87d519d..00000000000 --- a/console/src/main/java/org/apache/seata/console/security/CustomAuthenticationProvider.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.seata.console.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; - -/** - * auth provider - * - */ -@Component -public class CustomAuthenticationProvider implements AuthenticationProvider { - - @Autowired - private CustomUserDetailsServiceImpl userDetailsService; - - @Override - public Authentication authenticate(Authentication authentication) throws AuthenticationException { - - String username = (String)authentication.getPrincipal(); - String password = (String)authentication.getCredentials(); - UserDetails userDetails = userDetailsService.loadUserByUsername(username); - - if (!password.equals(userDetails.getPassword())) { - return new UsernamePasswordAuthenticationToken(username, null, null); - } - return null; - } - - @Override - public boolean supports(Class aClass) { - return aClass.equals(UsernamePasswordAuthenticationToken.class); - } - -} diff --git a/console/src/main/java/org/apache/seata/console/security/CustomUserDetails.java b/console/src/main/java/org/apache/seata/console/security/CustomUserDetails.java index 7218feea327..8b680c363fe 100644 --- a/console/src/main/java/org/apache/seata/console/security/CustomUserDetails.java +++ b/console/src/main/java/org/apache/seata/console/security/CustomUserDetails.java @@ -16,11 +16,10 @@ */ package org.apache.seata.console.security; -import java.util.Collection; - import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UserDetails; +import java.util.Collection; /** * custem user diff --git a/console/src/main/java/org/apache/seata/console/security/CustomUserDetailsServiceImpl.java b/console/src/main/java/org/apache/seata/console/security/CustomUserDetailsServiceImpl.java index fbc35832ee5..ae0af9fda2b 100644 --- a/console/src/main/java/org/apache/seata/console/security/CustomUserDetailsServiceImpl.java +++ b/console/src/main/java/org/apache/seata/console/security/CustomUserDetailsServiceImpl.java @@ -16,20 +16,19 @@ */ package org.apache.seata.console.security; -import javax.annotation.PostConstruct; - import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import javax.annotation.PostConstruct; /** * Custem user service * */ -@Service +@Service("consoleUserDetailsService") public class CustomUserDetailsServiceImpl implements UserDetailsService { @Value("${console.user.username}") diff --git a/console/src/main/java/org/apache/seata/console/security/JwtAuthenticationEntryPoint.java b/console/src/main/java/org/apache/seata/console/security/JwtAuthenticationEntryPoint.java index 95dc6ac69dc..054f4393b25 100644 --- a/console/src/main/java/org/apache/seata/console/security/JwtAuthenticationEntryPoint.java +++ b/console/src/main/java/org/apache/seata/console/security/JwtAuthenticationEntryPoint.java @@ -16,23 +16,21 @@ */ package org.apache.seata.console.security; -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; /** * jwt auth fail point * */ -@Component +@Component("consoleJwtAuthenticationEntryPoint") public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class); diff --git a/console/src/main/java/org/apache/seata/console/utils/JwtTokenUtils.java b/console/src/main/java/org/apache/seata/console/utils/JwtTokenUtils.java index dab0d1c58ad..713f1aeb83b 100644 --- a/console/src/main/java/org/apache/seata/console/utils/JwtTokenUtils.java +++ b/console/src/main/java/org/apache/seata/console/utils/JwtTokenUtils.java @@ -16,19 +16,13 @@ */ package org.apache.seata.console.utils; -import java.util.Date; -import java.util.List; - -import javax.crypto.spec.SecretKeySpec; - import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.security.SignatureException; +import org.apache.seata.common.result.Code; +import org.apache.seata.common.result.SingleResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -38,12 +32,15 @@ import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.stereotype.Component; +import javax.crypto.spec.SecretKeySpec; +import java.util.Date; +import java.util.List; /** * Jwt token tool * */ -@Component +@Component("consoleJwtTokenUtils") public class JwtTokenUtils { private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtils.class); @@ -53,22 +50,48 @@ public class JwtTokenUtils { /** * secret key */ - @Value("${seata.security.secretKey}") + @Value("${console.secretKey}") private String secretKey; /** - * Token validity time(ms) + * Access token validity time(ms) */ - @Value("${seata.security.tokenValidityInMilliseconds}") - private long tokenValidityInMilliseconds; + @Value("${console.accessTokenValidityInMilliseconds}") + private long accessTokenValidityInMilliseconds; /** - * Create token + * Refresh token validity time(ms) + */ + @Value("${console.refreshTokenValidityInMilliseconds}") + private long refreshTokenValidityInMilliseconds; + + /** + * Create access token * * @param authentication auth info * @return token string */ - public String createToken(Authentication authentication) { + public String createAccessToken(Authentication authentication) { + return createToken(authentication, accessTokenValidityInMilliseconds); + } + + /** + * Create refresh token + * + * @param authentication auth info + * @return token string + */ + public String createRefreshToken(Authentication authentication) { + return createToken(authentication, refreshTokenValidityInMilliseconds); + } + + /** + * Create token + * @param authentication auth info + * @param tokenValidityInMilliseconds token validity time in milliseconds + * @return token string + */ + private String createToken(Authentication authentication, long tokenValidityInMilliseconds) { /** * Current time */ @@ -81,59 +104,72 @@ public String createToken(Authentication authentication) { * Key */ SecretKeySpec secretKeySpec = new SecretKeySpec(Decoders.BASE64.decode(secretKey), - SignatureAlgorithm.HS256.getJcaName()); + SignatureAlgorithm.HS256.getJcaName()); /** * create token */ - return Jwts.builder().setSubject(authentication.getName()).claim(AUTHORITIES_KEY, "").setExpiration( - expirationDate).signWith(secretKeySpec, SignatureAlgorithm.HS256).compact(); + return Jwts.builder().setSubject(authentication.getName()).claim(AUTHORITIES_KEY, "") + .setExpiration(expirationDate).signWith(secretKeySpec, SignatureAlgorithm.HS256).compact(); } /** - * Get auth Info + * validate access token * * @param token token - * @return auth info + * @return validate result */ - public Authentication getAuthentication(String token) { - /** - * parse the payload of token - */ - Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody(); - - List authorities = AuthorityUtils.commaSeparatedStringToAuthorityList( - (String)claims.get(AUTHORITIES_KEY)); - - User principal = new User(claims.getSubject(), "", authorities); - return new UsernamePasswordAuthenticationToken(principal, "", authorities); + public SingleResult validateAccessToken(String token) { + try { + /** + * parse the payload of access token + */ + Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody(); + List authorities = AuthorityUtils.commaSeparatedStringToAuthorityList( + (String)claims.get(AUTHORITIES_KEY)); + User principal = new User(claims.getSubject(), "", authorities); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(principal, "", authorities); + if (System.currentTimeMillis() > claims.getExpiration().getTime() - accessTokenValidityInMilliseconds / 3) { + LOGGER.warn("jwt token will be expired, need refresh token"); + return new SingleResult<>(Code.ACCESS_TOKEN_NEAR_EXPIRATION, authenticationToken); + } + return new SingleResult<>(Code.SUCCESS, authenticationToken); + } catch (ExpiredJwtException e) { + LOGGER.warn("Expired JWT token."); + LOGGER.trace("Expired JWT token trace: {}", e); + return new SingleResult<>(Code.ACCESS_TOKEN_EXPIRED); + } catch (Exception e) { + LOGGER.warn("Unsupported JWT token."); + LOGGER.trace("Unsupported JWT token trace: {}", e); + return new SingleResult<>(Code.CHECK_TOKEN_FAILED); + } } /** - * validate token + * validate refresh token * * @param token token - * @return whether valid + * @return validate result */ - public boolean validateToken(String token) { + public SingleResult validateRefreshToken(String token) { try { - Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); - return true; - } catch (SignatureException e) { - LOGGER.warn("Invalid JWT signature."); - LOGGER.trace("Invalid JWT signature trace: {}", e); - } catch (MalformedJwtException e) { - LOGGER.warn("Invalid JWT token."); - LOGGER.trace("Invalid JWT token trace: {}", e); + /** + * parse the payload of refresh token + */ + Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody(); + List authorities = AuthorityUtils.commaSeparatedStringToAuthorityList( + (String)claims.get(AUTHORITIES_KEY)); + User principal = new User(claims.getSubject(), "", authorities); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(principal, "", authorities); + return new SingleResult<>(Code.SUCCESS, authenticationToken); } catch (ExpiredJwtException e) { LOGGER.warn("Expired JWT token."); LOGGER.trace("Expired JWT token trace: {}", e); - } catch (UnsupportedJwtException e) { - LOGGER.warn("Unsupported JWT token."); - LOGGER.trace("Unsupported JWT token trace: {}", e); - } catch (IllegalArgumentException e) { - LOGGER.warn("JWT token compact of handler are invalid."); - LOGGER.trace("JWT token compact of handler are invalid trace: {}", e); + return new SingleResult<>(Code.REFRESH_TOKEN_EXPIRED); + } catch (Exception e) { + LOGGER.warn("Invalid JWT token."); + LOGGER.trace("Invalid JWT token trace: {}", e); + return new SingleResult<>(Code.CHECK_TOKEN_FAILED); } - return false; } + } diff --git a/core/src/main/java/org/apache/seata/core/auth/AuthResult.java b/core/src/main/java/org/apache/seata/core/auth/AuthResult.java new file mode 100644 index 00000000000..01764a836c1 --- /dev/null +++ b/core/src/main/java/org/apache/seata/core/auth/AuthResult.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.core.auth; + +import org.apache.seata.core.protocol.ResultCode; + +public class AuthResult { + private ResultCode resultCode; + + private String accessToken; + + private String refreshToken; + + public AuthResult() { + } + + public AuthResult(AuthResultBuilder builder) { + this.resultCode = builder.getResultCode(); + this.accessToken = builder.getAccessToken(); + this.refreshToken = builder.getRefreshToken(); + } + + public ResultCode getResultCode() { + return resultCode; + } + + public void setResultCode(ResultCode resultCode) { + this.resultCode = resultCode; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + +} diff --git a/core/src/main/java/org/apache/seata/core/auth/AuthResultBuilder.java b/core/src/main/java/org/apache/seata/core/auth/AuthResultBuilder.java new file mode 100644 index 00000000000..bb289e24aa5 --- /dev/null +++ b/core/src/main/java/org/apache/seata/core/auth/AuthResultBuilder.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.core.auth; + +import org.apache.seata.core.protocol.ResultCode; + +public class AuthResultBuilder { + private ResultCode resultCode; + private String accessToken; + private String refreshToken; + + public ResultCode getResultCode() { + return resultCode; + } + + public String getAccessToken() { + return accessToken; + } + + public String getRefreshToken() { + return refreshToken; + } + + // set resultCode + public AuthResultBuilder setResultCode(ResultCode resultCode) { + this.resultCode = resultCode; + return this; + } + + // set accessToken + public AuthResultBuilder setAccessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + // set refreshToken + public AuthResultBuilder setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } + + // build AuthResult + public AuthResult build() { + return new AuthResult(this); + } +} diff --git a/discovery/seata-discovery-raft/src/main/java/org/apache/seata/discovery/registry/raft/RaftRegistryServiceImpl.java b/discovery/seata-discovery-raft/src/main/java/org/apache/seata/discovery/registry/raft/RaftRegistryServiceImpl.java index f52464ef4ae..09efe2cf66a 100644 --- a/discovery/seata-discovery-raft/src/main/java/org/apache/seata/discovery/registry/raft/RaftRegistryServiceImpl.java +++ b/discovery/seata-discovery-raft/src/main/java/org/apache/seata/discovery/registry/raft/RaftRegistryServiceImpl.java @@ -42,6 +42,7 @@ import org.apache.seata.common.metadata.Metadata; import org.apache.seata.common.metadata.MetadataResponse; import org.apache.seata.common.metadata.Node; +import org.apache.seata.common.result.Code; import org.apache.seata.common.thread.NamedThreadFactory; import org.apache.seata.common.util.CollectionUtils; import org.apache.seata.common.util.HttpClientUtil; @@ -50,12 +51,13 @@ import org.apache.seata.config.Configuration; import org.apache.seata.config.ConfigurationFactory; import org.apache.seata.discovery.registry.RegistryService; +import org.apache.http.Header; import org.apache.http.HttpStatus; import org.apache.http.StatusLine; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.entity.ContentType; -import org.apache.http.util.EntityUtils; import org.apache.http.protocol.HTTP; +import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -77,17 +79,19 @@ public class RaftRegistryServiceImpl implements RegistryService> ALIVE_NODES = new ConcurrentHashMap<>(); static { - TOKEN_EXPIRE_TIME_IN_MILLISECONDS = CONFIG.getLong(getTokenExpireTimeInMillisecondsKey(), 29 * 60 * 1000L); USERNAME = CONFIG.getConfig(getRaftUserNameKey()); PASSWORD = CONFIG.getConfig(getRaftPassWordKey()); } @@ -164,7 +167,7 @@ protected static void startQueryMetadata() { synchronized (INIT_ADDRESSES) { if (REFRESH_METADATA_EXECUTOR == null) { REFRESH_METADATA_EXECUTOR = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<>(), new NamedThreadFactory("refreshMetadata", 1, true)); + new LinkedBlockingQueue<>(), new NamedThreadFactory("refreshMetadata", 1, true)); REFRESH_METADATA_EXECUTOR.execute(() -> { long metadataMaxAgeMs = CONFIG.getLong(ConfigurationKeys.CLIENT_METADATA_MAX_AGE_MS, 30000L); long currentTime = System.currentTimeMillis(); @@ -221,7 +224,7 @@ private static String queryHttpAddress(String clusterName, String group) { List inetSocketAddresses = ALIVE_NODES.get(CURRENT_TRANSACTION_SERVICE_GROUP); if (CollectionUtils.isEmpty(inetSocketAddresses)) { addressList = - nodeList.stream().map(node -> node.getControl().createAddress()).collect(Collectors.toList()); + nodeList.stream().map(node -> node.getControl().createAddress()).collect(Collectors.toList()); } else { stream = inetSocketAddresses.stream(); } @@ -235,14 +238,14 @@ private static String queryHttpAddress(String clusterName, String group) { if (CollectionUtils.isNotEmpty(nodeList)) { for (Node node : nodeList) { map.put(new InetSocketAddress(node.getTransaction().getHost(), node.getTransaction().getPort()).getAddress().getHostAddress() - + IP_PORT_SPLIT_CHAR + node.getTransaction().getPort(), node); + + IP_PORT_SPLIT_CHAR + node.getTransaction().getPort(), node); } } addressList = stream.map(inetSocketAddress -> { String host = inetSocketAddress.getAddress().getHostAddress(); Node node = map.get(host + IP_PORT_SPLIT_CHAR + inetSocketAddress.getPort()); return host + IP_PORT_SPLIT_CHAR - + (node != null ? node.getControl().getPort() : inetSocketAddress.getPort()); + + (node != null ? node.getControl().getPort() : inetSocketAddress.getPort()); }).collect(Collectors.toList()); return addressList.get(ThreadLocalRandom.current().nextInt(addressList.size())); } @@ -250,30 +253,17 @@ private static String queryHttpAddress(String clusterName, String group) { private static String getRaftAddrFileKey() { return String.join(ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR, ConfigurationKeys.FILE_ROOT_REGISTRY, - REGISTRY_TYPE, PRO_SERVER_ADDR_KEY); + REGISTRY_TYPE, PRO_SERVER_ADDR_KEY); } private static String getRaftUserNameKey() { return String.join(ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR, ConfigurationKeys.FILE_ROOT_REGISTRY, - REGISTRY_TYPE, PRO_USERNAME_KEY); + REGISTRY_TYPE, PRO_USERNAME_KEY); } private static String getRaftPassWordKey() { return String.join(ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR, ConfigurationKeys.FILE_ROOT_REGISTRY, - REGISTRY_TYPE, PRO_PASSWORD_KEY); - } - - private static String getTokenExpireTimeInMillisecondsKey() { - return String.join(ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR, ConfigurationKeys.FILE_ROOT_REGISTRY, - REGISTRY_TYPE, TOKEN_VALID_TIME_MS_KEY); - } - - private static boolean isTokenExpired() { - if (tokenTimeStamp == -1) { - return true; - } - long tokenExpiredTime = tokenTimeStamp + TOKEN_EXPIRE_TIME_IN_MILLISECONDS; - return System.currentTimeMillis() >= tokenExpiredTime; + REGISTRY_TYPE, PRO_PASSWORD_KEY); } private InetSocketAddress convertInetSocketAddress(Node node) { @@ -307,24 +297,12 @@ private static boolean watch() throws RetryableException { groupTerms.forEach((k, v) -> param.put(k, String.valueOf(v))); for (String group : groupTerms.keySet()) { String tcAddress = queryHttpAddress(clusterName, group); - if (isTokenExpired()) { - refreshToken(tcAddress); - } - if (StringUtils.isNotBlank(jwtToken)) { - header.put(AUTHORIZATION_HEADER, jwtToken); - } - try (CloseableHttpResponse response = - HttpClientUtil.doPost("http://" + tcAddress + "/metadata/v1/watch", param, header, 30000)) { - if (response != null) { - StatusLine statusLine = response.getStatusLine(); - if (statusLine != null && statusLine.getStatusCode() == HttpStatus.SC_UNAUTHORIZED) { - if (StringUtils.isNotBlank(USERNAME) && StringUtils.isNotBlank(PASSWORD)) { - throw new RetryableException("Authentication failed!"); - } else { - throw new AuthenticationFailedException("Authentication failed! you should configure the correct username and password."); - } - } - return statusLine != null && statusLine.getStatusCode() == HttpStatus.SC_OK; + addAuthDataToHeader(header, tcAddress); + try (CloseableHttpResponse httpResponse = + HttpClientUtil.doPost("http://" + tcAddress + "/metadata/v1/watch", param, header, 30000)) { + if (httpResponse != null) { + handleResponse(httpResponse); + return false; } } catch (IOException e) { LOGGER.error("watch cluster node: {}, fail: {}", tcAddress, e.getMessage()); @@ -337,16 +315,16 @@ private static boolean watch() throws RetryableException { @Override public List refreshAliveLookup(String transactionServiceGroup, - List aliveAddress) { + List aliveAddress) { if (METADATA.isRaftMode()) { Node leader = METADATA.getLeader(getServiceGroup(transactionServiceGroup)); InetSocketAddress leaderAddress = convertInetSocketAddress(leader); return ALIVE_NODES.put(transactionServiceGroup, - aliveAddress.isEmpty() ? aliveAddress : aliveAddress.parallelStream().filter(inetSocketAddress -> { - // Since only follower will turn into leader, only the follower node needs to be listened to - return inetSocketAddress.getPort() != leaderAddress.getPort() || !inetSocketAddress.getAddress() - .getHostAddress().equals(leaderAddress.getAddress().getHostAddress()); - }).collect(Collectors.toList())); + aliveAddress.isEmpty() ? aliveAddress : aliveAddress.parallelStream().filter(inetSocketAddress -> { + // Since only follower will turn into leader, only the follower node needs to be listened to + return inetSocketAddress.getPort() != leaderAddress.getPort() || !inetSocketAddress.getAddress() + .getHostAddress().equals(leaderAddress.getAddress().getHostAddress()); + }).collect(Collectors.toList())); } else { return RegistryService.super.refreshAliveLookup(transactionServiceGroup, aliveAddress); } @@ -364,28 +342,15 @@ private static void acquireClusterMetaData(String clusterName, String group) thr String tcAddress = queryHttpAddress(clusterName, group); Map header = new HashMap<>(); header.put(HTTP.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType()); - if (isTokenExpired()) { - refreshToken(tcAddress); - } - if (StringUtils.isNotBlank(jwtToken)) { - header.put(AUTHORIZATION_HEADER, jwtToken); - } + addAuthDataToHeader(header, tcAddress); if (StringUtils.isNotBlank(tcAddress)) { Map param = new HashMap<>(); param.put("group", group); String response = null; try (CloseableHttpResponse httpResponse = - HttpClientUtil.doGet("http://" + tcAddress + "/metadata/v1/cluster", param, header, 1000)) { + HttpClientUtil.doGet("http://" + tcAddress + "/metadata/v1/cluster", param, header, 1000)) { if (httpResponse != null) { - if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { - response = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8); - } else if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) { - if (StringUtils.isNotBlank(USERNAME) && StringUtils.isNotBlank(PASSWORD)) { - throw new RetryableException("Authentication failed!"); - } else { - throw new AuthenticationFailedException("Authentication failed! you should configure the correct username and password."); - } - } + response = handleResponse(httpResponse); } MetadataResponse metadataResponse; if (StringUtils.isNotBlank(response)) { @@ -402,7 +367,7 @@ private static void acquireClusterMetaData(String clusterName, String group) thr } } - private static void refreshToken(String tcAddress) throws RetryableException { + private static void refreshDoubleToken(String tcAddress) throws RetryableException { // if username and password is not in config , return if (StringUtils.isBlank(USERNAME) || StringUtils.isBlank(PASSWORD)) { return; @@ -413,10 +378,9 @@ private static void refreshToken(String tcAddress) throws RetryableException { param.put(PRO_PASSWORD_KEY, PASSWORD); Map header = new HashMap<>(); header.put(HTTP.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType()); - String response = null; - tokenTimeStamp = System.currentTimeMillis(); + String response; try (CloseableHttpResponse httpResponse = - HttpClientUtil.doPost("http://" + tcAddress + "/api/v1/auth/login", param, header, 1000)) { + HttpClientUtil.doPost("http://" + tcAddress + "/metadata/v1/auth/login", param, header, 1000)) { if (httpResponse != null) { if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { response = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8); @@ -426,7 +390,9 @@ private static void refreshToken(String tcAddress) throws RetryableException { //authorized failed,throw exception to kill process throw new AuthenticationFailedException("Authentication failed! you should configure the correct username and password."); } - jwtToken = jsonNode.get("data").asText(); + accessToken = jsonNode.get("data").asText(); + refreshToken = httpResponse.getFirstHeader(PRO_REFRESH_TOKEN).getValue(); + isAccessTokenNearExpiration = false; } else { //authorized failed,throw exception to kill process throw new AuthenticationFailedException("Authentication failed! you should configure the correct username and password."); @@ -463,7 +429,7 @@ public List lookup(String key) throws Exception { INIT_ADDRESSES.put(clusterName, list); // init jwt token try { - refreshToken(queryHttpAddress(clusterName, key)); + refreshDoubleToken(queryHttpAddress(clusterName, key)); } catch (Exception e) { throw new RuntimeException("Init fetch token failed!", e); } @@ -479,4 +445,65 @@ public List lookup(String key) throws Exception { return Collections.emptyList(); } + private static String handleResponse(CloseableHttpResponse httpResponse) throws IOException, RetryableException { + StatusLine statusLine = httpResponse.getStatusLine(); + String response = null; + JsonNode jsonNode = null; + if (httpResponse.getEntity() != null) { + response = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8); + jsonNode = OBJECT_MAPPER.readTree(response); + } + if (statusLine != null && statusLine.getStatusCode() == HttpStatus.SC_UNAUTHORIZED) { + if (jsonNode != null) { + String message = jsonNode.get("message").asText(); + if (message.equals(Code.CHECK_TOKEN_FAILED.getMsg())) { + accessToken = null; + refreshToken = null; + } else if (message.equals(Code.ACCESS_TOKEN_EXPIRED.getMsg())) { + accessToken = null; + } + else if (message.equals(Code.REFRESH_TOKEN_EXPIRED.getMsg())) { + refreshToken = null; + } + } + if ((StringUtils.isNotBlank(USERNAME) && StringUtils.isNotBlank(PASSWORD)) + || StringUtils.isNotBlank(accessToken) || StringUtils.isNotBlank(refreshToken)) { + throw new RetryableException("Authentication failed!"); + } else { + throw new AuthenticationFailedException("Authentication failed! you should configure the correct username and password."); + } + } + if (statusLine != null && statusLine.getStatusCode() == HttpStatus.SC_OK) { + Header header = httpResponse.getFirstHeader(ACCESS_TOKEN_EXPIRATION_HEADER); + if ( header != null && header.getValue().equals("true")) { + isAccessTokenNearExpiration = true; + } + Header newAccessTokenHeader = httpResponse.getFirstHeader(AUTHORIZATION_HEADER); + if (newAccessTokenHeader != null && StringUtils.isNotBlank(newAccessTokenHeader.getValue())) { + accessToken = newAccessTokenHeader.getValue(); + isAccessTokenNearExpiration = false; + } + } + return response; + } + + /** + * get authentication data and add to header + * @param header add authentication data to the header + * @param tcAddress the tc address that may log in to get auth data + * @throws RetryableException + */ + private static void addAuthDataToHeader(Map header, String tcAddress) throws RetryableException { + if (StringUtils.isNotBlank(accessToken) && !isAccessTokenNearExpiration) { + header.put(AUTHORIZATION_HEADER, accessToken); + } else if (refreshToken != null) { + header.put(PRO_REFRESH_TOKEN, refreshToken); + } else { + refreshDoubleToken(tcAddress); + if (StringUtils.isNotBlank(accessToken)) { + header.put(AUTHORIZATION_HEADER, accessToken); + } + } + } + } diff --git a/script/client/conf/registry.conf b/script/client/conf/registry.conf index 1aabbc507f6..ca4a37ec102 100644 --- a/script/client/conf/registry.conf +++ b/script/client/conf/registry.conf @@ -24,7 +24,6 @@ registry { serverAddr = "127.0.0.1:7091" username = "seata" password = "seata" - tokenValidityInMilliseconds = 1740000 } nacos { diff --git a/script/client/spring/application.properties b/script/client/spring/application.properties index 2a72d1e5f79..94cff026c0c 100755 --- a/script/client/spring/application.properties +++ b/script/client/spring/application.properties @@ -27,6 +27,8 @@ seata.enable-auto-data-source-proxy=true seata.data-source-proxy-mode=AT seata.use-jdk-proxy=false seata.expose-proxy=false +security.username=seata +security.password=seata seata.client.rm.async-commit-buffer-limit=10000 seata.client.rm.report-retry-count=5 seata.client.rm.table-meta-check-enable=false diff --git a/script/client/spring/application.yml b/script/client/spring/application.yml index a6100f05740..746825eb70d 100755 --- a/script/client/spring/application.yml +++ b/script/client/spring/application.yml @@ -28,6 +28,9 @@ seata: scan-packages: firstPackage,secondPackage excludes-for-scanning: firstBeanNameForExclude,secondBeanNameForExclude excludes-for-auto-proxying: firstClassNameForExclude,secondClassNameForExclude + security: + username: seata + password: seata client: rm: async-commit-buffer-limit: 10000 @@ -141,7 +144,6 @@ seata: metadata-max-age-ms: 30000 username: seata password: seata - tokenValidityInMilliseconds: 1740000 file: name: file.conf consul: diff --git a/server/src/main/java/org/apache/seata/server/auth/AbstractCheckAuthHandler.java b/server/src/main/java/org/apache/seata/server/auth/AbstractCheckAuthHandler.java index 705ec2d2c9b..7ea227a0bf2 100644 --- a/server/src/main/java/org/apache/seata/server/auth/AbstractCheckAuthHandler.java +++ b/server/src/main/java/org/apache/seata/server/auth/AbstractCheckAuthHandler.java @@ -29,7 +29,7 @@ public abstract class AbstractCheckAuthHandler implements RegisterCheckAuthHandler { private static final Boolean ENABLE_CHECK_AUTH = ConfigurationFactory.getInstance().getBoolean( - ConfigurationKeys.SERVER_ENABLE_CHECK_AUTH, DEFAULT_SERVER_ENABLE_CHECK_AUTH); + ConfigurationKeys.SERVER_ENABLE_CHECK_AUTH, DEFAULT_SERVER_ENABLE_CHECK_AUTH); @Override public boolean regTransactionManagerCheckAuth(RegisterTMRequest request) { diff --git a/server/src/main/java/org/apache/seata/server/auth/config/ClusterSecurityConfig.java b/server/src/main/java/org/apache/seata/server/auth/config/ClusterSecurityConfig.java new file mode 100644 index 00000000000..be378ccdf7f --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/auth/config/ClusterSecurityConfig.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.server.auth.config; + +import org.apache.seata.server.auth.filter.ClusterAuthenticationTokenFilter; +import org.apache.seata.server.auth.security.CustomUserDetailsServiceImpl; +import org.apache.seata.server.auth.security.JwtAuthenticationEntryPoint; +import org.apache.seata.server.auth.utils.ClusterJwtTokenUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + + +/** + * Spring security config + * + */ +@Configuration(proxyBeanMethods = false) +@EnableGlobalMethodSecurity(prePostEnabled = true) +@Order(1) +public class ClusterSecurityConfig extends WebSecurityConfigurerAdapter { + + /** + * The constant AUTHORIZATION_HEADER. + */ + public static final String AUTHORIZATION_HEADER = "Authorization"; + + /** + * The constant REFRESH_TOKEN. + */ + public static final String REFRESH_TOKEN = "refresh_token"; + + /** + * The constant AUTHORIZATION_TOKEN. + */ + public static final String AUTHORIZATION_TOKEN = "access_token"; + + /** + * The constant ACCESS_TOKEN_NEAR_EXPIRATION. + */ + public static final String ACCESS_TOKEN_NEAR_EXPIRATION = "Access_token_near_expiration"; + + /** + * The constant SECURITY_IGNORE_URLS_SPILT_CHAR. + */ + public static final String SECURITY_IGNORE_URLS_SPILT_CHAR = ","; + + /** + * The constant TOKEN_PREFIX. + */ + public static final String TOKEN_PREFIX = "Bearer "; + + @Autowired + @Qualifier("clusterUserDetailsService") + private CustomUserDetailsServiceImpl userDetailsService; + + @Autowired + @Qualifier("clusterJwtAuthenticationEntryPoint") + private JwtAuthenticationEntryPoint unauthorizedHandler; + + @Autowired + @Qualifier("clusterJwtTokenUtils") + private ClusterJwtTokenUtils tokenProvider; + + @Autowired + private Environment env; + + @Bean("clusterAuthenticationManager") + @Override + @Primary + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); + } + + @Override + public void configure(WebSecurity web) { + String ignoreURLs = env.getProperty("seata.security.ignore.urls", "/**"); + for (String ignoreURL : ignoreURLs.trim().split(SECURITY_IGNORE_URLS_SPILT_CHAR)) { + web.ignoring().antMatchers(ignoreURL.trim()); + } + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests().anyRequest().authenticated().and() + // custom token authorize exception handler + .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() + // since we use jwt, session is not necessary + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + // since we use jwt, csrf is not necessary + .csrf().disable(); + http.requestMatchers().antMatchers("/metadata/v1/**").and() + .addFilterBefore(new ClusterAuthenticationTokenFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class); + + // disable cache + http.headers().cacheControl(); + } + + /** + * Password encoder password encoder. + * + * @return the password encoder + */ + @Bean("clusterPasswordEncoder") + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} diff --git a/server/src/main/java/org/apache/seata/server/auth/config/Swagger2Config.java b/server/src/main/java/org/apache/seata/server/auth/config/Swagger2Config.java new file mode 100644 index 00000000000..3fc337bbb16 --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/auth/config/Swagger2Config.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.server.auth.config; + +import com.google.common.base.Predicates; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.ParameterBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Parameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; +import java.util.ArrayList; +import java.util.List; + +@Configuration +@EnableSwagger2 +@EnableWebMvc +public class Swagger2Config { + @Bean + public Docket api() { + ParameterBuilder AccessTokenTicketPar = new ParameterBuilder(); + ParameterBuilder RefreshTokenTicketPar = new ParameterBuilder(); + List pars = new ArrayList<>(); + AccessTokenTicketPar.name("Authorization").description("Access token with prefix \"Bearer\". If no authentication is required, it can be empty") + .modelRef(new ModelRef("string")).parameterType("header") + .required(false).build(); + RefreshTokenTicketPar.name("refresh_token").description("Refresh token. If no authentication is required, it can be empty") + .modelRef(new ModelRef("string")).parameterType("header") + .required(false).build(); + + pars.add(AccessTokenTicketPar.build()); + pars.add(RefreshTokenTicketPar.build()); + + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .useDefaultResponseMessages(true) + .forCodeGeneration(false) + .select() + .apis(Predicates.or( + RequestHandlerSelectors.basePackage("org.apache.seata.server"), + RequestHandlerSelectors.basePackage("org.apache.seata.console") + )) + .paths(PathSelectors.any()) + .build() + .globalOperationParameters(pars); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("seata API") + .description("Learn more about seata: https://github.com/apache/incubator-seata") + .build(); + } +} diff --git a/server/src/main/java/org/apache/seata/server/auth/controller/ClusterAuthController.java b/server/src/main/java/org/apache/seata/server/auth/controller/ClusterAuthController.java new file mode 100644 index 00000000000..68e444658ec --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/auth/controller/ClusterAuthController.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.server.auth.controller; + +import org.apache.seata.common.result.Code; +import org.apache.seata.common.result.SingleResult; +import org.apache.seata.server.auth.config.ClusterSecurityConfig; +import org.apache.seata.server.auth.security.User; +import org.apache.seata.server.auth.utils.ClusterJwtTokenUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; + +@RestController +@RequestMapping("/metadata/v1/auth") +public class ClusterAuthController { + @Autowired + @Qualifier("clusterJwtTokenUtils") + private ClusterJwtTokenUtils jwtTokenUtils; + @Autowired + @Qualifier("clusterAuthenticationManager") + private AuthenticationManager authenticationManager; + + /** + * Whether the Seata is in broken states or not, and cannot recover except by being restarted + * + * @param response the response + * @param user the user + * @return HTTP code equal to 200 indicates that Seata is in right states. HTTP code equal to 500 indicates that + * Seata is in broken states. + */ + @PostMapping("/login") + public SingleResult login(HttpServletResponse response, @RequestBody User user) { + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + user.getUsername(), user.getPassword()); + try { + //AuthenticationManager(default ProviderManager) #authenticate check Authentication + Authentication authentication = authenticationManager.authenticate(authenticationToken); + //bind authentication to securityContext + SecurityContextHolder.getContext().setAuthentication(authentication); + //create token + String accessToken = jwtTokenUtils.createAccessToken(authentication); + String refreshToken = jwtTokenUtils.createRefreshToken(authentication); + + String authHeader = ClusterSecurityConfig.TOKEN_PREFIX + accessToken; + //put token into http header + response.addHeader(ClusterSecurityConfig.AUTHORIZATION_HEADER, authHeader); + response.addHeader(ClusterSecurityConfig.REFRESH_TOKEN, refreshToken); + return new SingleResult<>(Code.SUCCESS, authHeader); + } catch (BadCredentialsException authentication) { + return new SingleResult<>(Code.LOGIN_FAILED); + } + } +} diff --git a/server/src/main/java/org/apache/seata/server/auth/filter/ClusterAuthenticationTokenFilter.java b/server/src/main/java/org/apache/seata/server/auth/filter/ClusterAuthenticationTokenFilter.java new file mode 100644 index 00000000000..b674dedb67a --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/auth/filter/ClusterAuthenticationTokenFilter.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.server.auth.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.seata.common.result.Code; +import org.apache.seata.common.result.SingleResult; +import org.apache.seata.server.auth.config.ClusterSecurityConfig; +import org.apache.seata.server.auth.utils.ClusterJwtTokenUtils; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * jwt auth token filter + * + */ +public class ClusterAuthenticationTokenFilter extends OncePerRequestFilter { + + private ClusterJwtTokenUtils tokenProvider; + + /** + * Instantiates a new Jwt authentication token filter. + * + * @param tokenProvider the token provider + */ + public ClusterAuthenticationTokenFilter(ClusterJwtTokenUtils tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { + String accessToken = resolveAccessToken(request); + String refreshToken = resolveRefreshToken(request); + SingleResult result = new SingleResult(Code.CHECK_TOKEN_FAILED); + ObjectMapper objectMapper = new ObjectMapper(); + if (accessToken != null) { + result = this.tokenProvider.validateAccessToken(accessToken); + if (result.getMessage().equals(Code.ACCESS_TOKEN_NEAR_EXPIRATION.getMsg())) { + //access token is near expiration + response.addHeader(ClusterSecurityConfig.ACCESS_TOKEN_NEAR_EXPIRATION, "true"); + } + } else if (refreshToken != null) { + result = this.tokenProvider.validateRefreshToken(refreshToken); + if (result.getCode().equals(Code.SUCCESS.getCode())) { + //create access token + String newAccessToken = this.tokenProvider.createAccessToken((UsernamePasswordAuthenticationToken)result.getData()); + + String authHeader = ClusterSecurityConfig.TOKEN_PREFIX + newAccessToken; + //put token into http header + response.addHeader(ClusterSecurityConfig.AUTHORIZATION_HEADER, authHeader); + } + } + if (result.getCode().equals(Code.SUCCESS.getCode())) { + /** + * get auth info + */ + Authentication authentication = (UsernamePasswordAuthenticationToken)result.getData(); + /** + * save user info to securityContext + */ + SecurityContextHolder.getContext().setAuthentication(authentication); + chain.doFilter(request, response); + } else { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write(objectMapper.writeValueAsString(result)); + } + } + + /** + * Get access token from header + */ + private String resolveAccessToken(HttpServletRequest request) { + String bearerToken = request.getHeader(ClusterSecurityConfig.AUTHORIZATION_HEADER); + if (bearerToken != null && bearerToken.startsWith(ClusterSecurityConfig.TOKEN_PREFIX)) { + String accessToken = bearerToken.substring(ClusterSecurityConfig.TOKEN_PREFIX.length()); + return StringUtils.hasText(accessToken) ? accessToken : null; + } + String accessToken = request.getParameter(ClusterSecurityConfig.AUTHORIZATION_TOKEN); + if (StringUtils.hasText(accessToken)) { + return accessToken; + } + return null; + } + + /** + * Get refresh token from header + */ + private String resolveRefreshToken(HttpServletRequest request) { + String refreshToken = request.getHeader(ClusterSecurityConfig.REFRESH_TOKEN); + return StringUtils.hasText(refreshToken) ? refreshToken : null; + } +} + diff --git a/server/src/main/java/org/apache/seata/server/auth/security/CustomUserDetails.java b/server/src/main/java/org/apache/seata/server/auth/security/CustomUserDetails.java new file mode 100644 index 00000000000..84a781e737a --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/auth/security/CustomUserDetails.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.server.auth.security; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +/** + * custom user + * + */ +public class CustomUserDetails implements UserDetails { + private User user; + + /** + * Instantiates a new Custom user details. + * + * @param user the user + */ + public CustomUserDetails(User user) { + this.user = user; + } + + @Override + public Collection getAuthorities() { + // TODO: get authorities + return AuthorityUtils.commaSeparatedStringToAuthorityList(""); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getUsername(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/server/src/main/java/org/apache/seata/server/auth/security/CustomUserDetailsServiceImpl.java b/server/src/main/java/org/apache/seata/server/auth/security/CustomUserDetailsServiceImpl.java new file mode 100644 index 00000000000..0013ffe24fb --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/auth/security/CustomUserDetailsServiceImpl.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.server.auth.security; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; + +/** + * Custom user service + * + */ +@Service("clusterUserDetailsService") +public class CustomUserDetailsServiceImpl implements UserDetailsService { + + @Value("${seata.security.username}") + private String username; + + @Value("${seata.security.password}") + private String password; + + private User user; + + /** + * Init. + */ + @PostConstruct + public void init() { + // TODO: get userInfo by db + user = new User(); + user.setUsername(username); + user.setPassword(new BCryptPasswordEncoder().encode(password)); + } + + @Override + public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { + if (!user.getUsername().equals(userName)) { + throw new UsernameNotFoundException(userName); + } + return new CustomUserDetails(user); + } +} diff --git a/server/src/main/java/org/apache/seata/server/auth/security/JwtAuthenticationEntryPoint.java b/server/src/main/java/org/apache/seata/server/auth/security/JwtAuthenticationEntryPoint.java new file mode 100644 index 00000000000..5ef4ad700f2 --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/auth/security/JwtAuthenticationEntryPoint.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.server.auth.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * jwt auth fail point + * + */ +@Component("clusterJwtAuthenticationEntryPoint") +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class); + + @Override + public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, + AuthenticationException e) throws IOException, ServletException { + LOGGER.error("Responding with unauthorized error. Message - {}", e.getMessage()); + httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); + } +} diff --git a/server/src/main/java/org/apache/seata/server/auth/security/User.java b/server/src/main/java/org/apache/seata/server/auth/security/User.java new file mode 100644 index 00000000000..2932143a9b4 --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/auth/security/User.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.server.auth.security; + +/** + * mock user info + * + */ +public class User { + /** + * The Username. + */ + String username; + /** + * The Password. + */ + String password; + + + //region Getter && Setter + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + //endregion +} diff --git a/server/src/main/java/org/apache/seata/server/auth/utils/ClusterJwtTokenUtils.java b/server/src/main/java/org/apache/seata/server/auth/utils/ClusterJwtTokenUtils.java new file mode 100644 index 00000000000..96743a642fb --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/auth/utils/ClusterJwtTokenUtils.java @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.server.auth.utils; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import org.apache.seata.common.result.Code; +import org.apache.seata.common.result.SingleResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +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.User; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.crypto.spec.SecretKeySpec; +import java.util.Date; +import java.util.List; + +/** + * Jwt token tool + * + */ +@Component("clusterJwtTokenUtils") +public class ClusterJwtTokenUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(ClusterJwtTokenUtils.class); + + private static final String AUTHORITIES_KEY = "auth"; + + /** + * secret key + */ + @Value("${seata.security.secretKey}") + private String secretKey; + + /** + * Access token validity time(ms) + */ + @Value("${seata.security.accessTokenValidityInMilliseconds}") + private long accessTokenValidityInMilliseconds; + + /** + * Refresh token validity time(ms) + */ + @Value("${seata.security.refreshTokenValidityInMilliseconds}") + private long refreshTokenValidityInMilliseconds; + + @PostConstruct + public void warmupJwtGenerate() { + createToken("", accessTokenValidityInMilliseconds); + } + + /** + * Create access token + * + * @param authentication auth info + * @return token string + */ + public String createAccessToken(Authentication authentication) { + return createToken(authentication.getName(), accessTokenValidityInMilliseconds); + } + + /** + * Create refresh token + * + * @param authentication auth info + * @return token string + */ + public String createRefreshToken(Authentication authentication) { + return createToken(authentication.getName(), refreshTokenValidityInMilliseconds); + } + + /** + * Create token + * @param subject authentication name + * @param tokenValidityInMilliseconds token validity time in milliseconds + * @return token string + */ + private String createToken(String subject, long tokenValidityInMilliseconds) { + /** + * Current time + */ + long now = (new Date()).getTime(); + /** + * Expiration date + */ + Date expirationDate = new Date(now + tokenValidityInMilliseconds); + /** + * Key + */ + SecretKeySpec secretKeySpec = new SecretKeySpec(Decoders.BASE64.decode(secretKey), + SignatureAlgorithm.HS256.getJcaName()); + /** + * create token + */ + return Jwts.builder().setSubject(subject).signWith(secretKeySpec, SignatureAlgorithm.HS256) + .claim(AUTHORITIES_KEY, "").setExpiration(expirationDate).compact(); + } + + /** + * validate access token + * + * @param token token + * @return validate result + */ + public SingleResult validateAccessToken(String token) { + try { + /** + * parse the payload of access token + */ + Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody(); + List authorities = AuthorityUtils.commaSeparatedStringToAuthorityList( + (String)claims.get(AUTHORITIES_KEY)); + User principal = new User(claims.getSubject(), "", authorities); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(principal, "", authorities); + if (System.currentTimeMillis() > claims.getExpiration().getTime() - accessTokenValidityInMilliseconds / 3) { + LOGGER.warn("jwt token will be expired, need refresh token"); + return new SingleResult<>(Code.ACCESS_TOKEN_NEAR_EXPIRATION, authenticationToken); + } + return new SingleResult<>(Code.SUCCESS, authenticationToken); + } catch (ExpiredJwtException e) { + LOGGER.warn("Expired JWT token."); + LOGGER.trace("Expired JWT token trace: {}", e); + return new SingleResult<>(Code.ACCESS_TOKEN_EXPIRED); + } catch (Exception e) { + LOGGER.warn("Unsupported JWT token."); + LOGGER.trace("Unsupported JWT token trace: {}", e); + return new SingleResult<>(Code.CHECK_TOKEN_FAILED); + } + } + + /** + * validate refresh token + * + * @param token token + * @return validate result + */ + public SingleResult validateRefreshToken(String token) { + try { + /** + * parse the payload of refresh token + */ + Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody(); + List authorities = AuthorityUtils.commaSeparatedStringToAuthorityList( + (String)claims.get(AUTHORITIES_KEY)); + User principal = new User(claims.getSubject(), "", authorities); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(principal, "", authorities); + return new SingleResult<>(Code.SUCCESS, authenticationToken); + } catch (ExpiredJwtException e) { + LOGGER.warn("Expired JWT token."); + LOGGER.trace("Expired JWT token trace: {}", e); + return new SingleResult<>(Code.REFRESH_TOKEN_EXPIRED); + } catch (Exception e) { + LOGGER.warn("Unsupported JWT token."); + LOGGER.trace("Unsupported JWT token trace: {}", e); + return new SingleResult<>(Code.CHECK_TOKEN_FAILED); + } + } + +} diff --git a/server/src/main/java/org/apache/seata/server/console/controller/BranchSessionController.java b/server/src/main/java/org/apache/seata/server/console/controller/BranchSessionController.java index 1a194b17122..a1e07eae3ac 100644 --- a/server/src/main/java/org/apache/seata/server/console/controller/BranchSessionController.java +++ b/server/src/main/java/org/apache/seata/server/console/controller/BranchSessionController.java @@ -16,16 +16,17 @@ */ package org.apache.seata.server.console.controller; -import javax.annotation.Resource; import org.apache.seata.server.console.service.BranchSessionService; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import javax.annotation.Resource; + /** * Branch Session Controller */ @RestController -@RequestMapping("console/branchSession") +@RequestMapping("/api/v1/console/branchSession") public class BranchSessionController { @Resource(type = BranchSessionService.class) diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index dad4e4ba7f0..e2a32d5f4b7 100644 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -37,6 +37,12 @@ console: user: username: seata password: seata + secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017 + accessTokenValidityInMilliseconds: 600000 + refreshTokenValidityInMilliseconds: 86400000 + ignore: + urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/version.json,/health,/error,/vgroup/v1/**,/metadata/v1/auth/login + seata: config: # support: nacos, consul, apollo, zk, etcd3 @@ -50,8 +56,11 @@ seata: # server: # service-port: 8091 #If not configured, the default is '${server.port} + 1000' security: + username: seata + password: seata secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017 - tokenValidityInMilliseconds: 1800000 + accessTokenValidityInMilliseconds: 600000 + refreshTokenValidityInMilliseconds: 86400000 csrf-ignore-urls: /metadata/v1/** ignore: - urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/version.json,/health,/error,/vgroup/v1/** \ No newline at end of file + urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/version.json,/health,/error,/vgroup/v1/**,/metadata/v1/auth/login