客户端交互服务端依赖 AccessToken. (#34)

pull/39/head
chen.ma 3 years ago
parent 22af663eb1
commit fe66c716b5

@ -3,9 +3,12 @@ package cn.hippo4j.auth.config;
import cn.hippo4j.auth.constant.Constants;
import cn.hippo4j.auth.filter.JWTAuthenticationFilter;
import cn.hippo4j.auth.filter.JWTAuthorizationFilter;
import cn.hippo4j.auth.security.JwtTokenManager;
import cn.hippo4j.auth.service.impl.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
@ -36,6 +39,9 @@ public class GlobalSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
@Resource
private JwtTokenManager tokenManager;
@Bean
public UserDetailsService customUserService() {
return new UserDetailsServiceImpl();
@ -46,6 +52,12 @@ public class GlobalSecurityConfig extends WebSecurityConfigurerAdapter {
return new BCryptPasswordEncoder();
}
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
@ -70,24 +82,13 @@ public class GlobalSecurityConfig extends WebSecurityConfigurerAdapter {
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(tokenManager, authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
public void configure(WebSecurity web) throws Exception {
String[] ignores = Stream
.of(
"/hippo4j/v1/cs/apps/renew/**",
"/hippo4j/v1/cs/apps/remove/**",
"/hippo4j/v1/cs/apps/register/**",
"/hippo4j/v1/cs/configs/**",
"/hippo4j/v1/cs/listener/**",
"/hippo4j/v1/cs/monitor/**",
"/hippo4j/v1/cs/health/check/**",
"/hippo4j/v1/cs/notify/list/config/**"
)
.toArray(String[]::new);
String[] ignores = Stream.of("/hippo4j/v1/cs/auth/users/apply/token/**").toArray(String[]::new);
web.ignoring().antMatchers(ignores);
}

@ -12,4 +12,6 @@ public class Constants {
public static final String SPLIT_COMMA = ",";
public static final long TOKEN_VALIDITY_IN_SECONDS = 18000L;
}

@ -1,13 +1,16 @@
package cn.hippo4j.auth.filter;
import cn.hippo4j.auth.security.JwtTokenManager;
import cn.hippo4j.auth.toolkit.JwtTokenUtil;
import cn.hippo4j.common.toolkit.JSONUtil;
import cn.hippo4j.common.toolkit.UserContext;
import cn.hippo4j.common.web.base.Results;
import cn.hippo4j.common.web.exception.ServiceException;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
@ -19,6 +22,8 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import static cn.hippo4j.common.constant.Constants.ACCESS_TOKEN;
/**
* JWT authorization filter.
*
@ -28,21 +33,36 @@ import java.util.Collections;
@Slf4j
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
private final JwtTokenManager tokenManager;
public JWTAuthorizationFilter(JwtTokenManager tokenManager, AuthenticationManager authenticationManager) {
super(authenticationManager);
this.tokenManager = tokenManager;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 验证客户端交互时 Token
String accessToken = request.getParameter(ACCESS_TOKEN);
if (StrUtil.isNotBlank(accessToken)) {
tokenManager.validateToken(accessToken);
Authentication authentication = this.tokenManager.getAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
return;
}
String tokenHeader = request.getHeader(JwtTokenUtil.TOKEN_HEADER);
// 如果请求头中没有 Authorization 信息则直接放行
String tokenHeader = request.getHeader(JwtTokenUtil.TOKEN_HEADER);
if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 如果请求头中有 Token, 则进行解析, 并且设置认证信息
try {
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));

@ -0,0 +1,46 @@
package cn.hippo4j.auth.security;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.expression.AccessException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
/**
* Auth manager
*
* @author chen.ma
* @date 2021/12/20 20:34
*/
@Component
@AllArgsConstructor
public class AuthManager {
private final JwtTokenManager jwtTokenManager;
private final AuthenticationManager authenticationManager;
/**
* Resolve token from user.
*
* @param userName
* @param rawPassword
* @return
* @throws AccessException
*/
@SneakyThrows
public String resolveTokenFromUser(String userName, String rawPassword) {
try {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userName, rawPassword);
authenticationManager.authenticate(authenticationToken);
} catch (AuthenticationException e) {
throw new AccessException("Unknown user.");
}
return jwtTokenManager.createToken(userName);
}
}

@ -0,0 +1,61 @@
package cn.hippo4j.auth.security;
import cn.hutool.core.util.StrUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
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 java.util.Date;
import java.util.List;
import static cn.hippo4j.auth.constant.Constants.TOKEN_VALIDITY_IN_SECONDS;
import static cn.hippo4j.auth.toolkit.JwtTokenUtil.SECRET;
import static cn.hippo4j.common.constant.Constants.AUTHORITIES_KEY;
/**
* Jwt token manager.
*
* @author chen.ma
* @date 2021/12/20 20:36
*/
@Component
public class JwtTokenManager {
public String createToken(String userName) {
long now = System.currentTimeMillis();
Date validity;
validity = new Date(now + TOKEN_VALIDITY_IN_SECONDS * 1000L);
Claims claims = Jwts.claims().setSubject(userName);
return Jwts.builder().setClaims(claims).setExpiration(validity)
.signWith(SignatureAlgorithm.HS512, SECRET).compact();
}
public void validateToken(String token) {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
}
/**
* Get auth Info.
*
* @param token token
* @return auth info
*/
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
List<GrantedAuthority> authorities = AuthorityUtils
.commaSeparatedStringToAuthorityList((String) claims.get(AUTHORITIES_KEY));
User principal = new User(claims.getSubject(), StrUtil.EMPTY, authorities);
return new UsernamePasswordAuthenticationToken(principal, StrUtil.EMPTY, authorities);
}
}

@ -22,8 +22,8 @@ public class JwtTokenUtil {
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
private static final String SECRET = "Hippo4J_admin";
private static final String ISS = "admin";
public static final String SECRET = "SecretKey039245678901232039487623456783092349288901402967890140939827";
public static final String ISS = "admin";
/**
* Key
@ -110,10 +110,7 @@ public class JwtTokenUtil {
}
private static Claims getTokenBody(String token) {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
}
}

@ -16,6 +16,12 @@ public class Constants {
public static final String GROUP_KEY = "groupKey";
public static final String AUTHORITIES_KEY = "auth";
public static final String ACCESS_TOKEN = "accessToken";
public static final String TOKEN_TTL = "tokenTtl";
public static final String DEFAULT_NAMESPACE_ID = "public";
public static final String NULL = "";

@ -0,0 +1,28 @@
package cn.hippo4j.common.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Token info.
*
* @author chen.ma
* @date 2021/12/20 20:02
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TokenInfo {
/**
* accessToken
*/
private String accessToken;
/**
* tokenTtl
*/
private Long tokenTtl;
}

@ -1,19 +1,24 @@
package cn.hippo4j.console.controller;
import cn.hippo4j.common.toolkit.UserContext;
import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.hippo4j.auth.model.UserInfo;
import cn.hippo4j.auth.model.biz.user.UserQueryPageReqDTO;
import cn.hippo4j.auth.model.biz.user.UserReqDTO;
import cn.hippo4j.auth.model.biz.user.UserRespDTO;
import cn.hippo4j.auth.security.AuthManager;
import cn.hippo4j.auth.service.UserService;
import cn.hippo4j.common.constant.Constants;
import cn.hippo4j.common.model.TokenInfo;
import cn.hippo4j.common.toolkit.UserContext;
import cn.hippo4j.common.web.base.Result;
import cn.hippo4j.common.web.base.Results;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.hippo4j.auth.constant.Constants.TOKEN_VALIDITY_IN_SECONDS;
/**
* User controller.
*
@ -27,6 +32,15 @@ public class UserController {
private final UserService userService;
private final AuthManager authManager;
@PostMapping("/apply/token")
public Result<TokenInfo> applyToken(@RequestBody UserInfo userInfo) {
String accessToken = authManager.resolveTokenFromUser(userInfo.getUserName(), userInfo.getPassword());
TokenInfo tokenInfo = new TokenInfo(accessToken, TOKEN_VALIDITY_IN_SECONDS);
return Results.success(tokenInfo);
}
@PostMapping("/page")
public Result<IPage<UserRespDTO>> listUser(@RequestBody UserQueryPageReqDTO reqDTO) {
IPage<UserRespDTO> resultUserPage = userService.listUser(reqDTO);

@ -16,4 +16,6 @@ spring:
thread-pool:
server-addr: http://localhost:6691
namespace: prescription
item-id: ${spring.application.name}
item-id: dynamic-threadpool-example
username: admin
password: 123456

@ -19,6 +19,16 @@ public class BootstrapProperties {
public static final String PREFIX = "spring.dynamic.thread-pool";
/**
* Username.
*/
private String username;
/**
* Password.
*/
private String password;
/**
* Server addr
*/

@ -20,6 +20,22 @@ public class BeforeCheckConfiguration {
@Bean
public BeforeCheckConfiguration.BeforeCheck dynamicThreadPoolBeforeCheckBean(BootstrapProperties properties, ConfigurableEnvironment environment) {
String username = properties.getUsername();
if (StrUtil.isBlank(username)) {
throw new ConfigEmptyException(
"Web server failed to start. The dynamic thread pool username is empty.",
"Please check whether the [spring.dynamic.thread-pool.username] configuration is empty or an empty string."
);
}
String password = properties.getPassword();
if (StrUtil.isBlank(password)) {
throw new ConfigEmptyException(
"Web server failed to start. The dynamic thread pool password is empty.",
"Please check whether the [spring.dynamic.thread-pool.password] configuration is empty or an empty string."
);
}
String namespace = properties.getNamespace();
if (StrUtil.isBlank(namespace)) {
throw new ConfigEmptyException(

@ -1,11 +1,19 @@
package cn.hippo4j.starter.remote;
import cn.hippo4j.common.config.ApplicationContextHolder;
import cn.hippo4j.common.constant.Constants;
import cn.hippo4j.common.web.base.Result;
import cn.hippo4j.starter.config.BootstrapProperties;
import cn.hippo4j.starter.security.SecurityProxy;
import cn.hippo4j.starter.toolkit.HttpClientUtil;
import cn.hippo4j.starter.toolkit.thread.ThreadFactoryBuilder;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.Maps;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Server http agent.
@ -21,12 +29,32 @@ public class ServerHttpAgent implements HttpAgent {
private final HttpClientUtil httpClientUtil;
private SecurityProxy securityProxy;
private ServerHealthCheck serverHealthCheck;
private ScheduledExecutorService executorService;
private final long securityInfoRefreshIntervalMills = TimeUnit.SECONDS.toMillis(5);
public ServerHttpAgent(BootstrapProperties properties, HttpClientUtil httpClientUtil) {
this.dynamicThreadPoolProperties = properties;
this.httpClientUtil = httpClientUtil;
this.serverListManager = new ServerListManager(dynamicThreadPoolProperties);
this.securityProxy = new SecurityProxy(httpClientUtil, properties);
this.securityProxy.applyToken(this.serverListManager.getServerUrls());
this.executorService = new ScheduledThreadPoolExecutor(
new Integer(1),
ThreadFactoryBuilder.builder().daemon(true).prefix("token-security-updater").build()
);
this.executorService.scheduleWithFixedDelay(
() -> securityProxy.applyToken(serverListManager.getServerUrls()),
0,
securityInfoRefreshIntervalMills,
TimeUnit.MILLISECONDS
);
}
@Override
@ -46,30 +74,35 @@ public class ServerHttpAgent implements HttpAgent {
@Override
public Result httpGetSimple(String path) {
path = injectSecurityInfoByPath(path);
return httpClientUtil.restApiGetHealth(buildUrl(path), Result.class);
}
@Override
public Result httpPost(String path, Object body) {
isHealthStatus();
path = injectSecurityInfoByPath(path);
return httpClientUtil.restApiPost(buildUrl(path), body, Result.class);
}
@Override
public Result httpPostByDiscovery(String path, Object body) {
isHealthStatus();
path = injectSecurityInfoByPath(path);
return httpClientUtil.restApiPost(buildUrl(path), body, Result.class);
}
@Override
public Result httpGetByConfig(String path, Map<String, String> headers, Map<String, String> paramValues, long readTimeoutMs) {
isHealthStatus();
injectSecurityInfo(paramValues);
return httpClientUtil.restApiGetByThreadPool(buildUrl(path), headers, paramValues, readTimeoutMs, Result.class);
}
@Override
public Result httpPostByConfig(String path, Map<String, String> headers, Map<String, String> paramValues, long readTimeoutMs) {
isHealthStatus();
injectSecurityInfo(paramValues);
return httpClientUtil.restApiPostByThreadPool(buildUrl(path), headers, paramValues, readTimeoutMs, Result.class);
}
@ -90,4 +123,18 @@ public class ServerHttpAgent implements HttpAgent {
serverHealthCheck.isHealthStatus();
}
private Map injectSecurityInfo(Map<String, String> params) {
if (StrUtil.isNotBlank(securityProxy.getAccessToken())) {
params.put(Constants.ACCESS_TOKEN, securityProxy.getAccessToken());
}
return params;
}
@Deprecated
private String injectSecurityInfoByPath(String path) {
String resultPath = httpClientUtil.buildUrl(path, injectSecurityInfo(Maps.newHashMap()));
return resultPath;
}
}

@ -1,6 +1,8 @@
package cn.hippo4j.starter.remote;
import cn.hippo4j.starter.config.BootstrapProperties;
import cn.hutool.core.util.StrUtil;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
@ -21,6 +23,7 @@ public class ServerListManager {
private String serverAddrsStr;
@Getter
volatile List<String> serverUrls = new ArrayList();
private volatile String currentServerAddr;
@ -34,18 +37,21 @@ public class ServerListManager {
serverAddrsStr = properties.getServerAddr();
if (!StringUtils.isEmpty(serverAddrsStr)) {
List<String> serverAddrs = new ArrayList();
String[] serverAddrsArr = this.serverAddrsStr.split(",");
for (String serverAddr : serverAddrsArr) {
if (serverAddr.startsWith(HTTPS) || serverAddr.startsWith(HTTP)) {
// TODO Temporarily fixed write, later optimized
currentServerAddr = serverAddr;
serverAddrs.add(serverAddr);
List<String> serverAddrList = new ArrayList();
String[] serverAddrListArr = this.serverAddrsStr.split(",");
for (String serverAddr : serverAddrListArr) {
boolean whetherJoint = StrUtil.isNotBlank(serverAddr)
&& !serverAddr.startsWith(HTTPS) && !serverAddr.startsWith(HTTP);
if (whetherJoint) {
serverAddr = HTTP + serverAddr;
}
currentServerAddr = serverAddr;
serverAddrList.add(serverAddr);
}
this.serverUrls = serverAddrs;
this.serverUrls = serverAddrList;
}
}

@ -0,0 +1,102 @@
package cn.hippo4j.starter.security;
import cn.hippo4j.common.constant.Constants;
import cn.hippo4j.common.model.TokenInfo;
import cn.hippo4j.common.toolkit.JSONUtil;
import cn.hippo4j.common.web.base.Result;
import cn.hippo4j.starter.config.BootstrapProperties;
import cn.hippo4j.starter.toolkit.HttpClientUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Security proxy.
*
* @author chen.ma
* @date 2021/12/20 20:19
*/
@Slf4j
public class SecurityProxy {
private static final String APPLY_TOKEN_URL = Constants.BASE_PATH + "/auth/users/apply/token";
private final HttpClientUtil httpClientUtil;
private final String username;
private final String password;
private String accessToken;
private long tokenTtl;
private long lastRefreshTime;
private long tokenRefreshWindow;
public SecurityProxy(HttpClientUtil httpClientUtil, BootstrapProperties properties) {
username = properties.getUsername();
password = properties.getPassword();
this.httpClientUtil = httpClientUtil;
}
public boolean applyToken(List<String> servers) {
try {
if ((System.currentTimeMillis() - lastRefreshTime) < TimeUnit.SECONDS
.toMillis(tokenTtl - tokenRefreshWindow)) {
return true;
}
for (String server : servers) {
if (applyToken(server)) {
lastRefreshTime = System.currentTimeMillis();
return true;
}
}
} catch (Throwable ignore) {
// ignore
}
return false;
}
public boolean applyToken(String server) {
if (StrUtil.isAllNotBlank(username, password)) {
String url = server + APPLY_TOKEN_URL;
Map<String, String> bodyMap = new HashMap(2);
bodyMap.put("userName", username);
bodyMap.put("password", password);
try {
Result<String> result = httpClientUtil.restApiPost(url, bodyMap, Result.class);
if (!result.isSuccess()) {
log.error("Error getting access token. message :: {}", result.getMessage());
return false;
}
String tokenJsonStr = JSONUtil.toJSONString(result.getData());
TokenInfo tokenInfo = JSONUtil.parseObject(tokenJsonStr, TokenInfo.class);
accessToken = tokenInfo.getAccessToken();
tokenTtl = tokenInfo.getTokenTtl();
tokenRefreshWindow = tokenTtl / 10;
} catch (Throwable ex) {
log.error("Failed to apply for token. message :: {}", ex.getMessage());
return false;
}
}
return true;
}
public String getAccessToken() {
return accessToken;
}
}
Loading…
Cancel
Save