diff --git a/ecell-internationalize/ecell-internationalize-auth/src/main/java/com/ecell/internationalize/auth/entity/UserType.java b/ecell-internationalize/ecell-internationalize-auth/src/main/java/com/ecell/internationalize/auth/entity/UserType.java new file mode 100644 index 0000000..29ea86f --- /dev/null +++ b/ecell-internationalize/ecell-internationalize-auth/src/main/java/com/ecell/internationalize/auth/entity/UserType.java @@ -0,0 +1,19 @@ +package com.ecell.internationalize.auth.entity; + +import lombok.Data; + +/** + * @author borui + */ +@Data +public class UserType { + /** + * 用户名 + */ + private String username; + + /** + * 用户密码 + */ + private String password; +} diff --git a/ecell-internationalize/ecell-internationalize-auth/src/main/java/com/ecell/internationalize/auth/fallback/SysUserFeignServiceFallBack.java b/ecell-internationalize/ecell-internationalize-auth/src/main/java/com/ecell/internationalize/auth/fallback/SysUserFeignServiceFallBack.java new file mode 100644 index 0000000..bd4dc0a --- /dev/null +++ b/ecell-internationalize/ecell-internationalize-auth/src/main/java/com/ecell/internationalize/auth/fallback/SysUserFeignServiceFallBack.java @@ -0,0 +1,24 @@ +package com.ecell.internationalize.auth.fallback; + +import com.ecell.internationalize.auth.feign.SysUserFeignClient; +import com.ecell.internationalize.common.core.domain.UserLogin; +import com.ecell.internationalize.common.core.exception.ServiceException; +import com.ecell.internationalize.common.core.utils.locale.LocaleUtil; +import net.bytebuddy.implementation.bytecode.constant.FieldConstant; +import org.springframework.stereotype.Component; + +/** + * @author borui + */ +@Component +public class SysUserFeignServiceFallBack implements SysUserFeignClient { + @Override + public UserLogin queryByUser(String username) { + throw new ServiceException(LocaleUtil.getMessage("messages.fallback.info")); + } + + @Override + public void update(UserLogin userLogin) { + throw new ServiceException(LocaleUtil.getMessage("messages.fallback.info")); + } +} diff --git a/ecell-internationalize/ecell-internationalize-auth/src/main/java/com/ecell/internationalize/auth/feign/SysUserFeignClient.java b/ecell-internationalize/ecell-internationalize-auth/src/main/java/com/ecell/internationalize/auth/feign/SysUserFeignClient.java new file mode 100644 index 0000000..e32e893 --- /dev/null +++ b/ecell-internationalize/ecell-internationalize-auth/src/main/java/com/ecell/internationalize/auth/feign/SysUserFeignClient.java @@ -0,0 +1,31 @@ +package com.ecell.internationalize.auth.feign; + +import com.ecell.internationalize.auth.fallback.SysUserFeignServiceFallBack; +import com.ecell.internationalize.common.core.domain.UserLogin; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * @author borui + */ +@FeignClient(value = "yisai-system-security",fallback = SysUserFeignServiceFallBack.class,contextId ="yisai-system-security003") +public interface SysUserFeignClient { + /** + * 通过用户名查询用户信息 + * @param userName 用户名 + * @return 结果 + */ + @GetMapping("sys_user/user/queryByUserName") + UserLogin queryByUser(@RequestParam("userName") String userName); + /** + * 修改用户信息 + * @param userLogin 实体对象 + * @return 结果 + */ + @PostMapping("sys_user/user/updateLoginStatus") + void update(@RequestBody UserLogin userLogin); + +} diff --git a/ecell-internationalize/ecell-internationalize-auth/src/main/java/com/ecell/internationalize/auth/service/SysUserLoginService.java b/ecell-internationalize/ecell-internationalize-auth/src/main/java/com/ecell/internationalize/auth/service/SysUserLoginService.java new file mode 100644 index 0000000..a476c64 --- /dev/null +++ b/ecell-internationalize/ecell-internationalize-auth/src/main/java/com/ecell/internationalize/auth/service/SysUserLoginService.java @@ -0,0 +1,17 @@ +package com.ecell.internationalize.auth.service; + +import com.ecell.internationalize.common.core.domain.UserLogin; +import com.ecell.internationalize.common.core.web.domain.R; + +/** + * @author borui + */ +public interface SysUserLoginService { + /** + * 账号认证 + * @param username 用户名 + * @param password 密码 + * @return UserLogin + */ + R login(String username, String password); +} diff --git a/ecell-internationalize/ecell-internationalize-auth/src/main/java/com/ecell/internationalize/auth/service/impl/SysUserLoginServiceImpl.java b/ecell-internationalize/ecell-internationalize-auth/src/main/java/com/ecell/internationalize/auth/service/impl/SysUserLoginServiceImpl.java new file mode 100644 index 0000000..9e8428a --- /dev/null +++ b/ecell-internationalize/ecell-internationalize-auth/src/main/java/com/ecell/internationalize/auth/service/impl/SysUserLoginServiceImpl.java @@ -0,0 +1,66 @@ +package com.ecell.internationalize.auth.service.impl; + +import cn.hutool.http.HttpUtil; +import com.ecell.internationalize.auth.feign.SysUserFeignClient; +import com.ecell.internationalize.auth.service.SysUserLoginService; +import com.ecell.internationalize.common.core.domain.UserLogin; +import com.ecell.internationalize.common.core.enums.UserStatus; +import com.ecell.internationalize.common.core.utils.ServletUtils; +import com.ecell.internationalize.common.core.utils.StringUtils; +import com.ecell.internationalize.common.core.utils.ip.IpUtils; +import com.ecell.internationalize.common.core.utils.locale.LocaleUtil; +import com.ecell.internationalize.common.core.web.domain.R; +import com.ecell.internationalize.common.security.utils.SecurityUtils; +import net.bytebuddy.implementation.bytecode.constant.FieldConstant; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * @author borui + */ +@Service +public class SysUserLoginServiceImpl implements SysUserLoginService { + private final String DELETED="0"; + @Autowired + private SysUserFeignClient sysUserFeignClient; + + @Override + public R login(String username, String password) { + + // 用户名或密码为空 + if (StringUtils.isAnyBlank(username, password)) { + return R.fail(LocaleUtil.getMessage("messages.login.empty")); + } + UserLogin userInfo = sysUserFeignClient.queryByUser(username); + //查询错误 + if (StringUtils.isNull(userInfo)){ + return R.fail(LocaleUtil.getMessage("messages.login.error")); + } + if (!SecurityUtils.matchesPassword(password, userInfo.getPassword())){ + return R.fail(LocaleUtil.getMessage("messages.error.password")); + } + //账号被删除 + if (DELETED.equals(userInfo.getDelFlag())) { + return R.fail(LocaleUtil.getMessage("messages.account.delete")); + } + //账号被停用 + if (UserStatus.DISABLE.getCode().equals(userInfo.getStatus())) { + return R.fail(LocaleUtil.getMessage("messages.account.delete")); + } + System.out.println("userInfo:"+userInfo.getStatus()); + System.out.println("校验通过"); + //修改登录时间和IP + UserLogin userLogin=new UserLogin(); + userLogin.setUserId(userInfo.getUserId()); + userLogin.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest())); + SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss" ); + Date date= new Date(); + String str = sdf.format(date); + userLogin.setLoginDate(str); + sysUserFeignClient.update(userLogin); + return R.ok(userInfo); + } +} diff --git a/ecell-internationalize/ecell-internationalize-common/ecell-internationalize-core/src/main/java/com/ecell/internationalize/common/core/domain/UserLogin.java b/ecell-internationalize/ecell-internationalize-common/ecell-internationalize-core/src/main/java/com/ecell/internationalize/common/core/domain/UserLogin.java new file mode 100644 index 0000000..0712b26 --- /dev/null +++ b/ecell-internationalize/ecell-internationalize-common/ecell-internationalize-core/src/main/java/com/ecell/internationalize/common/core/domain/UserLogin.java @@ -0,0 +1,123 @@ +package com.ecell.internationalize.common.core.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; +import java.util.Set; + +/** + * @author borui + */ +@Data +public class UserLogin implements Serializable { + private static final long serialVersionUID = 1L; + /** + * 用户唯一标识 + */ + private String token; + + /** + * 主键Id + */ + private String userId; + + /** + * 用户昵称 + */ + private String nickName; + + /** + * 用户账号 + */ + private String account; + + /** + * 用户类型 + */ + private String userType; + + /** + * 用户头像 + */ + private String headImg; + + /** + * 密码 + */ + private String password; + + /** + * 帐号状态(0正常 1停用) + */ + private String status; + + /** + * 删除标识(0:已删除,1:正常) + */ + private String delFlag; + + /** + * 登录IP + */ + private String loginIp; + + /** + * 最后登录时间 + */ + private String loginDate; + + /** + * 创建人 + */ + private String createUser; + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + /** + * 修改人 + */ + private String updateUser; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + + /** + * 厂商Id + */ + private String firmId; + /** + * 代理商Id + */ + private String secondFirmId; + + /** + * 标识(1:厂商:2:代理商) + */ + private String firmFlag; + /** + * 登录时间 + */ + private Long loginTime; + + /** + * 过期时间 + */ + private Long expireTime; + + /** + * 权限列表 + */ + private Set permissions; + + /** + * 角色列表 + */ + private Set roles; + + +} diff --git a/ecell-internationalize/ecell-internationalize-common/ecell-internationalize-core/src/main/java/com/ecell/internationalize/common/core/enums/UserStatus.java b/ecell-internationalize/ecell-internationalize-common/ecell-internationalize-core/src/main/java/com/ecell/internationalize/common/core/enums/UserStatus.java index 03926f5..ba2d3c3 100644 --- a/ecell-internationalize/ecell-internationalize-common/ecell-internationalize-core/src/main/java/com/ecell/internationalize/common/core/enums/UserStatus.java +++ b/ecell-internationalize/ecell-internationalize-common/ecell-internationalize-core/src/main/java/com/ecell/internationalize/common/core/enums/UserStatus.java @@ -4,24 +4,24 @@ package com.ecell.internationalize.common.core.enums; * 用户状态 * @author borui */ -public class UserStatus { -// OK("0", "正常"),DISABLE("1", "停用"),DELETED("2", "删除"); -// private final String code; -// private final String info; -// -// UserStatus(String code, String info) -// { -// this.code = code; -// this.info = info; -// } -// -// public String getCode() -// { -// return code; -// } -// -// public String getInfo() -// { -// return info; -// } +public enum UserStatus { + OK("0", "正常"),DISABLE("1", "停用"),DELETED("2", "删除"); + private final String code; + private final String info; + + UserStatus(String code, String info) + { + this.code = code; + this.info = info; + } + + public String getCode() + { + return code; + } + + public String getInfo() + { + return info; + } } diff --git a/ecell-internationalize/ecell-internationalize-common/ecell-internationalize-security/src/main/java/com/ecell/internationalize/common/security/service/TokenService.java b/ecell-internationalize/ecell-internationalize-common/ecell-internationalize-security/src/main/java/com/ecell/internationalize/common/security/service/TokenService.java index 5099663..ebb5260 100644 --- a/ecell-internationalize/ecell-internationalize-common/ecell-internationalize-security/src/main/java/com/ecell/internationalize/common/security/service/TokenService.java +++ b/ecell-internationalize/ecell-internationalize-common/ecell-internationalize-security/src/main/java/com/ecell/internationalize/common/security/service/TokenService.java @@ -6,6 +6,7 @@ import com.ecell.internationalize.common.core.constant.CacheConstants; import com.ecell.internationalize.common.core.constant.SecurityConstants; import com.ecell.internationalize.common.core.domain.LoginUser; import com.ecell.internationalize.common.core.domain.SysUser; +import com.ecell.internationalize.common.core.domain.UserLogin; import com.ecell.internationalize.common.core.utils.JwtUtils; import com.ecell.internationalize.common.core.utils.ServletUtils; import com.ecell.internationalize.common.core.utils.StringUtils; @@ -13,6 +14,8 @@ import com.ecell.internationalize.common.core.utils.ip.IpUtils; import com.ecell.internationalize.common.core.utils.uuid.IdUtils; import com.ecell.internationalize.common.redis.service.RedisService; import com.ecell.internationalize.common.security.utils.SecurityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -26,6 +29,7 @@ import java.util.concurrent.TimeUnit; */ @Component public class TokenService { + private static final Logger log = LoggerFactory.getLogger(TokenService.class); @Autowired private RedisService redisService; @@ -38,6 +42,7 @@ public class TokenService { private final static String ACCESS_TOKEN = CacheConstants.LOGIN_TOKEN_KEY; private final static Long MILLIS_MINUTE_TEN = CacheConstants.REFRESH_TIME * MILLIS_MINUTE; + private static final long EXPIRATION = 720; /** * 创建令牌 @@ -67,6 +72,34 @@ public class TokenService { } /** + * yisai 创建令牌 + * @param userLogin + * @return + */ + public Map createToken(UserLogin userLogin) + { + String token = IdUtils.fastUUID(); + userLogin.setToken(token); + userLogin.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest())); + refreshToken(userLogin); + + // Jwt存储信息 + Map claimsMap = new HashMap<>(); + claimsMap.put(SecurityConstants.USER_KEY, token); + claimsMap.put(SecurityConstants.DETAILS_USER_ID, userLogin.getUserId()); + claimsMap.put(SecurityConstants.DETAILS_USERNAME, userLogin.getAccount()); + + // 接口返回信息 + Map rspMap = new HashMap<>(); + rspMap.put("access_token", JwtUtils.createToken(claimsMap)); + rspMap.put("expires_in",EXPIRATION); + return rspMap; + } + + + + + /** * 获取用户身份信息 * * @return 用户信息 @@ -88,6 +121,20 @@ public class TokenService { return getLoginUser(token); } + + /** + * yisai 获取用户身份信息 + * + * @return 用户信息 + */ + public UserLogin getUserLogin(HttpServletRequest request) + { + // 获取请求携带的令牌 + String token = SecurityUtils.getToken(request); + return getUserLogin(token); + } + + /** * 获取用户身份信息 * @@ -111,6 +158,33 @@ public class TokenService { return user; } + + /** + * 获取yisai用户身份信息 + * + * @return 用户信息 + */ + public UserLogin getUserLogin(String token) + { + UserLogin user = null; + try + { + if (StringUtils.isNotEmpty(token)) + { + String userkey = JwtUtils.getUserKey(token); + Object cacheObject = redisService.getCacheObject(getTokenKey(userkey)); + if (StringUtils.isNotNull(cacheObject)){ + user= JSON.parseObject(JSON.toJSONString(cacheObject),UserLogin.class); + } + return user; + } + } + catch (Exception e) + { + } + return user; + } + /** * 设置用户身份信息 */ @@ -149,6 +223,22 @@ public class TokenService { } } + + /** + * yisai 验证令牌有效期,相差不足120分钟,自动刷新缓存 + * + * @param loginUser + */ + public void verifyYiSaiToken(UserLogin loginUser) + { + long expireTime = loginUser.getExpireTime(); + long currentTime = System.currentTimeMillis(); + if (expireTime - currentTime <= MILLIS_MINUTE_TEN) + { + refreshToken(loginUser); + } + } + /** * 刷新令牌有效期 * @@ -163,6 +253,24 @@ public class TokenService { redisService.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); } + + /** + * yisai 刷新令牌有效期 + * + * @param userLogin 登录信息 + */ + public void refreshToken(UserLogin userLogin) + { + userLogin.setLoginTime(System.currentTimeMillis()); + userLogin.setExpireTime(userLogin.getLoginTime() + EXPIRATION * MILLIS_MINUTE); + // 根据uuid将loginUser缓存 + String userKey = getTokenKey(userLogin.getToken()); + redisService.setCacheObject(userKey, userLogin, EXPIRATION, TimeUnit.MINUTES); + + } + + + private String getTokenKey(String token) { return ACCESS_TOKEN + token; diff --git a/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/config/RouterFunctionConfiguration.java b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/config/RouterFunctionConfiguration.java new file mode 100644 index 0000000..bb751dd --- /dev/null +++ b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/config/RouterFunctionConfiguration.java @@ -0,0 +1,28 @@ +package com.ecell.internationalize.gateway.config; + +import com.ecell.internationalize.gateway.handler.ValidateCodeHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; + +/** + * @author borui + */ +@Configuration +public class RouterFunctionConfiguration { + @Autowired + private ValidateCodeHandler validateCodeHandler; + + @SuppressWarnings("rawtypes") + @Bean + public RouterFunction routerFunction() + { + return RouterFunctions.route( + RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), + validateCodeHandler); + } +} diff --git a/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/config/SwaggerProvider.java b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/config/SwaggerProvider.java new file mode 100644 index 0000000..08c2447 --- /dev/null +++ b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/config/SwaggerProvider.java @@ -0,0 +1,77 @@ +package com.ecell.internationalize.gateway.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.config.GatewayProperties; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.support.NameUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.config.ResourceHandlerRegistry; +import org.springframework.web.reactive.config.WebFluxConfigurer; +import springfox.documentation.swagger.web.SwaggerResource; +import springfox.documentation.swagger.web.SwaggerResourcesProvider; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author borui + */ +@Component +public class SwaggerProvider implements SwaggerResourcesProvider, WebFluxConfigurer { + /** + * Swagger2默认的url后缀 + */ + public static final String SWAGGER2URL = "/v2/api-docs"; + + /** + * 网关路由 + */ + @Lazy + @Autowired + private RouteLocator routeLocator; + + @Autowired + private GatewayProperties gatewayProperties; + + /** + * 聚合其他服务接口 + * + * @return + */ + @Override + public List get() + { + List resourceList = new ArrayList<>(); + List routes = new ArrayList<>(); + // 获取网关中配置的route + routeLocator.getRoutes().subscribe(route -> routes.add(route.getId())); + gatewayProperties.getRoutes().stream() + .filter(routeDefinition -> routes + .contains(routeDefinition.getId())) + .forEach(routeDefinition -> routeDefinition.getPredicates().stream() + .filter(predicateDefinition -> "Path".equalsIgnoreCase(predicateDefinition.getName())) + .filter(predicateDefinition -> !"ecell-auth".equalsIgnoreCase(routeDefinition.getId())) + .forEach(predicateDefinition -> resourceList + .add(swaggerResource(routeDefinition.getId(), predicateDefinition.getArgs() + .get(NameUtils.GENERATED_NAME_PREFIX + "0").replace("/**", SWAGGER2URL))))); + return resourceList; + } + + private SwaggerResource swaggerResource(String name, String location) + { + SwaggerResource swaggerResource = new SwaggerResource(); + swaggerResource.setName(name); + swaggerResource.setLocation(location); + swaggerResource.setSwaggerVersion("2.0"); + return swaggerResource; + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) + { + /** swagger-ui 地址 */ + registry.addResourceHandler("/swagger-ui/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/"); + } +} diff --git a/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/config/properties/XssProperties.java b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/config/properties/XssProperties.java new file mode 100644 index 0000000..8b3fef5 --- /dev/null +++ b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/config/properties/XssProperties.java @@ -0,0 +1,46 @@ +package com.ecell.internationalize.gateway.config.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author borui + */ +@Configuration +@RefreshScope +@ConfigurationProperties(prefix = "security.xss") +public class XssProperties { + /** + * Xss开关 + */ + private Boolean enabled; + + /** + * 排除路径 + */ + private List excludeUrls = new ArrayList<>(); + + public Boolean getEnabled() + { + return enabled; + } + + public void setEnabled(Boolean enabled) + { + this.enabled = enabled; + } + + public List getExcludeUrls() + { + return excludeUrls; + } + + public void setExcludeUrls(List excludeUrls) + { + this.excludeUrls = excludeUrls; + } +} diff --git a/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/filter/ValidateCodeFilter.java b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/filter/ValidateCodeFilter.java new file mode 100644 index 0000000..67d0621 --- /dev/null +++ b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/filter/ValidateCodeFilter.java @@ -0,0 +1,77 @@ +package com.ecell.internationalize.gateway.filter; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.ecell.internationalize.common.core.utils.ServletUtils; +import com.ecell.internationalize.common.core.utils.StringUtils; +import com.ecell.internationalize.gateway.config.properties.CaptchaProperties; +import com.ecell.internationalize.gateway.service.ValidateCodeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; + +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author borui + */ +@Component +public class ValidateCodeFilter extends AbstractGatewayFilterFactory { + private final static String[] VALIDATE_URL = new String[] { "/auth/login", "/auth/register" }; + + @Autowired + private ValidateCodeService validateCodeService; + + @Autowired + private CaptchaProperties captchaProperties; + + private static final String CODE = "code"; + + private static final String UUID = "uuid"; + + @Override + public GatewayFilter apply(Object config) + { + return (exchange, chain) -> { + ServerHttpRequest request = exchange.getRequest(); + + // 非登录/注册请求或验证码关闭,不处理 + if (!StringUtils.containsAnyIgnoreCase(request.getURI().getPath(), VALIDATE_URL) || !captchaProperties.getEnabled()) + { + return chain.filter(exchange); + } + + try + { + String rspStr = resolveBodyFromRequest(request); + JSONObject obj = JSON.parseObject(rspStr); + validateCodeService.checkCaptcha(obj.getString(CODE), obj.getString(UUID)); + } + catch (Exception e) + { + return ServletUtils.webFluxResponseWriter(exchange.getResponse(), e.getMessage()); + } + return chain.filter(exchange); + }; + } + + private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) + { + // 获取请求体 + Flux body = serverHttpRequest.getBody(); + AtomicReference bodyRef = new AtomicReference<>(); + body.subscribe(buffer -> { + CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer()); + DataBufferUtils.release(buffer); + bodyRef.set(charBuffer.toString()); + }); + return bodyRef.get(); + } +} diff --git a/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/filter/XssFilter.java b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/filter/XssFilter.java new file mode 100644 index 0000000..d6e5b3c --- /dev/null +++ b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/filter/XssFilter.java @@ -0,0 +1,118 @@ +package com.ecell.internationalize.gateway.filter; + +import com.ecell.internationalize.common.core.utils.StringUtils; +import com.ecell.internationalize.common.core.utils.html.EscapeUtil; +import com.ecell.internationalize.gateway.config.properties.XssProperties; +import io.netty.buffer.ByteBufAllocator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.core.io.buffer.*; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; + +/** + * @author borui + */ +@Component +@ConditionalOnProperty(value = "security.xss.enabled", havingValue = "true") +public class XssFilter implements GlobalFilter, Ordered { + // 跨站脚本的 xss 配置,nacos自行添加 + @Autowired + private XssProperties xss; + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) + { + ServerHttpRequest request = exchange.getRequest(); + // GET DELETE 不过滤 + HttpMethod method = request.getMethod(); + if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE) + { + return chain.filter(exchange); + } + // 非json类型,不过滤 + if (!isJsonRequest(exchange)) + { + return chain.filter(exchange); + } + // excludeUrls 不过滤 + String url = request.getURI().getPath(); + if (StringUtils.matches(url, xss.getExcludeUrls())) + { + return chain.filter(exchange); + } + ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange); + return chain.filter(exchange.mutate().request(httpRequestDecorator).build()); + + } + + private ServerHttpRequestDecorator requestDecorator(ServerWebExchange exchange) + { + ServerHttpRequestDecorator serverHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest()) + { + @Override + public Flux getBody() + { + Flux body = super.getBody(); + return body.buffer().map(dataBuffers -> { + DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); + DataBuffer join = dataBufferFactory.join(dataBuffers); + byte[] content = new byte[join.readableByteCount()]; + join.read(content); + DataBufferUtils.release(join); + String bodyStr = new String(content, StandardCharsets.UTF_8); + // 防xss攻击过滤 + bodyStr = EscapeUtil.clean(bodyStr); + // 转成字节 + byte[] bytes = bodyStr.getBytes(); + NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT); + DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length); + buffer.write(bytes); + return buffer; + }); + } + + @Override + public HttpHeaders getHeaders() + { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.putAll(super.getHeaders()); + // 由于修改了请求体的body,导致content-length长度不确定,因此需要删除原先的content-length + httpHeaders.remove(HttpHeaders.CONTENT_LENGTH); + httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked"); + return httpHeaders; + } + + }; + return serverHttpRequestDecorator; + } + + /** + * 是否是Json请求 + * + * @param exchange HTTP请求 + */ + public boolean isJsonRequest(ServerWebExchange exchange) + { + String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); + return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE); + } + + @Override + public int getOrder() + { + return -100; + } +} diff --git a/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/handler/SentinelFallbackHandler.java b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/handler/SentinelFallbackHandler.java new file mode 100644 index 0000000..3659735 --- /dev/null +++ b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/handler/SentinelFallbackHandler.java @@ -0,0 +1,38 @@ +package com.ecell.internationalize.gateway.handler; + +import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.ecell.internationalize.common.core.utils.ServletUtils; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebExceptionHandler; +import reactor.core.publisher.Mono; + +/** + * @author borui + */ +public class SentinelFallbackHandler implements WebExceptionHandler { + private Mono writeResponse(ServerResponse response, ServerWebExchange exchange) + { + return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求超过最大数,请稍候再试"); + } + + @Override + public Mono handle(ServerWebExchange exchange, Throwable ex) + { + if (exchange.getResponse().isCommitted()) + { + return Mono.error(ex); + } + if (!BlockException.isBlockException(ex)) + { + return Mono.error(ex); + } + return handleBlockedRequest(exchange, ex).flatMap(response -> writeResponse(response, exchange)); + } + + private Mono handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) + { + return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable); + } +} diff --git a/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/handler/SwaggerHandler.java b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/handler/SwaggerHandler.java new file mode 100644 index 0000000..9e1ca47 --- /dev/null +++ b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/handler/SwaggerHandler.java @@ -0,0 +1,56 @@ +package com.ecell.internationalize.gateway.handler; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; +import springfox.documentation.swagger.web.*; + +import java.util.Optional; + +/** + * @author borui + */ +@RestController +@RequestMapping("/swagger-resources") +public class SwaggerHandler { + @Autowired(required = false) + private SecurityConfiguration securityConfiguration; + + @Autowired(required = false) + private UiConfiguration uiConfiguration; + + private final SwaggerResourcesProvider swaggerResources; + + @Autowired + public SwaggerHandler(SwaggerResourcesProvider swaggerResources) + { + this.swaggerResources = swaggerResources; + } + + @GetMapping("/configuration/security") + public Mono> securityConfiguration() + { + return Mono.just(new ResponseEntity<>( + Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), + HttpStatus.OK)); + } + + @GetMapping("/configuration/ui") + public Mono> uiConfiguration() + { + return Mono.just(new ResponseEntity<>( + Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK)); + } + + @SuppressWarnings("rawtypes") + @GetMapping("") + public Mono swaggerResources() + { + return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK))); + } + +} diff --git a/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/handler/ValidateCodeHandler.java b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/handler/ValidateCodeHandler.java new file mode 100644 index 0000000..ac7f501 --- /dev/null +++ b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/handler/ValidateCodeHandler.java @@ -0,0 +1,39 @@ +package com.ecell.internationalize.gateway.handler; + +import com.ecell.internationalize.common.core.exception.CaptchaException; +import com.ecell.internationalize.common.core.web.domain.AjaxResult; +import com.ecell.internationalize.gateway.service.ValidateCodeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +import java.io.IOException; + +/** + * @author borui + */ +@Component +public class ValidateCodeHandler implements HandlerFunction { + @Autowired + private ValidateCodeService validateCodeService; + + @Override + public Mono handle(ServerRequest serverRequest) + { + AjaxResult ajax; + try + { + ajax = validateCodeService.createCaptcha(); + } + catch (CaptchaException | IOException e) + { + return Mono.error(e); + } + return ServerResponse.status(HttpStatus.OK).body(BodyInserters.fromValue(ajax)); + } +} diff --git a/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/service/ValidateCodeService.java b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/service/ValidateCodeService.java new file mode 100644 index 0000000..011ee95 --- /dev/null +++ b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/service/ValidateCodeService.java @@ -0,0 +1,21 @@ +package com.ecell.internationalize.gateway.service; + +import com.ecell.internationalize.common.core.exception.CaptchaException; +import com.ecell.internationalize.common.core.web.domain.AjaxResult; + +import java.io.IOException; + +/** + * @author borui + */ +public interface ValidateCodeService { + /** + * 生成验证码 + */ + public AjaxResult createCaptcha() throws IOException, CaptchaException; + + /** + * 校验验证码 + */ + public void checkCaptcha(String key, String value) throws CaptchaException; +} diff --git a/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/service/impl/ValidateCodeServiceImpl.java b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/service/impl/ValidateCodeServiceImpl.java new file mode 100644 index 0000000..a7ab75a --- /dev/null +++ b/ecell-internationalize/ecell-internationalize-gateway/src/main/java/com/ecell/internationalize/gateway/service/impl/ValidateCodeServiceImpl.java @@ -0,0 +1,116 @@ +package com.ecell.internationalize.gateway.service.impl; + +import com.ecell.internationalize.common.core.constant.Constants; +import com.ecell.internationalize.common.core.exception.CaptchaException; +import com.ecell.internationalize.common.core.utils.StringUtils; +import com.ecell.internationalize.common.core.utils.sign.Base64; +import com.ecell.internationalize.common.core.utils.uuid.IdUtils; +import com.ecell.internationalize.common.core.web.domain.AjaxResult; +import com.ecell.internationalize.common.redis.service.RedisService; +import com.ecell.internationalize.gateway.config.properties.CaptchaProperties; +import com.ecell.internationalize.gateway.service.ValidateCodeService; +import com.google.code.kaptcha.Producer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.FastByteArrayOutputStream; + +import javax.annotation.Resource; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * @author borui + */ +@Service +public class ValidateCodeServiceImpl implements ValidateCodeService { + @Resource(name = "captchaProducer") + private Producer captchaProducer; + + @Resource(name = "captchaProducerMath") + private Producer captchaProducerMath; + + @Autowired + private RedisService redisService; + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 生成验证码 + */ + @Override + public AjaxResult createCaptcha() throws IOException, CaptchaException + { + AjaxResult ajax = AjaxResult.success(); + boolean captchaOnOff = captchaProperties.getEnabled(); + ajax.put("captchaOnOff", captchaOnOff); + if (!captchaOnOff) + { + return ajax; + } + + // 保存验证码信息 + String uuid = IdUtils.simpleUUID(); + String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid; + + String capStr = null, code = null; + BufferedImage image = null; + + String captchaType = captchaProperties.getType(); + // 生成验证码 + if ("math".equals(captchaType)) + { + String capText = captchaProducerMath.createText(); + capStr = capText.substring(0, capText.lastIndexOf("@")); + code = capText.substring(capText.lastIndexOf("@") + 1); + image = captchaProducerMath.createImage(capStr); + } + else if ("char".equals(captchaType)) + { + capStr = code = captchaProducer.createText(); + image = captchaProducer.createImage(capStr); + } + + redisService.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); + // 转换流信息写出 + FastByteArrayOutputStream os = new FastByteArrayOutputStream(); + try + { + ImageIO.write(image, "jpg", os); + } + catch (IOException e) + { + return AjaxResult.error(e.getMessage()); + } + + ajax.put("uuid", uuid); + ajax.put("img", Base64.encode(os.toByteArray())); + return ajax; + } + + /** + * 校验验证码 + */ + @Override + public void checkCaptcha(String code, String uuid) throws CaptchaException + { + if (StringUtils.isEmpty(code)) + { + throw new CaptchaException("验证码不能为空"); + } + if (StringUtils.isEmpty(uuid)) + { + throw new CaptchaException("验证码已失效"); + } + String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid; + String captcha = redisService.getCacheObject(verifyKey); + redisService.deleteObject(verifyKey); + + if (!code.equalsIgnoreCase(captcha)) + { + throw new CaptchaException("验证码错误"); + } + } +}