diff --git a/itools-core/itools-common/src/main/java/com/itools/core/annotation/CharacterValidate.java b/itools-core/itools-common/src/main/java/com/itools/core/annotation/CharacterValidate.java new file mode 100644 index 0000000000000000000000000000000000000000..62c115ce64cde64124e2fd8252221bd77d8f66dd --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/annotation/CharacterValidate.java @@ -0,0 +1,64 @@ +package com.itools.core.annotation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-01 10:11 + */ +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = {CharacterValidateConstraint.class}) +public @interface CharacterValidate { + /** + * 默认错误消息 + * @return + */ + String message() default "字符串输入有误!"; + + /** + * 分组 + * @return + */ + Class[] groups() default {}; + + /** + * 负载 + * @return + */ + Class[] payload() default {}; + + /** + * 是否允许为空 + * @return + */ + boolean isAllowNull() default true; + + /** + * 正则 + * 身份证、邮箱、手机号等等 + * @return + */ + String regularization() default ""; + + /** + * 最小字符长度 + * @return + */ + int min() default 0; + /** + * 最大字符长度 + * @return + */ + int max() default 2147483647; +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/annotation/CharacterValidateConstraint.java b/itools-core/itools-common/src/main/java/com/itools/core/annotation/CharacterValidateConstraint.java new file mode 100644 index 0000000000000000000000000000000000000000..3d3c8ec0882b8e88f9f734453d3bf69aa124656d --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/annotation/CharacterValidateConstraint.java @@ -0,0 +1,63 @@ +package com.itools.core.annotation; + +import org.apache.commons.lang3.StringUtils; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-01 10:14 + */ +public class CharacterValidateConstraint implements ConstraintValidator { + + private String numberRegularization; + private boolean isValiNull; + private int min; + private int max; + /** + * 方法是用来初始化数据, 也就是存放注解里面的配置信息 + * @param constraintAnnotation + */ + @Override + public void initialize(CharacterValidate constraintAnnotation) { + isValiNull = constraintAnnotation.isAllowNull(); + numberRegularization = constraintAnnotation.regularization(); + min = constraintAnnotation.min(); + max = constraintAnnotation.max(); + } + + /** + * 校验逻辑的方法 + * @param value + * @param context + * @return + */ + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + //不允许为空且检验的对象为空 + if (!isValiNull && StringUtils.isBlank(value)){ + return false; + } + //允许为空且检验的对象为空 + if (isValiNull && StringUtils.isBlank(value)){ + return true; + } + + //正则 + if (StringUtils.isNotBlank(numberRegularization)){ + boolean matches = value.matches(numberRegularization); + if (!matches){ + return false; + } + } + //验证长度 + int length = value.length(); + if (length>max || length params = new ArrayList<>(); + for (int i=0;i[] groups() default {}; + + /** + * 负载 + * @return + */ + Class[] payload() default {}; + + /** + * 是否允许为空 + * @return + */ + boolean isValiNull() default true; + + /** + * 数字正则 + * 默认大于0数字正则,包含小数 + * @return + */ + String numberRegularization() default "^[+]{0,1}(\\d+)$|^[+]{0,1}(\\d+\\.\\d+)$"; +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/annotation/NumberValidateConstraint.java b/itools-core/itools-common/src/main/java/com/itools/core/annotation/NumberValidateConstraint.java new file mode 100644 index 0000000000000000000000000000000000000000..f22298d264ba67aee91e537ecca92006cfe02ba6 --- /dev/null +++ b/itools-core/itools-common/src/main/java/com/itools/core/annotation/NumberValidateConstraint.java @@ -0,0 +1,49 @@ +package com.itools.core.annotation; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-01 10:14 + */ +public class NumberValidateConstraint implements ConstraintValidator { + + private String numberRegularization; + private boolean isValiNull; + /** + * 方法是用来初始化数据, 也就是存放注解里面的配置信息 + * @param constraintAnnotation + */ + @Override + public void initialize(NumberValidate constraintAnnotation) { + isValiNull = constraintAnnotation.isValiNull(); + numberRegularization = constraintAnnotation.numberRegularization(); + } + + /** + * 校验逻辑的方法 + * @param value + * @param context + * @return + */ + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + //不允许为空且检验的对象为空 + if (!isValiNull && value == null){ + return false; + } + //允许为空且检验的对象为空 + if (isValiNull && value == null){ + return true; + } + String str = String.valueOf(value); + boolean matches = str.matches(numberRegularization); + if (matches){ + return true; + } + return false; + } +} diff --git a/itools-core/itools-common/src/main/java/com/itools/core/em/SystemCodeBean.java b/itools-core/itools-common/src/main/java/com/itools/core/em/SystemCodeBean.java index 5889dec17192e2a30079210de0e1388082013040..e50080fe416aba01f2ec6b16080f92d77ddde893 100644 --- a/itools-core/itools-common/src/main/java/com/itools/core/em/SystemCodeBean.java +++ b/itools-core/itools-common/src/main/java/com/itools/core/em/SystemCodeBean.java @@ -19,6 +19,8 @@ public class SystemCodeBean { */ SYSTEM_EXCEPTION("Code001" , "系统内部错误"), RETE_EXCEPTION("Code002" , "接口请求频繁,请稍后重试!"), + IDEMPOTENT_EXCEPTION("Code003" , "数据重复提交,请稍后重试!"), + CONTENT_NULL_EXCEPTION("Code004" , "暂无提交数据内容"), ; diff --git a/itools-core/itools-common/src/main/java/com/itools/core/exception/ExceptionHandle.java b/itools-core/itools-common/src/main/java/com/itools/core/exception/ExceptionHandle.java index 873098faf55d81b4af086f75ee7285938d37a6d5..780e16895db9c183f14037a23a49d59efd6c3cb5 100644 --- a/itools-core/itools-common/src/main/java/com/itools/core/exception/ExceptionHandle.java +++ b/itools-core/itools-common/src/main/java/com/itools/core/exception/ExceptionHandle.java @@ -4,7 +4,6 @@ import com.itools.core.base.CommonResult; import com.itools.core.code.SystemCodeService; import feign.codec.DecodeException; import java.nio.charset.StandardCharsets; -import java.util.Iterator; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -34,7 +33,7 @@ public class ExceptionHandle implements HandlerExceptionResolver, Ordered { private String systemErrorCode; @Value("${system.errorMsg:系统处理异常,请联系管理员!}") - private String DEFAULT_ERR_MSG; + private String defaultErrMsg; @Autowired private SystemCodeService systemCodeService; @@ -50,7 +49,7 @@ public class ExceptionHandle implements HandlerExceptionResolver, Ordered { } private ModelAndView resolveException0(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex){ - String errorCode = systemErrorCode, errorMsg = DEFAULT_ERR_MSG; + String errorCode = systemErrorCode, errorMsg = defaultErrMsg; if(ex instanceof DecodeException) { DecodeException decodeException = (DecodeException) ex; Throwable throwable = decodeException.getCause(); @@ -93,8 +92,8 @@ public class ExceptionHandle implements HandlerExceptionResolver, Ordered { LOGGER.error("Uncaught exception", ex); } - if(DEFAULT_ERR_MSG.equals(errorMsg)){ - errorMsg = systemCodeService.getMessageOptional(errorCode).orElse(DEFAULT_ERR_MSG); + if(defaultErrMsg.equals(errorMsg)){ + errorMsg = systemCodeService.getMessageOptional(errorCode).orElse(defaultErrMsg); } CommonResult commonOuterResponse = new CommonResult(); commonOuterResponse.setReturnCode(errorCode); diff --git a/itools-core/itools-common/src/main/java/com/itools/core/log/UserInfo.java b/itools-core/itools-common/src/main/java/com/itools/core/log/LogUserInfo.java similarity index 79% rename from itools-core/itools-common/src/main/java/com/itools/core/log/UserInfo.java rename to itools-core/itools-common/src/main/java/com/itools/core/log/LogUserInfo.java index 9ab11d5fd3222983ff4eedcc76790ae0a0b6cab4..cd3302e979cc3af99bb5e020bfc79740af57dfc6 100644 --- a/itools-core/itools-common/src/main/java/com/itools/core/log/UserInfo.java +++ b/itools-core/itools-common/src/main/java/com/itools/core/log/LogUserInfo.java @@ -7,12 +7,12 @@ import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; /** - * 描述 :用户信息 - * - * @author lidab - * @date 2017/11/15. + * @project: itools-backend + * @description: 用户信息 + * @author: XUCHANG + * @create: 2021-04-05 14:43 */ -public class UserInfo { +public class LogUserInfo { String userId; String userType; @@ -37,7 +37,7 @@ public class UserInfo { * * @return */ - public static UserInfo getUserInfo() { + public static LogUserInfo getUserInfo() { String userId = "NONE"; String userType = "NONE"; RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); @@ -48,10 +48,10 @@ public class UserInfo { userType = request.getHeader(Constants.HeadKey.USERTYPE.code) == null ? "NONE" : request.getHeader(Constants.HeadKey.USERTYPE.code); } } - UserInfo userInfo = new UserInfo(); - userInfo.setUserId(userId); - userInfo.setUserType(userType); - return userInfo; + LogUserInfo logUserInfo = new LogUserInfo(); + logUserInfo.setUserId(userId); + logUserInfo.setUserType(userType); + return logUserInfo; } } diff --git a/itools-core/itools-common/src/main/java/com/itools/core/snowflake/config/SequenceAutoConfiguration.java b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/config/SequenceAutoConfiguration.java index 27e9a224039e19ab96f873708b9f60f9daa12a65..ba868d6306a3a579e12d78bd4b720bbf8dd708e9 100644 --- a/itools-core/itools-common/src/main/java/com/itools/core/snowflake/config/SequenceAutoConfiguration.java +++ b/itools-core/itools-common/src/main/java/com/itools/core/snowflake/config/SequenceAutoConfiguration.java @@ -51,7 +51,7 @@ public class SequenceAutoConfiguration { havingValue = "snowflake") public SequenceService snowFlakeService(WorkNodeGenerate workNodeGenerate) { if(redisTemplate == null){ - throw new IllegalStateException("Snowflake sequence service need RedisTemplate bean."); + throw new IllegalStateException("Snowflake sequence com.itools.service need RedisTemplate bean."); } SnowflakeSequenceService snowflakeSequenceService = new SnowflakeSequenceService(); snowflakeSequenceService.setRedisTemplate(redisTemplate); diff --git a/itools-core/pom.xml b/itools-core/pom.xml index b5c7361966f6bfc32a125d106e893fd8240e4ce7..215552e6a5309d3928569c60f9fc3d8917ed5c86 100644 --- a/itools-core/pom.xml +++ b/itools-core/pom.xml @@ -21,7 +21,9 @@ 1.8 8.5.28 - 2.0.1.RELEASE + Hoxton.RELEASE + 2.2.0.RELEASE + 2.2.0.RELEASE 5.0.5.RELEASE 3.3.2 1.1.6 @@ -49,14 +51,22 @@ org.springframework.boot spring-boot-starter-parent - ${spring-boot.version} + ${spring.boot.version} pom import org.springframework.cloud spring-cloud-dependencies - Finchley.SR1 + ${spring-cloud.version} + pom + import + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${spring-cloud-alibaba.version} pom import @@ -188,17 +198,6 @@ false - - - - - - - - - - - diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/MybatisPlusConfig.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/MybatisPlusConfig.java index 56a51101d22b2a1fbbb89ddb08bd311c054e7111..3aeff5af6980e70c99ec980534878a8667b2dc34 100644 --- a/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/MybatisPlusConfig.java +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/MybatisPlusConfig.java @@ -15,7 +15,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; */ @Configuration @EnableTransactionManagement -@MapperScan("com.itools.core.mapper") +@MapperScan("com.itools.core.com.itools.mapper") public class MybatisPlusConfig { @Bean public PaginationInterceptor paginationInterceptor() { diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/ignore/MinioAutoConfiguration.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/ignore/MinioAutoConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..405e4a11b0f9ca65f7c3f8ad12fe24ed9a3a20d8 --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/ignore/MinioAutoConfiguration.java @@ -0,0 +1,36 @@ +//package com.itools.core.com.itools.config; +// +//import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +//import org.springframework.boot.context.properties.EnableConfigurationProperties; +//import org.springframework.context.annotation.Bean; +// +///** +// * @author xuchang +// */ +//@EnableConfigurationProperties(MinioProperties.class) +//public class MinioAutoConfiguration { +// +// private MinioProperties minioProperties; +// +// // 通过构造方法将MinioProperties注入进来 +// public MinioAutoConfiguration(MinioProperties minioProperties){ +// notNull(minioProperties.getEndpoint() == null, "url不能为空"); +// notNull(minioProperties.getAccessKey() == null, "accessKey不能为空"); +// notNull(minioProperties.getSecretKey() == null, "secretKey不能为空"); +// notNull(minioProperties.getBucketName() == null, "buckets不能为空"); +// this.minioProperties = minioProperties; +// } +// +// @ConditionalOnMissingBean(MinioUtils.class) +// @Bean +// public MinioUtils minioUtils(){ +// return new MinioUtils(minioProperties); +// } +// +// public static void notNull(boolean expression, String msg){ +// if(expression){ +// throw new NullPointerException(msg); +// } +// } +// +//} \ No newline at end of file diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/ignore/MinioProperties.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/ignore/MinioProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..b5d74371f8e21e8c59f79ebcbe494992b9a593a9 --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/ignore/MinioProperties.java @@ -0,0 +1,37 @@ +//package com.itools.core.com.itools.config; +// +//import io.swagger.annotations.ApiModelProperty; +//import lombok.Data; +//import org.springframework.boot.context.properties.ConfigurationProperties; +//import org.springframework.stereotype.Component; +// +///** +// * @author xuchang +// */ +//@Component +//@Data +//@ConfigurationProperties(prefix = "fms.minio") +//public class MinioProperties { +// +// @ApiModelProperty("endPoint是一个URL,域名,IPv4或者IPv6地址") +// private String endpoint; +// +// @ApiModelProperty("TCP/IP端口号") +// private int port; +// +// @ApiModelProperty("accessKey类似于用户ID,用于唯一标识你的账户") +// private String accessKey; +// +// @ApiModelProperty("secretKey是你账户的密码") +// private String secretKey; +// +// @ApiModelProperty("如果是true,则用的是https而不是http,默认值是true") +// private Boolean secure; +// +// @ApiModelProperty("默认存储桶") +// private String bucketName; +// +// @ApiModelProperty("配置目录") +// private String configDir; +// +//} \ No newline at end of file diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/ignore/MinioUtils.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/ignore/MinioUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..273568fa8fc2c87c729ffc994b8dbf89305b22e4 --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/config/ignore/MinioUtils.java @@ -0,0 +1,82 @@ +//package com.itools.core.com.itools.config; +// +//import com.sun.deploy.util.ArrayUtil; +//import io.minio.MinioClient; +//import io.minio.errors.*; +//import org.springframework.util.StringUtils; +//import org.xmlpull.v1.XmlPullParserException; +// +//import java.io.IOException; +//import java.security.InvalidKeyException; +//import java.security.NoSuchAlgorithmException; +//import java.util.ArrayList; +//import java.util.Arrays; +//import java.util.List; +// +///** +// * 这个类目前只写了初始化客户端以及添加存储桶的方法。 +// * 放值和取值没有写 +// * 这里主要讲自动装配 +// * @author xuchang +// */ +//public class MinioUtils { +// +// private MinioClient minioClient; +// +// private List buckets = new ArrayList(); +// +// public MinioUtils(MinioProperties minioProperties) { +// try { +// initMinioClient(minioProperties); +// addBuckets(StringUtils.split(",", minioProperties.getBucketName())); +// makeBuckets(); +// } catch (Exception e) { +// e.printStackTrace(); +// throw new RuntimeException("MinioClient初始化异常"); +// } +// } +// +// /** +// * 添加存储桶 +// * @param buckets +// */ +// public void addBuckets(String... buckets) { +// this.buckets.addAll(Arrays.asList(buckets)); +// } +// +// /** +// * 添加存储桶 +// * @param buckets +// */ +// public void addBucketsAndMake(String... buckets) { +// this.buckets.addAll(Arrays.asList(buckets)); +// try { +// makeBuckets(); +// } catch (Exception e) { +// e.printStackTrace(); +// throw new RuntimeException("创建存储桶异常"); +// } +// } +// +// /** +// * 初始化客户端 +// * @param minioProperties +// */ +// private void initMinioClient(MinioProperties minioProperties) throws InvalidPortException, InvalidEndpointException { +// minioClient = new MinioClient(minioProperties.getEndpoint(), minioProperties.getAccessKey(), minioProperties.getSecretKey()); +// } +// +// /** +// * 创建存储桶 +// */ +// public void makeBuckets() throws NoSuchAlgorithmException, IOException, InvalidKeyException, InsufficientDataException, ErrorResponseException, InvalidBucketNameException, InternalException, RegionConflictException, InvalidResponseException, XmlParserException { +// for (String bucket : buckets) { +// boolean isExist = false; +// isExist = minioClient.bucketExists(bucket); +// if (!isExist) { +// minioClient.makeBucket(bucket); +// } +// +// } +// } +//} \ No newline at end of file diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/context/FileStrategyServiceContext.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/context/FileStrategyServiceContext.java index 8c01f0f9024ec51e9300cc42141a76ddc0c90c78..1c7448ed81b8ee336356398d99a542681e3bbb5e 100644 --- a/itools-fms/itools-fms-core/src/main/java/com/itools/core/context/FileStrategyServiceContext.java +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/context/FileStrategyServiceContext.java @@ -23,11 +23,7 @@ public class FileStrategyServiceContext { @Autowired private Environment environment; @Getter - private final static Map fileServiceMap; - - static { - fileServiceMap = new HashMap<>(); - } + private final static Map fileServiceMap = new HashMap<>(); public FileManagerService get(String type) { return fileServiceMap.get(type); diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/listener/FileStrategyListener.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/listener/FileStrategyListener.java index 06b83d88643b09bfa6cd5315fbd00b625e6ff2d0..9eb970ed0ec7ccfd124de843f519a7951a1a43d4 100644 --- a/itools-fms/itools-fms-core/src/main/java/com/itools/core/listener/FileStrategyListener.java +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/listener/FileStrategyListener.java @@ -35,10 +35,6 @@ public class FileStrategyListener implements ApplicationListener { String type = StrategyType.getType(k); fileStrategyServiceContext.put(type, service); -// if (StringUtils.equals(k,)) -// Class clazz = service.getClass(); -// FileStrategy fileStrategy = (FileStrategy)clazz.getAnnotation(FileStrategy.class); -// fileStrategyServiceContext.put(fileStrategy.value().getType(), service); }); } } diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/FileHandleService.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/FileHandleService.java index 40c560fa7569637f0bce987d034282532dce5733..6ece8dc8af58df5ed4cadbc5b2ca9f2069e3c0b3 100644 --- a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/FileHandleService.java +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/FileHandleService.java @@ -2,11 +2,10 @@ package com.itools.core.service; import com.itools.core.base.CommonResult; import com.itools.core.param.FmsUploadTokenParam; -import com.itools.core.result.FmsFileUploadResult; -import com.itools.core.result.FmsMultipartFileResult; -import com.itools.core.result.FmsUploadTokenResult; +import com.itools.core.result.*; import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; import java.util.List; /** @@ -52,4 +51,34 @@ public interface FileHandleService { * @return */ CommonResult createAndPersistDBRecord(MultipartFile file, FmsUploadTokenParam fmsUploadTokenParam, String uuid); + + /** + * 获取文件访问路径 + * @param uniqueIds List文件唯一id + * @param expiredTime 过期时间 + * @param maxAccessCount 最大访问次数 + * @param type 文件下载 download/展示 show + * @return + */ + CommonResult> getFileUrlByUniqueIds(List uniqueIds, Integer expiredTime, Integer maxAccessCount, String type); + /** + * 根据文件唯一id查询文件的上传记录 + * @param uniqueIds 文件唯一id + * @return + */ + CommonResult> queryFileRecordsByUniqueIds(List uniqueIds); + /** + * 根据文件唯一id查询文件的上传记录 + * @param uniqueId 文件唯一id + * @return + */ + CommonResult getFileUploadRecord(String uniqueId); + + /** + * 下载文件 + * @param uniqueId 文件唯一id + * @param fileAccessToken 文件的访问token + * @param response + */ + void downloadFileNio(String fileAccessToken, HttpServletResponse response, String uniqueId); } diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/FileManagerService.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/FileManagerService.java index 263e6df7745c10658ed1c63afe00fba3a2eb6120..8d6c5205cd7bdea1a044e8c0c0e155e06727d533 100644 --- a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/FileManagerService.java +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/FileManagerService.java @@ -3,10 +3,12 @@ package com.itools.core.service; import com.itools.core.base.CommonResult; import com.itools.core.param.FmsUploadTokenParam; import com.itools.core.result.FmsBusinessTypeResult; +import com.itools.core.result.FmsFileRecordPathResult; import com.itools.core.result.FmsFileUploadResult; import com.itools.core.result.FmsMultipartFileResult; import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; import java.util.List; /** @@ -33,8 +35,29 @@ public interface FileManagerService { * @param originalFilename 原名称 * @return */ - CommonResult singletonUploadFiles(MultipartFile file, - String uploadToken, - FmsUploadTokenParam uploadTokenParam, - String originalFilename); + CommonResult singletonUploadFile(MultipartFile file, + String uploadToken, + FmsUploadTokenParam uploadTokenParam, + String originalFilename); + + /** + * 获取文件访问路径 + * @param uniqueIds List文件唯一id + * @param expiredTime 过期时间 + * @param maxAccessCount 最大访问次数 + * @param type 文件下载 download/展示 show + * @return + */ + CommonResult> getFileUrlByUniqueIds(List uniqueIds, + Integer expiredTime, + Integer maxAccessCount, + String type); + + /** + * 下载文件 + * @param uniqueId 文件唯一id + * @param fileAccessToken 文件的访问token + * @param response + */ + void downloadFile(String fileAccessToken, HttpServletResponse response, String uniqueId); } diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/FastDfsFileManagerServiceImpl.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/FastDfsFileManagerServiceImpl.java index dec48e0546003ef65758587f5b35d93e633ddc7a..02f8052b43b1bd9a2eb9da8979eaea3d13ad9536 100644 --- a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/FastDfsFileManagerServiceImpl.java +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/FastDfsFileManagerServiceImpl.java @@ -5,11 +5,13 @@ import com.itools.core.base.CommonResult; import com.itools.core.em.StrategyType; import com.itools.core.param.FmsUploadTokenParam; import com.itools.core.result.FmsBusinessTypeResult; +import com.itools.core.result.FmsFileRecordPathResult; import com.itools.core.result.FmsFileUploadResult; import com.itools.core.result.FmsMultipartFileResult; import com.itools.core.service.FileManagerService; import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; import java.util.List; /** @@ -44,10 +46,36 @@ public class FastDfsFileManagerServiceImpl implements FileManagerService { * @return */ @Override - public CommonResult singletonUploadFiles(MultipartFile file, - String uploadToken, - FmsUploadTokenParam uploadTokenParam, - String originalFilename) { + public CommonResult singletonUploadFile(MultipartFile file, + String uploadToken, + FmsUploadTokenParam uploadTokenParam, + String originalFilename) { return null; } + + /** + * 获取文件访问路径 + * + * @param uniqueIds List文件唯一id + * @param expiredTime 过期时间 + * @param maxAccessCount 最大访问次数 + * @param type 文件下载 download/展示 show + * @return + */ + @Override + public CommonResult> getFileUrlByUniqueIds(List uniqueIds, Integer expiredTime, Integer maxAccessCount, String type) { + return null; + } + + /** + * 下载文件 + * + * @param fileAccessToken 文件的访问token + * @param response + * @param uniqueId 文件唯一id + */ + @Override + public void downloadFile(String fileAccessToken, HttpServletResponse response, String uniqueId) { + + } } diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/FmsFileUploadMinioTask.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/FmsFileUploadMinioTask.java new file mode 100644 index 0000000000000000000000000000000000000000..dee94acf1443a4950bb58e05b1aa283e5e97a9cc --- /dev/null +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/FmsFileUploadMinioTask.java @@ -0,0 +1,60 @@ +package com.itools.core.service.impl; + +import com.itools.core.param.FmsUploadTokenParam; +import com.itools.core.result.FmsMultipartFileResult; +import com.itools.core.service.FileHandleService; +import com.itools.core.service.MinioClientService; +import org.springframework.web.multipart.MultipartFile; + +import java.util.concurrent.Callable; + +/** + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +public class FmsFileUploadMinioTask implements Callable { + + /** + * 上传的文件 + */ + private MultipartFile file; + /** + * 文件的访问的token获取的信息 + */ + private FmsUploadTokenParam param; + /** + * 生成的UUID + */ + private String uniqueId; + /** + * 文件夹目录 + */ + private String fileStoreRootPath; + + private FileHandleService fileHandleService; + + private MinioClientService minioClientService; + + public FmsFileUploadMinioTask(MultipartFile file, FmsUploadTokenParam param, String uniqueId, String fileStoreRootPath, FileHandleService fileHandleService, MinioClientService minioClientService) { + this.file = file; + this.param = param; + this.uniqueId = uniqueId; + this.fileStoreRootPath = fileStoreRootPath; + this.fileHandleService = fileHandleService; + this.minioClientService = minioClientService; + } + + @Override + public FmsMultipartFileResult call() { + String originalFilename = file.getOriginalFilename(); + String fileSuffix = originalFilename.substring(originalFilename.lastIndexOf("."),originalFilename.length()); + fileHandleService.createAndPersistDBRecord(file,param,uniqueId); + minioClientService.putObject(fileStoreRootPath,file,uniqueId+fileSuffix); + FmsMultipartFileResult fmsMultipartFileResult = new FmsMultipartFileResult(); + fmsMultipartFileResult.setFileName(file.getOriginalFilename()); + fmsMultipartFileResult.setUniqueId(uniqueId); + return fmsMultipartFileResult; + } + +} diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/MinioClientServiceImpl.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/MinioClientServiceImpl.java index 85321c59e549155639b8a3172ac46f9dd09e73de..91dc1f4d6f1aa5ae56418c2d29c6f1c3d7459973 100644 --- a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/MinioClientServiceImpl.java +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/MinioClientServiceImpl.java @@ -20,6 +20,7 @@ import org.springframework.web.multipart.MultipartFile; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.io.InputStream; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -434,11 +435,16 @@ public class MinioClientServiceImpl extends AbstractService implements MinioClie try { InputStream file = minioClient.getObject(bucketName, fileName); - String filename = new String(fileName.getBytes("ISO8859-1"), StandardCharsets.UTF_8); + //引入的文件名与原文件名称 if (StringUtils.isNotEmpty(originalName)) { fileName = originalName; } - response.setHeader("Content-Disposition", "attachment;filename=" + filename); +// response.setHeader("Content-Disposition", "attachment;filename=" + fileName); + String fileSuffix = fileName.substring(fileName.lastIndexOf(".")+1,fileName.length()); + // Content-disposition 以附件的方式下载文件, 文件名用encode编码 + response.setHeader("Content-Disposition", "attachment; filename=\""+ URLEncoder.encode(fileName,"utf-8")+"\""); + // 告诉浏览器文件的编码格式 + response.setContentType("application/"+fileSuffix+";charset=UTF-8"); ServletOutputStream servletOutputStream = response.getOutputStream(); int len; byte[] buffer = new byte[1024]; diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/MinioFileManagerServiceImpl.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/MinioFileManagerServiceImpl.java index a065b7d4d06c10bd841c0fb96619b5b31f4a0038..6ab8dc47bc45c468cee005ff7ad1236b2e7e492d 100644 --- a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/MinioFileManagerServiceImpl.java +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/MinioFileManagerServiceImpl.java @@ -7,12 +7,15 @@ import com.itools.core.em.FmsConstants; import com.itools.core.em.StrategyType; import com.itools.core.exception.AppException; import com.itools.core.param.FmsUploadTokenParam; +import com.itools.core.result.FmsDetailRecordResult; +import com.itools.core.result.FmsFileRecordPathResult; import com.itools.core.result.FmsFileUploadResult; import com.itools.core.result.FmsMultipartFileResult; import com.itools.core.service.FileHandleService; import com.itools.core.service.FileManagerService; import com.itools.core.service.MinioClientService; import com.itools.core.system.AbstractService; +import com.itools.core.utils.CollectionUtils; import com.itools.core.utils.UUIDUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -22,7 +25,13 @@ import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.concurrent.*; +import java.util.function.Function; +import java.util.stream.Collectors; /** * @project: itools-backend @@ -42,6 +51,11 @@ public class MinioFileManagerServiceImpl extends AbstractService implements File String fileStoreRootPath; @Autowired private FileHandleService fileHandleService; + /** + * 线程池:核心线程:10,非核心线程:40,阻塞队列:100 + */ + private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 50, 0, TimeUnit.SECONDS, new LinkedBlockingDeque(100)); + /** * 批量上传文件 * @param files 文件 @@ -53,7 +67,49 @@ public class MinioFileManagerServiceImpl extends AbstractService implements File public CommonResult> mutipartUploadFiles(MultipartFile[] files, String uploadToken, FmsUploadTokenParam uploadTokenParam) { - return null; + //验证文件目录,不存在目录创建目录 + try { + //执行单个文件上传到minio服务器、 + boolean bucketExists = minioClientService.bucketExists(fileStoreRootPath); + if (!bucketExists){ + minioClientService.makeBucket(fileStoreRootPath); + } + }catch (Exception e){ + logger.error("文件上传到MINIO服务器失败",e); + throw new AppException(FmsCodeBean.FmsCode.FAIL_UPLOAD_FILE_TO_MINIO.getCode()); + } + List results = new ArrayList<>(); + List> taskList = new ArrayList<>(); + //文件写入服务器 + for (MultipartFile file : files) { + //文件记录写入数据库 + String uniqueId = UUIDUtils.uuid(); + //创建FutureTask的任务 + FutureTask futureTask = new FutureTask(new FmsFileUploadMinioTask(file, + uploadTokenParam,uniqueId,fileStoreRootPath,fileHandleService,minioClientService)); + taskList.add(futureTask); + try { + threadPoolExecutor.execute(futureTask); + }catch (Exception e){ + logger.error("批量上传文件失败"); + throw new AppException(FmsCodeBean.FmsCode.FILE_MULTIPART_UPLOAD.code); + } + } + for(FutureTask task: taskList){ + if (task == null){ + logger.error("批量上传文件失败"); + throw new AppException(FmsCodeBean.FmsCode.FILE_MULTIPART_UPLOAD.code); + } + try { + FmsMultipartFileResult fmsMultipartFileResult = task.get(); + results.add(fmsMultipartFileResult); + } catch (Exception e) { + logger.error("文件获取失败",e); + } + } + //文件上传成功,删除文件访问的token + stringRedisTemplate.delete(FmsConstants.FILE_TOKEN_NAME_SPACE + uploadToken); + return CommonResult.success(results); } /** @@ -67,20 +123,21 @@ public class MinioFileManagerServiceImpl extends AbstractService implements File */ @Override @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class) - public CommonResult singletonUploadFiles(MultipartFile file, - String uploadToken, - FmsUploadTokenParam uploadTokenParam, - String originalFilename) { + public CommonResult singletonUploadFile(MultipartFile file, + String uploadToken, + FmsUploadTokenParam uploadTokenParam, + String originalFilename) { //文件记录写入数据库 String uniqueId = UUIDUtils.uuid(); fileHandleService.createAndPersistDBRecord(file,uploadTokenParam,uniqueId); try { - //执行单个文件上传到minio服务器、 + //执行单个文件上传到minio服务器 boolean bucketExists = minioClientService.bucketExists(fileStoreRootPath); if (!bucketExists){ minioClientService.makeBucket(fileStoreRootPath); } - minioClientService.putObject(fileStoreRootPath,file,uniqueId); + String fileSuffix = originalFilename.substring(originalFilename.lastIndexOf("."),originalFilename.length()); + minioClientService.putObject(fileStoreRootPath,file,uniqueId+fileSuffix); }catch (Exception e){ logger.error("文件上传到MINIO服务器失败",e); throw new AppException(FmsCodeBean.FmsCode.FAIL_UPLOAD_FILE_TO_MINIO.getCode()); @@ -93,4 +150,61 @@ public class MinioFileManagerServiceImpl extends AbstractService implements File result.setFileName(originalFilename); return CommonResult.success(result); } + + /** + * 获取文件访问路径 + * + * @param uniqueIds List,文件唯一id + * @param expiredTime 过期时间 + * @param maxAccessCount 最大访问次数 + * @param type 文件下载 download/展示 show + * @return + */ + @Override + public CommonResult> getFileUrlByUniqueIds(List uniqueIds, Integer expiredTime, Integer maxAccessCount, String type) { + List results = new ArrayList<>(); + if (CollectionUtils.isEmpty(uniqueIds)){ + return CommonResult.success(results); + } + //获取文件唯一id的文件信息 + CommonResult> recordsResult = fileHandleService.queryFileRecordsByUniqueIds(uniqueIds); + //key:uniqueId val:文件信息 + Map recordResultMap = recordsResult.getData().stream().collect(Collectors.toMap(FmsDetailRecordResult::getUniqueId, Function.identity(),(key1,key2)->key2)); + for (String uniqueId : uniqueIds) { + if (!recordResultMap.containsKey(uniqueId)){ + continue; + } + FmsDetailRecordResult record = recordResultMap.get(uniqueId); + String originalFilename = record.getOrigFileName(); + String fileSuffix = originalFilename.substring(originalFilename.lastIndexOf("."),originalFilename.length()); + //获取预览地址 + String objectUrl = minioClientService.presignedGetObject(fileStoreRootPath, uniqueId+fileSuffix,3600*24*7); + FmsFileRecordPathResult fmsFileRecordPathResult = new FmsFileRecordPathResult(); + fmsFileRecordPathResult.setPath(objectUrl); + fmsFileRecordPathResult.setUniqueId(uniqueId); + results.add(fmsFileRecordPathResult); + } + return CommonResult.success(results); + } + + /** + * 下载文件 + * + * @param fileAccessToken 文件的访问token + * @param response + * @param uniqueId 文件唯一id + */ + @Override + public void downloadFile(String fileAccessToken, HttpServletResponse response, String uniqueId) { + //校验预览或者下载的文件的令牌 + + CommonResult detailRecordResult = fileHandleService.getFileUploadRecord(uniqueId); + FmsDetailRecordResult fmsDetailRecordResult = detailRecordResult.getData(); + String originalFilename = fmsDetailRecordResult.getOrigFileName(); + String fileSuffix = originalFilename.substring(originalFilename.lastIndexOf("."),originalFilename.length()); + + //minio处理文件下载逻辑 + minioClientService.downloadFile(fileStoreRootPath,uniqueId+fileSuffix,originalFilename,response); + + } } diff --git a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/NioFileManagerServiceImpl.java b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/NioFileManagerServiceImpl.java index f00f5219e5123aaf95b69f1223728af6fd7c2990..b915f0aa476236a892c181224121dc002818e3e3 100644 --- a/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/NioFileManagerServiceImpl.java +++ b/itools-fms/itools-fms-core/src/main/java/com/itools/core/service/impl/NioFileManagerServiceImpl.java @@ -6,6 +6,7 @@ import com.itools.core.em.StrategyType; import com.itools.core.log.Logable; import com.itools.core.param.FmsUploadTokenParam; import com.itools.core.result.FmsBusinessTypeResult; +import com.itools.core.result.FmsFileRecordPathResult; import com.itools.core.result.FmsFileUploadResult; import com.itools.core.result.FmsMultipartFileResult; import com.itools.core.service.FileHandleService; @@ -16,6 +17,7 @@ import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; import java.util.List; /** @@ -57,11 +59,37 @@ public class NioFileManagerServiceImpl implements FileManagerService { @Override @Logable @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class) - public CommonResult singletonUploadFiles(MultipartFile file, - String uploadToken, - FmsUploadTokenParam uploadTokenParam, - String originalFilename) { + public CommonResult singletonUploadFile(MultipartFile file, + String uploadToken, + FmsUploadTokenParam uploadTokenParam, + String originalFilename) { return fileHandleService.saveFileForNio(file, uploadTokenParam, uploadToken, originalFilename); } + + /** + * 获取文件访问路径 + * + * @param uniqueIds List文件唯一id + * @param expiredTime 过期时间 + * @param maxAccessCount 最大访问次数 + * @param type 文件下载 download/展示 show + * @return + */ + @Override + public CommonResult> getFileUrlByUniqueIds(List uniqueIds, Integer expiredTime, Integer maxAccessCount, String type) { + return fileHandleService.getFileUrlByUniqueIds(uniqueIds,expiredTime,maxAccessCount,type); + } + + /** + * 下载文件 + * + * @param fileAccessToken 文件的访问token + * @param response + * @param uniqueId 文件唯一id + */ + @Override + public void downloadFile(String fileAccessToken, HttpServletResponse response, String uniqueId) { + fileHandleService.downloadFileNio(fileAccessToken,response,uniqueId); + } } diff --git a/itools-fms/itools-fms-server/pom.xml b/itools-fms/itools-fms-server/pom.xml index cf3bae00bf164f55c390e5404a69df973e20af9e..eb45a1836e3afd862ea61023de4d3087884e973d 100644 --- a/itools-fms/itools-fms-server/pom.xml +++ b/itools-fms/itools-fms-server/pom.xml @@ -84,6 +84,11 @@ org.apache.commons commons-pool2 + + org.flywaydb + flyway-core + 5.2.1 + diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileHandleStrategyController.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileHandleStrategyController.java index cbc3ed61b599c44686bd3f5597c6b3f036f6eb27..118e04db3e6f3a6bd79289b88dbac5545886eb00 100644 --- a/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileHandleStrategyController.java +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileHandleStrategyController.java @@ -2,6 +2,7 @@ package com.itools.core.controller; import com.itools.core.base.CommonResult; import com.itools.core.log.Logable; +import com.itools.core.result.FmsFileRecordPathResult; import com.itools.core.result.FmsFileUploadResult; import com.itools.core.result.FmsMultipartFileResult; import com.itools.core.service.FmsFileHandleStrategyService; @@ -15,6 +16,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; import javax.validation.constraints.NotNull; import java.util.List; @@ -66,4 +68,42 @@ public class FmsFileHandleStrategyController { return fmsFileHandleStrategyService.filesMultipartUpload(files,uploadToken); } + /** + * 预览文件 + * 获取文件访问路径 + * @param uniqueIds + * @param expiredTime + * @param maxAccessCount + * @param type + * @return + */ + @PostMapping("/get/files/path") + @Logable + @ApiOperation(value = "获取文件访问路径", notes = "获取文件访问路径", httpMethod = "POST") + @ApiImplicitParams({ + @ApiImplicitParam(name = "uniqueIds", value = "文件的唯一IDS数组", required = true, dataType = "String", paramType = "query"), + @ApiImplicitParam(name = "expiredTime", value = "有效时长,单位分钟", required = true, dataType = "Long", paramType = "query"), + @ApiImplicitParam(name = "maxAccessCount", value = "最大访问次数", required = false, dataType = "Long", paramType = "query"), + @ApiImplicitParam(name = "type", value = "文件下载 download/展示 show", required = true, dataType = "String", paramType = "query") + }) + public CommonResult> getFileAccessUrl(@RequestParam(value = "uniqueIds",required = false) List uniqueIds, + @RequestParam(value = "expiredTime",required = false) Integer expiredTime, + @RequestParam(value = "maxAccessCount",defaultValue = "-1", required = false) Integer maxAccessCount, + @RequestParam(value = "type",required = false) String type) { + return fmsFileHandleStrategyService.getFileUrlByUniqueIds(uniqueIds, expiredTime, maxAccessCount, type); + } + /** + * 下载文件 + * @param uniqueId 文件唯一id + * @param fileAccessToken 文件的访问token + * @param response + */ + @ApiOperation(value = "下载文件", notes = "下载文件", httpMethod = "GET") + @PostMapping("/download/file") + @Logable + public void downloadFile(@RequestParam("uniqueId") String uniqueId, + @RequestParam("fileAccessToken") String fileAccessToken, + HttpServletResponse response) { + fmsFileHandleStrategyService.downloadFile(fileAccessToken,response,uniqueId); + } } diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileSystemController.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileSystemController.java index 09e23122add9509e97752d3a9be8d7526da5e6a7..dba49247fb362baffef40d1bfb9cf6eac0f2f932 100644 --- a/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileSystemController.java +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/FmsFileSystemController.java @@ -73,30 +73,6 @@ public class FmsFileSystemController { return fmsFileSystemService.getFileUrlByFileId(uniqueId, expiredTime, maxAccessCount, type); } - /** - * 获取文件访问路径 - * @param uniqueIds - * @param expiredTime - * @param maxAccessCount - * @param type - * @return - */ - @PostMapping("/get/files/path") - @Logable - @ApiOperation(value = "获取文件访问路径", notes = "获取文件访问路径", httpMethod = "POST") - @ApiImplicitParams({ - @ApiImplicitParam(name = "uniqueIds", value = "文件的唯一IDS数组", required = true, dataType = "String", paramType = "query"), - @ApiImplicitParam(name = "expiredTime", value = "有效时长,单位分钟", required = true, dataType = "Long", paramType = "query"), - @ApiImplicitParam(name = "maxAccessCount", value = "最大访问次数", required = false, dataType = "Long", paramType = "query"), - @ApiImplicitParam(name = "type", value = "文件下载 download/展示 show", required = true, dataType = "String", paramType = "query") - }) - public CommonResult> getFileAccessUrl(@RequestParam(value = "uniqueIds",required = false) List uniqueIds, - @RequestParam(value = "expiredTime",required = false) Integer expiredTime, - @RequestParam(value = "maxAccessCount",defaultValue = "-1", required = false) Integer maxAccessCount, - @RequestParam(value = "type",required = false) String type) { - return fmsFileSystemService.getFileUrlByFileIds(uniqueIds, expiredTime, maxAccessCount, type); - } - /** * 下载文件 * @param uniqueId 文件唯一id diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/TestController.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/TestController.java new file mode 100644 index 0000000000000000000000000000000000000000..e61ab1263628e530d1652f675d4893380d0bb277 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/controller/TestController.java @@ -0,0 +1,21 @@ +package com.itools.core.controller; + +import java.util.HashMap; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-05-18 17:27 + */ +public class TestController { + public static void main(String[] args) { + HashMap map1 = new HashMap(); + map1.put("a","b"); + HashMap map2 = new HashMap(); + map2.put("a","a"); + + boolean equals = map1.equals(map2); + System.out.println(equals); + } +} diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsFileHandleStrategyService.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsFileHandleStrategyService.java index c04163ed11618bed8b7a33aedb9e054e06e39bce..eae133bd8e537e854be1150574a11692ba1a9073 100644 --- a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsFileHandleStrategyService.java +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsFileHandleStrategyService.java @@ -1,10 +1,12 @@ package com.itools.core.service; import com.itools.core.base.CommonResult; +import com.itools.core.result.FmsFileRecordPathResult; import com.itools.core.result.FmsFileUploadResult; import com.itools.core.result.FmsMultipartFileResult; import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; import java.util.List; /** @@ -28,4 +30,21 @@ public interface FmsFileHandleStrategyService { * @return */ CommonResult> filesMultipartUpload(MultipartFile[] files, String uploadToken); + + /** + * 获取文件访问路径 + * @param uniqueIds List文件唯一id + * @param expiredTime 过期时间 + * @param maxAccessCount 最大访问次数 + * @param type 文件下载 download/展示 show + * @return + */ + CommonResult> getFileUrlByUniqueIds(List uniqueIds, Integer expiredTime, Integer maxAccessCount, String type); + /** + * 下载文件 + * @param uniqueId 文件唯一id + * @param fileAccessToken 文件的访问token + * @param response + */ + void downloadFile(String fileAccessToken, HttpServletResponse response, String uniqueId); } diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsFileSystemService.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsFileSystemService.java index 713ffa148584a5a7113695dd9e0f434fbaf8fcd0..36ce5a16a0b9739f43f0d1ded3617a2be05f3733 100644 --- a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsFileSystemService.java +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/FmsFileSystemService.java @@ -98,6 +98,13 @@ public interface FmsFileSystemService { */ FmsDetailRecordResult getFileUploadRecord(String uniqueId); + /** + * 根据文件唯一id查询文件的上传记录 + * @param uniqueIds 文件唯一id + * @return + */ + List queryFileRecordsByUniqueIds(List uniqueIds); + /** * 文件批量上传 * @param files diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FileHandleServiceImpl.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FileHandleServiceImpl.java index ad46379d94b7652fcfeefe78d5ab626a5afb7683..d018333581385c3821762f3311238223ce5f009b 100644 --- a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FileHandleServiceImpl.java +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FileHandleServiceImpl.java @@ -6,9 +6,7 @@ import com.itools.core.em.FmsCodeBean; import com.itools.core.em.FmsConstants; import com.itools.core.exception.AppException; import com.itools.core.param.FmsUploadTokenParam; -import com.itools.core.result.FmsFileUploadResult; -import com.itools.core.result.FmsMultipartFileResult; -import com.itools.core.result.FmsUploadTokenResult; +import com.itools.core.result.*; import com.itools.core.service.FileHandleService; import com.itools.core.service.FmsAccessTokenService; import com.itools.core.service.FmsBusinessTypeService; @@ -22,6 +20,7 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; @@ -65,7 +64,7 @@ public class FileHandleServiceImpl extends AbstractService implements FileHandle try { fmsUploadTokenResult = objectMapper.readValue(tokenParam,FmsUploadTokenResult.class); } catch (IOException e) { - logger.error("JacKJson序列化失败"); + logger.error("JacKJson序列化失败",e); } return CommonResult.success(fmsUploadTokenResult); } @@ -122,4 +121,54 @@ public class FileHandleServiceImpl extends AbstractService implements FileHandle String andPersistDBRecord = fmsFileSystemService.createAndPersistDBRecord(file, fmsUploadTokenParam, uuid); return CommonResult.success(andPersistDBRecord); } + + /** + * 获取文件访问路径 + * + * @param uniqueIds List文件唯一id + * @param expiredTime 过期时间 + * @param maxAccessCount 最大访问次数 + * @param type 文件下载 download/展示 show + * @return + */ + @Override + public CommonResult> getFileUrlByUniqueIds(List uniqueIds, Integer expiredTime, Integer maxAccessCount, String type) { + return fmsFileSystemService.getFileUrlByFileIds(uniqueIds,expiredTime,maxAccessCount,type); + } + + /** + * 根据文件唯一id查询文件的上传记录 + * + * @param uniqueIds 文件唯一id + * @return + */ + @Override + public CommonResult> queryFileRecordsByUniqueIds(List uniqueIds) { + List recordResults = fmsFileSystemService.queryFileRecordsByUniqueIds(uniqueIds); + return CommonResult.success(recordResults); + } + + /** + * 根据文件唯一id查询文件的上传记录 + * + * @param uniqueId 文件唯一id + * @return + */ + @Override + public CommonResult getFileUploadRecord(String uniqueId) { + FmsDetailRecordResult fileUploadRecord = fmsFileSystemService.getFileUploadRecord(uniqueId); + return CommonResult.success(fileUploadRecord); + } + + /** + * 下载文件 + * + * @param fileAccessToken 文件的访问token + * @param response + * @param uniqueId 文件唯一id + */ + @Override + public void downloadFileNio(String fileAccessToken, HttpServletResponse response, String uniqueId) { + fmsFileSystemService.downloadFile(fileAccessToken,response,uniqueId); + } } diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileHandleStrategyServiceImpl.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileHandleStrategyServiceImpl.java index 18f2d4985202af615b019d1b4e44df275b756cee..d6a5f25884aa7a8c2ac877a968034d856aaa4cf6 100644 --- a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileHandleStrategyServiceImpl.java +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileHandleStrategyServiceImpl.java @@ -4,10 +4,7 @@ import com.itools.core.base.CommonResult; import com.itools.core.bean.BeanCopyUtils; import com.itools.core.context.FileStrategyServiceContext; import com.itools.core.param.FmsUploadTokenParam; -import com.itools.core.result.FmsBusinessTypeResult; -import com.itools.core.result.FmsFileUploadResult; -import com.itools.core.result.FmsMultipartFileResult; -import com.itools.core.result.FmsUploadTokenResult; +import com.itools.core.result.*; import com.itools.core.service.FileHandleService; import com.itools.core.service.FmsFileHandleStrategyService; import com.itools.core.service.FmsFileSystemService; @@ -18,6 +15,7 @@ import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; import java.util.List; /** @@ -56,7 +54,7 @@ public class FmsFileHandleStrategyServiceImpl extends AbstractService implements FmsUploadTokenParam uploadTokenParam = new FmsUploadTokenParam(); BeanCopyUtils.copyProperties(fmsUploadTokenResultCommonResult.getData(),uploadTokenParam); //执行文件上传,根据配置的不同策略处理文件上传 - return fileStrategyServiceContext.get().singletonUploadFiles(file, uploadToken, uploadTokenParam, originalFilename); + return fileStrategyServiceContext.get().singletonUploadFile(file, uploadToken, uploadTokenParam, originalFilename); } /** @@ -79,4 +77,30 @@ public class FmsFileHandleStrategyServiceImpl extends AbstractService implements //执行文件上传,根据配置的不同策略处理文件上传 return fileStrategyServiceContext.get().mutipartUploadFiles(files, uploadToken, uploadTokenParam); } + + /** + * 获取文件访问路径 + * + * @param uniqueIds List文件唯一id + * @param expiredTime 过期时间 + * @param maxAccessCount 最大访问次数 + * @param type 文件下载 download/展示 show + * @return + */ + @Override + public CommonResult> getFileUrlByUniqueIds(List uniqueIds, Integer expiredTime, Integer maxAccessCount, String type) { + return fileStrategyServiceContext.get().getFileUrlByUniqueIds(uniqueIds,expiredTime,maxAccessCount,type); + } + + /** + * 下载文件 + * + * @param fileAccessToken 文件的访问token + * @param response + * @param uniqueId 文件唯一id + */ + @Override + public void downloadFile(String fileAccessToken, HttpServletResponse response, String uniqueId) { + fileStrategyServiceContext.get().downloadFile(fileAccessToken,response,uniqueId); + } } diff --git a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileSystemServiceImpl.java b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileSystemServiceImpl.java index ef9e4543f7f532e33a782504e4a7e3becfcad1a6..7d3e250bc66d108cd7afe4c8de4d4580647ff83a 100644 --- a/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileSystemServiceImpl.java +++ b/itools-fms/itools-fms-server/src/main/java/com/itools/core/service/impl/FmsFileSystemServiceImpl.java @@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSON; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.itools.core.base.CommonResult; +import com.itools.core.bean.BeanCopyUtils; import com.itools.core.dto.accessToken.FmsAccessTokenDTO; import com.itools.core.dto.fileRecord.FmsRecordDTO; import com.itools.core.em.FmsCodeBean; @@ -411,6 +412,24 @@ public class FmsFileSystemServiceImpl implements FmsFileSystemService { return result; } + /** + * 根据文件唯一id查询文件的上传记录 + * + * @param uniqueIds + * @return + */ + @Override + public List queryFileRecordsByUniqueIds(List uniqueIds) { + if (CollectionUtils.isEmpty(uniqueIds)){ + return new ArrayList<>(); + } + FmsRecordDTO fmsRecordDTO = new FmsRecordDTO(); + fmsRecordDTO.setUniqueIds(uniqueIds); + List recordDTOS = fmsFileRecordMapper.selectbySelective(fmsRecordDTO); + List recordResults = BeanCopyUtils.copy(recordDTOS,FmsDetailRecordResult.class); + return recordResults; + } + /** * 文件批量上传 * @param files @@ -538,9 +557,9 @@ public class FmsFileSystemServiceImpl implements FmsFileSystemService { return CommonResult.success(new ArrayList<>()); } //根据uniqueIds查询任务记录 - FmsRecordDTO tmp = new FmsRecordDTO(); - tmp.setUniqueIds(uniqueIds); - List fsUploadRecords = fmsFileRecordMapper.selectbySelective(tmp); + FmsRecordDTO fmsRecordDTO = new FmsRecordDTO(); + fmsRecordDTO.setUniqueIds(uniqueIds); + List fsUploadRecords = fmsFileRecordMapper.selectbySelective(fmsRecordDTO); if (CollectionUtils.isEmpty(fsUploadRecords)) { logger.error(FmsCodeBean.FmsCode.NOT_EXIST_FILE.message,new AppException(FmsCodeBean.FmsCode.NOT_EXIST_FILE.code, FmsCodeBean.FmsCode.NOT_EXIST_FILE.message)); return CommonResult.success(new ArrayList<>()); @@ -552,14 +571,9 @@ public class FmsFileSystemServiceImpl implements FmsFileSystemService { for (FmsRecordDTO fssRecord : fsUploadRecords){ String uniqueId = fssRecord.getUniqueId(); String fileAccessToken = UUIDUtils.uuid(); - //String fileAccessUrlForOutter = "http://192.168.0.135:8150/fs/File/";//本地路径 + //String fileAccessUrlForOutter = "http://1270.0.0.1:8080/fms/get/file/";//本地路径 String path = fileAccessUrlForOutter + uniqueId + "?fileAccessToken=" + fileAccessToken; - // 缓存文件token(方式一) -// FsAccessTokenResult fsAccessToken = fsAccessService.cacheFileAccessToken(fileAccessToken, expiredTime, maxAccessCount, uniqueId, type); -// if (fsAccessToken == null) { -// throw new AppException(FSSCodeBean.FSSCode.FAIL.code); -// } - // 缓存文件token(方式二) + // 缓存文件token FmsAccessTokenResult fmsAccessTokenResult = new FmsAccessTokenResult(expiredTime, maxAccessCount, uniqueId, type); if (expiredTime > 0){ redisTemplate.opsForValue().set(fileAccessToken , JSON.toJSONString(fmsAccessTokenResult) , expiredTime , TimeUnit.MINUTES); diff --git a/itools-fms/itools-fms-server/src/main/resources/application-dev.yml b/itools-fms/itools-fms-server/src/main/resources/application-dev.yml index 933c2aeb1c0ca6ecdca70e1522edd22f9b31f319..405874fdfd448cc1e10bb13cb345bb0c73ff22e9 100644 --- a/itools-fms/itools-fms-server/src/main/resources/application-dev.yml +++ b/itools-fms/itools-fms-server/src/main/resources/application-dev.yml @@ -7,7 +7,7 @@ sequence: type: snowflake generate: simple -#fms文件系统配置项 +#fms文件系统配置项、flyway配置 spring: servlet: multipart: @@ -18,10 +18,24 @@ spring: # 上传文件的临时目录 enabled: true # location: /data/fmsFile/temp - + #flyway配置 + flyway: + # 是否启用flyway + enabled: true + # 到新的环境中数据库中有数据,且没有flyway_schema_history表时,是否执行迁移操作,如果设置为false,在启动时会报错,并停止迁移;如果为true,则生成history表并完成所有的迁移,要根据实际情况设置; + baseline-on-migrate: true + clean-disabled: true + # 在迁移时,是否校验脚本,假设V1.0__初始.sql已经迁移过了,在下次启动时会校验该脚本是否有变更过,则抛出异常 + validate-on-migrate: true + # 脚本位置 + locations: classpath:db/migration + # 检测迁移脚本的路径是否存在,如不存在,则抛出异常 + check-location: true + # 执行执行时标记的tag 默认为<> + baseline-description: <> fms: #系统文件管理策略【nio原生的文件系统,minio文件系统,fastdfs文件系统】 - strategy: minio + strategy: nio #文件的token的过期时间单位分钟 token-expiration-time: 5 #文件配置 @@ -61,7 +75,7 @@ initDB: # mybatis-plus相关配置 mybatis-plus: - mapper-locations: classpath:/mapper/**/*Mapper.xml + mapper-locations: classpath:/com.itools.mapper/**/*Mapper.xml typeAliasesPackage: com.itools.core global-config: db-column-underline: true diff --git a/itools-fms/itools-fms-server/src/main/resources/application.yml b/itools-fms/itools-fms-server/src/main/resources/application.yml index c197c554dc7269a4e972f042bdc1a96d2fc99196..44225ea80f4d41b67d3310d3e6a222d8ce9cb3a7 100644 --- a/itools-fms/itools-fms-server/src/main/resources/application.yml +++ b/itools-fms/itools-fms-server/src/main/resources/application.yml @@ -18,7 +18,7 @@ spring: datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/itools?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC + url: jdbc:mysql://localhost:3306/itools?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root hikari: diff --git a/itools-fms/itools-fms-server/src/main/resources/mapper/FmsAccessTokenMapper.xml b/itools-fms/itools-fms-server/src/main/resources/com/itools/mapper/FmsAccessTokenMapper.xml similarity index 93% rename from itools-fms/itools-fms-server/src/main/resources/mapper/FmsAccessTokenMapper.xml rename to itools-fms/itools-fms-server/src/main/resources/com/itools/mapper/FmsAccessTokenMapper.xml index 766b770495e7cdebb05f4cac0b79f7fc77ebc840..8a2061328f16f061a56525e90d621a14d2deaf98 100644 --- a/itools-fms/itools-fms-server/src/main/resources/mapper/FmsAccessTokenMapper.xml +++ b/itools-fms/itools-fms-server/src/main/resources/com/itools/mapper/FmsAccessTokenMapper.xml @@ -1,5 +1,5 @@ - - + + diff --git a/itools-fms/itools-fms-server/src/main/resources/mapper/FmsBusinessTypeMapper.xml b/itools-fms/itools-fms-server/src/main/resources/com/itools/mapper/FmsBusinessTypeMapper.xml similarity index 98% rename from itools-fms/itools-fms-server/src/main/resources/mapper/FmsBusinessTypeMapper.xml rename to itools-fms/itools-fms-server/src/main/resources/com/itools/mapper/FmsBusinessTypeMapper.xml index 66f2f052f4d2657945587965bf0832f123490773..69e03908cb01c1814eb2004fb9d52c50ca8ac35b 100644 --- a/itools-fms/itools-fms-server/src/main/resources/mapper/FmsBusinessTypeMapper.xml +++ b/itools-fms/itools-fms-server/src/main/resources/com/itools/mapper/FmsBusinessTypeMapper.xml @@ -1,5 +1,5 @@ - - + + diff --git a/itools-fms/itools-fms-server/src/main/resources/mapper/FmsFileRecordMapper.xml b/itools-fms/itools-fms-server/src/main/resources/com/itools/mapper/FmsFileRecordMapper.xml similarity index 98% rename from itools-fms/itools-fms-server/src/main/resources/mapper/FmsFileRecordMapper.xml rename to itools-fms/itools-fms-server/src/main/resources/com/itools/mapper/FmsFileRecordMapper.xml index 33c2199480354421958816e3b782759c25544cd2..86b7baa28a07d407251fa413ec3c8c973227ba4d 100644 --- a/itools-fms/itools-fms-server/src/main/resources/mapper/FmsFileRecordMapper.xml +++ b/itools-fms/itools-fms-server/src/main/resources/com/itools/mapper/FmsFileRecordMapper.xml @@ -1,5 +1,5 @@ - - + + diff --git a/itools-fms/itools-fms-server/src/main/resources/db/migration/V1.2.1__test1.sql b/itools-fms/itools-fms-server/src/main/resources/db/migration/V1.2.1__test1.sql new file mode 100644 index 0000000000000000000000000000000000000000..37427d70dad6ba1e18dcd2089e7442261611c8bf --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/resources/db/migration/V1.2.1__test1.sql @@ -0,0 +1,5 @@ +CREATE TABLE `tb_test1` ( + `id` int(11) NOT NULL, + `name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; \ No newline at end of file diff --git a/itools-fms/itools-fms-server/src/main/resources/db/migration/V1.2__test.sql b/itools-fms/itools-fms-server/src/main/resources/db/migration/V1.2__test.sql new file mode 100644 index 0000000000000000000000000000000000000000..400f67a914c87f1090b179b7c0f16bfaa07c6268 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/resources/db/migration/V1.2__test.sql @@ -0,0 +1,5 @@ +CREATE TABLE `tb_test` ( + `id` int(11) NOT NULL, + `name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; \ No newline at end of file diff --git a/itools-fms/itools-fms-server/src/main/resources/db/migration/afterMigrateError.sql b/itools-fms/itools-fms-server/src/main/resources/db/migration/afterMigrateError.sql new file mode 100644 index 0000000000000000000000000000000000000000..802af8a21bcc0dbf70612a717a32f65008ed1352 --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/resources/db/migration/afterMigrateError.sql @@ -0,0 +1,2 @@ +#下次启动服务时将自动清除上次失败的数据 +DELETE FROM flyway_schema_history WHERE success=0; diff --git "a/itools-fms/itools-fms-server/src/main/resources/db/migration/\346\263\250\346\204\217" "b/itools-fms/itools-fms-server/src/main/resources/db/migration/\346\263\250\346\204\217" new file mode 100644 index 0000000000000000000000000000000000000000..f0c5c960759a07ef780a446e72aff0821a6eb6c8 --- /dev/null +++ "b/itools-fms/itools-fms-server/src/main/resources/db/migration/\346\263\250\346\204\217" @@ -0,0 +1,17 @@ +文件命名方式 + +1.Version Migrations:通常命名形式为 Vversion__description.sql (两个下划线) + 默认version 长 度 小 于 50 , version长度小于50,version长度小于50,description长度小于500,以__分隔 + version 按 升 序 , version按升序,version按升序,description可以重复 + +2.Undo Migrations:Version Migrations的相反操作,flyway pro支持,但事实上目前需求通过Version Migrations也能够实现。 + +3.Repeatable Migrations:可重复操作文件。由于可重复操作,但上线发版却只有一次,即只会执行最后一次提交的结果。 +原则上不推荐使用此操作文件,除非只在原文件基础上只增不减。Repeatable Migrations在Version Migrations 执行完后再执行。 +同时可以有多个Repeatable Migrations文件,按升序执行。 + + +注意,默认版本号与描述之间以两个“_”隔开 +示例:V1.0.0.0__initDatabase.sql +1、如果为空数据库,起始版本号可以等于1 +2、如果数据库不为空,起始版本号需大于1 \ No newline at end of file diff --git a/itools-fms/itools-fms-server/src/main/resources/logback-spring.xml b/itools-fms/itools-fms-server/src/main/resources/logback-spring.xml new file mode 100644 index 0000000000000000000000000000000000000000..1b9ce8c07b537e53044edbe196e2bbd17ae6fb7a --- /dev/null +++ b/itools-fms/itools-fms-server/src/main/resources/logback-spring.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/itools-ums/pom.xml b/itools-generator/pom.xml similarity index 32% rename from itools-ums/pom.xml rename to itools-generator/pom.xml index e909b8c024791dc899378a24a5a077f1e0e859e7..3a400d90fb70d4d1f80367d271abda9221fe962e 100644 --- a/itools-ums/pom.xml +++ b/itools-generator/pom.xml @@ -8,8 +8,34 @@ 1.0-SNAPSHOT 4.0.0 - pom - itools-ums + itools-generator + + 3.0.1 + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + com.baomidou + mybatis-plus-generator + ${mybatis-plus.version} + + + org.projectlombok + lombok + 1.18.10 + + + + + mysql + mysql-connector-java + 8.0.20 + + \ No newline at end of file diff --git a/itools-generator/src/main/java/com/itools/core/generator/CodeGenerator.java b/itools-generator/src/main/java/com/itools/core/generator/CodeGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..8a5e5a91af30e9df692e03d491c0b69e4ff9d988 --- /dev/null +++ b/itools-generator/src/main/java/com/itools/core/generator/CodeGenerator.java @@ -0,0 +1,185 @@ +package com.itools.core.generator; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.generator.AutoGenerator; +import com.baomidou.mybatisplus.generator.InjectionConfig; +import com.baomidou.mybatisplus.generator.config.*; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import com.baomidou.mybatisplus.generator.config.rules.DateType; +import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; +import com.sun.javafx.PlatformUtil; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +public class CodeGenerator { + /** + * 代码生成位置 + */ + public static final String PARENT_NAME = "com.dongxin"; +// public static final String PARENT_NAME = "com.itools.core"; + + /** + * modular 名字 + */ + public static final String MODULAR_NAME = ""; + + /** + * 基本路径 + */ + public static final String SRC_MAIN_JAVA = "src/main/java/"; + + /** + * 作者 + */ + public static final String AUTHOR = "XUCHANG"; + + /** + * 是否是 rest 接口 + */ + private static final boolean REST_CONTROLLER_STYLE = true; + +// public static final String JDBC_MYSQL_URL = "jdbc:mysql://localhost:3306/itools?" + +// "serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8"; +// public static final String JDBC_MYSQL_URL = "jdbc:mysql://10.0.10.249:3306/unattended?useUnicode=true&useSSL=false&characterEncoding=utf8"; + public static final String JDBC_MYSQL_URL = "jdbc:mysql://10.0.10.249:3306/db_video_distinguish_dev?useUnicode=true&useSSL=false&characterEncoding=utf8"; + +// public static final String JDBC_DRIVER_NAME = "com.mysql.cj.jdbc.Driver"; + public static final String JDBC_DRIVER_NAME = "com.mysql.jdbc.Driver"; + +// public static final String JDBC_USERNAME = "root"; + public static final String JDBC_USERNAME = "program"; + +// public static final String JDBC_PASSWORD = "root"; + public static final String JDBC_PASSWORD = "z#Mshkk5"; + + public static void main(String[] args) { + String moduleName = scanner("模块名(无前缀输入#)").replaceAll("#", ""); + String tableName = scanner("表名"); + String tablePrefix = scanner("表前缀(无前缀输入#)").replaceAll("#", ""); + autoGenerator(moduleName, tableName, tablePrefix); + } + + public static void autoGenerator(String moduleName, String tableName, String tablePrefix) { + String[] tableNames = tableName.split(","); + new AutoGenerator() + .setGlobalConfig(getGlobalConfig()) + .setDataSource(getDataSourceConfig()) + .setPackageInfo(getPackageConfig(moduleName)) + .setStrategy(getStrategyConfig(tableNames, tablePrefix)) + .setCfg(getInjectionConfig(moduleName)) + .setTemplate(getTemplateConfig()) + .execute(); + } + + private static String getDateTime() { + LocalDateTime localDate = LocalDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + return localDate.format(formatter); + } + + private static InjectionConfig getInjectionConfig(final String moduleName) { + return new InjectionConfig() { + @Override + public void initMap() { + Map map = new HashMap(); + map.put("dateTime", getDateTime()); + setMap(map); + final String projectPath = System.getProperty("user.dir"); + List fileOutConfigList = new ArrayList(); + // 自定义配置会被优先输出 + fileOutConfigList.add(new FileOutConfig("/templates/mapper.xml.vm") { + @Override + public String outputFile(TableInfo tableInfo) { + // 自定义输出文件名,如果entity设置了前后缀,此次注意xml的名称也会跟着发生变化 + return projectPath + "/src/main/resources/com.itools.mapper/" + + moduleName + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; + } + }); + setFileOutConfigList(fileOutConfigList); + } + }; + } + + + private static StrategyConfig getStrategyConfig(String[] tableName, String tablePrefix) { + return new StrategyConfig() + .setNaming(NamingStrategy.underline_to_camel) + .setColumnNaming(NamingStrategy.underline_to_camel) + .setInclude(tableName) + .setRestControllerStyle(REST_CONTROLLER_STYLE) + .setEntityBuilderModel(true) + .setControllerMappingHyphenStyle(true) +// .setEntityTableFieldAnnotationEnable(true) + .entityTableFieldAnnotationEnable(true) + .setTablePrefix(tablePrefix + "_"); + } + + private static PackageConfig getPackageConfig(String moduleName) { + return new PackageConfig() + /*.setModuleName(moduleName) + .setParent(PARENT_NAME) + .setService("com.itools.service") + .setServiceImpl("com.itools.service.impl") + .setController("com.itools.controller") + .setEntity("entity")*/; + } + + private static DataSourceConfig getDataSourceConfig() { + return new DataSourceConfig() + .setUrl(JDBC_MYSQL_URL) + .setDriverName(JDBC_DRIVER_NAME) + .setUsername(JDBC_USERNAME) + .setPassword(JDBC_PASSWORD); + } + + private static GlobalConfig getGlobalConfig() { + String projectPath = System.getProperty("user.dir"); + String filePath = projectPath + "/" + MODULAR_NAME + SRC_MAIN_JAVA; + if (PlatformUtil.isWindows()) { + filePath = filePath.replaceAll("/+|\\\\+", "\\\\"); + } else { + filePath = filePath.replaceAll("/+|\\\\+", "/"); + } + return new GlobalConfig() +// .setOutputDir(filePath) + .setOutputDir("D:\\generatorCode") + .setDateType(DateType.ONLY_DATE) + .setIdType(IdType.UUID) + .setAuthor(AUTHOR) + .setBaseColumnList(true) + .setSwagger2(true) + .setEnableCache(false) + .setBaseResultMap(true) + .setServiceName("%sService")//去掉服务默认前缀 + .setOpen(false); + } + + private static TemplateConfig getTemplateConfig() { + return new TemplateConfig() + .setController("/templates/controller.java.vm") + .setService("/templates/service.java.vm") + .setServiceImpl("/templates/serviceImpl.java.vm") + .setEntity("/templates/entity.java.vm") + .setMapper("/templates/mapper.java.vm") + .setXml("/templates/mapper.xml.vm"); + } + + private static String scanner(String tip) { + Scanner scanner = new Scanner(System.in); + StringBuilder sb = new StringBuilder(); + sb.append("please input " + tip + " : "); + System.out.println(sb.toString()); + if (scanner.hasNext()) { + String ipt = scanner.next(); + if (StringUtils.isNotEmpty(ipt)) { + return ipt; + } + } + throw new MybatisPlusException("please input the correct " + tip + ". "); + } +} \ No newline at end of file diff --git a/itools-oauth2/itools-oauth2-client01/pom.xml b/itools-oauth2/itools-oauth2-client01/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..b43411c2d742f38a819614026d8d7a637ac7c1e7 --- /dev/null +++ b/itools-oauth2/itools-oauth2-client01/pom.xml @@ -0,0 +1,92 @@ + + + + itools-oauth2 + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-oauth2-client01 + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + + + com.baomidou + mybatis-plus-boot-starter + + + + org.springframework.security + spring-security-crypto + + + + com.baomidou + mybatis-plus-generator + + + + mysql + mysql-connector-java + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-oauth2 + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + + + + src/main/resources + true + + **/*.jks + + + + src/main/resources + false + + **/*.jks + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/Oauth2Client01Application.java b/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/Oauth2Client01Application.java new file mode 100644 index 0000000000000000000000000000000000000000..a69ea84a405ab1c917d8c5ae52bb2eb9c6ffd438 --- /dev/null +++ b/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/Oauth2Client01Application.java @@ -0,0 +1,25 @@ +package com.itools.core; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.Banner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Scope; +import org.springframework.web.context.WebApplicationContext; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@SpringBootApplication +@MapperScan("com.itools.core.com.itools.mapper") +public class Oauth2Client01Application { + public static void main(String[] args) { + SpringApplication springApplication = new SpringApplication(Oauth2Client01Application.class); + springApplication.setBannerMode(Banner.Mode.OFF); + springApplication.run(args); + } + +} diff --git a/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/config/ResourceServerConfig.java b/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/config/ResourceServerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..43ac1fd4f194ec5f486e42088226d6c644e94552 --- /dev/null +++ b/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/config/ResourceServerConfig.java @@ -0,0 +1,49 @@ +package com.itools.core.config; + +import com.itools.core.endpoint.AuthExceptionEntryPoint; +import com.itools.core.endpoint.CustomAccessDeniedHandler; +import com.itools.core.endpoint.CustomTokenExtractor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; + +import javax.servlet.http.HttpServletResponse; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-21 16:13 + */ +@Configuration +@EnableResourceServer +public class ResourceServerConfig extends ResourceServerConfigurerAdapter { + + @Override + public void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + .exceptionHandling() + .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) + .and() + .authorizeRequests() + .anyRequest().authenticated() + .and() + .httpBasic(); + } + @Autowired + private AuthExceptionEntryPoint authExceptionEntryPoint; + @Autowired + private CustomAccessDeniedHandler customAccessDeniedHandler; + @Autowired + private CustomTokenExtractor customTokenExtractor; + @Override + public void configure(ResourceServerSecurityConfigurer resources) { + resources.tokenExtractor(customTokenExtractor); + resources.authenticationEntryPoint(authExceptionEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler); + } +} diff --git a/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/config/WebSecurityConfigurer.java b/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/config/WebSecurityConfigurer.java new file mode 100644 index 0000000000000000000000000000000000000000..284031c9f323fcb82be287ca7f95e412c36b5562 --- /dev/null +++ b/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/config/WebSecurityConfigurer.java @@ -0,0 +1,34 @@ +package com.itools.core.config; + +import org.springframework.context.annotation.Configuration; +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.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-22 10:03 + */ +@Configuration +@EnableWebSecurity(debug = true) +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { +// http.csrf().disable(); + ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = http + .authorizeRequests(); + //设置可放行的请求 +// for (String url : ignoreUrlsConfig().getUrls()) { +// registry.antMatchers(url).permitAll(); +// } + registry.antMatchers("/oauth/**").permitAll() + .anyRequest().authenticated() + .and() + .csrf().disable(); + } +} diff --git a/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/controller/AccountController.java b/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/controller/AccountController.java new file mode 100644 index 0000000000000000000000000000000000000000..43a46b6a634d95737602a5fb2a4a5d79e79989d0 --- /dev/null +++ b/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/controller/AccountController.java @@ -0,0 +1,43 @@ +package com.itools.core.controller; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-21 16:13 + */ +@RestController +public class AccountController { + + @GetMapping("/current") + public Principal user(Principal principal) { + return principal; + } + + @GetMapping("/query") + @PreAuthorize("hasAnyAuthority('query')") + public Authentication query() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication; + } + + @GetMapping("/query2") + @PreAuthorize("hasAnyAuthority('query2')") + public String query2() { + return "具有query2权限"; + } + + @PreAuthorize("hasAnyAuthority('test1')") + @GetMapping("/test1") + public String test() { + return "test1"; + } +} diff --git a/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/endpoint/AuthExceptionEntryPoint.java b/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/endpoint/AuthExceptionEntryPoint.java new file mode 100644 index 0000000000000000000000000000000000000000..8e9ec17e4e866b4223dd4ace7135ffd7a184f4e1 --- /dev/null +++ b/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/endpoint/AuthExceptionEntryPoint.java @@ -0,0 +1,50 @@ +package com.itools.core.endpoint; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +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; +import java.util.HashMap; +import java.util.Map; + +/** + * @project: itools-backend + * @description: 自定义返回结果:未登录或登录过期 + * @author: XUCHANG + * @create: 2021-06-22 13:37 + */ +@Component +@Slf4j +public class AuthExceptionEntryPoint implements AuthenticationEntryPoint { + @Autowired + private ObjectMapper objectMapper; + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws ServletException { + Map map = new HashMap<>(); + map.put("code",401); + map.put("success",false); + map.put("data",null); + Throwable cause = authException.getCause(); + + response.setStatus(HttpStatus.OK.value()); + response.setHeader("Content-Type", "application/json;charset=UTF-8"); + try { + map.put("message","暂未登录或token已经过期"); + response.getWriter().write(objectMapper.writeValueAsString(map)); + } catch (IOException e) { + log.error("无效 token 异常类重写",e); + } + } +} diff --git a/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/endpoint/CustomAccessDeniedHandler.java b/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/endpoint/CustomAccessDeniedHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..dacf13975e5dbd113d55a97775d496212bf6662c --- /dev/null +++ b/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/endpoint/CustomAccessDeniedHandler.java @@ -0,0 +1,47 @@ +package com.itools.core.endpoint; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * @project: itools-backend + * @description: 自定义返回结果:没有权限访问时 + * @author: XUCHANG + * @create: 2021-06-22 13:45 + */ +@Slf4j +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + @Autowired + private ObjectMapper objectMapper; + @Override + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) + throws IOException, ServletException { + Map map = new HashMap<>(); + map.put("code",401); + map.put("success",false); + map.put("data",null); + response.setStatus(HttpStatus.OK.value()); + response.setHeader("Content-Type", "application/json;charset=UTF-8"); + try { + map.put("message","用户权限不足"); + response.getWriter().write(objectMapper.writeValueAsString(map)); + } catch (IOException e) { + log.error("用户权限不足",e); + } + } +} diff --git a/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/endpoint/CustomTokenExtractor.java b/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/endpoint/CustomTokenExtractor.java new file mode 100644 index 0000000000000000000000000000000000000000..1cc8bae63eed5055be28001b8c9f434a2bf04a17 --- /dev/null +++ b/itools-oauth2/itools-oauth2-client01/src/main/java/com/itools/core/endpoint/CustomTokenExtractor.java @@ -0,0 +1,95 @@ +package com.itools.core.endpoint; + +import lombok.extern.log4j.Log4j2; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; +import org.springframework.security.oauth2.provider.authentication.TokenExtractor; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.stereotype.Component; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.util.Enumeration; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-22 13:50 + */ +@Log4j2 +@Component +public class CustomTokenExtractor implements TokenExtractor { + + @Override + public Authentication extract(HttpServletRequest request) { + String tokenValue = this.extractToken(request); + if (tokenValue != null) { + return new PreAuthenticatedAuthenticationToken(tokenValue, ""); + } else { + //抛出异常 + log.info("token为空,请输入合法token"); + return null; + } + } + + public String extractToken(HttpServletRequest request) { + String token = this.extractHeaderToken(request); + if (token == null) { + log.info("Token not found in headers. Trying request parameters."); + token = request.getParameter("access_token"); + if (token == null) { + log.info("Token not found in request parameters. Trying request cookies."); + + token = extractCookieToken(request); + if (token == null) { + log.info("Token not found in cookies. Not an OAuth2 request."); + } else { + request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, "Bearer"); + } + } + } + return token; + } + + private String extractHeaderToken(HttpServletRequest request) { + Enumeration headers = request.getHeaders("Authorization"); + + String value; + do { + if (!headers.hasMoreElements()) { + return null; + } + + value = (String) headers.nextElement(); + } while (!value.toLowerCase().startsWith("Bearer".toLowerCase())); + + String authHeaderValue = value.substring("Bearer".length()).trim(); + request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, value.substring(0, "Bearer".length()).trim()); + int commaIndex = authHeaderValue.indexOf(44); + if (commaIndex > 0) { + authHeaderValue = authHeaderValue.substring(0, commaIndex); + } + if (authHeaderValue.equals("")) { + return null; + } + return authHeaderValue; + } + + private String extractCookieToken(HttpServletRequest request) { + + String cookieToken = null; + //根据请求数据,找到cookie数组 + Cookie[] cookies = request.getCookies(); + + if (null != cookies && cookies.length > 0) { + for (Cookie cookie : cookies) { + if (null != cookie.getName() && cookie.getName().trim().equalsIgnoreCase("access_token")) { + cookieToken = cookie.getValue().trim(); + break; + } + } + } + return cookieToken; + } +} diff --git a/itools-oauth2/itools-oauth2-client01/src/main/resources/application-dev.yml b/itools-oauth2/itools-oauth2-client01/src/main/resources/application-dev.yml new file mode 100644 index 0000000000000000000000000000000000000000..7f30aad4aeda5e3eba1d794211a6b036b1b2a73a --- /dev/null +++ b/itools-oauth2/itools-oauth2-client01/src/main/resources/application-dev.yml @@ -0,0 +1,22 @@ +#组件中的全局异常处理的code +system: + errorCode: Code001 +#分布式雪花算法策略 +sequence: + enable: true + type: snowflake + generate: simple + +# mybatis-plus相关配置 +mybatis-plus: + mapper-locations: classpath:/com.itools.mapper/**/*Mapper.xml + typeAliasesPackage: com.itools.core + global-config: + db-column-underline: true + db-config: + logic-delete-field: del_flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + configuration: + default-enum-type-handler: com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl diff --git a/itools-oauth2/itools-oauth2-client01/src/main/resources/application.yml b/itools-oauth2/itools-oauth2-client01/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..c353b261e0dd22a433e7b4e46a88dca3cd516487 --- /dev/null +++ b/itools-oauth2/itools-oauth2-client01/src/main/resources/application.yml @@ -0,0 +1,32 @@ +server: + port: 17002 +# servlet: +# session: +# cookie: +# name: SESSIONID +spring: + application: + name: itools-oms-auth-client01 + + redis: + host: 127.0.0.1 + database: 0 + + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/oauth2-sso?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC + username: root + password: root + hikari: + minimum-idle: 5 + idle-timeout: 600000 + maximum-pool-size: 10 + auto-commit: true + pool-name: MyHikariCP + max-lifetime: 1800000 + connection-timeout: 30000 + connection-test-query: SELECT 1 + main: + allow-bean-definition-overriding: true + diff --git a/itools-oauth2/itools-oauth2-client01/src/main/resources/bootstrap.yml b/itools-oauth2/itools-oauth2-client01/src/main/resources/bootstrap.yml new file mode 100644 index 0000000000000000000000000000000000000000..c09c1ecc84553938860f52ad42fe7d7e89434ce0 --- /dev/null +++ b/itools-oauth2/itools-oauth2-client01/src/main/resources/bootstrap.yml @@ -0,0 +1,33 @@ +# Nacos Server 的地址 +spring: + profiles: + active: dev + cloud: + nacos: + discovery: + server-addr: 106.54.85.156:8848 + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev + +itools: + jwt: + keyPairName: jwt.jks + keyPairAlias: jwt + keyPairSecret: 123123 + keyPairStoreSecret: 123123 + +jwt: + tokenHeader: Authorization #JWT存储的请求头 + secret: 123123 #JWT加解密使用的密钥 + expiration: 604800 #JWT的超期限时间(60*60*24) + tokenHead: bearer #JWT负载中拿到开头 + +security: + oauth2: + client: + access-token-uri: http://localhost:17003/oauth/token + user-authorization-uri: http://localhost:17003/oauth/authorize +# client-id: webapp + resource: + user-info-uri: http://localhost:17003/user + prefer-token-info: false diff --git a/itools-oauth2/itools-oauth2-client01/src/main/resources/logback-spring.xml b/itools-oauth2/itools-oauth2-client01/src/main/resources/logback-spring.xml new file mode 100644 index 0000000000000000000000000000000000000000..1b9ce8c07b537e53044edbe196e2bbd17ae6fb7a --- /dev/null +++ b/itools-oauth2/itools-oauth2-client01/src/main/resources/logback-spring.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/itools-oauth2/itools-oauth2-server01/pom.xml b/itools-oauth2/itools-oauth2-server01/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..6d7849bad2cee1f27c0b989bb979be2c8ede0ab5 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/pom.xml @@ -0,0 +1,97 @@ + + + + itools-oauth2 + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-oauth2-server01 + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + + + com.baomidou + mybatis-plus-boot-starter + + + + com.baomidou + mybatis-plus-generator + + + + mysql + mysql-connector-java + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-oauth2 + + + io.jsonwebtoken + jjwt + + + cn.hutool + hutool-all + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + + src/main/resources + true + + **/*.jks + + + + src/main/resources + false + + **/*.jks + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/Oauth2ServerApplication.java b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/Oauth2ServerApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..a52f110d5706beacfe2d9f2d97fdc489cf908765 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/Oauth2ServerApplication.java @@ -0,0 +1,28 @@ +package com.itools.core; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.Banner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@SpringBootApplication +@MapperScan("com.itools.core.mapper") +public class Oauth2ServerApplication { + public static void main(String[] args) { + SpringApplication springApplication = new SpringApplication(Oauth2ServerApplication.class); + springApplication.setBannerMode(Banner.Mode.OFF); + springApplication.run(args); + } + +} diff --git a/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/AuthorizationServerConfig.java b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/AuthorizationServerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..9f75881ca90d98b801c0971f58164751f726bdd0 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/AuthorizationServerConfig.java @@ -0,0 +1,106 @@ +package com.itools.core.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; +import org.springframework.security.oauth2.provider.token.TokenEnhancer; +import org.springframework.security.oauth2.provider.token.TokenEnhancerChain; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; + +import javax.sql.DataSource; +import java.util.ArrayList; +import java.util.List; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-21 15:23 + */ +@Configuration +@EnableAuthorizationServer +//@EnableAutoConfiguration +//@EnableOAuth2Sso +public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { + + @Autowired + private DataSource dataSource; + @Override + public void configure(ClientDetailsServiceConfigurer clients) throws Exception { + // 第三方信息的存储 基于jdbc + clients.withClientDetails(clientDetailsService()); + + } + + @Autowired + @Qualifier("jwtTokenStore") + private TokenStore tokenStore; + + @Autowired + private JwtAccessTokenConverter jwtAccessTokenConverter; + + @Autowired + private UserDetailsService userDetailsService; + + @Autowired + private AuthenticationManager authenticationManagerBean; + + @Autowired + private OauthTokenEnhancer oauthTokenEnhancer; + + @Override + public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { + //配置JWT的内容增强器 + TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); + List delegates = new ArrayList<>(); + delegates.add(oauthTokenEnhancer); + delegates.add(jwtAccessTokenConverter); + enhancerChain.setTokenEnhancers(delegates); + + //使用密码模式需要配置 + endpoints.authenticationManager(authenticationManagerBean) + .reuseRefreshTokens(false) //refresh_token是否重复使用 + .userDetailsService(userDetailsService) //刷新令牌授权包含对用户信息的检查 + .tokenStore(tokenStore) //指定token存储策略是jwt + .accessTokenConverter(jwtAccessTokenConverter) + .tokenEnhancer(enhancerChain) //配置tokenEnhancer + .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支持GET,POST请求 + } + + /** + * 授权服务器安全配置 + * @param security + * @throws Exception + */ + @Override + public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { + + //第三方客户端校验token需要带入 clientId 和clientSecret来校验 + security.checkTokenAccess("isAuthenticated()") + .tokenKeyAccess("isAuthenticated()");//来获取我们的tokenKey需要带入clientId,clientSecret + + //允许表单认证 + security.allowFormAuthenticationForClients(); + } + + @Bean + public ClientDetailsService clientDetailsService(){ + return new JdbcClientDetailsService(dataSource); + } + +} + diff --git a/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/JwtCAProperties.java b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/JwtCAProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..8fccc3e1b4dd1d6943a66ff518ca966eb002b6fe --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/JwtCAProperties.java @@ -0,0 +1,37 @@ +package com.itools.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-21 15:32 + */ +@Data +@ConfigurationProperties(prefix = "itools.jwt") +public class JwtCAProperties { + + /** + * 证书名称 + */ + private String keyPairName; + + + /** + * 证书别名 + */ + private String keyPairAlias; + + /** + * 证书私钥 + */ + private String keyPairSecret; + + /** + * 证书存储密钥 + */ + private String keyPairStoreSecret; + +} \ No newline at end of file diff --git a/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/JwtTokenStoreConfig.java b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/JwtTokenStoreConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..9e0b3bb32741b8afb29b1694543fba8e74d741a7 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/JwtTokenStoreConfig.java @@ -0,0 +1,61 @@ +package com.itools.core.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; +import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; +import org.springframework.security.rsa.crypto.KeyStoreKeyFactory; + +import java.security.KeyPair; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-21 15:23 + */ +@Configuration +@EnableConfigurationProperties(value = JwtCAProperties.class) +public class JwtTokenStoreConfig { + + @Bean + public TokenStore jwtTokenStore(){ + return new JwtTokenStore(jwtAccessTokenConverter()); + } + + @Bean + public OauthTokenEnhancer tulingTokenEnhancer() { + return new OauthTokenEnhancer(); + } + + @Bean + public JwtAccessTokenConverter jwtAccessTokenConverter(){ + JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); + //配置JWT使用的秘钥 + accessTokenConverter.setSigningKey("123123"); + return accessTokenConverter; + } +// @Bean +// public JwtAccessTokenConverter jwtAccessTokenConverter(){ +// JwtAccessTokenConverter accessTokenConverter = new +// JwtAccessTokenConverter(); +// //配置JWT使用的秘钥 +// //accessTokenConverter.setSigningKey("123123"); +// //配置JWT使用的秘钥 非对称加密 +// accessTokenConverter.setKeyPair(keyPair()); +// return accessTokenConverter; +// } +// +// @Autowired +// private JwtCAProperties jwtCAProperties; +// +// @Bean +// public KeyPair keyPair() { +// KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource(jwtCAProperties.getKeyPairName()), jwtCAProperties.getKeyPairSecret().toCharArray()); +// return keyStoreKeyFactory.getKeyPair(jwtCAProperties.getKeyPairAlias(), jwtCAProperties.getKeyPairStoreSecret().toCharArray()); +// } +} diff --git a/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/OauthTokenEnhancer.java b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/OauthTokenEnhancer.java new file mode 100644 index 0000000000000000000000000000000000000000..0d3186a84c4782c3e8ad23acc0f3cbcfadf1bcae --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/OauthTokenEnhancer.java @@ -0,0 +1,35 @@ +package com.itools.core.config; + +import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.token.TokenEnhancer; + +import java.util.HashMap; +import java.util.Map; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-21 15:33 + */ +public class OauthTokenEnhancer implements TokenEnhancer { + @Override + public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { + +// MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal(); + final Map additionalInfo = new HashMap<>(); + final Map retMap = new HashMap<>(); + + //todo 这里暴露memberId到Jwt的令牌中,后期可以根据自己的业务需要 进行添加字段 + additionalInfo.put("memberId","123"); + + retMap.put("additionalInfo",additionalInfo); + + ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(retMap); + + return accessToken; + } +} + diff --git a/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/ResourceServerConfig.java b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/ResourceServerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..48be889b77cc847b57d5f5231e5b1d32523cfe0a --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/ResourceServerConfig.java @@ -0,0 +1,24 @@ +package com.itools.core.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-21 15:22 + */ +@Configuration +@EnableResourceServer +public class ResourceServerConfig extends ResourceServerConfigurerAdapter { + + @Override + public void configure(HttpSecurity http) throws Exception { + http.authorizeRequests() + .anyRequest().authenticated(); + + } +} diff --git a/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/WebSecurityConfig.java b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/WebSecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..876c8aeb981ceb8931af1ddfd9928449db9f55c7 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/config/WebSecurityConfig.java @@ -0,0 +1,62 @@ +package com.itools.core.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-21 15:22 + */ +@Configuration +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private UserDetailsService userDetailsService; + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService); + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.formLogin().permitAll() + .and().authorizeRequests() + .antMatchers("/oauth/**").permitAll() + .anyRequest() + .authenticated() + .and().logout().permitAll() + .and().csrf().disable(); + } + +// @Bean +// public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) { +// FilterRegistrationBean registration = new FilterRegistrationBean(); +// registration.setFilter(filter); +// registration.setOrder(-100); +// return registration; +// } +} diff --git a/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/controller/UserController.java b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..9e57a5b7b340ba9453b0e3073f6b1ee0cbd1a5d2 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/controller/UserController.java @@ -0,0 +1,15 @@ +package com.itools.core.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; + + +@RestController +public class UserController { + @GetMapping("/user") + public Principal user(Principal user){ + return user; + } +} diff --git a/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/mapper/PermissionMapper.java b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/mapper/PermissionMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..47fbbb510f6e50953065e96399048139aa6d2982 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/mapper/PermissionMapper.java @@ -0,0 +1,33 @@ +package com.itools.core.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.pojo.SysPermission; +import org.apache.ibatis.annotations.Select; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-29 13:57 + */ +@Repository +public interface PermissionMapper extends BaseMapper { + + @Select("SELECT\n" + + " p.*\n" + + "FROM\n" + + " tb_user AS u\n" + + " LEFT JOIN tb_user_role AS ur\n" + + " ON u.id = ur.user_id\n" + + " LEFT JOIN tb_role AS r\n" + + " ON r.id = ur.role_id\n" + + " LEFT JOIN tb_role_permission AS rp\n" + + " ON r.id = rp.role_id\n" + + " LEFT JOIN tb_permission AS p\n" + + " ON p.id = rp.permission_id\n" + + "WHERE u.id = #{userId}") + List selectByUserId(Long userId); +} diff --git a/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/mapper/UserInfoMapper.java b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/mapper/UserInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..06c77424e8b6c84deffde5405414328eb2072218 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/mapper/UserInfoMapper.java @@ -0,0 +1,19 @@ +package com.itools.core.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.pojo.SysUser; +import org.apache.ibatis.annotations.Select; +import org.springframework.stereotype.Repository; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-29 13:57 + */ +@Repository +public interface UserInfoMapper extends BaseMapper { + + @Select("select * from tb_user where username=#{username}") + SysUser getByUsername(String username); +} diff --git a/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/pojo/SysPermission.java b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/pojo/SysPermission.java new file mode 100644 index 0000000000000000000000000000000000000000..778b8d1d8cd218c41a8042973bf98d6eb4faab31 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/pojo/SysPermission.java @@ -0,0 +1,33 @@ +package com.itools.core.pojo; + +import lombok.Data; + +import java.util.Date; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Data +public class SysPermission implements java.io.Serializable { + private static final long serialVersionUID = 1L; + + private Long id; + + private Long parentId; + + private String name; + + private String enname; + + private String url; + + private String description; + + private Date created; + + private Date updated; + +} diff --git a/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/pojo/SysRole.java b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/pojo/SysRole.java new file mode 100644 index 0000000000000000000000000000000000000000..1090010627365a23d60d347dbdabca3ac5a5b556 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/pojo/SysRole.java @@ -0,0 +1,31 @@ +package com.itools.core.pojo; + +import lombok.Data; + +import java.util.Date; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Data +public class SysRole implements java.io.Serializable { + private static final long serialVersionUID = 1L; + + private Long id; + + private Long parentId; + + private String name; + + private String enname; + + private String description; + + private Date created; + + private Date updated; + +} diff --git a/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/pojo/SysUser.java b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/pojo/SysUser.java new file mode 100644 index 0000000000000000000000000000000000000000..be1a2a4671c26ad834ce6b01dc1a9ed45299ea59 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/pojo/SysUser.java @@ -0,0 +1,33 @@ +package com.itools.core.pojo; + +import lombok.Data; + +import java.util.Date; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Data +public class SysUser implements java.io.Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + private String username; + + private String password; + + private String phone; + + private String email; + + private Date created; + + private Date updated; + + +} diff --git a/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/service/UserService.java b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/service/UserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6738c02c2dc853389cf68f307709f9b824370fa6 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/service/UserService.java @@ -0,0 +1,9 @@ +package com.itools.core.service; + +import com.itools.core.pojo.SysUser; +import org.springframework.security.core.userdetails.UserDetailsService; + +public interface UserService extends UserDetailsService { + + SysUser getByUsername(String username); +} \ No newline at end of file diff --git a/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/service/impl/UserServiceImpl.java b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/service/impl/UserServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..f397ee435833404bfcbd4ec58e6a870707697402 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/service/impl/UserServiceImpl.java @@ -0,0 +1,58 @@ +package com.itools.core.service.impl; + + +import com.itools.core.mapper.PermissionMapper; +import com.itools.core.mapper.UserInfoMapper; +import com.itools.core.pojo.SysPermission; +import com.itools.core.pojo.SysUser; +import com.itools.core.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-29 14:00 + */ +@Service("userDetailsService") +public class UserServiceImpl implements UserService { + + @Autowired + private UserInfoMapper userInfoMapper; + @Autowired + private PermissionMapper permissionMapper; + + @Override + public SysUser getByUsername(String username) { + return userInfoMapper.getByUsername(username); + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + SysUser user = getByUsername(username); + List authorities = new ArrayList<>(); + if (user==null){ + throw new UsernameNotFoundException("用户名不存在"); + } + List permissions = permissionMapper.selectByUserId(user.getId()); + + permissions.forEach(permission -> { + if (permission!=null && !StringUtils.isEmpty(permission.getEnname())){ + GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getEnname()); + authorities.add(grantedAuthority); + } + }); + return new User(user.getUsername(),user.getPassword(),authorities); + } + +} diff --git a/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/utils/JwtTokenUtil.java b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/utils/JwtTokenUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..94e6af10755d0a58c650722546564b300d27f3ab --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/java/com/itools/core/utils/JwtTokenUtil.java @@ -0,0 +1,170 @@ +package com.itools.core.utils; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * JwtToken生成的工具类 + * JWT token的格式:header.payload.signature + * header的格式(算法、token的类型): + * {"alg": "HS512","typ": "JWT"} + * payload的格式(用户名、创建时间、生成时间): + * {"sub":"wang","created":1489079981393,"exp":1489684781} + * signature的生成算法: + * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) + * Created by macro on 2018/4/26. + */ +public class JwtTokenUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class); + private static final String CLAIM_KEY_USERNAME = "sub"; + private static final String CLAIM_KEY_CREATED = "created"; + @Value("${jwt.secret}") + private String secret; + @Value("${jwt.expiration}") + private Long expiration; + @Value("${jwt.tokenHead}") + private String tokenHead; + + /** + * 根据负责生成JWT的token + */ + private String generateToken(Map claims) { + return Jwts.builder() + .setClaims(claims) + .setExpiration(generateExpirationDate()) + .signWith(SignatureAlgorithm.HS512, secret) + .compact(); + } + + /** + * 从token中获取JWT中的负载 + */ + private Claims getClaimsFromToken(String token) { + Claims claims = null; + try { + claims = Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } catch (Exception e) { + LOGGER.info("JWT格式验证失败:{}", token); + } + return claims; + } + + /** + * 生成token的过期时间 + */ + private Date generateExpirationDate() { + return new Date(System.currentTimeMillis() + expiration * 1000); + } + + /** + * 从token中获取登录用户名 + */ + public String getUserNameFromToken(String token) { + String username; + try { + Claims claims = getClaimsFromToken(token); + username = claims.getSubject(); + } catch (Exception e) { + username = null; + } + return username; + } + + /** + * 验证token是否还有效 + * + * @param token 客户端传入的token + * @param userDetails 从数据库中查询出来的用户信息 + */ + public boolean validateToken(String token, UserDetails userDetails) { + String username = getUserNameFromToken(token); + return username.equals(userDetails.getUsername()) && !isTokenExpired(token); + } + + /** + * 判断token是否已经失效 + */ + private boolean isTokenExpired(String token) { + Date expiredDate = getExpiredDateFromToken(token); + return expiredDate.before(new Date()); + } + + /** + * 从token中获取过期时间 + */ + private Date getExpiredDateFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.getExpiration(); + } + + /** + * 根据用户信息生成token + */ + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); + claims.put(CLAIM_KEY_CREATED, new Date()); + return generateToken(claims); + } + + /** + * 当原来的token没过期时是可以刷新的 + * + * @param oldToken 带tokenHead的token + */ + public String refreshHeadToken(String oldToken) { + if(StrUtil.isEmpty(oldToken)){ + return null; + } + String token = oldToken.substring(tokenHead.length()); + if(StrUtil.isEmpty(token)){ + return null; + } + //token校验不通过 + Claims claims = getClaimsFromToken(token); + if(claims==null){ + return null; + } + //如果token已经过期,不支持刷新 + if(isTokenExpired(token)){ + return null; + } + //如果token在30分钟之内刚刷新过,返回原token + if(tokenRefreshJustBefore(token,30*60)){ + return token; + }else{ + claims.put(CLAIM_KEY_CREATED, new Date()); + return generateToken(claims); + } + } + + /** + * 判断token在指定时间内是否刚刚刷新过 + * @param token 原token + * @param time 指定时间(秒) + */ + private boolean tokenRefreshJustBefore(String token, int time) { + Claims claims = getClaimsFromToken(token); + Date created = claims.get(CLAIM_KEY_CREATED, Date.class); + Date refreshDate = new Date(); + //刷新时间在创建时间的指定时间内 + if(refreshDate.after(created)&&refreshDate.before(DateUtil.offsetSecond(created,time))){ + return true; + } + return false; + } +} diff --git a/itools-oauth2/itools-oauth2-server01/src/main/resources/application-dev.yml b/itools-oauth2/itools-oauth2-server01/src/main/resources/application-dev.yml new file mode 100644 index 0000000000000000000000000000000000000000..7f30aad4aeda5e3eba1d794211a6b036b1b2a73a --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/resources/application-dev.yml @@ -0,0 +1,22 @@ +#组件中的全局异常处理的code +system: + errorCode: Code001 +#分布式雪花算法策略 +sequence: + enable: true + type: snowflake + generate: simple + +# mybatis-plus相关配置 +mybatis-plus: + mapper-locations: classpath:/com.itools.mapper/**/*Mapper.xml + typeAliasesPackage: com.itools.core + global-config: + db-column-underline: true + db-config: + logic-delete-field: del_flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + configuration: + default-enum-type-handler: com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl diff --git a/itools-oauth2/itools-oauth2-server01/src/main/resources/application.yml b/itools-oauth2/itools-oauth2-server01/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..d3bd8f5b53cf87ccc5c11dc628d981a7cc913725 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/resources/application.yml @@ -0,0 +1,29 @@ +server: + port: 17001 +spring: + application: + name: itools-oms-auth-server + + redis: + host: 127.0.0.1 + database: 0 + + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + # url: jdbc:mysql://localhost:3306/oauth2-sso?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC + url: jdbc:mysql://localhost:3306/cloudauth?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC + username: root + password: root + hikari: + minimum-idle: 5 + idle-timeout: 600000 + maximum-pool-size: 10 + auto-commit: true + pool-name: MyHikariCP + max-lifetime: 1800000 + connection-timeout: 30000 + connection-test-query: SELECT 1 + main: + allow-bean-definition-overriding: true + diff --git a/itools-oauth2/itools-oauth2-server01/src/main/resources/bootstrap.yml b/itools-oauth2/itools-oauth2-server01/src/main/resources/bootstrap.yml new file mode 100644 index 0000000000000000000000000000000000000000..d0d10cd56caa07a8bf931d6b8c258b103df592ed --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/resources/bootstrap.yml @@ -0,0 +1,24 @@ +# Nacos Server 的地址 +spring: + profiles: + active: dev + cloud: + nacos: + config: + server-addr: 106.54.85.156:8848 + file-extension: yaml + prefix: itools-oms-server + remote-first: true + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev + discovery: + server-addr: 106.54.85.156:8848 + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev + +itools: + jwt: + keyPairName: jwt.jks + keyPairAlias: jwt + keyPairSecret: 123123 + keyPairStoreSecret: 123123 \ No newline at end of file diff --git a/itools-oauth2/itools-oauth2-server01/src/main/resources/jwt.jks b/itools-oauth2/itools-oauth2-server01/src/main/resources/jwt.jks new file mode 100644 index 0000000000000000000000000000000000000000..4880551ec3eeed062b0b6a50dfd76a24c944ceb2 Binary files /dev/null and b/itools-oauth2/itools-oauth2-server01/src/main/resources/jwt.jks differ diff --git a/itools-oauth2/itools-oauth2-server01/src/main/resources/logback-spring.xml b/itools-oauth2/itools-oauth2-server01/src/main/resources/logback-spring.xml new file mode 100644 index 0000000000000000000000000000000000000000..1b9ce8c07b537e53044edbe196e2bbd17ae6fb7a --- /dev/null +++ b/itools-oauth2/itools-oauth2-server01/src/main/resources/logback-spring.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/itools-oauth2/itools-oauth2-server02/pom.xml b/itools-oauth2/itools-oauth2-server02/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..78ba1ded43697ee1f6008a3ebc89214cff29a4b5 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/pom.xml @@ -0,0 +1,93 @@ + + + + itools-oauth2 + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-oauth2-server02 + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + + + com.baomidou + mybatis-plus-boot-starter + + + + com.baomidou + mybatis-plus-generator + + + + mysql + mysql-connector-java + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-oauth2 + + + org.springframework.cloud + spring-cloud-starter-security + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + + src/main/resources + true + + **/*.jks + + + + src/main/resources + false + + **/*.jks + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/Oauth2Server02Application.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/Oauth2Server02Application.java new file mode 100644 index 0000000000000000000000000000000000000000..9ca37c8874c37f850e9e82da1db8c2ad455003db --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/Oauth2Server02Application.java @@ -0,0 +1,23 @@ +package com.itools.core; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.Banner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@SpringBootApplication +@MapperScan("com.itools.core.mapper") +public class Oauth2Server02Application { + public static void main(String[] args) { + SpringApplication springApplication = new SpringApplication(Oauth2Server02Application.class); + springApplication.setBannerMode(Banner.Mode.OFF); + springApplication.run(args); + } + +} diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/config/AuthorizationServerConfig.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/config/AuthorizationServerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..42063c79d08013c42b7f7ad795dbb649d341508d --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/config/AuthorizationServerConfig.java @@ -0,0 +1,98 @@ +package com.itools.core.config; + +import com.itools.core.endpoint.CustomClientAuthenticationEntryPoint; +import com.itools.core.endpoint.CustomClientCredentialsTokenEndpointFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; + +import javax.sql.DataSource; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-21 15:23 + */ +@Configuration +@EnableAuthorizationServer +public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { + + @Qualifier("dataSource") + @Autowired + private DataSource dataSource; + @Override + public void configure(ClientDetailsServiceConfigurer clients) throws Exception { + // 第三方信息的存储 基于jdbc + clients.withClientDetails(clientDetailsService()); + + } + + @Autowired + private RedisConnectionFactory connectionFactory; + @Bean + public RedisTokenStore tokenStore() { + return new RedisTokenStore(connectionFactory); + } + @Autowired + private UserDetailsService userDetailsService; + + @Autowired + private AuthenticationManager authenticationManagerBean; + + + @Override + public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { + + //使用密码模式需要配置 + endpoints.authenticationManager(authenticationManagerBean) + .userDetailsService(userDetailsService) //刷新令牌授权包含对用户信息的检查 + .tokenStore(tokenStore()) //指定token存储策略是jwt + //refresh_token是否重复使用 + .reuseRefreshTokens(false) + .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE); + } + @Autowired + private CustomClientAuthenticationEntryPoint customAuthenticationEntryPoint; + /** + * 授权服务器安全配置 + * @param security + * @throws Exception + */ + @Override + public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { + CustomClientCredentialsTokenEndpointFilter endpointFilter = new CustomClientCredentialsTokenEndpointFilter(security); + endpointFilter.afterPropertiesSet(); + endpointFilter.setAuthenticationEntryPoint(customAuthenticationEntryPoint); + //第三方客户端校验token需要带入 clientId 和clientSecret来校验 + security.checkTokenAccess("isAuthenticated()") + .tokenKeyAccess("isAuthenticated()");//来获取我们的tokenKey需要带入clientId,clientSecret + + //允许表单认证 + security.allowFormAuthenticationForClients(); + security.addTokenEndpointAuthenticationFilter(endpointFilter); + } + + @Bean + public ClientDetailsService clientDetailsService(){ + return new JdbcClientDetailsService(dataSource); + } + +} + diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/config/IgnoreUrlsConfig.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/config/IgnoreUrlsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..86933df58a9c6ec9a519eea32a573bbe170ab681 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/config/IgnoreUrlsConfig.java @@ -0,0 +1,24 @@ +package com.itools.core.config; + +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; + +/** + * @project: itools-backend + * @description: 用于配置不需要保护的资源路径 + * @author: XUCHANG + * @create: 2021-06-22 15:41 + */ +//@Data +//@ConfigurationProperties(prefix = "secure.ignored") +//public class IgnoreUrlsConfig { +// +// private LinkedHashSet shouldSkipUrls = new LinkedHashSet<>(); +// +//} diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/config/ResourceServerConfig.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/config/ResourceServerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..508e32acddd5a449895c787cb1f7f11fe9227bb0 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/config/ResourceServerConfig.java @@ -0,0 +1,53 @@ +package com.itools.core.config; + +import com.itools.core.endpoint.AuthExceptionEntryPoint; +import com.itools.core.endpoint.CustomAccessDeniedHandler; +import com.itools.core.endpoint.CustomTokenExtractor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; + +import javax.servlet.http.HttpServletResponse; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-21 15:22 + */ +//@Order(value = 3) +@Configuration +@EnableResourceServer +public class ResourceServerConfig extends ResourceServerConfigurerAdapter { + + @Override + public void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + .exceptionHandling() + .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) + .and() + .authorizeRequests() + .antMatchers("/oauth/**").permitAll() + .anyRequest().authenticated() + .and() + .httpBasic(); + } + //服务端不需要设置token和权限的endpoint +// @Autowired +// private AuthExceptionEntryPoint authExceptionEntryPoint; +// @Autowired +// private CustomAccessDeniedHandler customAccessDeniedHandler; +// @Autowired +// private CustomTokenExtractor customTokenExtractor; +// @Override +// public void configure(ResourceServerSecurityConfigurer resources) { +// resources.tokenExtractor(customTokenExtractor); +// resources.authenticationEntryPoint(authExceptionEntryPoint) +// .accessDeniedHandler(customAccessDeniedHandler); +// } +} diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/config/WebSecurityConfig.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/config/WebSecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..4c3675119b687390fc9e47cb29d11bafcd5c44ec --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/config/WebSecurityConfig.java @@ -0,0 +1,73 @@ +package com.itools.core.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +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.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-21 15:22 + */ +@Configuration +@EnableWebSecurity(debug = true) +//@EnableGlobalMethodSecurity(prePostEnabled=true) +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private UserDetailsService userDetailsService; + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService); + //账号不存在或账号密码错误的异常类 + auth.authenticationProvider(authenticationProvider()); + } + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setHideUserNotFoundExceptions(false); + provider.setUserDetailsService(userDetailsService); + provider.setPasswordEncoder(passwordEncoder()); + return provider; + } + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = http + .authorizeRequests(); + //设置可放行的请求 +// for (String url : ignoreUrlsConfig().getUrls()) { +// registry.antMatchers(url).permitAll(); +// } + registry.antMatchers("/oauth/**").permitAll() + .anyRequest().authenticated() + .and() + .csrf().disable(); + } + + +} diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/controller/UserController.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..9e57a5b7b340ba9453b0e3073f6b1ee0cbd1a5d2 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/controller/UserController.java @@ -0,0 +1,15 @@ +package com.itools.core.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; + + +@RestController +public class UserController { + @GetMapping("/user") + public Principal user(Principal user){ + return user; + } +} diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/endpoint/AuthExceptionEntryPoint.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/endpoint/AuthExceptionEntryPoint.java new file mode 100644 index 0000000000000000000000000000000000000000..24056b2753a596440e14a0447e61969ad0acc759 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/endpoint/AuthExceptionEntryPoint.java @@ -0,0 +1,49 @@ +package com.itools.core.endpoint; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +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; +import java.util.HashMap; +import java.util.Map; + +/** + * @project: itools-backend + * @description: 无效 token 异常类重写 + * @author: XUCHANG + * @create: 2021-06-22 13:37 + */ +@Component +@Slf4j +public class AuthExceptionEntryPoint implements AuthenticationEntryPoint { + @Autowired + private ObjectMapper objectMapper; + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws ServletException { + Map map = new HashMap<>(); + map.put("code",401); + map.put("success",false); + map.put("data",null); + + response.setStatus(HttpStatus.OK.value()); + response.setHeader("Content-Type", "application/json;charset=UTF-8"); + try { + map.put("message","暂未登录或token已经过期"); + response.getWriter().write(objectMapper.writeValueAsString(map)); + } catch (IOException e) { + log.error("无效 token 异常类重写",e); + } + } +} diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/endpoint/CustomAccessDeniedHandler.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/endpoint/CustomAccessDeniedHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..4931836a19ff9cf2613d286cf7b12bb6f0eb9aba --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/endpoint/CustomAccessDeniedHandler.java @@ -0,0 +1,45 @@ +package com.itools.core.endpoint; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * @project: itools-backend + * @description: 权限不足异常类重写 + * @author: XUCHANG + * @create: 2021-06-22 13:45 + */ +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + @Autowired + private ObjectMapper objectMapper; + @Override + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) + throws IOException, ServletException { + Map map = new HashMap<>(); + map.put("code",401); + map.put("success",false); + map.put("data",null); + response.setStatus(HttpStatus.OK.value()); + response.setHeader("Content-Type", "application/json;charset=UTF-8"); + try { + map.put("message","用户权限不足"); + response.getWriter().write(objectMapper.writeValueAsString(map)); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/endpoint/CustomClientAuthenticationEntryPoint.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/endpoint/CustomClientAuthenticationEntryPoint.java new file mode 100644 index 0000000000000000000000000000000000000000..708a448cd641fb5cead3b3e48d1e81d1a1973ed0 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/endpoint/CustomClientAuthenticationEntryPoint.java @@ -0,0 +1,37 @@ +package com.itools.core.endpoint; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * @author xuchang + */ +@Component +public class CustomClientAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Autowired + private ObjectMapper objectMapper; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + response.setStatus(HttpStatus.OK.value()); + Map map = new HashMap(); + map.put("code", HttpStatus.UNAUTHORIZED.value()); + map.put("message", "client_id或client_secret错误"); + map.put("success", false); + map.put("data", null); + response.setHeader("Content-Type", "application/json;charset=utf-8"); + + response.getWriter().print(objectMapper.writeValueAsString(map)); + response.getWriter().flush(); + } +} \ No newline at end of file diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/endpoint/CustomClientCredentialsTokenEndpointFilter.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/endpoint/CustomClientCredentialsTokenEndpointFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..3aeb2d1db01fc081aaab61134aca2b9e231c1fc9 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/endpoint/CustomClientCredentialsTokenEndpointFilter.java @@ -0,0 +1,36 @@ +package com.itools.core.endpoint; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; +import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter; +import org.springframework.security.web.AuthenticationEntryPoint; + +/** + * @author xuchang + */ +public class CustomClientCredentialsTokenEndpointFilter extends ClientCredentialsTokenEndpointFilter { + private final AuthorizationServerSecurityConfigurer configurer; + private AuthenticationEntryPoint authenticationEntryPoint; + + public CustomClientCredentialsTokenEndpointFilter(AuthorizationServerSecurityConfigurer configurer) { + this.configurer = configurer; + } + + @Override + public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) { + this.authenticationEntryPoint = authenticationEntryPoint; + } + + @Override + protected AuthenticationManager getAuthenticationManager() { + return configurer.and().getSharedObject(AuthenticationManager.class); + } + + @Override + public void afterPropertiesSet() { + setAuthenticationFailureHandler((request, response, exception) -> authenticationEntryPoint.commence(request, response, exception)); + setAuthenticationSuccessHandler((request, response, authentication) -> { + // no-op - just allow filter chain to continue to token endpoint + }); + } +} \ No newline at end of file diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/endpoint/CustomTokenExtractor.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/endpoint/CustomTokenExtractor.java new file mode 100644 index 0000000000000000000000000000000000000000..f0209235e66b86d8389aa000ce599e54648a74e6 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/endpoint/CustomTokenExtractor.java @@ -0,0 +1,95 @@ +package com.itools.core.endpoint; + +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; +import org.springframework.security.oauth2.provider.authentication.TokenExtractor; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.stereotype.Component; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.util.Enumeration; +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-22 13:50 + */ +@Log4j2 +@Component +public class CustomTokenExtractor implements TokenExtractor { + + @Override + public Authentication extract(HttpServletRequest request) { + String tokenValue = this.extractToken(request); + if (StringUtils.isNotBlank(tokenValue)) { + return new PreAuthenticatedAuthenticationToken(tokenValue, ""); + } else { + //抛出异常 + log.info("token为空,请输入合法token"); + return null; + } + } + + public String extractToken(HttpServletRequest request) { + String token = this.extractHeaderToken(request); + if (token == null) { + log.info("Token not found in headers. Trying request parameters."); + token = request.getParameter("access_token"); + if (token == null) { + log.info("Token not found in request parameters. Trying request cookies."); + + token = extractCookieToken(request); + if (token == null) { + log.info("Token not found in cookies. Not an OAuth2 request."); + } else { + request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, "Bearer"); + } + } + } + return token; + } + + private String extractHeaderToken(HttpServletRequest request) { + Enumeration headers = request.getHeaders("Authorization"); + + String value; + do { + if (!headers.hasMoreElements()) { + return null; + } + + value = (String) headers.nextElement(); + } while (!value.toLowerCase().startsWith("Bearer".toLowerCase())); + + String authHeaderValue = value.substring("Bearer".length()).trim(); + request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, value.substring(0, "Bearer".length()).trim()); + int commaIndex = authHeaderValue.indexOf(44); + if (commaIndex > 0) { + authHeaderValue = authHeaderValue.substring(0, commaIndex); + } + if (authHeaderValue.equals("")) { + return null; + } + return authHeaderValue; + } + + private String extractCookieToken(HttpServletRequest request) { + + String cookieToken = null; + //根据请求数据,找到cookie数组 + Cookie[] cookies = request.getCookies(); + + if (null != cookies && cookies.length > 0) { + for (Cookie cookie : cookies) { + if (null != cookie.getName() && cookie.getName().trim().equalsIgnoreCase("access_token")) { + cookieToken = cookie.getValue().trim(); + break; + } + } + } + return cookieToken; + } +} diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/mapper/PermissionMapper.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/mapper/PermissionMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..47fbbb510f6e50953065e96399048139aa6d2982 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/mapper/PermissionMapper.java @@ -0,0 +1,33 @@ +package com.itools.core.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.pojo.SysPermission; +import org.apache.ibatis.annotations.Select; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-29 13:57 + */ +@Repository +public interface PermissionMapper extends BaseMapper { + + @Select("SELECT\n" + + " p.*\n" + + "FROM\n" + + " tb_user AS u\n" + + " LEFT JOIN tb_user_role AS ur\n" + + " ON u.id = ur.user_id\n" + + " LEFT JOIN tb_role AS r\n" + + " ON r.id = ur.role_id\n" + + " LEFT JOIN tb_role_permission AS rp\n" + + " ON r.id = rp.role_id\n" + + " LEFT JOIN tb_permission AS p\n" + + " ON p.id = rp.permission_id\n" + + "WHERE u.id = #{userId}") + List selectByUserId(Long userId); +} diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/mapper/UserInfoMapper.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/mapper/UserInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..06c77424e8b6c84deffde5405414328eb2072218 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/mapper/UserInfoMapper.java @@ -0,0 +1,19 @@ +package com.itools.core.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.pojo.SysUser; +import org.apache.ibatis.annotations.Select; +import org.springframework.stereotype.Repository; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-29 13:57 + */ +@Repository +public interface UserInfoMapper extends BaseMapper { + + @Select("select * from tb_user where username=#{username}") + SysUser getByUsername(String username); +} diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/pojo/SysPermission.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/pojo/SysPermission.java new file mode 100644 index 0000000000000000000000000000000000000000..778b8d1d8cd218c41a8042973bf98d6eb4faab31 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/pojo/SysPermission.java @@ -0,0 +1,33 @@ +package com.itools.core.pojo; + +import lombok.Data; + +import java.util.Date; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Data +public class SysPermission implements java.io.Serializable { + private static final long serialVersionUID = 1L; + + private Long id; + + private Long parentId; + + private String name; + + private String enname; + + private String url; + + private String description; + + private Date created; + + private Date updated; + +} diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/pojo/SysRole.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/pojo/SysRole.java new file mode 100644 index 0000000000000000000000000000000000000000..1090010627365a23d60d347dbdabca3ac5a5b556 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/pojo/SysRole.java @@ -0,0 +1,31 @@ +package com.itools.core.pojo; + +import lombok.Data; + +import java.util.Date; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Data +public class SysRole implements java.io.Serializable { + private static final long serialVersionUID = 1L; + + private Long id; + + private Long parentId; + + private String name; + + private String enname; + + private String description; + + private Date created; + + private Date updated; + +} diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/pojo/SysUser.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/pojo/SysUser.java new file mode 100644 index 0000000000000000000000000000000000000000..be1a2a4671c26ad834ce6b01dc1a9ed45299ea59 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/pojo/SysUser.java @@ -0,0 +1,33 @@ +package com.itools.core.pojo; + +import lombok.Data; + +import java.util.Date; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Data +public class SysUser implements java.io.Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + private String username; + + private String password; + + private String phone; + + private String email; + + private Date created; + + private Date updated; + + +} diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/service/UserService.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/service/UserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6738c02c2dc853389cf68f307709f9b824370fa6 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/service/UserService.java @@ -0,0 +1,9 @@ +package com.itools.core.service; + +import com.itools.core.pojo.SysUser; +import org.springframework.security.core.userdetails.UserDetailsService; + +public interface UserService extends UserDetailsService { + + SysUser getByUsername(String username); +} \ No newline at end of file diff --git a/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/service/impl/UserServiceImpl.java b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/service/impl/UserServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..54a4276ffea72943da423b8b96c022d2a66e67a7 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/java/com/itools/core/service/impl/UserServiceImpl.java @@ -0,0 +1,60 @@ +package com.itools.core.service.impl; + + +import com.itools.core.mapper.PermissionMapper; +import com.itools.core.mapper.UserInfoMapper; +import com.itools.core.pojo.*; +import com.itools.core.service.UserService; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-29 14:00 + */ +@Service("userDetailsService") +@Log4j2 +public class UserServiceImpl implements UserService { + + @Autowired + private UserInfoMapper userInfoMapper; + @Autowired + private PermissionMapper permissionMapper; + + @Override + public SysUser getByUsername(String username) { + return userInfoMapper.getByUsername(username); + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + SysUser user = getByUsername(username); + if (user==null){ + log.error("账号不存在或账号密码错误"); + throw new UsernameNotFoundException("账号不存在或账号密码错误"); + } + List authorities = new ArrayList<>(); + List permissions = permissionMapper.selectByUserId(user.getId()); + + permissions.forEach(permission -> { + if (permission!=null && !StringUtils.isEmpty(permission.getEnname())){ + GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getEnname()); + authorities.add(grantedAuthority); + } + }); + return new User(user.getUsername(),user.getPassword(),authorities); + } + +} diff --git a/itools-oauth2/itools-oauth2-server02/src/main/resources/application-dev.yml b/itools-oauth2/itools-oauth2-server02/src/main/resources/application-dev.yml new file mode 100644 index 0000000000000000000000000000000000000000..7f30aad4aeda5e3eba1d794211a6b036b1b2a73a --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/resources/application-dev.yml @@ -0,0 +1,22 @@ +#组件中的全局异常处理的code +system: + errorCode: Code001 +#分布式雪花算法策略 +sequence: + enable: true + type: snowflake + generate: simple + +# mybatis-plus相关配置 +mybatis-plus: + mapper-locations: classpath:/com.itools.mapper/**/*Mapper.xml + typeAliasesPackage: com.itools.core + global-config: + db-column-underline: true + db-config: + logic-delete-field: del_flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + configuration: + default-enum-type-handler: com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl diff --git a/itools-oauth2/itools-oauth2-server02/src/main/resources/application.yml b/itools-oauth2/itools-oauth2-server02/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..9fdc3cd5335f56d053ed6943a52bcac452bc306d --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/resources/application.yml @@ -0,0 +1,29 @@ +server: + port: 17003 +spring: + application: + name: itools-oms-auth-server + + redis: + host: 127.0.0.1 + database: 0 + + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver +# url: jdbc:mysql://localhost:3306/oauth2-sso?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC + url: jdbc:mysql://localhost:3306/cloudauth?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC + username: root + password: root + hikari: + minimum-idle: 5 + idle-timeout: 600000 + maximum-pool-size: 10 + auto-commit: true + pool-name: MyHikariCP + max-lifetime: 1800000 + connection-timeout: 30000 + connection-test-query: SELECT 1 + main: + allow-bean-definition-overriding: true + diff --git a/itools-oauth2/itools-oauth2-server02/src/main/resources/bootstrap.yml b/itools-oauth2/itools-oauth2-server02/src/main/resources/bootstrap.yml new file mode 100644 index 0000000000000000000000000000000000000000..56e3eb83288da40dfdacd24ca651a7e16ca7ebbd --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/resources/bootstrap.yml @@ -0,0 +1,18 @@ +# Nacos Server 的地址 +spring: + profiles: + active: dev + cloud: + nacos: + config: + server-addr: 106.54.85.156:8848 + file-extension: yaml + prefix: itools-oms-server + remote-first: true + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev + discovery: + server-addr: 106.54.85.156:8848 + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev + diff --git a/itools-oauth2/itools-oauth2-server02/src/main/resources/logback-spring.xml b/itools-oauth2/itools-oauth2-server02/src/main/resources/logback-spring.xml new file mode 100644 index 0000000000000000000000000000000000000000..84b713e214bf711e7ac0cbe16a32ad2b3b061569 --- /dev/null +++ b/itools-oauth2/itools-oauth2-server02/src/main/resources/logback-spring.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/itools-oauth2/pom.xml b/itools-oauth2/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d7860567ca754d23970116173500a07cc228cf6b --- /dev/null +++ b/itools-oauth2/pom.xml @@ -0,0 +1,148 @@ + + + + itools-backend + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-oauth2 + pom + + itools-oauth2-server01 + itools-oauth2-client01 + itools-oauth2-server02 + + + true + http://192.168.6.132:2375 + 1.1.0 + 1.8 + 1.2.10 + 5.1.8 + 1.1.10 + 4.5.7 + 2.7.0 + 1.3.7 + 3.4.6 + 8.0.15 + 2.1.5.RELEASE + 0.9.0 + 2.5.0 + 5.3 + 2.1.2 + 3.3.2 + Greenwich.SR3 + 2.1.2.RELEASE + 2.1.7.RELEASE + + + + + + org.springframework.boot + spring-boot-starter-parent + ${spring.boot.version} + pom + import + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${spring-cloud-alibaba.version} + pom + import + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + ${pagehelper-starter.version} + + + + com.github.pagehelper + pagehelper + ${pagehelper.version} + + + + cn.hutool + hutool-all + ${hutool.version} + + + + io.springfox + springfox-swagger2 + ${swagger2.version} + + + io.springfox + springfox-swagger-ui + ${swagger2.version} + + + + + mysql + mysql-connector-java + ${mysql-connector.version} + + + + org.springframework.data + spring-data-commons + ${spring-data-commons.version} + + + + io.jsonwebtoken + jjwt + ${jjwt.version} + + + + com.aliyun.oss + aliyun-sdk-oss + ${aliyun-oss.version} + + + + net.logstash.logback + logstash-logback-encoder + ${logstash-logback.version} + + + + de.codecentric + spring-boot-admin-starter-server + ${admin-starter-server.version} + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + com.baomidou + mybatis-plus-generator + ${mybatis-plus.version} + + + + + \ No newline at end of file diff --git a/itools-oms/itools-oms-auth-server/pom.xml b/itools-oms/itools-oms-auth-server/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..6e39336707214bf20ed83b7eb2f4e72a0e8101a8 --- /dev/null +++ b/itools-oms/itools-oms-auth-server/pom.xml @@ -0,0 +1,96 @@ + + + + itools-oms + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-oms-auth-server + + + + com.itools.core + itools-oms-common + 1.0-SNAPSHOT + + + com.itools.core + itools-oms-core + 1.0-SNAPSHOT + + + com.itools.core + itools-oms-model + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-web + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + org.projectlombok + lombok + + + com.baomidou + mybatis-plus-boot-starter + + + org.springframework.boot + spring-boot-starter-jdbc + + + com.baomidou + mybatis-plus-generator + + + + mysql + mysql-connector-java + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.apache.commons + commons-pool2 + + + + org.springframework.cloud + spring-cloud-starter-oauth2 + + + org.springframework.security + spring-security-jwt + 1.0.9.RELEASE + + + + io.jsonwebtoken + jjwt + 0.9.1 + + + com.itools.core + itools-common + 1.0-SNAPSHOT + + + + \ No newline at end of file diff --git a/itools-oms/itools-oms-auth-server/src/main/java/com/itools/core/Oauth2AuthApplication.java b/itools-oms/itools-oms-auth-server/src/main/java/com/itools/core/Oauth2AuthApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..faa0250524a272fc6aa86cbe8e1be35b27eec18b --- /dev/null +++ b/itools-oms/itools-oms-auth-server/src/main/java/com/itools/core/Oauth2AuthApplication.java @@ -0,0 +1,56 @@ +package com.itools.core; + +import com.itools.core.snowflake.config.EnableSequenceService; +import com.itools.core.validate.EnableValidator; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.Banner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@SpringBootApplication +@EnableSequenceService +@EnableValidator +@EnableAuthorizationServer +@MapperScan("com.itools.core.com.itools.mapper") +public class Oauth2AuthApplication { + public static void main(String[] args) { + SpringApplication springApplication = new SpringApplication(Oauth2AuthApplication.class); + springApplication.setBannerMode(Banner.Mode.OFF); + springApplication.run(args); + } + @Bean + public CorsFilter corsFilter() { + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + final CorsConfiguration config = new CorsConfiguration(); + // 允许cookies跨域 + config.setAllowCredentials(true); + // 允许向该服务器提交请求的URI,*表示全部允许。。这里尽量限制来源域,比如http://xxxx:8080 ,以降低安全风险。。 + config.addAllowedOrigin("*"); + // 允许访问的头信息,*表示全部 + config.addAllowedHeader("*"); + // 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了 + config.setMaxAge(18000L); + // 允许提交请求的方法,*表示全部允许,也可以单独设置GET、PUT等 + config.addAllowedMethod("*"); + config.addAllowedMethod("HEAD"); + // 允许Get的请求方法 + config.addAllowedMethod("GET"); + config.addAllowedMethod("PUT"); + config.addAllowedMethod("POST"); + config.addAllowedMethod("DELETE"); + config.addAllowedMethod("PATCH"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} diff --git a/itools-oms/itools-oms-auth-server/src/main/java/com/itools/core/config/AuthorizationServerConfiguration.java b/itools-oms/itools-oms-auth-server/src/main/java/com/itools/core/config/AuthorizationServerConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..5b26f2c6a803378e903ad06e9453ab5746a4c6b4 --- /dev/null +++ b/itools-oms/itools-oms-auth-server/src/main/java/com/itools/core/config/AuthorizationServerConfiguration.java @@ -0,0 +1,84 @@ +package com.itools.core.config; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.config.annotation.builders.InMemoryClientDetailsServiceBuilder; +import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.client.InMemoryClientDetailsService; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; +import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-12 14:37 + */ +@EnableAuthorizationServer +@Configuration +public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter{ + @Autowired + private PasswordEncoder passwordEncoder; + /** + * 重写PasswordEncoder 接口中的方法,实例化加密策略 + * 这里采用bcrypt的加密策略,采用这种策略的好处是Bcrypt是单向Hash加密算法,类似Pbkdf2算法 不可反向破解生成明文,而且还兼容之前采用别的加密算法 + */ + @Bean + public PasswordEncoder passwordEncoder(){ + return new BCryptPasswordEncoder(); + } + @Override + public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { + security.allowFormAuthenticationForClients() + .tokenKeyAccess("isAuthenticated()"); + } + + @Override + public void configure(ClientDetailsServiceConfigurer clients) throws Exception { + clients.withClientDetails(clientService()); + } + + public ClientDetailsService clientService() throws Exception { + return new InMemoryClientDetailsServiceBuilder() + .withClient("client01") + .secret(passwordEncoder.encode("client01")) + .scopes("all") + .authorizedGrantTypes("password", "refresh_token") + .redirectUris("http://www.baidu.com") + .accessTokenValiditySeconds(7200) + .autoApprove(true) + .and() + .withClient("client02") + .secret(passwordEncoder.encode("client02")) + .scopes("all") + .authorizedGrantTypes("authorization_code", "refresh_token") + .redirectUris("http://www.baidu.com") + .accessTokenValiditySeconds(7200) + .autoApprove(true) + .and().build(); + } + + @Override + public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { + endpoints + .accessTokenConverter(jwtAccessTokenConverter()) + .tokenStore(jwtTokenStore()); + } + @Bean + public JwtTokenStore jwtTokenStore() { + return new JwtTokenStore(jwtAccessTokenConverter()); + } + @Bean + public JwtAccessTokenConverter jwtAccessTokenConverter() { + JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); + jwtAccessTokenConverter.setSigningKey("123456"); + return jwtAccessTokenConverter; + } +} diff --git a/itools-oms/itools-oms-auth-server/src/main/java/com/itools/core/config/WebSecurityConfiguration.java b/itools-oms/itools-oms-auth-server/src/main/java/com/itools/core/config/WebSecurityConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..7a4512ecd3b4ee61138cb96a783ada2610faa357 --- /dev/null +++ b/itools-oms/itools-oms-auth-server/src/main/java/com/itools/core/config/WebSecurityConfiguration.java @@ -0,0 +1,57 @@ +package com.itools.core.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-04-12 14:45 + */ +@Configuration +public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { + @Autowired + private PasswordEncoder passwordEncoder; + @Override + @Bean + public AuthenticationManager authenticationManager() throws Exception { + return super.authenticationManager(); + } + + @Autowired + private UserDetailsService userDetailsService; + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); + authenticationProvider.setUserDetailsService(userDetailsService); + authenticationProvider.setPasswordEncoder(passwordEncoder); + authenticationProvider.setHideUserNotFoundExceptions(false); + return authenticationProvider; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + + http + .requestMatchers().antMatchers("/oauth/authorize","/oauth/**","/login/**","/logout/**") + .and() + .authorizeRequests() + .antMatchers("/oauth/**").authenticated() + .and() + .formLogin().permitAll(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.authenticationProvider(authenticationProvider()); + } +} diff --git a/itools-oms/itools-oms-auth-server/src/main/java/com/itools/core/service/UserDetailsService.java b/itools-oms/itools-oms-auth-server/src/main/java/com/itools/core/service/UserDetailsService.java new file mode 100644 index 0000000000000000000000000000000000000000..e5d254cf6177405423eed92f000f691480269a57 --- /dev/null +++ b/itools-oms/itools-oms-auth-server/src/main/java/com/itools/core/service/UserDetailsService.java @@ -0,0 +1,27 @@ +package com.itools.core.service; + +import com.itools.core.utils.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +@Component +public class UserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService { + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { + + if(!StringUtils.equals("user1",s)) { + throw new UsernameNotFoundException("用户" + s + "不存在" ); + } + + return new User( s, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_NORMAL,ROLE_MEDIUM")); + } +} diff --git a/itools-oms/itools-oms-auth-server/src/main/resources/application-dev.yml b/itools-oms/itools-oms-auth-server/src/main/resources/application-dev.yml new file mode 100644 index 0000000000000000000000000000000000000000..363a7cc4a65273506e640c85e8d062390733a74f --- /dev/null +++ b/itools-oms/itools-oms-auth-server/src/main/resources/application-dev.yml @@ -0,0 +1,48 @@ +#组件中的全局异常处理的code +system: + errorCode: Code001 +#分布式雪花算法策略 +sequence: + enable: true + type: snowflake + generate: simple +#脚本自动初始化执行 +initDB: + #启用开关 + is-use: false + # ddl脚本,可以支持多个,用,分割,文件建议最多5个 + ddl: init/ddl.sql + # dml脚本,可以支持多个,用,分割,文件建议最多5个 + dml: init/dml.sql + # function脚本,可以支持多个,用,分割,文件建议最多5个 + function: init/function.sql + # 分支环境 + env: dev + # 版本 + version: 1.0 + # 脚本设置的分隔符 + delimiter: ; + +# mybatis-plus相关配置 +mybatis-plus: + mapper-locations: classpath:/com.itools.mapper/**/*Mapper.xml + typeAliasesPackage: com.itools.core + global-config: + db-column-underline: true + db-config: + logic-delete-field: del_flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + configuration: + default-enum-type-handler: com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl +#security: +# oauth2: +# client: +# client-id: product-server +# client-secret: 123456 +# user-authorization-uri: http://127.0.0.1:8888/oauth/authorize +# access-token-uri: http://127.0.0.1:8888/oauth/token +# resource: +# jwt: +# key-uri: http://127.0.0.1:8888/oauth/token_key \ No newline at end of file diff --git a/itools-oms/itools-oms-auth-server/src/main/resources/application.yml b/itools-oms/itools-oms-auth-server/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..e0cb9e84e4b9bc4b60de25292fc0783261f90e02 --- /dev/null +++ b/itools-oms/itools-oms-auth-server/src/main/resources/application.yml @@ -0,0 +1,28 @@ +server: + port: 8810 +spring: + application: + name: itools-oms-auth-server +# profiles: +# active: dev + + redis: + host: 127.0.0.1 + database: 0 + + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/oauth2-sso?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC + username: root + password: root + hikari: + minimum-idle: 5 + idle-timeout: 600000 + maximum-pool-size: 10 + auto-commit: true + pool-name: MyHikariCP + max-lifetime: 1800000 + connection-timeout: 30000 + connection-test-query: SELECT 1 + diff --git a/itools-oms/itools-oms-auth-server/src/main/resources/bootstrap.yml b/itools-oms/itools-oms-auth-server/src/main/resources/bootstrap.yml new file mode 100644 index 0000000000000000000000000000000000000000..cc10673b57d21e4c29c5bea9cbc45b323f6375ce --- /dev/null +++ b/itools-oms/itools-oms-auth-server/src/main/resources/bootstrap.yml @@ -0,0 +1,17 @@ +# Nacos Server 的地址 +spring: + profiles: + active: dev + cloud: + nacos: + config: + server-addr: 106.54.85.156:8848 + file-extension: yaml + prefix: itools-oms-server + remote-first: true + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev + discovery: + server-addr: 106.54.85.156:8848 + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev diff --git a/itools-oms/itools-oms-jwt-sample-example/pom.xml b/itools-oms/itools-oms-jwt-sample-example/pom.xml index 548c710bd7fedd3bc74134c427ccc587eb874f72..d04f874200514601c4a0d7af116e76f83e2a7188 100644 --- a/itools-oms/itools-oms-jwt-sample-example/pom.xml +++ b/itools-oms/itools-oms-jwt-sample-example/pom.xml @@ -24,12 +24,16 @@ org.springframework.cloud spring-cloud-starter-oauth2 - - io.jsonwebtoken - jjwt - 0.9.1 + org.springframework.cloud + spring-cloud-starter-security + + + + + + diff --git a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/ClientWebsecurityConfigurer.java b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/ClientWebsecurityConfigurer.java index 8d12cec6c3af651d5b10c6df2a70609b6262b6cc..b0f25295a6bef83adf53a7e5ab0e1a84f5e9f5d6 100644 --- a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/ClientWebsecurityConfigurer.java +++ b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/ClientWebsecurityConfigurer.java @@ -1,31 +1,31 @@ -package com.itools.core.config; - -import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -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.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -@Configuration -@EnableWebSecurity(debug = true) -@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true,jsr250Enabled = true) -@EnableOAuth2Sso -public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter { - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Override - public void configure(HttpSecurity http) throws Exception { - http - .antMatcher("/**") - .authorizeRequests() - .antMatchers("/oauth/**","/login/**","/logout/**").permitAll() - .anyRequest().authenticated(); - } -} +//package com.itools.core.com.itools.config; +// +//import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.security.com.itools.config.annotation.method.configuration.EnableGlobalMethodSecurity; +//import org.springframework.security.com.itools.config.annotation.web.builders.HttpSecurity; +//import org.springframework.security.com.itools.config.annotation.web.configuration.EnableWebSecurity; +//import org.springframework.security.com.itools.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +//import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +//import org.springframework.security.crypto.password.PasswordEncoder; +// +//@Configuration +//@EnableWebSecurity(debug = true) +//@EnableGlobalMethodSecurity(prePostEnabled = true) +//@EnableOAuth2Sso +//public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter { +// @Bean +// public PasswordEncoder passwordEncoder() { +// return new BCryptPasswordEncoder(); +// } +// +// @Override +// public void configure(HttpSecurity http) throws Exception { +// http +// .antMatcher("/**") +// .authorizeRequests() +// .antMatchers("/oauth/**","/login/**","/logout/**").permitAll() +// .anyRequest().authenticated(); +// } +//} diff --git a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/JwtTokenEnhancer.java b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/JwtTokenEnhancer.java index 8472d8a308e171dbf3a05aecc93c39486e972d7f..951536644d41ac450be3ecb2fd2063aace588de7 100644 --- a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/JwtTokenEnhancer.java +++ b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/JwtTokenEnhancer.java @@ -1,23 +1,23 @@ -package com.itools.core.config; - -import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; -import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.security.oauth2.provider.OAuth2Authentication; -import org.springframework.security.oauth2.provider.token.TokenEnhancer; - -import java.util.HashMap; -import java.util.Map; - -public class JwtTokenEnhancer implements TokenEnhancer { - - @Override - public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, - OAuth2Authentication authentication) { - Map info = new HashMap<>(); - info.put("enhance", "enhance info"); - info.put("appId", "appId"); - ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); - return accessToken; - } - -} \ No newline at end of file +//package com.itools.core.com.itools.config; +// +//import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; +//import org.springframework.security.oauth2.common.OAuth2AccessToken; +//import org.springframework.security.oauth2.provider.OAuth2Authentication; +//import org.springframework.security.oauth2.provider.token.TokenEnhancer; +// +//import java.util.HashMap; +//import java.util.Map; +// +//public class JwtTokenEnhancer implements TokenEnhancer { +// +// @Override +// public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, +// OAuth2Authentication authentication) { +// Map info = new HashMap<>(); +// info.put("enhance", "enhance info"); +// info.put("appId", "appId"); +// ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); +// return accessToken; +// } +// +//} \ No newline at end of file diff --git a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/JwtTokenStoreConfig.java b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/JwtTokenStoreConfig.java index 90075763f41bc7423c3a90ba6a87f8c02a576541..6874f06257b245e158e6456bf52bf24ba7ce5f46 100644 --- a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/JwtTokenStoreConfig.java +++ b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/JwtTokenStoreConfig.java @@ -1,29 +1,29 @@ -package com.itools.core.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; -import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; - -@Configuration -public class JwtTokenStoreConfig { - - @Bean - public TokenStore jwtTokenStore(){ - return new JwtTokenStore(jwtAccessTokenConverter()); - } - - @Bean - public JwtAccessTokenConverter jwtAccessTokenConverter(){ - JwtAccessTokenConverter accessTokenConverter = new - JwtAccessTokenConverter(); - //配置JWT使用的秘钥 - accessTokenConverter.setSigningKey("123123"); - return accessTokenConverter; - } - @Bean - public JwtTokenEnhancer jwtTokenEnhancer() { - return new JwtTokenEnhancer(); - } -} \ No newline at end of file +//package com.itools.core.com.itools.config; +// +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.security.oauth2.provider.token.TokenStore; +//import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; +//import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; +// +//@Configuration +//public class JwtTokenStoreConfig { +// +// @Bean +// public TokenStore jwtTokenStore(){ +// return new JwtTokenStore(jwtAccessTokenConverter()); +// } +// +// @Bean +// public JwtAccessTokenConverter jwtAccessTokenConverter(){ +// JwtAccessTokenConverter accessTokenConverter = new +// JwtAccessTokenConverter(); +// //配置JWT使用的秘钥 +// accessTokenConverter.setSigningKey("123123"); +// return accessTokenConverter; +// } +// @Bean +// public JwtTokenEnhancer jwtTokenEnhancer() { +// return new JwtTokenEnhancer(); +// } +//} \ No newline at end of file diff --git a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/ResourceServerConfig.java b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/ResourceServerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0f0cdcf17e1e4a15002049f9ae54faf851e5100c --- /dev/null +++ b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/ResourceServerConfig.java @@ -0,0 +1,28 @@ +package com.itools.core.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; + +import javax.servlet.http.HttpServletResponse; + +@Order(value = 3) +@Configuration +@EnableResourceServer +public class ResourceServerConfig extends ResourceServerConfigurerAdapter { + + @Override + public void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + .exceptionHandling() + .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) + .and() + .authorizeRequests() + .anyRequest().authenticated() + .and() + .httpBasic(); + } +} diff --git a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/SecurityConfig.java b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/SecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..e47cced5909e976e5d6c733b43e6e1b683419954 --- /dev/null +++ b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/SecurityConfig.java @@ -0,0 +1,23 @@ +package com.itools.core.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + + +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable(); + } +} diff --git a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/Solution.java b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/Solution.java deleted file mode 100644 index d29fdbed5d2a564d8be57d2bd34567278c741c27..0000000000000000000000000000000000000000 --- a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/config/Solution.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.itools.core.config; - -import java.util.ArrayList; -import java.util.List; - -class Solution { - public static int clumsy(int N) { - List sit = new ArrayList<>(); - sit.add("*"); - sit.add("/"); - sit.add("+"); - sit.add("-"); - int sum = N; - if(N<=0){ - return sum; - } - int index = 0; - StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append(N); - while((N-1) >0 ){ - if(index >= sit.size()){ - index = 0; - } - - stringBuilder.append(sit.get(index)); - stringBuilder.append(N-1); - sum = run(sit.get(index),sum,N-1); - index++; - N--; - } - System.out.println(stringBuilder.toString()); - return sum; - - } - private static int run(String sit,int left,int right){ - int res = 0; - System.out.println(left+sit+right); - if(sit.equals("*")){ - res = left * right; - }else if(sit.equals("/")){ - res = left / right; - }else if(sit.equals("+")){ - res = left + right; - }else{ - res = left - right; - } - - return res; - } - - public static void main(String[] args) { - System.out.println(clumsy(10)); - } -} \ No newline at end of file diff --git a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/controller/ProductController.java b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/controller/ProductController.java index 6884fb0dbb3d3c0e393c229be68fbd1e3876c745..863a760f45ea7e485f6d757b92c9def23455bc34 100644 --- a/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/controller/ProductController.java +++ b/itools-oms/itools-oms-jwt-sample-example/src/main/java/com/itools/core/controller/ProductController.java @@ -1,5 +1,6 @@ package com.itools.core.controller; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -7,6 +8,7 @@ import org.springframework.web.bind.annotation.RestController; @RequestMapping("/product") public class ProductController { + @PreAuthorize("hasAnyAuthority('selectProductInfoById')") @RequestMapping("/selectProductInfoById") public String selectProductInfoById(long id) { diff --git a/itools-oms/itools-oms-jwt-sample-example/src/main/resources/application.yml b/itools-oms/itools-oms-jwt-sample-example/src/main/resources/application.yml index 7bad5bedb4de920b6e98a850d4487fde72e784ea..350b26af8ceb200ddf55f5c28261e7490a5dd58b 100644 --- a/itools-oms/itools-oms-jwt-sample-example/src/main/resources/application.yml +++ b/itools-oms/itools-oms-jwt-sample-example/src/main/resources/application.yml @@ -15,3 +15,4 @@ security: resource: jwt: key-uri: http://127.0.0.1:8888/oauth/token_key + diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/Oauth2Application.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/Oauth2Application.java index 44f9dc002601253b029f4d30ac3bba2790370d40..7da57a652d4b3d6dd7774c822d0f16ba159dc20d 100644 --- a/itools-oms/itools-oms-server/src/main/java/com/itools/core/Oauth2Application.java +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/Oauth2Application.java @@ -21,8 +21,7 @@ import org.springframework.web.filter.CorsFilter; @SpringBootApplication @EnableSequenceService @EnableValidator -@EnableAuthorizationServer -@MapperScan("com.itools.core.mapper") +@MapperScan("com.itools.core.com.itools.mapper") public class Oauth2Application { public static void main(String[] args) { SpringApplication springApplication = new SpringApplication(Oauth2Application.class); diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/AuthorizationServerConfig.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/AuthorizationServerConfig.java index 5004e9b94225a02665c86f2dfb7e18821238feb7..cbf64875ec265863a164070fe1452725f1368eca 100644 --- a/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/AuthorizationServerConfig.java +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/AuthorizationServerConfig.java @@ -17,6 +17,7 @@ import org.springframework.security.oauth2.config.annotation.web.configurers.Aut import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; +import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import org.springframework.security.oauth2.provider.token.TokenEnhancerChain; import org.springframework.security.oauth2.provider.token.TokenStore; @@ -50,9 +51,7 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap @Autowired private RedisConnectionFactory redisConnectionFactory; -// @Autowired -// @Qualifier("jwtTokenStore") -// private TokenStore tokenStore; + @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; @Autowired @@ -86,21 +85,11 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap return new JdbcClientDetailsService(dataSource); } -// @Override -// public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { -// //使用密码模式需要配置 -// endpoints.authenticationManager(authenticationManager) -// //指定token存储到redis -//// .tokenStore(tokenStore()) -// .tokenStore(tokenStore) -// .accessTokenConverter(jwtAccessTokenConverter) -// //refresh_token是否重复使用 -// .reuseRefreshTokens(false) -// //刷新令牌授权包含对用户信息的检查 -// .userDetailsService(userDetailsService) -// //支持GET,POST请求 -// .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); -// } + /** + * 令牌访问端点 + * @param endpoints + * @throws Exception + */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //配置JWT的内容增强器 @@ -109,10 +98,16 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap delegates.add(jwtTokenEnhancer); delegates.add(jwtAccessTokenConverter); enhancerChain.setTokenEnhancers(delegates); - //使用密码模式需要配置 - endpoints.authenticationManager(authenticationManager) + + endpoints + //使用密码模式需要配置 + .authenticationManager(authenticationManager) //配置存储令牌策略 // .tokenStore(tokenStore) + //授权码模式需要 +// .authorizationCodeServices(authorizationCodeServices) + //令牌服务管理 +// .tokenServices() .tokenStore(tokenStore()) .accessTokenConverter(jwtAccessTokenConverter) //配置tokenEnhancer @@ -124,25 +119,23 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap //支持GET,POST请求 PUT DELETE endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST,HttpMethod.PUT,HttpMethod.DELETE); } -// @Override -// public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { -// //允许表单认证 -// security.allowFormAuthenticationForClients() -// // 配置校验token需要带入clientId 和clientSeret配置 -// .checkTokenAccess("isAuthenticated()"); -// } + + /** + * 令牌访问端点的安全策略 + * @param oauthServer + * @throws Exception + */ @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { +// oauthServer +// .tokenKeyAccess("permitAll()") +// //oauth/check_token放开权限 +// .checkTokenAccess("permitAll()") +// //允许表单验证 +// .allowFormAuthenticationForClients(); oauthServer + .allowFormAuthenticationForClients() .tokenKeyAccess("permitAll()") - .checkTokenAccess("permitAll()") - .allowFormAuthenticationForClients(); + .checkTokenAccess("isAuthenticated()"); } -// @Bean -// public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter oAuth2ClientContextFilter) { -// FilterRegistrationBean registration = new FilterRegistrationBean(); -// registration.setFilter(oAuth2ClientContextFilter); -// registration.setOrder(-100); -// return registration; -// } } diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/JwtTokenEnhancer.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/JwtTokenEnhancer.java index fd85b844914a3c86e648fee2f180bf72057dc762..5db2dedc6b344330b474e2e4ab9fb3104dae100c 100644 --- a/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/JwtTokenEnhancer.java +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/JwtTokenEnhancer.java @@ -9,7 +9,7 @@ import java.util.HashMap; import java.util.Map; /** * @project: itools-backend - * @description: + * @description: token的信息加密的增强 * @author: XUCHANG * @create: 2021-03-28 15:51 */ diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/ResourceServerConfig.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/ResourceServerConfig.java index cdb8fb0a00c9f5287c158c565daf4d50a37f534c..58589acf2cd09b6266a07ea6f0036ed8cfa6bbcd 100644 --- a/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/ResourceServerConfig.java +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/ResourceServerConfig.java @@ -1,26 +1,37 @@ -//package com.itools.core.config; -// -//import org.springframework.context.annotation.Configuration; -//import org.springframework.core.annotation.Order; -//import org.springframework.security.config.annotation.web.builders.HttpSecurity; -//import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; -//import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; -// -///** -// * @project: itools-backend -// * @description: 这个类表明了此应用是OAuth2 的资源服务器,此处主要指定了受资源服务器保护的资源链接 -// * @author: XUCHANG -// * @create: 2021-03-28 15:51 -// */ -//@Order(6) -//@Configuration -//@EnableResourceServer -//public class ResourceServerConfig extends ResourceServerConfigurerAdapter { -// -// @Override -// public void configure(HttpSecurity http) throws Exception { -// -// +package com.itools.core.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; + +import javax.servlet.http.HttpServletResponse; + +/** + * @project: itools-backend + * @description: 这个类表明了此应用是OAuth2 的资源服务器,此处主要指定了受资源服务器保护的资源链接 + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Order(3) +@Configuration +@EnableResourceServer +public class ResourceServerConfig extends ResourceServerConfigurerAdapter { + + @Override + public void configure(HttpSecurity http) throws Exception { + + http + .csrf().disable() + .exceptionHandling() + .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) + .and() + .authorizeRequests() + .antMatchers("/test","/oauth/token","/user/getCurrentUser").permitAll() + .anyRequest().authenticated() + .and() + .httpBasic(); // http.csrf().disable()//禁用了 csrf 功能 // .authorizeRequests()//限定签名成功的请求 //// .antMatchers("/decision/**","/govern/**").hasAnyRole("USER","ADMIN") @@ -31,6 +42,6 @@ // .and().anonymous()//对于没有配置权限的其他请求允许匿名访问 // .and().formLogin()//使用 spring security 默认登录页面 // .and().httpBasic();//启用http 基础验证 -// } -// -//} \ No newline at end of file + } + +} \ No newline at end of file diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/WebSecurityConfig.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/WebSecurityConfig.java index 99d049083688212e0fd5dca30407359e813276b5..841259d42973766fd44c26f5f83ee842fc1e286e 100644 --- a/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/WebSecurityConfig.java +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/config/WebSecurityConfig.java @@ -14,6 +14,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; /** * @project: itools-backend @@ -21,10 +22,10 @@ import org.springframework.security.crypto.password.PasswordEncoder; * @author: XUCHANG * @create: 2021-03-28 15:51 */ -@Order(2) @Configuration @EnableWebSecurity(debug = true) -@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true,jsr250Enabled = true) +@EnableGlobalMethodSecurity(prePostEnabled = true) +@EnableAuthorizationServer public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired @@ -54,35 +55,11 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { http.formLogin().permitAll() .and().authorizeRequests() - .antMatchers("/user/getCurrentUser","/login","/oauth/**","/oauth/authorize","/login/**","/logout/**","/error").permitAll() +// .antMatchers("/user/getCurrentUser","/login","/oauth/**","/oauth/authorize","/login/**","/logout/**","/error").permitAll() + .antMatchers("/oauth/authorize","/oauth/**","/login/**","/logout/**","/error","/user/getCurrentUser").permitAll() .anyRequest().authenticated() .and().httpBasic() .and().csrf().disable(); -// http -// .antMatcher("/**") -// .authorizeRequests() -// .antMatchers( -// HttpMethod.GET, -// "/*.html", -// "/**/*.html", -// "/**/*.css", -// "/**/*.js", -// "/**/*.jpg", -// "/**/*.png", -// "/error", -// "/favicon.ico" -// ).permitAll() -// .antMatchers("/swagger-ui.html").anonymous() -// .antMatchers("/swagger-resources/**").anonymous() -// .antMatchers("/user/getCurrentUser","/oauth/**","/oauth2/**","/login/**","/logout/**").permitAll() -// .anyRequest().authenticated(); -// http -// .antMatcher("/**") -// .authorizeRequests() -// .antMatchers("/oauth/**","/login/**","/logout/**").permitAll() -// .anyRequest().authenticated() -// .and() -// .formLogin().permitAll(); } } diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/controller/UserController.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/controller/UserController.java index 7ab985c3f2cc03fc46b73361c760642a960b07c4..db0ca0ce8273938208a12db4196dc0a351b946c1 100644 --- a/itools-oms/itools-oms-server/src/main/java/com/itools/core/controller/UserController.java +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/controller/UserController.java @@ -3,6 +3,7 @@ package com.itools.core.controller; import io.jsonwebtoken.Jwts; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.TokenStore; @@ -10,6 +11,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import javax.annotation.security.RolesAllowed; import javax.servlet.http.HttpServletRequest; import java.nio.charset.StandardCharsets; @@ -33,6 +35,8 @@ public class UserController { // // return oAuth2Authentication.getUserAuthentication()/*.getPrincipal()*/; // } + @PreAuthorize("hasAnyAuthority('getCurrentUser')") +// @RolesAllowed("hasAnyRole('getCurrentUser')") @RequestMapping("/getCurrentUser") public Object getCurrentUser(Authentication authentication, HttpServletRequest request) { diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/endpoint/RevokeTokenEndpoint.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/endpoint/RevokeTokenEndpoint.java new file mode 100644 index 0000000000000000000000000000000000000000..331d91ae40ffaaea4b3f85947f1b271cd0e0f225 --- /dev/null +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/endpoint/RevokeTokenEndpoint.java @@ -0,0 +1,26 @@ +package com.itools.core.endpoint; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.oauth2.provider.endpoint.FrameworkEndpoint; +import org.springframework.security.oauth2.provider.token.ConsumerTokenServices; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + + +@FrameworkEndpoint +public class RevokeTokenEndpoint { + @Autowired + @Qualifier("consumerTokenServices") + ConsumerTokenServices consumerTokenServices; + @RequestMapping(method = RequestMethod.DELETE, value = "/oauth/token") + @ResponseBody + public String revokeToken(String access_token) { + if (consumerTokenServices.revokeToken(access_token)) { + return "注销成功"; + } else { + return "注销失败"; + } + } +} diff --git a/itools-oms/itools-oms-server/src/main/java/com/itools/core/service/impl/UserServiceImpl.java b/itools-oms/itools-oms-server/src/main/java/com/itools/core/service/impl/UserServiceImpl.java index 8943eb2557e4202f1e76eadcc7bb2939e3deae93..2bce007932708d438153aec570b2b0f794f27198 100644 --- a/itools-oms/itools-oms-server/src/main/java/com/itools/core/service/impl/UserServiceImpl.java +++ b/itools-oms/itools-oms-server/src/main/java/com/itools/core/service/impl/UserServiceImpl.java @@ -47,7 +47,7 @@ public class UserServiceImpl implements UserService { permissions.forEach(permission -> { if (permission!=null && !StringUtils.isEmpty(permission.getEnname())){ - GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getUrl()); + GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getEnname()); authorities.add(grantedAuthority); } }); diff --git a/itools-oms/itools-oms-server/src/main/resources/application-dev.yml b/itools-oms/itools-oms-server/src/main/resources/application-dev.yml index f7ecb63eff2fbe49a81923d1b7adfe52c0f4a395..2b9c6a1be8747826206541b091b6a1ac8d2dc2f8 100644 --- a/itools-oms/itools-oms-server/src/main/resources/application-dev.yml +++ b/itools-oms/itools-oms-server/src/main/resources/application-dev.yml @@ -1,3 +1,6 @@ +#配置端口 +server: + port: 8888 #组件中的全局异常处理的code system: errorCode: Code001 @@ -25,7 +28,7 @@ initDB: # mybatis-plus相关配置 mybatis-plus: - mapper-locations: classpath:/mapper/**/*Mapper.xml + mapper-locations: classpath:/com.itools.mapper/**/*Mapper.xml typeAliasesPackage: com.itools.core global-config: db-column-underline: true diff --git a/itools-oms/itools-oms-server/src/main/resources/application.yml b/itools-oms/itools-oms-server/src/main/resources/application.yml index 54620a6b6eec4245be937d2c43687c02b89844cf..094aac7ce52f2e502c48dca32577e597e575ffb0 100644 --- a/itools-oms/itools-oms-server/src/main/resources/application.yml +++ b/itools-oms/itools-oms-server/src/main/resources/application.yml @@ -1,5 +1,4 @@ -server: - port: 8888 + spring: application: name: itools-oms-server diff --git a/itools-oms/pom.xml b/itools-oms/pom.xml index 020d67b8f3a526416a5ba02b9e1572104cf22648..8d3fdbfd65ec5b0953687b8399a1bc3d6cf18f4c 100644 --- a/itools-oms/pom.xml +++ b/itools-oms/pom.xml @@ -18,6 +18,7 @@ itools-oms-core itools-oms-server itools-oms-jwt-sample-example + itools-oms-auth-server diff --git a/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/test/java/com/xxl/app/sample/test/util/HttpClientUtil.java b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/test/java/com/xxl/app/sample/test/util/HttpClientUtil.java index e1cc92612a7448c980d2433fb3fd94ab520fae0c..60e01e169cd1dd56ed75f36d7e0860af79a0461a 100644 --- a/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/test/java/com/xxl/app/sample/test/util/HttpClientUtil.java +++ b/itools-sso/itools-sso-samples/itools-sso-token-sample-springboot/src/test/java/com/xxl/app/sample/test/util/HttpClientUtil.java @@ -31,7 +31,7 @@ public class HttpClientUtil { HttpPost httpPost = null; CloseableHttpClient httpClient = null; try{ - // httpPost config + // httpPost com.itools.config httpPost = new HttpPost(url); if (params != null && !params.isEmpty()) { List formParams = new ArrayList(); diff --git a/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/interceptor/MyWebMvcConfigurer.java b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/interceptor/MyWebMvcConfigurer.java index 6ef1a9af3fb30ce517ee082769803dad1668a6f9..4653c9d5d0668b1f3292392dfc9b70e12bf22001 100644 --- a/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/interceptor/MyWebMvcConfigurer.java +++ b/itools-sso/itools-sso-server/src/main/java/com/itools/sso/server/controller/interceptor/MyWebMvcConfigurer.java @@ -5,7 +5,7 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; /** - * web mvc config + * web mvc com.itools.config * * @author xuxueli 2018-04-02 20:48:20 */ diff --git a/itools-sso/itools-sso-server/src/main/resources/static/plugins/ionicons-2.0.1/fonts/ionicons.svg b/itools-sso/itools-sso-server/src/main/resources/static/plugins/ionicons-2.0.1/fonts/ionicons.svg index 49fc8f367404798ee10aac4f1f2a5b38498fe625..8626d051431dbf1f1b7545be9132d87100b6757f 100644 --- a/itools-sso/itools-sso-server/src/main/resources/static/plugins/ionicons-2.0.1/fonts/ionicons.svg +++ b/itools-sso/itools-sso-server/src/main/resources/static/plugins/ionicons-2.0.1/fonts/ionicons.svg @@ -1027,21 +1027,21 @@ M295 82l70 1c18 26 30 58 33 92l-54 46l-50 -23l-20 -77zM207 345l47 32c-15 4 -30 6 - - - - diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-core/pom.xml b/itools-ucenter-oauth2/itools-ucenter-oauth2-core/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..10ac526f52d19b83f9082f16b130b8195f062b11 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-core/pom.xml @@ -0,0 +1,91 @@ + + + + itools-ucenter-oauth2 + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-ucenter-oauth2-core + + + + + com.itools.core + itools-common + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + + + com.baomidou + mybatis-plus-boot-starter + + + + + mysql + mysql-connector-java + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-oauth2 + + + org.springframework.cloud + spring-cloud-starter-security + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + + src/main/resources + true + + **/*.jks + + + + src/main/resources + false + + **/*.jks + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/config/IgnoreUrlsConfig.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/config/IgnoreUrlsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ce993c543a03f8a86571289701dc27223ed1f54 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/config/IgnoreUrlsConfig.java @@ -0,0 +1,23 @@ +package com.itools.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +import java.util.LinkedHashSet; + +/** + * @project: itools-backend + * @description: 用于配置不需要保护的资源路径 + * @author: XUCHANG + * @create: 2021-06-22 15:41 + */ +@Data +@ConfigurationProperties(prefix = "secure.ignored") +@Configuration +public class IgnoreUrlsConfig { + + private LinkedHashSet shouldSkipUrls = new LinkedHashSet<>(); + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/config/ResourceServerConfig.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/config/ResourceServerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..38d1bb12a39b12e1d86f8fdeed0d7091562fe4be --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/config/ResourceServerConfig.java @@ -0,0 +1,55 @@ +package com.itools.core.config; + +import com.itools.core.endpoint.AuthExceptionEntryPoint; +import com.itools.core.endpoint.CustomAccessDeniedHandler; +import com.itools.core.endpoint.CustomTokenExtractor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-21 16:13 + */ +@Configuration +@EnableResourceServer +public class ResourceServerConfig extends ResourceServerConfigurerAdapter { + @Resource + private IgnoreUrlsConfig ignoreUrlsConfig; + @Override + public void configure(HttpSecurity http) throws Exception { + ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry = http + .csrf().disable() + .exceptionHandling() + .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) + .and() + .authorizeRequests(); + expressionInterceptUrlRegistry.antMatchers("/oauth/**").permitAll(); + //设置可放行的请求 + for (String url : ignoreUrlsConfig.getShouldSkipUrls()) { + expressionInterceptUrlRegistry.antMatchers(url).permitAll(); + } + expressionInterceptUrlRegistry.anyRequest().authenticated().and().httpBasic(); + } + @Resource + private AuthExceptionEntryPoint authExceptionEntryPoint; + @Resource + private CustomAccessDeniedHandler customAccessDeniedHandler; + @Resource + private CustomTokenExtractor customTokenExtractor; + @Override + public void configure(ResourceServerSecurityConfigurer resources) { + resources.tokenExtractor(customTokenExtractor); + resources.authenticationEntryPoint(authExceptionEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler); + } +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/config/WebSecurityConfigurer.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/config/WebSecurityConfigurer.java new file mode 100644 index 0000000000000000000000000000000000000000..d7cd7b3fb34e32582434d70ffd4de3ec3c9ef262 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/config/WebSecurityConfigurer.java @@ -0,0 +1,38 @@ +package com.itools.core.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +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.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +import javax.annotation.Resource; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-22 10:03 + */ +@Configuration +@EnableWebSecurity(debug = true) +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { + @Resource + private IgnoreUrlsConfig ignoreUrlsConfig; + @Override + protected void configure(HttpSecurity http) throws Exception { + ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = http + .authorizeRequests(); + //设置可放行的请求 + for (String url : ignoreUrlsConfig.getShouldSkipUrls()) { + registry.antMatchers(url).permitAll(); + } + registry.antMatchers("/oauth/**").permitAll() + .anyRequest().authenticated() + .and() + .csrf().disable(); + } +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/endpoint/AuthExceptionEntryPoint.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/endpoint/AuthExceptionEntryPoint.java new file mode 100644 index 0000000000000000000000000000000000000000..bdd04b05de930b133e0caeb37532e029458f2752 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/endpoint/AuthExceptionEntryPoint.java @@ -0,0 +1,49 @@ +package com.itools.core.endpoint; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +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; +import java.util.HashMap; +import java.util.Map; + +/** + * @project: itools-backend + * @description: 自定义返回结果:未登录或登录过期 + * @author: XUCHANG + * @create: 2021-06-22 13:37 + */ +@Component +@Slf4j +public class AuthExceptionEntryPoint implements AuthenticationEntryPoint { + @Autowired + private ObjectMapper objectMapper; + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws ServletException { + Map map = new HashMap<>(); + map.put("returnCode",401); + map.put("success",false); + map.put("data",null); + Throwable cause = authException.getCause(); + + response.setStatus(HttpStatus.OK.value()); + response.setHeader("Content-Type", "application/json;charset=UTF-8"); + try { + map.put("returnMsg","暂未登录或token已经过期"); + response.getWriter().write(objectMapper.writeValueAsString(map)); + } catch (IOException e) { + log.error("无效 token 异常类重写",e); + } + } +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/endpoint/CustomAccessDeniedHandler.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/endpoint/CustomAccessDeniedHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..59bfd560deeef4df16a1481c6f1c5f7956c337f5 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/endpoint/CustomAccessDeniedHandler.java @@ -0,0 +1,47 @@ +package com.itools.core.endpoint; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * @project: itools-backend + * @description: 自定义返回结果:没有权限访问时 + * @author: XUCHANG + * @create: 2021-06-22 13:45 + */ +@Slf4j +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + @Autowired + private ObjectMapper objectMapper; + @Override + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) + throws IOException, ServletException { + Map map = new HashMap<>(); + map.put("returnCode",403); + map.put("success",false); + map.put("data",null); + response.setStatus(HttpStatus.OK.value()); + response.setHeader("Content-Type", "application/json;charset=UTF-8"); + try { + map.put("returnMsg","用户权限不足"); + response.getWriter().write(objectMapper.writeValueAsString(map)); + } catch (IOException e) { + log.error("用户权限不足",e); + } + } +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/endpoint/CustomTokenExtractor.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/endpoint/CustomTokenExtractor.java new file mode 100644 index 0000000000000000000000000000000000000000..1cc8bae63eed5055be28001b8c9f434a2bf04a17 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-core/src/main/java/com/itools/core/endpoint/CustomTokenExtractor.java @@ -0,0 +1,95 @@ +package com.itools.core.endpoint; + +import lombok.extern.log4j.Log4j2; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; +import org.springframework.security.oauth2.provider.authentication.TokenExtractor; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.stereotype.Component; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.util.Enumeration; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-22 13:50 + */ +@Log4j2 +@Component +public class CustomTokenExtractor implements TokenExtractor { + + @Override + public Authentication extract(HttpServletRequest request) { + String tokenValue = this.extractToken(request); + if (tokenValue != null) { + return new PreAuthenticatedAuthenticationToken(tokenValue, ""); + } else { + //抛出异常 + log.info("token为空,请输入合法token"); + return null; + } + } + + public String extractToken(HttpServletRequest request) { + String token = this.extractHeaderToken(request); + if (token == null) { + log.info("Token not found in headers. Trying request parameters."); + token = request.getParameter("access_token"); + if (token == null) { + log.info("Token not found in request parameters. Trying request cookies."); + + token = extractCookieToken(request); + if (token == null) { + log.info("Token not found in cookies. Not an OAuth2 request."); + } else { + request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, "Bearer"); + } + } + } + return token; + } + + private String extractHeaderToken(HttpServletRequest request) { + Enumeration headers = request.getHeaders("Authorization"); + + String value; + do { + if (!headers.hasMoreElements()) { + return null; + } + + value = (String) headers.nextElement(); + } while (!value.toLowerCase().startsWith("Bearer".toLowerCase())); + + String authHeaderValue = value.substring("Bearer".length()).trim(); + request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, value.substring(0, "Bearer".length()).trim()); + int commaIndex = authHeaderValue.indexOf(44); + if (commaIndex > 0) { + authHeaderValue = authHeaderValue.substring(0, commaIndex); + } + if (authHeaderValue.equals("")) { + return null; + } + return authHeaderValue; + } + + private String extractCookieToken(HttpServletRequest request) { + + String cookieToken = null; + //根据请求数据,找到cookie数组 + Cookie[] cookies = request.getCookies(); + + if (null != cookies && cookies.length > 0) { + for (Cookie cookie : cookies) { + if (null != cookie.getName() && cookie.getName().trim().equalsIgnoreCase("access_token")) { + cookieToken = cookie.getValue().trim(); + break; + } + } + } + return cookieToken; + } +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-model/pom.xml b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..970c76e6801d9ba4a1aec5265bf2ce6c2cdafb63 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/pom.xml @@ -0,0 +1,90 @@ + + + + itools-ucenter-oauth2 + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-ucenter-oauth2-model + + + + com.itools.core + itools-common + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + + + com.baomidou + mybatis-plus-boot-starter + + + + + mysql + mysql-connector-java + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-oauth2 + + + org.springframework.cloud + spring-cloud-starter-security + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + + src/main/resources + true + + **/*.jks + + + + src/main/resources + false + + **/*.jks + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/ApplicationInfo.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/ApplicationInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..7de46ceabbc10f26027fc44dfdd66a8a99ebf3b7 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/ApplicationInfo.java @@ -0,0 +1,69 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.TableId; +import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_application_info") +@ApiModel(value="ApplicationInfo对象", description="") +public class ApplicationInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键") + @TableId(value = "id", type = IdType.ID_WORKER_STR) + private String id; + + @ApiModelProperty(value = "系统名称") + private String appName; + + @ApiModelProperty(value = "系统唯一id") + private String appInstanceId; + + @ApiModelProperty(value = "系统所有者") + private String appOwner; + + @ApiModelProperty(value = "状态") + private Integer status; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建人") + private String createUserId; + + @ApiModelProperty(value = "最近更新时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "最近更新人") + private String lastUpdateUserId; + + @ApiModelProperty(value = "系统编码") + private String appCode; + + @ApiModelProperty(value = "系统所有者邮箱") + private String appOwnerEmail; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "类型【内部应用:1;外部应用:2】") + private Integer appType; + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/AuthResourceRef.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/AuthResourceRef.java new file mode 100644 index 0000000000000000000000000000000000000000..823e72942bacd4d54cd60771b27c645070c69e05 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/AuthResourceRef.java @@ -0,0 +1,60 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.TableId; +import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_auth_resource_ref") +@ApiModel(value="AuthResourceRef对象", description="") +public class AuthResourceRef implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "ID") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "权限ID") + private String authorizationId; + + @ApiModelProperty(value = "资源ID") + private String resourceId; + + @ApiModelProperty(value = "权限资源类型【数据权限项:1;资源:2】") + private Integer type; + + @ApiModelProperty(value = "角色权限状态【1:有效;2:超级管理员或父级置为失效;3:权限拥有者置为失效】") + private Integer status; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户") + private String lastUpdateUserId; + + @ApiModelProperty(value = "应用ID") + private String applicationId; + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/AuthorizationInfo.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/AuthorizationInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..9bd350d9601c723f571d850c600a63912dbd60ec --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/AuthorizationInfo.java @@ -0,0 +1,66 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.TableId; +import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_authorization_info") +@ApiModel(value="AuthorizationInfo对象", description="") +public class AuthorizationInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "ID") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "名称") + private String name; + + @ApiModelProperty(value = "父ID") + private String parentId; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "状态【启用:1;未启用:2】") + private Integer status; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户") + private String lastUpdateUserId; + + @ApiModelProperty(value = "应用ID") + private String applicationId; + + @ApiModelProperty(value = "code") + private String code; + + @ApiModelProperty(value = "排序字段") + private Integer orderFiled; + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/GroupInfo.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/GroupInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..79a84c60915e8997ed4dfdf4def3184f38a62936 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/GroupInfo.java @@ -0,0 +1,60 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.TableId; +import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_group_info") +@ApiModel(value="GroupInfo对象", description="") +public class GroupInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键id") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户id") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户id") + private String lastUpdateUserId; + + @ApiModelProperty(value = "应用id") + private String applicationId; + + @ApiModelProperty(value = "编号") + private String code; + + @ApiModelProperty(value = "组名") + private String name; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "状态【启用:1;未启用:2】") + private Integer status; + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/OauthClientDetails.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/OauthClientDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..e134e5239961751b1e815faffc57a706dcaea7d5 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/OauthClientDetails.java @@ -0,0 +1,49 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import java.io.Serializable; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import lombok.Data; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("oauth_client_details") +@ApiModel(value="OauthClientDetails对象", description="") +public class OauthClientDetails implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "client_id", type = IdType.AUTO) + private String clientId; + + private String resourceIds; + + private String clientSecret; + + private String scope; + + private String authorizedGrantTypes; + + private String webServerRedirectUri; + + private String authorities; + + private Integer accessTokenValidity; + + private Integer refreshTokenValidity; + + private String additionalInformation; + + private String autoapprove; + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/OrganizationInfo.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/OrganizationInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..0a2e50e9a92860c0ebfe4b402d061fc3321a3c4b --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/OrganizationInfo.java @@ -0,0 +1,95 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.TableId; +import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_organization_info") +@ApiModel(value="OrganizationInfo对象", description="") +public class OrganizationInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "ID") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "机构名称") + private String name; + + @ApiModelProperty(value = "父ID") + private String parentId; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户") + private String lastUpdateUserId; + + @ApiModelProperty(value = "地址") + private String address; + + @ApiModelProperty(value = "应用ID") + private String applicationId; + + @ApiModelProperty(value = "code") + private String code; + + @ApiModelProperty(value = "联系人部门") + private String linkManDept; + + @ApiModelProperty(value = "联系人邮箱") + private String linkManEmail; + + @ApiModelProperty(value = "联系人名称") + private String linkManName; + + @ApiModelProperty(value = "联系人手机号") + private String linkManPhone; + + @ApiModelProperty(value = "等级") + private Integer rank; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "状态【启用:1;未启用:2】") + private Integer status; + + @ApiModelProperty(value = "组织电话") + private String phone; + + @ApiModelProperty(value = "组织类型【部门:1;区:2;机构:3】") + private Integer type; + + @ApiModelProperty(value = "机构编号") + private String organizationNumber; + + @ApiModelProperty(value = "管理员id") + private String adminUserId; + + @ApiModelProperty(value = "排序字段") + private Integer orderField; +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/ResourceInfo.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/ResourceInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..63c74b5eb28a97bdec14c7cabf518a8fb3597594 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/ResourceInfo.java @@ -0,0 +1,75 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.TableId; +import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_resource_info") +@ApiModel(value="ResourceInfo对象", description="") +public class ResourceInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "ID") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "资源名称") + private String name; + + @ApiModelProperty(value = "父ID") + private String parentId; + + @ApiModelProperty(value = "状态【启用:1;未启用:2】") + private Integer status; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户ID") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户") + private String lastUpdateUserId; + + @ApiModelProperty(value = "应用ID") + private String applicationId; + + @ApiModelProperty(value = "code") + private String code; + + @ApiModelProperty(value = "排序") + private Integer orderField; + + @ApiModelProperty(value = "图标") + private String pictureName; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "类型") + private Integer type; + + @ApiModelProperty(value = "路由") + private String url; + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/RoleAuthorizationRef.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/RoleAuthorizationRef.java new file mode 100644 index 0000000000000000000000000000000000000000..76c9e22b7755077c03384ec46fa6acdc5673b9b8 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/RoleAuthorizationRef.java @@ -0,0 +1,57 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.TableId; +import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_role_authorization_ref") +@ApiModel(value="RoleAuthorizationRef对象", description="") +public class RoleAuthorizationRef implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键id") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户id") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户id") + private String lastUpdateUserId; + + @ApiModelProperty(value = "应用id") + private String applicationId; + + @ApiModelProperty(value = "权限id") + private String authorizationId; + + @ApiModelProperty(value = "角色id") + private String roleId; + + @ApiModelProperty(value = "角色权限状态【1:有效;2:超级管理员或父级置为失效;3:权限拥有者置为失效】") + private Integer status; + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/RoleInfo.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/RoleInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..ef9415d561a68a3ca23cd52ce79989235fff1dc3 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/RoleInfo.java @@ -0,0 +1,69 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.TableId; +import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_role_info") +@ApiModel(value="RoleInfo对象", description="") +public class RoleInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键ID") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "父ID") + private String parentId; + + @ApiModelProperty(value = "角色名称") + private String name; + + @ApiModelProperty(value = "应用ID") + private String applicationId; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "code") + private String code; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户") + private String lastUpdateUserId; + + @ApiModelProperty(value = "角色状态【启用:1;未启用:2】") + private Integer status; + + @ApiModelProperty(value = "排序字段") + private Integer orderField; + + @ApiModelProperty(value = "角色类型【0:功能角色,1:数据角色】") + private Integer type; + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/RoleResourceRef.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/RoleResourceRef.java new file mode 100644 index 0000000000000000000000000000000000000000..29591a1ce7f724601ac16cfa2d233a08c7813ace --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/RoleResourceRef.java @@ -0,0 +1,60 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.TableId; +import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_role_resource_ref") +@ApiModel(value="RoleResourceRef对象", description="") +public class RoleResourceRef implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "用户ID") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户") + private String lastUpdateUserId; + + @ApiModelProperty(value = "应用ID") + private String applicationId; + + @ApiModelProperty(value = "角色名称") + private String roleId; + + @ApiModelProperty(value = "资源ID") + private String resourceId; + + @ApiModelProperty(value = "角色资源状态【1:有效;2:超级管理员或父级置为失效;3:权限拥有者置为失效】") + private Integer status; + + @ApiModelProperty(value = "角色资源类型【1:数据权限项;2:资源】") + private Integer type; + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/UserInfo.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/UserInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..973c13854341c71690a66da258b9ba0d8006aac8 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/UserInfo.java @@ -0,0 +1,100 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.TableId; +import java.sql.Blob; +import java.io.Serializable; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_user_info") +@ApiModel(value="UserInfo对象", description="") +public class UserInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键id") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户") + private String lastUpdateUserId; + + @ApiModelProperty(value = "公司id") + private String companyId; + + @ApiModelProperty(value = "部门id") + private String orgId; + + @ApiModelProperty(value = "用户名") + private String name; + + @ApiModelProperty(value = "用户类型") + private Integer userType; + + @ApiModelProperty(value = "性别") + private Integer sex; + + @ApiModelProperty(value = "生日") + private String birthday; + + @ApiModelProperty(value = "邮箱") + private String email; + + @ApiModelProperty(value = "位置") + private String position; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "手机号") + private String phone; + + @ApiModelProperty(value = "编号") + private String userCode; + + @ApiModelProperty(value = "等级") + private Integer userLevel; + + @ApiModelProperty(value = "头像") + private Blob faceUrl; + + @ApiModelProperty(value = "工号") + private String jobNumber; + + @ApiModelProperty(value = "账号") + private String identifier; + + @ApiModelProperty(value = "账号类型【手机号:1;邮箱:2,;账号:3】") + private Integer identityType; + + @ApiModelProperty(value = "账号密码") + private String password; + + @ApiModelProperty(value = "上次登录时间") + private LocalDateTime lastLoginTime; + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/UserRoleRef.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/UserRoleRef.java new file mode 100644 index 0000000000000000000000000000000000000000..da7cf7c9f5c009f78398364015760b119e05c7c3 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/entity/UserRoleRef.java @@ -0,0 +1,56 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import java.io.Serializable; +import java.time.LocalDateTime; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_user_role_ref") +@ApiModel(value="UserRoleRef对象", description="") +public class UserRoleRef implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键id") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "应用id") + private String applicationId; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户") + private String lastUpdateUserId; + + @ApiModelProperty(value = "角色ID") + private String roleId; + + @ApiModelProperty(value = "用户ID") + private String userId; + + @ApiModelProperty(value = "用户角色类型【1:功能角色;2:数据角色】") + private Integer type; + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/pojo/SysPermission.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/pojo/SysPermission.java new file mode 100644 index 0000000000000000000000000000000000000000..019a68931ca3c6117a959e7bc6fc734b4bcd63b3 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/pojo/SysPermission.java @@ -0,0 +1,33 @@ +package com.itools.core.ucenter.pojo; + +import lombok.Data; + +import java.util.Date; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Data +public class SysPermission implements java.io.Serializable { + private static final long serialVersionUID = 1L; + + private Long id; + + private Long parentId; + + private String name; + + private String enname; + + private String url; + + private String description; + + private Date created; + + private Date updated; + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/pojo/SysRole.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/pojo/SysRole.java new file mode 100644 index 0000000000000000000000000000000000000000..f787460351430b8594d596d3e33720572ad01120 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/pojo/SysRole.java @@ -0,0 +1,31 @@ +package com.itools.core.ucenter.pojo; + +import lombok.Data; + +import java.util.Date; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Data +public class SysRole implements java.io.Serializable { + private static final long serialVersionUID = 1L; + + private Long id; + + private Long parentId; + + private String name; + + private String enname; + + private String description; + + private Date created; + + private Date updated; + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/pojo/SysUser.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/pojo/SysUser.java new file mode 100644 index 0000000000000000000000000000000000000000..5a74fb8813745c4a199f5f04ad817dd1d6b96974 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-model/src/main/java/com/itools/core/ucenter/pojo/SysUser.java @@ -0,0 +1,33 @@ +package com.itools.core.ucenter.pojo; + +import lombok.Data; + +import java.util.Date; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@Data +public class SysUser implements java.io.Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + private String username; + + private String password; + + private String phone; + + private String email; + + private Date created; + + private Date updated; + + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/pom.xml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..dd04b0b6adc3570840452f3296b377476d054e51 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/pom.xml @@ -0,0 +1,95 @@ + + + + 4.0.0 + + itools-ucenter-oauth2 + com.itools.core + 1.0-SNAPSHOT + + itools-ucenter-oauth2-server + + + com.itools.core + itools-ucenter-oauth2-model + 1.0-SNAPSHOT + + + com.itools.core + itools-common + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + + + com.baomidou + mybatis-plus-boot-starter + + + + + mysql + mysql-connector-java + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-oauth2 + + + org.springframework.cloud + spring-cloud-starter-security + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + + src/main/resources + true + + **/*.jks + + + + src/main/resources + false + + **/*.jks + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/UCenterServerApplication.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/UCenterServerApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..d738994cc6256e34b693c1c766d8324b54792a98 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/UCenterServerApplication.java @@ -0,0 +1,56 @@ +package com.itools.core.ucenter; + +import com.itools.core.snowflake.config.EnableSequenceService; +import com.itools.core.validate.EnableValidator; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.Banner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@SpringBootApplication(scanBasePackages = {"com.itools.core"}) +@EnableSequenceService +@EnableValidator +@MapperScan("com.itools.core.ucenter.mapper") +@EnableAspectJAutoProxy(exposeProxy = true)//exposeProxy类内部可以获取到当前类的代理对象 +public class UCenterServerApplication { + public static void main(String[] args) { + SpringApplication springApplication = new SpringApplication(UCenterServerApplication.class); + springApplication.setBannerMode(Banner.Mode.OFF); + springApplication.run(args); + } + @Bean + public CorsFilter corsFilter() { + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + final CorsConfiguration config = new CorsConfiguration(); + // 允许cookies跨域 + config.setAllowCredentials(true); + // 允许向该服务器提交请求的URI,*表示全部允许。。这里尽量限制来源域,比如http://xxxx:8080 ,以降低安全风险。。 + config.addAllowedOrigin("*"); + // 允许访问的头信息,*表示全部 + config.addAllowedHeader("*"); + // 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了 + config.setMaxAge(18000L); + // 允许提交请求的方法,*表示全部允许,也可以单独设置GET、PUT等 + config.addAllowedMethod("*"); + config.addAllowedMethod("HEAD"); + // 允许Get的请求方法 + config.addAllowedMethod("GET"); + config.addAllowedMethod("PUT"); + config.addAllowedMethod("POST"); + config.addAllowedMethod("DELETE"); + config.addAllowedMethod("PATCH"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/config/AuthorizationServerConfig.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/config/AuthorizationServerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..16667200c707d402a77d72702ed82b5e8584f446 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/config/AuthorizationServerConfig.java @@ -0,0 +1,94 @@ +package com.itools.core.ucenter.config; + +import com.itools.core.ucenter.endpoint.CustomClientAuthenticationEntryPoint; +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.data.redis.connection.RedisConnectionFactory; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; +import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; + +import javax.sql.DataSource; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-21 15:23 + */ +@Configuration +@EnableAuthorizationServer +public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { + + @Qualifier("dataSource") + @Autowired + private DataSource dataSource; + @Override + public void configure(ClientDetailsServiceConfigurer clients) throws Exception { + // 第三方信息的存储 基于jdbc + clients.withClientDetails(clientDetailsService()); + + } + + @Autowired + private RedisConnectionFactory connectionFactory; + @Bean + public RedisTokenStore tokenStore() { + return new RedisTokenStore(connectionFactory); + } + @Autowired + private UserDetailsService userDetailsService; + + @Autowired + private AuthenticationManager authenticationManagerBean; + + + @Override + public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { + + //使用密码模式需要配置 + endpoints.authenticationManager(authenticationManagerBean) + //刷新令牌授权包含对用户信息的检查 + .userDetailsService(userDetailsService) + //指定token存储策略是jwt + .tokenStore(tokenStore()) + //refresh_token是否重复使用 + .reuseRefreshTokens(false) + .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE); + } + @Autowired + private CustomClientAuthenticationEntryPoint customAuthenticationEntryPoint; + /** + * 授权服务器安全配置 + * @param security + * @throws Exception + */ + @Override + public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { + + //第三方客户端校验token需要带入 clientId 和clientSecret来校验 + //来获取我们的tokenKey需要带入clientId,clientSecret + security.checkTokenAccess("isAuthenticated()") + .tokenKeyAccess("isAuthenticated()"); + + //允许表单认证 + security.allowFormAuthenticationForClients(); + } + + @Bean + public ClientDetailsService clientDetailsService(){ + return new JdbcClientDetailsService(dataSource); + } + +} + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/config/ResourceServerConfig.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/config/ResourceServerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..7a4aef941e14dab95b3231f18b3772309f6db895 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/config/ResourceServerConfig.java @@ -0,0 +1,50 @@ +package com.itools.core.ucenter.config; + +import com.itools.core.ucenter.endpoint.AuthExceptionEntryPoint; +import com.itools.core.ucenter.endpoint.CustomAccessDeniedHandler; +import com.itools.core.ucenter.endpoint.CustomTokenExtractor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; + +import javax.servlet.http.HttpServletResponse; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-21 15:22 + */ +@Configuration +@EnableResourceServer +public class ResourceServerConfig extends ResourceServerConfigurerAdapter { + + @Override + public void configure(HttpSecurity http) throws Exception { + ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry = http + .csrf().disable() + .exceptionHandling() + .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) + .and() + .authorizeRequests(); + expressionInterceptUrlRegistry.antMatchers("/oauth/**").permitAll(); + + expressionInterceptUrlRegistry.anyRequest().authenticated().and().httpBasic(); + } + @Autowired + private AuthExceptionEntryPoint authExceptionEntryPoint; + @Autowired + private CustomAccessDeniedHandler customAccessDeniedHandler; + @Autowired + private CustomTokenExtractor customTokenExtractor; + @Override + public void configure(ResourceServerSecurityConfigurer resources) { + resources.tokenExtractor(customTokenExtractor); + resources.authenticationEntryPoint(authExceptionEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler); + } +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/config/WebSecurityConfig.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/config/WebSecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2cc75e1ea212a564b087cbdc1b1ada8c23fffb05 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/config/WebSecurityConfig.java @@ -0,0 +1,68 @@ +package com.itools.core.ucenter.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +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.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-21 15:22 + */ +@Configuration +@EnableWebSecurity(debug = true) +@EnableGlobalMethodSecurity(prePostEnabled=true) +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private UserDetailsService userDetailsService; + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService); + //账号不存在或账号密码错误的异常类 + auth.authenticationProvider(authenticationProvider()); + } + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setHideUserNotFoundExceptions(false); + provider.setUserDetailsService(userDetailsService); + provider.setPasswordEncoder(passwordEncoder()); + return provider; + } + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = http + .authorizeRequests(); + + registry.antMatchers("/oauth/**").permitAll() + .anyRequest().authenticated() + .and() + .csrf().disable(); + } + + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/ApplicationInfoController.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/ApplicationInfoController.java new file mode 100644 index 0000000000000000000000000000000000000000..b69c8ccea61b554715fe30d990ff8556a7ef5b3b --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/ApplicationInfoController.java @@ -0,0 +1,35 @@ +package com.itools.core.ucenter.controller; + + +import com.itools.core.ucenter.service.ApplicationInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/application") +public class ApplicationInfoController { + @Autowired + private ApplicationInfoService applicationInfoService; + + @GetMapping("/query") + @PreAuthorize("hasAnyAuthority('query')") + public Authentication query() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication; + } +} + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/AuthResourceRefController.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/AuthResourceRefController.java new file mode 100644 index 0000000000000000000000000000000000000000..9dd3505b590aafcdfbb5a9fecd24b826550bab33 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/AuthResourceRefController.java @@ -0,0 +1,21 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; + +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/authResource") +public class AuthResourceRefController { + +} + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/AuthorizationInfoController.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/AuthorizationInfoController.java new file mode 100644 index 0000000000000000000000000000000000000000..615946ad560c16af2bd7af1fa1afadd7849669ce --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/AuthorizationInfoController.java @@ -0,0 +1,21 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; + +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/authorization") +public class AuthorizationInfoController { + +} + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/GroupInfoController.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/GroupInfoController.java new file mode 100644 index 0000000000000000000000000000000000000000..f9d90fc413ce0136cc736eafc38647c0fe01e2ea --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/GroupInfoController.java @@ -0,0 +1,21 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; + +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/group") +public class GroupInfoController { + +} + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/OrganizationInfoController.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/OrganizationInfoController.java new file mode 100644 index 0000000000000000000000000000000000000000..b72891821b63b392e4faefbe0a1eb5d7741cad14 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/OrganizationInfoController.java @@ -0,0 +1,21 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; + +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/organization") +public class OrganizationInfoController { + +} + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/ResourceInfoController.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/ResourceInfoController.java new file mode 100644 index 0000000000000000000000000000000000000000..cecc7c9fc93ec98eed4aed587ce69a4edfd50b38 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/ResourceInfoController.java @@ -0,0 +1,21 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; + +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/resource") +public class ResourceInfoController { + +} + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/RoleAuthorizationRefController.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/RoleAuthorizationRefController.java new file mode 100644 index 0000000000000000000000000000000000000000..7c2d6cce7f61c87c32c370aef9c3a82b3ca002cb --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/RoleAuthorizationRefController.java @@ -0,0 +1,21 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; + +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/roleAuth") +public class RoleAuthorizationRefController { + +} + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/RoleInfoController.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/RoleInfoController.java new file mode 100644 index 0000000000000000000000000000000000000000..9b5aad75c7d2d04e92cc8f49c5138c2857a2ce3b --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/RoleInfoController.java @@ -0,0 +1,21 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; + +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/role") +public class RoleInfoController { + +} + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/RoleResourceRefController.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/RoleResourceRefController.java new file mode 100644 index 0000000000000000000000000000000000000000..9ac00e2549523187ac24772b0c075678de43db82 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/RoleResourceRefController.java @@ -0,0 +1,21 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; + +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/roleResource") +public class RoleResourceRefController { + +} + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/UserInfoController.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/UserInfoController.java new file mode 100644 index 0000000000000000000000000000000000000000..dd519b9c9a285606dd89711a9946ac7530cb27f3 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/UserInfoController.java @@ -0,0 +1,21 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; + +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/user") +public class UserInfoController { + +} + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/UserLoginController.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/UserLoginController.java new file mode 100644 index 0000000000000000000000000000000000000000..4d11ce0a043a33a83911c312e1efe159ccaf1d53 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/UserLoginController.java @@ -0,0 +1,54 @@ +package com.itools.core.ucenter.controller; + +import com.itools.core.base.CommonResult; +import com.itools.core.ucenter.em.UcenterCodeBean; +import com.itools.core.exception.AppException; +import com.itools.core.ucenter.service.UserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; +import java.util.Map; + +@Slf4j +@RestController +public class UserLoginController { + @Autowired + private TokenEndpoint tokenEndpoint; + @Autowired + private UserService userService; + @GetMapping("/user") + public Principal user(Principal user){ + return user; + } + + @RequestMapping(value = "/oauth/token", method= RequestMethod.GET) + public CommonResult getAccessToken(Principal principal, + @RequestParam Map parameters){ + + userService.verifyUserByPasswordType(parameters); + try { + //验证用户、设备、密码强度 等登录信息 + return CommonResult.success(tokenEndpoint.getAccessToken(principal,parameters).getBody()); + }catch (Exception e){ + log.error("登录失败:",e); + throw new AppException(UcenterCodeBean.UcenterCode.FAIL_LOGIN.code); + } + } + + @RequestMapping(value = "/oauth/token", method=RequestMethod.POST) + public CommonResult postAccessToken(Principal principal, + @RequestParam Map parameters){ +// Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + userService.verifyUserByPasswordType(parameters); + try { + return CommonResult.success(tokenEndpoint.postAccessToken(principal,parameters).getBody()); + }catch (Exception e){ + log.error("登录失败:",e); + throw new AppException(UcenterCodeBean.UcenterCode.FAIL_LOGIN.code); + } + } +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/UserRoleRefController.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/UserRoleRefController.java new file mode 100644 index 0000000000000000000000000000000000000000..97a800cc59476db238f81ba8998550ec3327b7b5 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/controller/UserRoleRefController.java @@ -0,0 +1,21 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; + +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/userRole") +public class UserRoleRefController { + +} + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/em/LoginTypeEnum.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/em/LoginTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..8d8205086b81e3a512057ac5b96f63706eb86320 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/em/LoginTypeEnum.java @@ -0,0 +1,24 @@ +package com.itools.core.ucenter.em; + +import com.baomidou.mybatisplus.core.enums.IEnum; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * authorization_code,implicit,password,client_credentials,refresh_token + */ +@Getter +@NoArgsConstructor +@AllArgsConstructor +public enum LoginTypeEnum implements IEnum { + + AUTHORIZATION_CODE("authorization_code",0), + IMPLICIT("implicit",1), + PASSWORD("password",2), + CLIENT_CREDENTIALS("client_credentials",3), + REFRESH_TOKEN("refresh_token",4); + + private String name; + private Integer value; +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/em/UcenterCodeBean.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/em/UcenterCodeBean.java new file mode 100644 index 0000000000000000000000000000000000000000..6d79681ffe333faeb761229ad278c10799dcb11a --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/em/UcenterCodeBean.java @@ -0,0 +1,42 @@ +package com.itools.core.ucenter.em; + + +import com.itools.core.code.SystemCode; + +/** + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@SystemCode +public class UcenterCodeBean { + + public enum UcenterCode { + + FAIL_SYSTEM("FMSCODE00", "系统内部错误,请联系管理员!"), + /** + * 文件系统内部错误 + */ + USERNAME_NOT_FOUND_EXCEPTION("UCODE01", "账号不存在或账号密码错误"), + FAIL_LOGIN("UCODE02", "用户登录失败"), + FAIL_USERNAME_PASSWORD("UCODE03", "账号密码未输入"), + USER_NOT_EXIST("UCODE04", "账号不存在或账号密码错误"), + ; + + public final String code; + public final String message; + + UcenterCode(String code, String message) { + this.code = code; + this.message = message; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } + } +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/endpoint/AuthExceptionEntryPoint.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/endpoint/AuthExceptionEntryPoint.java new file mode 100644 index 0000000000000000000000000000000000000000..8e06808e83762289a57e089aac2551810eb4f417 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/endpoint/AuthExceptionEntryPoint.java @@ -0,0 +1,49 @@ +package com.itools.core.ucenter.endpoint; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +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; +import java.util.HashMap; +import java.util.Map; + +/** + * @project: itools-backend + * @description: 无效 token 异常类重写 + * @author: XUCHANG + * @create: 2021-06-22 13:37 + */ +@Component +@Slf4j +public class AuthExceptionEntryPoint implements AuthenticationEntryPoint { + @Autowired + private ObjectMapper objectMapper; + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws ServletException { + Map map = new HashMap<>(); + map.put("returnCode",401); + map.put("success",false); + map.put("data",null); + + response.setStatus(HttpStatus.OK.value()); + response.setHeader("Content-Type", "application/json;charset=UTF-8"); + try { + map.put("returnMsg","暂未登录或token已经过期"); + response.getWriter().write(objectMapper.writeValueAsString(map)); + } catch (IOException e) { + log.error("无效 token 异常类重写",e); + } + } +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/endpoint/CustomAccessDeniedHandler.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/endpoint/CustomAccessDeniedHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..8b471cfeb8d4aed88865bfab3e71ecec06fcd529 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/endpoint/CustomAccessDeniedHandler.java @@ -0,0 +1,45 @@ +package com.itools.core.ucenter.endpoint; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * @project: itools-backend + * @description: 权限不足异常类重写 + * @author: XUCHANG + * @create: 2021-06-22 13:45 + */ +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + @Autowired + private ObjectMapper objectMapper; + @Override + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) + throws IOException, ServletException { + Map map = new HashMap<>(); + map.put("returnCode",403); + map.put("success",false); + map.put("data",null); + response.setStatus(HttpStatus.OK.value()); + response.setHeader("Content-Type", "application/json;charset=UTF-8"); + try { + map.put("returnMsg","用户权限不足"); + response.getWriter().write(objectMapper.writeValueAsString(map)); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/endpoint/CustomClientAuthenticationEntryPoint.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/endpoint/CustomClientAuthenticationEntryPoint.java new file mode 100644 index 0000000000000000000000000000000000000000..ecab13755d8fc153c51693bc02524d8f844f1f1b --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/endpoint/CustomClientAuthenticationEntryPoint.java @@ -0,0 +1,37 @@ +package com.itools.core.ucenter.endpoint; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * @author xuchang + */ +@Component +public class CustomClientAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Autowired + private ObjectMapper objectMapper; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + response.setStatus(HttpStatus.OK.value()); + Map map = new HashMap(); + map.put("returnCode", HttpStatus.UNAUTHORIZED.value()); + map.put("returnMsg", "client_id或client_secret错误"); + map.put("success", false); + map.put("data", null); + response.setHeader("Content-Type", "application/json;charset=utf-8"); + + response.getWriter().print(objectMapper.writeValueAsString(map)); + response.getWriter().flush(); + } +} \ No newline at end of file diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/endpoint/CustomTokenExtractor.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/endpoint/CustomTokenExtractor.java new file mode 100644 index 0000000000000000000000000000000000000000..df0d9403ed36af9675580acbc5dc575947b5f323 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/endpoint/CustomTokenExtractor.java @@ -0,0 +1,95 @@ +package com.itools.core.ucenter.endpoint; + +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; +import org.springframework.security.oauth2.provider.authentication.TokenExtractor; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.stereotype.Component; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.util.Enumeration; +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-06-22 13:50 + */ +@Log4j2 +@Component +public class CustomTokenExtractor implements TokenExtractor { + + @Override + public Authentication extract(HttpServletRequest request) { + String tokenValue = this.extractToken(request); + if (StringUtils.isNotBlank(tokenValue)) { + return new PreAuthenticatedAuthenticationToken(tokenValue, ""); + } else { + //抛出异常 + log.info("token为空,请输入合法token"); + return null; + } + } + + public String extractToken(HttpServletRequest request) { + String token = this.extractHeaderToken(request); + if (token == null) { + log.info("Token not found in headers. Trying request parameters."); + token = request.getParameter("access_token"); + if (token == null) { + log.info("Token not found in request parameters. Trying request cookies."); + + token = extractCookieToken(request); + if (token == null) { + log.info("Token not found in cookies. Not an OAuth2 request."); + } else { + request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, "Bearer"); + } + } + } + return token; + } + + private String extractHeaderToken(HttpServletRequest request) { + Enumeration headers = request.getHeaders("Authorization"); + + String value; + do { + if (!headers.hasMoreElements()) { + return null; + } + + value = (String) headers.nextElement(); + } while (!value.toLowerCase().startsWith("Bearer".toLowerCase())); + + String authHeaderValue = value.substring("Bearer".length()).trim(); + request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, value.substring(0, "Bearer".length()).trim()); + int commaIndex = authHeaderValue.indexOf(44); + if (commaIndex > 0) { + authHeaderValue = authHeaderValue.substring(0, commaIndex); + } + if (authHeaderValue.equals("")) { + return null; + } + return authHeaderValue; + } + + private String extractCookieToken(HttpServletRequest request) { + + String cookieToken = null; + //根据请求数据,找到cookie数组 + Cookie[] cookies = request.getCookies(); + + if (null != cookies && cookies.length > 0) { + for (Cookie cookie : cookies) { + if (null != cookie.getName() && cookie.getName().trim().equalsIgnoreCase("access_token")) { + cookieToken = cookie.getValue().trim(); + break; + } + } + } + return cookieToken; + } +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/endpoint/CustomWebResponseExceptionTranslator.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/endpoint/CustomWebResponseExceptionTranslator.java new file mode 100644 index 0000000000000000000000000000000000000000..cd9860e78f987cbfa8c8bbad6cf80d527aa49181 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/endpoint/CustomWebResponseExceptionTranslator.java @@ -0,0 +1,10 @@ +package com.itools.core.ucenter.endpoint; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-07-22 16:23 + */ +public class CustomWebResponseExceptionTranslator { +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/ApplicationInfoMapper.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/ApplicationInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..c1c10997be2b6f60ffd7b80cc36711ca3669c5b7 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/ApplicationInfoMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.ApplicationInfo; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface ApplicationInfoMapper extends BaseMapper { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/AuthResourceRefMapper.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/AuthResourceRefMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..ca4c37866395bdc30287961c0ae32866ef9f441b --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/AuthResourceRefMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.AuthResourceRef; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface AuthResourceRefMapper extends BaseMapper { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/AuthorizationInfoMapper.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/AuthorizationInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..aab13cc2ee2c5852006cedc35e9187371eb3659e --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/AuthorizationInfoMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.AuthorizationInfo; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface AuthorizationInfoMapper extends BaseMapper { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/GroupInfoMapper.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/GroupInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..29cf40d73f8a5cc4aa0f209389a42805386938da --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/GroupInfoMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.GroupInfo; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface GroupInfoMapper extends BaseMapper { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/IUserInfoMapper.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/IUserInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..fc77cd61d1e1a90a81fd95e5136746120f3e0e59 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/IUserInfoMapper.java @@ -0,0 +1,19 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.pojo.SysUser; +import org.apache.ibatis.annotations.Select; +import org.springframework.stereotype.Repository; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-29 13:57 + */ +@Repository +public interface IUserInfoMapper extends BaseMapper { + + @Select("select * from tb_user where username=#{username}") + SysUser getByUsername(String username); +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/OauthClientDetailsMapper.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/OauthClientDetailsMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..99e1639bac6ac2996a45ae527dfcc5035b13ae52 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/OauthClientDetailsMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.OauthClientDetails; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface OauthClientDetailsMapper extends BaseMapper { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/OrganizationInfoMapper.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/OrganizationInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..c8e3e41af238cf352d70b729f3d03e483d0096eb --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/OrganizationInfoMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.OrganizationInfo; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface OrganizationInfoMapper extends BaseMapper { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/PermissionMapper.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/PermissionMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..400a488fbbed53691fbd8fb9a9f85c15e0dfafde --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/PermissionMapper.java @@ -0,0 +1,33 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.pojo.SysPermission; +import org.apache.ibatis.annotations.Select; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-29 13:57 + */ +@Repository +public interface PermissionMapper extends BaseMapper { + + @Select("SELECT\n" + + " p.*\n" + + "FROM\n" + + " tb_user AS u\n" + + " LEFT JOIN tb_user_role AS ur\n" + + " ON u.id = ur.user_id\n" + + " LEFT JOIN tb_role AS r\n" + + " ON r.id = ur.role_id\n" + + " LEFT JOIN tb_role_permission AS rp\n" + + " ON r.id = rp.role_id\n" + + " LEFT JOIN tb_permission AS p\n" + + " ON p.id = rp.permission_id\n" + + "WHERE u.id = #{userId}") + List selectByUserId(Long userId); +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/ResourceInfoMapper.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/ResourceInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..422f072a48a9fad3e1b651920d9149b33c220e5f --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/ResourceInfoMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.ResourceInfo; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface ResourceInfoMapper extends BaseMapper { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/RoleAuthorizationRefMapper.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/RoleAuthorizationRefMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..e5a812822746e8fc161582e20a500cd098e54ae6 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/RoleAuthorizationRefMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.RoleAuthorizationRef; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface RoleAuthorizationRefMapper extends BaseMapper { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/RoleInfoMapper.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/RoleInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..7bf6ae2a9f43bb55101fe84c69a1567a1c213004 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/RoleInfoMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.RoleInfo; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface RoleInfoMapper extends BaseMapper { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/RoleResourceRefMapper.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/RoleResourceRefMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..e72b56b62e7e0a3d9e1421b5c04fff46b3e296ee --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/RoleResourceRefMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.RoleResourceRef; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface RoleResourceRefMapper extends BaseMapper { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/UserInfoMapper.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/UserInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..4901cf9416e3c5f3dacc9510d65972c5f948a6c2 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/UserInfoMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.UserInfo; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface UserInfoMapper extends BaseMapper { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/UserRoleRefMapper.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/UserRoleRefMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..17cdc1d83f80f6e14b0af08952a839e48cc0d725 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/mapper/UserRoleRefMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.UserRoleRef; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface UserRoleRefMapper extends BaseMapper { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/ApplicationInfoService.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/ApplicationInfoService.java new file mode 100644 index 0000000000000000000000000000000000000000..62fcd92d4fcd043b5e614721bbe6e9ab080064ef --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/ApplicationInfoService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.ApplicationInfo; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface ApplicationInfoService extends IService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/AuthResourceRefService.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/AuthResourceRefService.java new file mode 100644 index 0000000000000000000000000000000000000000..c46676969721732c5458f4a009df807443f6625c --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/AuthResourceRefService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.AuthResourceRef; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface AuthResourceRefService extends IService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/AuthorizationInfoService.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/AuthorizationInfoService.java new file mode 100644 index 0000000000000000000000000000000000000000..14d3939ac0e67a4ddf3a14abf356903193305c9d --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/AuthorizationInfoService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.AuthorizationInfo; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface AuthorizationInfoService extends IService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/GroupInfoService.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/GroupInfoService.java new file mode 100644 index 0000000000000000000000000000000000000000..b102b6e7cfa7edda53fded53e9a41aa38b8b76a2 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/GroupInfoService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.GroupInfo; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface GroupInfoService extends IService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/OauthClientDetailsService.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/OauthClientDetailsService.java new file mode 100644 index 0000000000000000000000000000000000000000..c1f5d4d8c0e4bf8f09210c82380b978073ac545d --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/OauthClientDetailsService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.OauthClientDetails; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface OauthClientDetailsService extends IService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/OrganizationInfoService.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/OrganizationInfoService.java new file mode 100644 index 0000000000000000000000000000000000000000..6ddd423af90e39f6f614ce1a28739e87a69cfa64 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/OrganizationInfoService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.OrganizationInfo; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface OrganizationInfoService extends IService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/ResourceInfoService.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/ResourceInfoService.java new file mode 100644 index 0000000000000000000000000000000000000000..bd22c098bb542e5ebd1e9b00f9680741f062e329 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/ResourceInfoService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.ResourceInfo; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface ResourceInfoService extends IService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/RoleAuthorizationRefService.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/RoleAuthorizationRefService.java new file mode 100644 index 0000000000000000000000000000000000000000..72acdfa243393c9484fb9181fa42ddedd372d125 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/RoleAuthorizationRefService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.RoleAuthorizationRef; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface RoleAuthorizationRefService extends IService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/RoleInfoService.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/RoleInfoService.java new file mode 100644 index 0000000000000000000000000000000000000000..d98171cf62cc0c0cb4930143df0d01ab0bd60505 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/RoleInfoService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.RoleInfo; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface RoleInfoService extends IService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/RoleResourceRefService.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/RoleResourceRefService.java new file mode 100644 index 0000000000000000000000000000000000000000..80ec9853561867d82da81180cf5686c4613f66f0 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/RoleResourceRefService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.RoleResourceRef; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface RoleResourceRefService extends IService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/UserInfoService.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/UserInfoService.java new file mode 100644 index 0000000000000000000000000000000000000000..91d0e9c4f843e800556bf4c1ca1881645d2dfad6 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/UserInfoService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.UserInfo; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface UserInfoService extends IService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/UserRoleRefService.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/UserRoleRefService.java new file mode 100644 index 0000000000000000000000000000000000000000..88da731d614b07db80028f8512d9deddc88ee589 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/UserRoleRefService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.UserRoleRef; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface UserRoleRefService extends IService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/UserService.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/UserService.java new file mode 100644 index 0000000000000000000000000000000000000000..bb147f30e470d92820e2d199ce39636e8158afde --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/UserService.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.service; + +import com.itools.core.ucenter.pojo.SysUser; +import org.springframework.security.core.userdetails.UserDetailsService; + +import java.util.Map; + +public interface UserService extends UserDetailsService { + + SysUser getByUsername(String username); + + /** + * 验证密码模式的账号和客户端应用 + * @param parameters + * @return + */ + boolean verifyUserByPasswordType(Map parameters); +} \ No newline at end of file diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/ApplicationInfoServiceImpl.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/ApplicationInfoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..05c81edb094462a95103725978cc92888ceff480 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/ApplicationInfoServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.ApplicationInfo; +import com.itools.core.ucenter.mapper.ApplicationInfoMapper; +import com.itools.core.ucenter.service.ApplicationInfoService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class ApplicationInfoServiceImpl extends ServiceImpl implements ApplicationInfoService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/AuthResourceRefServiceImpl.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/AuthResourceRefServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..90420f86ec6fb82cbe9f13cfd23e0efa5ffec7c2 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/AuthResourceRefServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.AuthResourceRef; +import com.itools.core.ucenter.mapper.AuthResourceRefMapper; +import com.itools.core.ucenter.service.AuthResourceRefService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class AuthResourceRefServiceImpl extends ServiceImpl implements AuthResourceRefService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/AuthorizationInfoServiceImpl.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/AuthorizationInfoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..f8424de0f497dea6738a1d618dab1f02a920b471 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/AuthorizationInfoServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.AuthorizationInfo; +import com.itools.core.ucenter.mapper.AuthorizationInfoMapper; +import com.itools.core.ucenter.service.AuthorizationInfoService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class AuthorizationInfoServiceImpl extends ServiceImpl implements AuthorizationInfoService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/GroupInfoServiceImpl.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/GroupInfoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..2e4d74a1aea0671b109af2e9d9591cafdfc6d8cc --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/GroupInfoServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.GroupInfo; +import com.itools.core.ucenter.mapper.GroupInfoMapper; +import com.itools.core.ucenter.service.GroupInfoService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class GroupInfoServiceImpl extends ServiceImpl implements GroupInfoService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/OauthClientDetailsServiceImpl.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/OauthClientDetailsServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..271cfae3f8a1af4b5e805c6b8d1248792874a353 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/OauthClientDetailsServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.OauthClientDetails; +import com.itools.core.ucenter.mapper.OauthClientDetailsMapper; +import com.itools.core.ucenter.service.OauthClientDetailsService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class OauthClientDetailsServiceImpl extends ServiceImpl implements OauthClientDetailsService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/OrganizationInfoServiceImpl.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/OrganizationInfoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..da726b888af9ea2653464f25d75db71d37c158d1 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/OrganizationInfoServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.OrganizationInfo; +import com.itools.core.ucenter.mapper.OrganizationInfoMapper; +import com.itools.core.ucenter.service.OrganizationInfoService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class OrganizationInfoServiceImpl extends ServiceImpl implements OrganizationInfoService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/ResourceInfoServiceImpl.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/ResourceInfoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..454c3beed8b1b6859c8d8ac2883ecfec6da32e27 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/ResourceInfoServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.ResourceInfo; +import com.itools.core.ucenter.mapper.ResourceInfoMapper; +import com.itools.core.ucenter.service.ResourceInfoService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class ResourceInfoServiceImpl extends ServiceImpl implements ResourceInfoService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/RoleAuthorizationRefServiceImpl.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/RoleAuthorizationRefServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..c60b2b7d948aeece148e28e6df2c1bf77c2b47f2 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/RoleAuthorizationRefServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.RoleAuthorizationRef; +import com.itools.core.ucenter.mapper.RoleAuthorizationRefMapper; +import com.itools.core.ucenter.service.RoleAuthorizationRefService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class RoleAuthorizationRefServiceImpl extends ServiceImpl implements RoleAuthorizationRefService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/RoleInfoServiceImpl.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/RoleInfoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..982fb2a721d4e3c499a909f152844a3cc2755474 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/RoleInfoServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.RoleInfo; +import com.itools.core.ucenter.mapper.RoleInfoMapper; +import com.itools.core.ucenter.service.RoleInfoService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class RoleInfoServiceImpl extends ServiceImpl implements RoleInfoService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/RoleResourceRefServiceImpl.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/RoleResourceRefServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..ada1080ae68c0f6a82496ce2800aac9d97029f7e --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/RoleResourceRefServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.RoleResourceRef; +import com.itools.core.ucenter.mapper.RoleResourceRefMapper; +import com.itools.core.ucenter.service.RoleResourceRefService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class RoleResourceRefServiceImpl extends ServiceImpl implements RoleResourceRefService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/UserInfoServiceImpl.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/UserInfoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..04a9000d5a46902d93869e40d71ce578fbeaffee --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/UserInfoServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.UserInfo; +import com.itools.core.ucenter.mapper.UserInfoMapper; +import com.itools.core.ucenter.service.UserInfoService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class UserInfoServiceImpl extends ServiceImpl implements UserInfoService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/UserRoleRefServiceImpl.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/UserRoleRefServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..b02270a8213c3c7d6a4353eb21900b905ebcf291 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/UserRoleRefServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.UserRoleRef; +import com.itools.core.ucenter.mapper.UserRoleRefMapper; +import com.itools.core.ucenter.service.UserRoleRefService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class UserRoleRefServiceImpl extends ServiceImpl implements UserRoleRefService { + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/UserServiceImpl.java b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/UserServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a72bf6f44c53c0f422d8bb3a6f688e0621ac23d8 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/java/com/itools/core/ucenter/service/impl/UserServiceImpl.java @@ -0,0 +1,100 @@ +package com.itools.core.ucenter.service.impl; + + +import com.itools.core.ucenter.em.LoginTypeEnum; +import com.itools.core.ucenter.em.UcenterCodeBean; +import com.itools.core.exception.AppException; +import com.itools.core.ucenter.mapper.PermissionMapper; +import com.itools.core.ucenter.mapper.IUserInfoMapper; +import com.itools.core.ucenter.pojo.SysPermission; +import com.itools.core.ucenter.pojo.SysUser; +import com.itools.core.ucenter.service.UserService; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-03-29 14:00 + */ +@Service("userDetailsService") +@Log4j2 +public class UserServiceImpl implements UserService { + + @Autowired + private IUserInfoMapper IUserInfoMapper; + @Autowired + private PermissionMapper permissionMapper; + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + public SysUser getByUsername(String username) { + return IUserInfoMapper.getByUsername(username); + } + + /** + * 验证密码模式的用户账号、应用 + * @param parameters + * @return + */ + @Override + public boolean verifyUserByPasswordType(Map parameters) { + + if (!parameters.containsKey("grant_type") || !parameters.get("grant_type").equals(LoginTypeEnum.PASSWORD.getName())){ + return false; + } + if (!parameters.containsKey("username") || !parameters.containsKey("password")){ + log.error("账号密码未输入"); + throw new AppException(UcenterCodeBean.UcenterCode.FAIL_USERNAME_PASSWORD.code); + } + String username = parameters.get("username"); + String password = parameters.get("password"); + + SysUser sysUser = getByUsername(username); + if (sysUser == null){ + log.error("账号不存在或账号密码错误"); + throw new AppException(UcenterCodeBean.UcenterCode.USERNAME_NOT_FOUND_EXCEPTION.code); + } + + if(!passwordEncoder.matches(password,sysUser.getPassword())){ + log.error("账号不存在或账号密码错误"); + throw new AppException(UcenterCodeBean.UcenterCode.USERNAME_NOT_FOUND_EXCEPTION.code); + } + + return true; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + SysUser user = getByUsername(username); + if (user==null){ + log.error("账号不存在或账号密码错误"); + throw new UsernameNotFoundException("账号不存在或账号密码错误"); + } + List authorities = new ArrayList<>(); + List permissions = permissionMapper.selectByUserId(user.getId()); + + permissions.forEach(permission -> { + if (permission!=null && !StringUtils.isEmpty(permission.getEnname())){ + GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getEnname()); + authorities.add(grantedAuthority); + } + }); + return new User(user.getUsername(),user.getPassword(),authorities); + } + +} diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/application-dev.yml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/application-dev.yml new file mode 100644 index 0000000000000000000000000000000000000000..ea5cc0a6cf549f3fe4ae938829e7b748932b1a5d --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/application-dev.yml @@ -0,0 +1,50 @@ + +########################################## 组件配置 ###################################################### +system: + errorCode: Code001 +#分布式雪花算法策略 +sequence: + enable: true + type: snowflake + generate: simple +########################################## 组件配置 ###################################################### + + +########################################## mybatis-plus相关配置 ###################################################### +mybatis-plus: + mapper-locations: classpath:/com.itools.mapper/**/*Mapper.xml + typeAliasesPackage: com.itools.core + global-config: + db-column-underline: true + db-config: + logic-delete-field: del_flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + configuration: + default-enum-type-handler: com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +########################################## mybatis-plus相关配置 ###################################################### + + +########################################### 用户中心放行接口 ##################################################### +#secure: +# ignored: +# shouldSkipUrls: +# - /user/test +#jwt: +# tokenHeader: Authorization #JWT存储的请求头 +# secret: 123123 #JWT加解密使用的密钥 +# expiration: 604800 #JWT的超期限时间(60*60*24) +# tokenHead: bearer #JWT负载中拿到开头 +# +#security: +# oauth2: +# client: +# access-token-uri: http://localhost:17003/oauth/token +# user-authorization-uri: http://localhost:17003/oauth/authorize +# # client-id: webapp +# resource: +# user-info-uri: http://localhost:17003/user +# prefer-token-info: false +########################################### 用户中心放行接口 ##################################################### \ No newline at end of file diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/application.yml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..b89c8ab88f575aad9baf9e099ddc8e3799cfd165 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/application.yml @@ -0,0 +1,28 @@ +server: + port: 17003 +spring: + application: + name: itools-oms-auth-server + + redis: + host: 127.0.0.1 + database: 0 + + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/cloudauth?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC + username: root + password: root + hikari: + minimum-idle: 5 + idle-timeout: 600000 + maximum-pool-size: 10 + auto-commit: true + pool-name: MyHikariCP + max-lifetime: 1800000 + connection-timeout: 30000 + connection-test-query: SELECT 1 + main: + allow-bean-definition-overriding: true + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/bootstrap.yml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/bootstrap.yml new file mode 100644 index 0000000000000000000000000000000000000000..56e3eb83288da40dfdacd24ca651a7e16ca7ebbd --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/bootstrap.yml @@ -0,0 +1,18 @@ +# Nacos Server 的地址 +spring: + profiles: + active: dev + cloud: + nacos: + config: + server-addr: 106.54.85.156:8848 + file-extension: yaml + prefix: itools-oms-server + remote-first: true + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev + discovery: + server-addr: 106.54.85.156:8848 + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/ApplicationInfoMapper.xml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/ApplicationInfoMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..2bf3e42b383a43c231ca57fc5dcdd1e4fae42ce3 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/ApplicationInfoMapper.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + id, app_name, app_instance_id, app_owner, status, create_time, create_user_id, last_update_time, last_update_user_id, app_code, app_owner_email, remark, app_type + + + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/AuthResourceRefMapper.xml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/AuthResourceRefMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..ad003d280453b8db3be647cc0335ee4111bc6b51 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/AuthResourceRefMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + id, authorization_id, resource_id, type, status, create_time, create_user_id, last_update_time, last_update_user_id, application_id + + + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/AuthorizationInfoMapper.xml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/AuthorizationInfoMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a1bde8eb82772a2dd79097d4dc55f7a7bd78e66f --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/AuthorizationInfoMapper.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + id, name, parent_id, remark, status, create_time, create_user_id, last_update_time, last_update_user_id, application_id, code, order_filed + + + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/GroupInfoMapper.xml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/GroupInfoMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..b138e0d6c5f33fe7444fb2c6e51e3de551faece1 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/GroupInfoMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + id, create_time, create_user_id, last_update_time, last_update_user_id, application_id, code, name, remark, status + + + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/OauthClientDetailsMapper.xml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/OauthClientDetailsMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f8130239a9c49d88b02fa33591e4a9e33a4f447 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/OauthClientDetailsMapper.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + client_id, resource_ids, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove + + + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/OrganizationInfoMapper.xml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/OrganizationInfoMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..61202bc10475c37615f25012043c28d0f7a18846 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/OrganizationInfoMapper.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id, name, parent_id, create_time, create_user_id, last_update_time, last_update_user_id, address, application_id, code, link_man_dept, link_man_email, link_man_name, link_man_phone, rank, remark, status, phone, type, organization_number, admin_user_id, order_field + + + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/ResourceInfoMapper.xml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/ResourceInfoMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..53ce05770ef92fefb902f596a583fdd649b3b204 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/ResourceInfoMapper.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + id, name, parent_id, status, create_time, create_user_id, last_update_time, last_update_user_id, application_id, code, order_field, picture_name, remark, type, url + + + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/RoleAuthorizationRefMapper.xml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/RoleAuthorizationRefMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..6bc3bfe367ec07f47206cf38365a2d18e0fac7a4 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/RoleAuthorizationRefMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + id, create_time, create_user_id, last_update_time, last_update_user_id, application_id, authorization_id, role_id, status + + + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/RoleInfoMapper.xml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/RoleInfoMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..0580a4eac879e357690861cbc8ee94e65da292a0 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/RoleInfoMapper.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + id, parent_id, name, application_id, remark, code, create_time, create_user_id, last_update_time, last_update_user_id, status, order_field, type + + + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/RoleResourceRefMapper.xml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/RoleResourceRefMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..383ace40bc9369f0f246cc476eac7ae322601453 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/RoleResourceRefMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + id, create_time, create_user_id, last_update_time, last_update_user_id, application_id, role_id, resource_id, status, type + + + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/UserInfoMapper.xml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/UserInfoMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..6652f70028a8f503c0847bba3407d26503c15eca --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/UserInfoMapper.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id, create_time, create_user_id, last_update_time, last_update_user_id, company_id, org_id, name, user_type, sex, birthday, email, position, remark, phone, user_code, user_level, face_url, job_number, identifier, identity_type, password, last_login_time + + + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/UserRoleRefMapper.xml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/UserRoleRefMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..faee6a60015f8b2699ec0b9d1b9e362655bfe2ba --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/com/itools/mapper/UserRoleRefMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + id, application_id, create_time, create_user_id, last_update_time, last_update_user_id, role_id, user_id, type + + + diff --git a/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/logback-spring.xml b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/logback-spring.xml new file mode 100644 index 0000000000000000000000000000000000000000..84b713e214bf711e7ac0cbe16a32ad2b3b061569 --- /dev/null +++ b/itools-ucenter-oauth2/itools-ucenter-oauth2-server/src/main/resources/logback-spring.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/itools-ucenter-oauth2/pom.xml b/itools-ucenter-oauth2/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f75b05685e91d26f9d644c0e92b155adb1ab8788 --- /dev/null +++ b/itools-ucenter-oauth2/pom.xml @@ -0,0 +1,122 @@ + + + + 4.0.0 + + itools-ucenter-oauth2 + com.itools.core + 1.0-SNAPSHOT + pom + + itools-ucenter-oauth2-server + itools-ucenter-oauth2-core + itools-ucenter-oauth2-model + + + + true + http://192.168.6.132:2375 + 1.1.0 + 1.8 + 1.2.10 + 5.1.8 + 1.1.10 + 4.5.7 + 2.7.0 + 1.3.7 + 3.4.6 + 8.0.15 + 2.1.5.RELEASE + 0.9.0 + 2.5.0 + 5.3 + 2.1.2 + 3.3.2 + Hoxton.RELEASE + 2.2.0.RELEASE + 2.2.0.RELEASE + + + + + org.springframework.boot + spring-boot-starter-parent + ${spring.boot.version} + pom + import + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${spring-cloud-alibaba.version} + pom + import + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + ${pagehelper-starter.version} + + + + com.github.pagehelper + pagehelper + ${pagehelper.version} + + + + cn.hutool + hutool-all + ${hutool.version} + + + + io.springfox + springfox-swagger2 + ${swagger2.version} + + + io.springfox + springfox-swagger-ui + ${swagger2.version} + + + + + mysql + mysql-connector-java + ${mysql-connector.version} + + + + io.jsonwebtoken + jjwt + ${jjwt.version} + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + com.baomidou + mybatis-plus-generator + ${mybatis-plus.version} + + + + \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-client01/pom.xml b/itools-ucenter-sso/itools-ucenter-sso-client01/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c90f518554074ff62fc69f9208049f066b518495 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-client01/pom.xml @@ -0,0 +1,92 @@ + + + + itools-ucenter-sso + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-ucenter-sso-client01 + + + com.itools.core + itools-ucenter-sso-model + 1.0-SNAPSHOT + + + com.itools.core + itools-ucenter-sso-core + 1.0-SNAPSHOT + + + com.itools.core + itools-common + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + + + com.baomidou + mybatis-plus-boot-starter + + + + + mysql + mysql-connector-java + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + + src/main/resources + true + + **/*.jks + + + + src/main/resources + false + + **/*.jks + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/java/com/itools/core/ucenter/UcenterSsoClientApplication.java b/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/java/com/itools/core/ucenter/UcenterSsoClientApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..28e353ae00241c5e88ca284f61971d6e34ae0d53 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/java/com/itools/core/ucenter/UcenterSsoClientApplication.java @@ -0,0 +1,14 @@ +package com.itools.core.ucenter; + + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UcenterSsoClientApplication { + + public static void main(String[] args) { + SpringApplication.run(UcenterSsoClientApplication.class, args); + } + +} \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/java/com/itools/core/ucenter/config/XxlSsoConfig.java b/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/java/com/itools/core/ucenter/config/XxlSsoConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8acc021012110effd628e6a04ac6cd7d8e9f0414 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/java/com/itools/core/ucenter/config/XxlSsoConfig.java @@ -0,0 +1,58 @@ +package com.itools.core.ucenter.config; + + +import com.itools.core.ucenter.conf.Conf; +import com.itools.core.ucenter.filter.SsoTokenFilter; +import com.itools.core.ucenter.util.JedisUtil; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +@Configuration +public class XxlSsoConfig implements DisposableBean { + + + @Value("${xxl.sso.server}") + private String xxlSsoServer; + + @Value("${xxl.sso.logout.path}") + private String xxlSsoLogoutPath; + + @Value("${xxl.sso.redis.address}") + private String xxlSsoRedisAddress; + + @Value("${xxl-sso.excluded.paths}") + private String xxlSsoExcludedPaths; + + + @Bean + public FilterRegistrationBean xxlSsoFilterRegistration() { + + // xxl-sso, redis init + JedisUtil.init(xxlSsoRedisAddress); + + // xxl-sso, filter init + FilterRegistrationBean registration = new FilterRegistrationBean(); + + registration.setName("XxlSsoWebFilter"); + registration.setOrder(1); + registration.addUrlPatterns("/*"); + registration.setFilter(new SsoTokenFilter()); + registration.addInitParameter(Conf.SSO_SERVER, xxlSsoServer); + registration.addInitParameter(Conf.SSO_LOGOUT_PATH, xxlSsoLogoutPath); + registration.addInitParameter(Conf.SSO_EXCLUDED_PATHS, xxlSsoExcludedPaths); + + return registration; + } + + @Override + public void destroy() throws Exception { + + // xxl-sso, redis close + JedisUtil.close(); + } + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/java/com/itools/core/ucenter/controller/IndexController.java b/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/java/com/itools/core/ucenter/controller/IndexController.java new file mode 100644 index 0000000000000000000000000000000000000000..277c5cacada0b6988d2bf78d6dd2f6f19620cfdd --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/java/com/itools/core/ucenter/controller/IndexController.java @@ -0,0 +1,24 @@ +package com.itools.core.ucenter.controller; + + +import com.itools.core.ucenter.conf.Conf; +import com.itools.core.ucenter.entity.ReturnT; +import com.itools.core.ucenter.user.XxlSsoUser; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.servlet.http.HttpServletRequest; + + +@Controller +public class IndexController { + + @RequestMapping("/") + @ResponseBody + public ReturnT index(HttpServletRequest request) { + XxlSsoUser xxlUser = (XxlSsoUser) request.getAttribute(Conf.SSO_USER); + return new ReturnT(xxlUser); + } + +} \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/resources/application-dev.yml b/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/resources/application-dev.yml new file mode 100644 index 0000000000000000000000000000000000000000..243ba39c7eb8eb4a794fea8296043b63ab4bd0f6 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/resources/application-dev.yml @@ -0,0 +1,50 @@ + +########################################## 组件配置 ###################################################### +system: + errorCode: Code001 +#分布式雪花算法策略 +sequence: + enable: true + type: snowflake + generate: simple +########################################## 组件配置 ###################################################### + + +########################################## mybatis-plus相关配置 ###################################################### +#mybatis-plus: +# mapper-locations: classpath:/com.itools.mapper/**/*Mapper.xml +# typeAliasesPackage: com.itools.core +# global-config: +# db-column-underline: true +# db-config: +# logic-delete-field: del_flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) +# logic-delete-value: 1 # 逻辑已删除值(默认为 1) +# logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) +# configuration: +# default-enum-type-handler: com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler +# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +########################################## mybatis-plus相关配置 ###################################################### + + +########################################### 用户中心放行接口 ##################################################### +#secure: +# ignored: +# shouldSkipUrls: +# - /user/test +#jwt: +# tokenHeader: Authorization #JWT存储的请求头 +# secret: 123123 #JWT加解密使用的密钥 +# expiration: 604800 #JWT的超期限时间(60*60*24) +# tokenHead: bearer #JWT负载中拿到开头 +# +#security: +# oauth2: +# client: +# access-token-uri: http://localhost:17003/oauth/token +# user-authorization-uri: http://localhost:17003/oauth/authorize +# # client-id: webapp +# resource: +# user-info-uri: http://localhost:17003/user +# prefer-token-info: false +########################################### 用户中心放行接口 ##################################################### \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/resources/application.properties b/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..d9734c113abc169280c607ecda61fffc958184e8 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/resources/application.properties @@ -0,0 +1,7 @@ + + +### xxl-sso +xxl.sso.server=http://127.0.0.1:20001/xxl-sso-server +xxl.sso.logout.path=/logout +xxl-sso.excluded.paths= +xxl.sso.redis.address=redis://127.0.0.1:6379 diff --git a/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/resources/application.yml b/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..e10070f4659c8a296459c93e645091ea244d3602 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/resources/application.yml @@ -0,0 +1,30 @@ +server: + port: 20002 + servlet: + context-path: /ucenter-client01 +spring: + application: + name: itools-ucnter-sso-client01 + + redis: + host: 127.0.0.1 + database: 0 + + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/cloudauth?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC + username: root + password: root + hikari: + minimum-idle: 5 + idle-timeout: 600000 + maximum-pool-size: 10 + auto-commit: true + pool-name: MyHikariCP + max-lifetime: 1800000 + connection-timeout: 30000 + connection-test-query: SELECT 1 + main: + allow-bean-definition-overriding: true + diff --git a/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/resources/bootstrap.yml b/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/resources/bootstrap.yml new file mode 100644 index 0000000000000000000000000000000000000000..56e3eb83288da40dfdacd24ca651a7e16ca7ebbd --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/resources/bootstrap.yml @@ -0,0 +1,18 @@ +# Nacos Server 的地址 +spring: + profiles: + active: dev + cloud: + nacos: + config: + server-addr: 106.54.85.156:8848 + file-extension: yaml + prefix: itools-oms-server + remote-first: true + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev + discovery: + server-addr: 106.54.85.156:8848 + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev + diff --git a/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/resources/logback-spring.xml b/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/resources/logback-spring.xml new file mode 100644 index 0000000000000000000000000000000000000000..84b713e214bf711e7ac0cbe16a32ad2b3b061569 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-client01/src/main/resources/logback-spring.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-core/pom.xml b/itools-ucenter-sso/itools-ucenter-sso-core/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..af86427c12a52fbc1da31a0a1996007b310c53e5 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-core/pom.xml @@ -0,0 +1,98 @@ + + + + itools-ucenter-sso + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-ucenter-sso-core + + + + com.itools.core + itools-ucenter-sso-model + 1.0-SNAPSHOT + + + com.itools.core + itools-common + 1.0-SNAPSHOT + + + + javax.servlet + javax.servlet-api + ${javax.servlet-api.version} + + + + redis.clients + jedis + ${jedis.version} + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + + + com.baomidou + mybatis-plus-boot-starter + + + + + mysql + mysql-connector-java + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + + src/main/resources + true + + **/*.jks + + + + src/main/resources + false + + **/*.jks + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/conf/Conf.java b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/conf/Conf.java new file mode 100644 index 0000000000000000000000000000000000000000..bde36d8de7b771ee01006939d6d24a5c21c70095 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/conf/Conf.java @@ -0,0 +1,62 @@ +package com.itools.core.ucenter.conf; + +import com.itools.core.ucenter.entity.ReturnT; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 12:51 + */ +public class Conf { + + /** + * sso sessionid, between browser and sso-server (web + token client) + */ + public static final String SSO_SESSIONID = "xxl_sso_sessionid"; + + + /** + * redirect url (web client) + */ + public static final String REDIRECT_URL = "redirect_url"; + + /** + * sso user, request attribute (web client) + */ + public static final String SSO_USER = "xxl_sso_user"; + + + /** + * sso server address (web + token client) + */ + public static final String SSO_SERVER = "sso_server"; + + /** + * login url, server relative path (web client) + */ + public static final String SSO_LOGIN = "/login"; + /** + * logout url, server relative path (web client) + */ + public static final String SSO_LOGOUT = "/logout"; + + + /** + * logout path, client relatice path + */ + public static final String SSO_LOGOUT_PATH = "SSO_LOGOUT_PATH"; + + /** + * excluded paths, client relatice path, include path can be set by "filter-mapping" + */ + public static final String SSO_EXCLUDED_PATHS = "SSO_EXCLUDED_PATHS"; + + + /** + * login fail result + */ + public static final ReturnT SSO_LOGIN_FAIL_RESULT = new ReturnT(501, "sso not login."); + + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/entity/ReturnT.java b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/entity/ReturnT.java new file mode 100644 index 0000000000000000000000000000000000000000..798df562142d705daf2d9b61bb9e09c635d7c002 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/entity/ReturnT.java @@ -0,0 +1,51 @@ +package com.itools.core.ucenter.entity; + +import java.io.Serializable; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 12:51 + */ +public class ReturnT implements Serializable { + public static final long serialVersionUID = 42L; + + public static final int SUCCESS_CODE = 200; + public static final int FAIL_CODE = 500; + public static final ReturnT SUCCESS = new ReturnT(null); + public static final ReturnT FAIL = new ReturnT(FAIL_CODE, null); + + private int code; + private String msg; + private T data; + + public ReturnT(int code, String msg) { + this.code = code; + this.msg = msg; + } + public ReturnT(T data) { + this.code = SUCCESS_CODE; + this.data = data; + } + + public int getCode() { + return code; + } + public void setCode(int code) { + this.code = code; + } + public String getMsg() { + return msg; + } + public void setMsg(String msg) { + this.msg = msg; + } + public T getData() { + return data; + } + public void setData(T data) { + this.data = data; + } + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/exception/XxlSsoException.java b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/exception/XxlSsoException.java new file mode 100644 index 0000000000000000000000000000000000000000..aa6a3b91b7ac0028fb7caf60dbeaa8dc13bb33b7 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/exception/XxlSsoException.java @@ -0,0 +1,25 @@ +package com.itools.core.ucenter.exception; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 12:51 + */ +public class XxlSsoException extends RuntimeException { + + private static final long serialVersionUID = 42L; + + public XxlSsoException(String msg) { + super(msg); + } + + public XxlSsoException(String msg, Throwable cause) { + super(msg, cause); + } + + public XxlSsoException(Throwable cause) { + super(cause); + } + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/filter/SsoTokenFilter.java b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/filter/SsoTokenFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..30d112c6441bda710e846eb7d0f31c27451afbae --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/filter/SsoTokenFilter.java @@ -0,0 +1,102 @@ +package com.itools.core.ucenter.filter; + +import com.itools.core.ucenter.conf.Conf; +import com.itools.core.ucenter.entity.ReturnT; +import com.itools.core.ucenter.login.SsoTokenLoginHelper; +import com.itools.core.ucenter.path.impl.AntPathMatcher; +import com.itools.core.ucenter.user.XxlSsoUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.*; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 12:51 + */ +public class SsoTokenFilter extends HttpServlet implements Filter { + private static Logger logger = LoggerFactory.getLogger(SsoTokenFilter.class); + + private static final AntPathMatcher antPathMatcher = new AntPathMatcher(); + + private String ssoServer; + private String logoutPath; + private String excludedPaths; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + ssoServer = filterConfig.getInitParameter(Conf.SSO_SERVER); + logoutPath = filterConfig.getInitParameter(Conf.SSO_LOGOUT_PATH); + excludedPaths = filterConfig.getInitParameter(Conf.SSO_EXCLUDED_PATHS); + + logger.info("XxlSsoTokenFilter init."); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse res = (HttpServletResponse) response; + + // make url + String servletPath = req.getServletPath(); + + // excluded path check + if (excludedPaths!=null && excludedPaths.trim().length()>0) { + for (String excludedPath:excludedPaths.split(",")) { + String uriPattern = excludedPath.trim(); + + // 支持ANT表达式 + if (antPathMatcher.match(uriPattern, servletPath)) { + // excluded path, allow + chain.doFilter(request, response); + return; + } + + } + } + + // logout filter + if (logoutPath!=null + && logoutPath.trim().length()>0 + && logoutPath.equals(servletPath)) { + + // logout + SsoTokenLoginHelper.logout(req); + + // response + res.setStatus(HttpServletResponse.SC_OK); + res.setContentType("application/json;charset=UTF-8"); + res.getWriter().println("{\"code\":"+ ReturnT.SUCCESS_CODE+", \"msg\":\"\"}"); + + return; + } + + // login filter + XxlSsoUser xxlUser = SsoTokenLoginHelper.loginCheck(req); + if (xxlUser == null) { + + // response + res.setStatus(HttpServletResponse.SC_OK); + res.setContentType("application/json;charset=UTF-8"); + res.getWriter().println("{\"code\":"+Conf.SSO_LOGIN_FAIL_RESULT.getCode()+", \"msg\":\""+ Conf.SSO_LOGIN_FAIL_RESULT.getMsg() +"\"}"); + return; + } + + // ser sso user + request.setAttribute(Conf.SSO_USER, xxlUser); + + + // already login, allow + chain.doFilter(request, response); + return; + } + + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/filter/SsoWebFilter.java b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/filter/SsoWebFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..e398073571b95cf840d7290e38cec6326515d1c4 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/filter/SsoWebFilter.java @@ -0,0 +1,118 @@ +package com.itools.core.ucenter.filter; + + +import com.itools.core.ucenter.conf.Conf; +import com.itools.core.ucenter.login.SsoWebLoginHelper; +import com.itools.core.ucenter.path.impl.AntPathMatcher; +import com.itools.core.ucenter.user.XxlSsoUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.*; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 12:51 + */ +public class SsoWebFilter extends HttpServlet implements Filter { + private static Logger logger = LoggerFactory.getLogger(SsoWebFilter.class); + + private static final AntPathMatcher antPathMatcher = new AntPathMatcher(); + + private String ssoServer; + private String logoutPath; + private String excludedPaths; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + ssoServer = filterConfig.getInitParameter(Conf.SSO_SERVER); + logoutPath = filterConfig.getInitParameter(Conf.SSO_LOGOUT_PATH); + excludedPaths = filterConfig.getInitParameter(Conf.SSO_EXCLUDED_PATHS); + + logger.info("XxlSsoWebFilter init."); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse res = (HttpServletResponse) response; + + // make url + String servletPath = req.getServletPath(); + + // excluded path check + if (excludedPaths!=null && excludedPaths.trim().length()>0) { + for (String excludedPath:excludedPaths.split(",")) { + String uriPattern = excludedPath.trim(); + + // 支持ANT表达式 + if (antPathMatcher.match(uriPattern, servletPath)) { + // excluded path, allow + chain.doFilter(request, response); + return; + } + + } + } + + // logout path check + if (logoutPath!=null + && logoutPath.trim().length()>0 + && logoutPath.equals(servletPath)) { + + // remove cookie + SsoWebLoginHelper.removeSessionIdByCookie(req, res); + + // redirect logout + String logoutPageUrl = ssoServer.concat(Conf.SSO_LOGOUT); + res.sendRedirect(logoutPageUrl); + + return; + } + + // valid login user, cookie + redirect + XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(req, res); + + // valid login fail + if (xxlUser == null) { + + String header = req.getHeader("content-type"); + boolean isJson= header!=null && header.contains("json"); + if (isJson) { + + // json msg + res.setContentType("application/json;charset=utf-8"); + res.getWriter().println("{\"code\":"+Conf.SSO_LOGIN_FAIL_RESULT.getCode()+", \"msg\":\""+ Conf.SSO_LOGIN_FAIL_RESULT.getMsg() +"\"}"); + return; + } else { + + // total link + String link = req.getRequestURL().toString(); + + // redirect logout + String loginPageUrl = ssoServer.concat(Conf.SSO_LOGIN) + + "?" + Conf.REDIRECT_URL + "=" + link; + + res.sendRedirect(loginPageUrl); + return; + } + + } + + // ser sso user + request.setAttribute(Conf.SSO_USER, xxlUser); + + + // already login, allow + chain.doFilter(request, response); + return; + } + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/login/SsoTokenLoginHelper.java b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/login/SsoTokenLoginHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..e270f2029939f9e3318c9ebe48ef3845e466735f --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/login/SsoTokenLoginHelper.java @@ -0,0 +1,103 @@ +package com.itools.core.ucenter.login; + + +import com.itools.core.ucenter.conf.Conf; +import com.itools.core.ucenter.store.SsoLoginStore; +import com.itools.core.ucenter.store.SsoSessionIdHelper; +import com.itools.core.ucenter.user.XxlSsoUser; + +import javax.servlet.http.HttpServletRequest; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 12:51 + */ +public class SsoTokenLoginHelper { + + /** + * client login + * + * @param sessionId + * @param xxlUser + */ + public static void login(String sessionId, XxlSsoUser xxlUser) { + + String storeKey = SsoSessionIdHelper.parseStoreKey(sessionId); + if (storeKey == null) { + throw new RuntimeException("parseStoreKey Fail, sessionId:" + sessionId); + } + + SsoLoginStore.put(storeKey, xxlUser); + } + + /** + * client logout + * + * @param sessionId + */ + public static void logout(String sessionId) { + + String storeKey = SsoSessionIdHelper.parseStoreKey(sessionId); + if (storeKey == null) { + return; + } + + SsoLoginStore.remove(storeKey); + } + /** + * client logout + * + * @param request + */ + public static void logout(HttpServletRequest request) { + String headerSessionId = request.getHeader(Conf.SSO_SESSIONID); + logout(headerSessionId); + } + + + /** + * login check + * + * @param sessionId + * @return + */ + public static XxlSsoUser loginCheck(String sessionId){ + + String storeKey = SsoSessionIdHelper.parseStoreKey(sessionId); + if (storeKey == null) { + return null; + } + + XxlSsoUser xxlUser = SsoLoginStore.get(storeKey); + if (xxlUser != null) { + String version = SsoSessionIdHelper.parseVersion(sessionId); + if (xxlUser.getVersion().equals(version)) { + + // After the expiration time has passed half, Auto refresh + if ((System.currentTimeMillis() - xxlUser.getExpireFreshTime()) > xxlUser.getExpireMinite()/2) { + xxlUser.setExpireFreshTime(System.currentTimeMillis()); + SsoLoginStore.put(storeKey, xxlUser); + } + + return xxlUser; + } + } + return null; + } + + + /** + * login check + * + * @param request + * @return + */ + public static XxlSsoUser loginCheck(HttpServletRequest request){ + String headerSessionId = request.getHeader(Conf.SSO_SESSIONID); + return loginCheck(headerSessionId); + } + + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/login/SsoWebLoginHelper.java b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/login/SsoWebLoginHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..25a16a784cef58a60dcc007e5f5b71cd46e2aca2 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/login/SsoWebLoginHelper.java @@ -0,0 +1,124 @@ +package com.itools.core.ucenter.login; + + + +import com.itools.core.ucenter.conf.Conf; +import com.itools.core.ucenter.store.SsoLoginStore; +import com.itools.core.ucenter.store.SsoSessionIdHelper; +import com.itools.core.ucenter.user.XxlSsoUser; +import com.itools.core.ucenter.util.CookieUtil; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 12:51 + */ +public class SsoWebLoginHelper { + + /** + * client login + * + * @param response + * @param sessionId + * @param ifRemember true: cookie not expire, false: expire when browser close (server cookie) + * @param xxlUser + */ + public static void login(HttpServletResponse response, + String sessionId, + XxlSsoUser xxlUser, + boolean ifRemember) { + + String storeKey = SsoSessionIdHelper.parseStoreKey(sessionId); + if (storeKey == null) { + throw new RuntimeException("parseStoreKey Fail, sessionId:" + sessionId); + } + + SsoLoginStore.put(storeKey, xxlUser); + CookieUtil.set(response, Conf.SSO_SESSIONID, sessionId, ifRemember); + } + + /** + * client logout + * + * @param request + * @param response + */ + public static void logout(HttpServletRequest request, + HttpServletResponse response) { + + String cookieSessionId = CookieUtil.getValue(request, Conf.SSO_SESSIONID); + if (cookieSessionId==null) { + return; + } + + String storeKey = SsoSessionIdHelper.parseStoreKey(cookieSessionId); + if (storeKey != null) { + SsoLoginStore.remove(storeKey); + } + + CookieUtil.remove(request, response, Conf.SSO_SESSIONID); + } + + + + /** + * login check + * + * @param request + * @param response + * @return + */ + public static XxlSsoUser loginCheck(HttpServletRequest request, HttpServletResponse response){ + + String cookieSessionId = CookieUtil.getValue(request, Conf.SSO_SESSIONID); + + // cookie user + XxlSsoUser xxlUser = SsoTokenLoginHelper.loginCheck(cookieSessionId); + if (xxlUser != null) { + return xxlUser; + } + + // redirect user + + // remove old cookie + SsoWebLoginHelper.removeSessionIdByCookie(request, response); + + // set new cookie + String paramSessionId = request.getParameter(Conf.SSO_SESSIONID); + xxlUser = SsoTokenLoginHelper.loginCheck(paramSessionId); + if (xxlUser != null) { + CookieUtil.set(response, Conf.SSO_SESSIONID, paramSessionId, false); // expire when browser close (client cookie) + return xxlUser; + } + + return null; + } + + + /** + * client logout, cookie only + * + * @param request + * @param response + */ + public static void removeSessionIdByCookie(HttpServletRequest request, HttpServletResponse response) { + CookieUtil.remove(request, response, Conf.SSO_SESSIONID); + } + + /** + * get sessionid by cookie + * + * @param request + * @return + */ + public static String getSessionIdByCookie(HttpServletRequest request) { + String cookieSessionId = CookieUtil.getValue(request, Conf.SSO_SESSIONID); + return cookieSessionId; + } + + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/path/PathMatcher.java b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/path/PathMatcher.java new file mode 100644 index 0000000000000000000000000000000000000000..04ebcdde8742395c9a6f62bf6ced50919737b67c --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/path/PathMatcher.java @@ -0,0 +1,106 @@ +package com.itools.core.ucenter.path; + + +import java.util.Comparator; +import java.util.Map; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 12:51 + */ +public interface PathMatcher { + + /** + * Does the given {@code path} represent a pattern that can be matched by an implementation of this interface? + * + *

If the return value is {@code false}, then the {@link #match} + * method does not have to be used because direct equality comparisons + * on the static path Strings will lead to the same result. + * + * @param path the path String to check + * @return {@code true} if the given {@code path} represents a pattern + */ + boolean isPattern(String path); + + /** + * Match the given {@code path} against the given {@code pattern}, according to this PathMatcher's matching strategy. + * + * @param pattern the pattern to match against + * @param path the path String to test + * @return {@code true} if the supplied {@code path} matched, + * {@code false} if it didn't + */ + boolean match(String pattern, String path); + + /** + * Match the given {@code path} against the corresponding part of the given {@code pattern}, according to this PathMatcher's matching strategy. + * + *

Determines whether the pattern at least matches as far as the given base path goes, assuming that a full path may then match as well. + * + * @param pattern the pattern to match against + * @param path the path String to test + * @return {@code true} if the supplied {@code path} matched, + * {@code false} if it didn't + */ + boolean matchStart(String pattern, String path); + + /** + * Given a pattern and a full path, determine the pattern-mapped part. + * + *

This method is supposed to find out which part of the path is matched dynamically through an actual pattern, + * that is, it strips off a statically defined leading path from the given full path, + * returning only the actually pattern-matched part of the path. + * + *

For example: For "myroot/*.html" as pattern and "myroot/myfile.html" as full path, this method should return "myfile.html". + * The detailed determination rules are specified to this PathMatcher's matching strategy. + * + *

A simple implementation may return the given full path as-is in case of an actual pattern, + * and the empty String in case of the pattern not containing any dynamic parts (i.e. the {@code pattern} parameter being + * a static path that wouldn't qualify as an actual {@link #isPattern pattern}). + * A sophisticated implementation will differentiate between the static parts and the dynamic parts of the given path pattern. + * + * @param pattern the path pattern + * @param path the full path to introspect + * @return the pattern-mapped part of the given {@code path} + * (never {@code null}) + */ + String extractPathWithinPattern(String pattern, String path); + + /** + * Given a pattern and a full path, extract the URI template variables. URI template variables are expressed through curly brackets ('{' and '}'). + * + *

For example: For pattern "/hotels/{hotel}" and path "/hotels/1", this method will return a map containing "hotel"->"1". + * + * @param pattern the path pattern, possibly containing URI templates + * @param path the full path to extract template variables from + * @return a map, containing variable names as keys; variables values as values + */ + Map extractUriTemplateVariables(String pattern, String path); + + /** + * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order of explicitness for that path. + * + *

The full algorithm used depends on the underlying implementation, but generally, + * the returned {@code Comparator} will {@linkplain java.util.Collections#sort(java.util.List, Comparator) sort} + * a list so that more specific patterns come before generic patterns. + * + * @param path the full path to use for comparison + * @return a comparator capable of sorting patterns in order of explicitness + */ + Comparator getPatternComparator(String path); + + /** + * Combines two patterns into a new pattern that is returned. + * + *

The full algorithm used for combining the two pattern depends on the underlying implementation. + * + * @param pattern1 the first pattern + * @param pattern2 the second pattern + * @return the combination of the two patterns + * @throws IllegalArgumentException when the two patterns cannot be combined + */ + String combine(String pattern1, String pattern2); + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/path/impl/AntPathMatcher.java b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/path/impl/AntPathMatcher.java new file mode 100644 index 0000000000000000000000000000000000000000..71cd68d38d23fe9a0e415a198b660724aefbbab8 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/path/impl/AntPathMatcher.java @@ -0,0 +1,859 @@ +package com.itools.core.ucenter.path.impl; + + +import com.itools.core.ucenter.path.PathMatcher; +import com.itools.core.utils.StringUtils; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 12:51 + */ +public class AntPathMatcher implements PathMatcher { + + /** Default path separator: "/" */ + public static final String DEFAULT_PATH_SEPARATOR = "/"; + + private static final int CACHE_TURNOFF_THRESHOLD = 65536; + + private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?\\}"); + + private static final char[] WILDCARD_CHARS = { '*', '?', '{' }; + + + private String pathSeparator; + + private PathSeparatorPatternCache pathSeparatorPatternCache; + + private boolean caseSensitive = true; + + private boolean trimTokens = false; + + private volatile Boolean cachePatterns; + + private final Map tokenizedPatternCache = new ConcurrentHashMap(256); + + final Map stringMatcherCache = new ConcurrentHashMap(256); + + + /** + * Create a new instance with the {@link #DEFAULT_PATH_SEPARATOR}. + */ + public AntPathMatcher() { + this.pathSeparator = DEFAULT_PATH_SEPARATOR; + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(DEFAULT_PATH_SEPARATOR); + } + + /** + * A convenient, alternative constructor to use with a custom path separator. + * @param pathSeparator the path separator to use, must not be {@code null}. + * @since 4.1 + */ + public AntPathMatcher(String pathSeparator) { + if (pathSeparator == null) { + throw new IllegalArgumentException("'pathSeparator' is required"); + } + + this.pathSeparator = pathSeparator; + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(pathSeparator); + } + + + /** + * Set the path separator to use for pattern parsing. + *

Default is "/", as in Ant. + */ + public void setPathSeparator(String pathSeparator) { + this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR); + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(this.pathSeparator); + } + + /** + * Specify whether to perform pattern matching in a case-sensitive fashion. + *

Default is {@code true}. Switch this to {@code false} for case-insensitive matching. + * @since 4.2 + */ + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + /** + * Specify whether to trim tokenized paths and patterns. + *

Default is {@code false}. + */ + public void setTrimTokens(boolean trimTokens) { + this.trimTokens = trimTokens; + } + + /** + * Specify whether to cache parsed pattern metadata for patterns passed + * into this matcher's {@link #match} method. A value of {@code true} + * activates an unlimited pattern cache; a value of {@code false} turns + * the pattern cache off completely. + *

Default is for the cache to be on, but with the variant to automatically + * turn it off when encountering too many patterns to cache at runtime + * (the threshold is 65536), assuming that arbitrary permutations of patterns + * are coming in, with little chance for encountering a recurring pattern. + * @since 4.0.1 + * @see #getStringMatcher(String) + */ + public void setCachePatterns(boolean cachePatterns) { + this.cachePatterns = cachePatterns; + } + + private void deactivatePatternCache() { + this.cachePatterns = false; + this.tokenizedPatternCache.clear(); + this.stringMatcherCache.clear(); + } + + + @Override + public boolean isPattern(String path) { + return (path.indexOf('*') != -1 || path.indexOf('?') != -1); + } + + @Override + public boolean match(String pattern, String path) { + return doMatch(pattern, path, true, null); + } + + @Override + public boolean matchStart(String pattern, String path) { + return doMatch(pattern, path, false, null); + } + + /** + * Actually match the given {@code path} against the given {@code pattern}. + * @param pattern the pattern to match against + * @param path the path String to test + * @param fullMatch whether a full pattern match is required (else a pattern match + * as far as the given base path goes is sufficient) + * @return {@code true} if the supplied {@code path} matched, {@code false} if it didn't + */ + protected boolean doMatch(String pattern, String path, boolean fullMatch, Map uriTemplateVariables) { + if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { + return false; + } + + String[] pattDirs = tokenizePattern(pattern); + if (fullMatch && this.caseSensitive && !isPotentialMatch(path, pattDirs)) { + return false; + } + + String[] pathDirs = tokenizePath(path); + + int pattIdxStart = 0; + int pattIdxEnd = pattDirs.length - 1; + int pathIdxStart = 0; + int pathIdxEnd = pathDirs.length - 1; + + // Match all elements up to the first ** + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = pattDirs[pattIdxStart]; + if ("**".equals(pattDir)) { + break; + } + if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) { + return false; + } + pattIdxStart++; + pathIdxStart++; + } + + if (pathIdxStart > pathIdxEnd) { + // Path is exhausted, only match if rest of pattern is * or **'s + if (pattIdxStart > pattIdxEnd) { + return (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator)); + } + if (!fullMatch) { + return true; + } + if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) { + return true; + } + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } + else if (pattIdxStart > pattIdxEnd) { + // String not exhausted, but pattern is. Failure. + return false; + } + else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) { + // Path start definitely matches due to "**" part in pattern. + return true; + } + + // up to last '**' + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = pattDirs[pattIdxEnd]; + if (pattDir.equals("**")) { + break; + } + if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) { + return false; + } + pattIdxEnd--; + pathIdxEnd--; + } + if (pathIdxStart > pathIdxEnd) { + // String is exhausted + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } + + while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { + int patIdxTmp = -1; + for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { + if (pattDirs[i].equals("**")) { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == pattIdxStart + 1) { + // '**/**' situation, so skip one + pattIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = (patIdxTmp - pattIdxStart - 1); + int strLength = (pathIdxEnd - pathIdxStart + 1); + int foundIdx = -1; + + strLoop: + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + String subPat = pattDirs[pattIdxStart + j + 1]; + String subStr = pathDirs[pathIdxStart + i + j]; + if (!matchStrings(subPat, subStr, uriTemplateVariables)) { + continue strLoop; + } + } + foundIdx = pathIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + pattIdxStart = patIdxTmp; + pathIdxStart = foundIdx + patLength; + } + + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + + return true; + } + + private boolean isPotentialMatch(String path, String[] pattDirs) { + if (!this.trimTokens) { + int pos = 0; + for (String pattDir : pattDirs) { + int skipped = skipSeparator(path, pos, this.pathSeparator); + pos += skipped; + skipped = skipSegment(path, pos, pattDir); + if (skipped < pattDir.length()) { + return (skipped > 0 || (pattDir.length() > 0 && isWildcardChar(pattDir.charAt(0)))); + } + pos += skipped; + } + } + return true; + } + + private int skipSegment(String path, int pos, String prefix) { + int skipped = 0; + for (int i = 0; i < prefix.length(); i++) { + char c = prefix.charAt(i); + if (isWildcardChar(c)) { + return skipped; + } + int currPos = pos + skipped; + if (currPos >= path.length()) { + return 0; + } + if (c == path.charAt(currPos)) { + skipped++; + } + } + return skipped; + } + + private int skipSeparator(String path, int pos, String separator) { + int skipped = 0; + while (path.startsWith(separator, pos + skipped)) { + skipped += separator.length(); + } + return skipped; + } + + private boolean isWildcardChar(char c) { + for (char candidate : WILDCARD_CHARS) { + if (c == candidate) { + return true; + } + } + return false; + } + + /** + * Tokenize the given path pattern into parts, based on this matcher's settings. + *

Performs caching based on {@link #setCachePatterns}, delegating to + * {@link #tokenizePath(String)} for the actual tokenization algorithm. + * @param pattern the pattern to tokenize + * @return the tokenized pattern parts + */ + protected String[] tokenizePattern(String pattern) { + String[] tokenized = null; + Boolean cachePatterns = this.cachePatterns; + if (cachePatterns == null || cachePatterns.booleanValue()) { + tokenized = this.tokenizedPatternCache.get(pattern); + } + if (tokenized == null) { + tokenized = tokenizePath(pattern); + if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) { + // Try to adapt to the runtime situation that we're encountering: + // There are obviously too many different patterns coming in here... + // So let's turn off the cache since the patterns are unlikely to be reoccurring. + deactivatePatternCache(); + return tokenized; + } + if (cachePatterns == null || cachePatterns.booleanValue()) { + this.tokenizedPatternCache.put(pattern, tokenized); + } + } + return tokenized; + } + + /** + * Tokenize the given path String into parts, based on this matcher's settings. + * @param path the path to tokenize + * @return the tokenized path parts + */ + protected String[] tokenizePath(String path) { + return StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); + } + + /** + * Test whether or not a string matches against a pattern. + * @param pattern the pattern to match against (never {@code null}) + * @param str the String which must be matched against the pattern (never {@code null}) + * @return {@code true} if the string matches against the pattern, or {@code false} otherwise + */ + private boolean matchStrings(String pattern, String str, Map uriTemplateVariables) { + return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables); + } + + /** + * Build or retrieve an {@link AntPathStringMatcher} for the given pattern. + *

The default implementation checks this AntPathMatcher's internal cache + * (see {@link #setCachePatterns}), creating a new AntPathStringMatcher instance + * if no cached copy is found. + *

When encountering too many patterns to cache at runtime (the threshold is 65536), + * it turns the default cache off, assuming that arbitrary permutations of patterns + * are coming in, with little chance for encountering a recurring pattern. + *

This method may be overridden to implement a custom cache strategy. + * @param pattern the pattern to match against (never {@code null}) + * @return a corresponding AntPathStringMatcher (never {@code null}) + * @see #setCachePatterns + */ + protected AntPathStringMatcher getStringMatcher(String pattern) { + AntPathStringMatcher matcher = null; + Boolean cachePatterns = this.cachePatterns; + if (cachePatterns == null || cachePatterns.booleanValue()) { + matcher = this.stringMatcherCache.get(pattern); + } + if (matcher == null) { + matcher = new AntPathStringMatcher(pattern, this.caseSensitive); + if (cachePatterns == null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) { + // Try to adapt to the runtime situation that we're encountering: + // There are obviously too many different patterns coming in here... + // So let's turn off the cache since the patterns are unlikely to be reoccurring. + deactivatePatternCache(); + return matcher; + } + if (cachePatterns == null || cachePatterns.booleanValue()) { + this.stringMatcherCache.put(pattern, matcher); + } + } + return matcher; + } + + /** + * Given a pattern and a full path, determine the pattern-mapped part.

For example:

    + *
  • '{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} -> ''
  • + *
  • '{@code /docs/*}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
  • + *
  • '{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code commit.html}'
  • + *
  • '{@code /docs/**}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
  • + *
  • '{@code /docs/**\/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code cvs/commit.html}'
  • + *
  • '{@code /*.html}' and '{@code /docs/cvs/commit.html} -> '{@code docs/cvs/commit.html}'
  • + *
  • '{@code *.html}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'
  • + *
  • '{@code *}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'
+ *

Assumes that {@link #match} returns {@code true} for '{@code pattern}' and '{@code path}', but + * does not enforce this. + */ + @Override + public String extractPathWithinPattern(String pattern, String path) { + String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator, this.trimTokens, true); + String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); + StringBuilder builder = new StringBuilder(); + boolean pathStarted = false; + + for (int segment = 0; segment < patternParts.length; segment++) { + String patternPart = patternParts[segment]; + if (patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) { + for (; segment < pathParts.length; segment++) { + if (pathStarted || (segment == 0 && !pattern.startsWith(this.pathSeparator))) { + builder.append(this.pathSeparator); + } + builder.append(pathParts[segment]); + pathStarted = true; + } + } + } + + return builder.toString(); + } + + @Override + public Map extractUriTemplateVariables(String pattern, String path) { + Map variables = new LinkedHashMap(); + boolean result = doMatch(pattern, path, true, variables); + if (!result) { + throw new IllegalStateException("Pattern \"" + pattern + "\" is not a match for \"" + path + "\""); + } + return variables; + } + + /** + * Combine two patterns into a new pattern. + *

This implementation simply concatenates the two patterns, unless + * the first pattern contains a file extension match (e.g., {@code *.html}). + * In that case, the second pattern will be merged into the first. Otherwise, + * an {@code IllegalArgumentException} will be thrown. + *

Examples

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Pattern 1Pattern 2Result
{@code null}{@code null} 
/hotels{@code null}/hotels
{@code null}/hotels/hotels
/hotels/bookings/hotels/bookings
/hotelsbookings/hotels/bookings
/hotels/*/bookings/hotels/bookings
/hotels/**/bookings/hotels/**/bookings
/hotels{hotel}/hotels/{hotel}
/hotels/*{hotel}/hotels/{hotel}
/hotels/**{hotel}/hotels/**/{hotel}
/*.html/hotels.html/hotels.html
/*.html/hotels/hotels.html
/*.html/*.txt{@code IllegalArgumentException}
+ * @param pattern1 the first pattern + * @param pattern2 the second pattern + * @return the combination of the two patterns + * @throws IllegalArgumentException if the two patterns cannot be combined + */ + @Override + public String combine(String pattern1, String pattern2) { + if (!StringUtils.hasText(pattern1) && !StringUtils.hasText(pattern2)) { + return ""; + } + if (!StringUtils.hasText(pattern1)) { + return pattern2; + } + if (!StringUtils.hasText(pattern2)) { + return pattern1; + } + + boolean pattern1ContainsUriVar = (pattern1.indexOf('{') != -1); + if (!pattern1.equals(pattern2) && !pattern1ContainsUriVar && match(pattern1, pattern2)) { + // /* + /hotel -> /hotel ; "/*.*" + "/*.html" -> /*.html + // However /user + /user -> /usr/user ; /{foo} + /bar -> /{foo}/bar + return pattern2; + } + + // /hotels/* + /booking -> /hotels/booking + // /hotels/* + booking -> /hotels/booking + if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnWildCard())) { + return concat(pattern1.substring(0, pattern1.length() - 2), pattern2); + } + + // /hotels/** + /booking -> /hotels/**/booking + // /hotels/** + booking -> /hotels/**/booking + if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnDoubleWildCard())) { + return concat(pattern1, pattern2); + } + + int starDotPos1 = pattern1.indexOf("*."); + if (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(".")) { + // simply concatenate the two patterns + return concat(pattern1, pattern2); + } + + String ext1 = pattern1.substring(starDotPos1 + 1); + int dotPos2 = pattern2.indexOf('.'); + String file2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2)); + String ext2 = (dotPos2 == -1 ? "" : pattern2.substring(dotPos2)); + boolean ext1All = (ext1.equals(".*") || ext1.equals("")); + boolean ext2All = (ext2.equals(".*") || ext2.equals("")); + if (!ext1All && !ext2All) { + throw new IllegalArgumentException("Cannot combine patterns: " + pattern1 + " vs " + pattern2); + } + String ext = (ext1All ? ext2 : ext1); + return file2 + ext; + } + + private String concat(String path1, String path2) { + boolean path1EndsWithSeparator = path1.endsWith(this.pathSeparator); + boolean path2StartsWithSeparator = path2.startsWith(this.pathSeparator); + + if (path1EndsWithSeparator && path2StartsWithSeparator) { + return path1 + path2.substring(1); + } + else if (path1EndsWithSeparator || path2StartsWithSeparator) { + return path1 + path2; + } + else { + return path1 + this.pathSeparator + path2; + } + } + + /** + * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order of + * explicitness. + *

This{@code Comparator} will {@linkplain Collections#sort(List, Comparator) sort} + * a list so that more specific patterns (without uri templates or wild cards) come before + * generic patterns. So given a list with the following patterns: + *

    + *
  1. {@code /hotels/new}
  2. + *
  3. {@code /hotels/{hotel}}
  4. {@code /hotels/*}
  5. + *
+ * the returned comparator will sort this list so that the order will be as indicated. + *

The full path given as parameter is used to test for exact matches. So when the given path + * is {@code /hotels/2}, the pattern {@code /hotels/2} will be sorted before {@code /hotels/1}. + * @param path the full path to use for comparison + * @return a comparator capable of sorting patterns in order of explicitness + */ + @Override + public Comparator getPatternComparator(String path) { + return new AntPatternComparator(path); + } + + + /** + * Tests whether or not a string matches against a pattern via a {@link Pattern}. + *

The pattern may contain special characters: '*' means zero or more characters; '?' means one and + * only one character; '{' and '}' indicate a URI template pattern. For example /users/{user}. + */ + protected static class AntPathStringMatcher { + + private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}"); + + private static final String DEFAULT_VARIABLE_PATTERN = "(.*)"; + + private final Pattern pattern; + + private final List variableNames = new LinkedList(); + + public AntPathStringMatcher(String pattern) { + this(pattern, true); + } + + public AntPathStringMatcher(String pattern, boolean caseSensitive) { + StringBuilder patternBuilder = new StringBuilder(); + Matcher matcher = GLOB_PATTERN.matcher(pattern); + int end = 0; + while (matcher.find()) { + patternBuilder.append(quote(pattern, end, matcher.start())); + String match = matcher.group(); + if ("?".equals(match)) { + patternBuilder.append('.'); + } + else if ("*".equals(match)) { + patternBuilder.append(".*"); + } + else if (match.startsWith("{") && match.endsWith("}")) { + int colonIdx = match.indexOf(':'); + if (colonIdx == -1) { + patternBuilder.append(DEFAULT_VARIABLE_PATTERN); + this.variableNames.add(matcher.group(1)); + } + else { + String variablePattern = match.substring(colonIdx + 1, match.length() - 1); + patternBuilder.append('('); + patternBuilder.append(variablePattern); + patternBuilder.append(')'); + String variableName = match.substring(1, colonIdx); + this.variableNames.add(variableName); + } + } + end = matcher.end(); + } + patternBuilder.append(quote(pattern, end, pattern.length())); + this.pattern = (caseSensitive ? Pattern.compile(patternBuilder.toString()) : + Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE)); + } + + private String quote(String s, int start, int end) { + if (start == end) { + return ""; + } + return Pattern.quote(s.substring(start, end)); + } + + /** + * Main entry point. + * @return {@code true} if the string matches against the pattern, or {@code false} otherwise. + */ + public boolean matchStrings(String str, Map uriTemplateVariables) { + Matcher matcher = this.pattern.matcher(str); + if (matcher.matches()) { + if (uriTemplateVariables != null) { + // SPR-8455 + if (this.variableNames.size() != matcher.groupCount()) { + throw new IllegalArgumentException("The number of capturing groups in the pattern segment " + + this.pattern + " does not match the number of URI template variables it defines, " + + "which can occur if capturing groups are used in a URI template regex. " + + "Use non-capturing groups instead."); + } + for (int i = 1; i <= matcher.groupCount(); i++) { + String name = this.variableNames.get(i - 1); + String value = matcher.group(i); + uriTemplateVariables.put(name, value); + } + } + return true; + } + else { + return false; + } + } + } + + + /** + * The default {@link Comparator} implementation returned by + * {@link #getPatternComparator(String)}. + *

In order, the most "generic" pattern is determined by the following: + *

    + *
  • if it's null or a capture all pattern (i.e. it is equal to "/**")
  • + *
  • if the other pattern is an actual match
  • + *
  • if it's a catch-all pattern (i.e. it ends with "**"
  • + *
  • if it's got more "*" than the other pattern
  • + *
  • if it's got more "{foo}" than the other pattern
  • + *
  • if it's shorter than the other pattern
  • + *
+ */ + protected static class AntPatternComparator implements Comparator { + + private final String path; + + public AntPatternComparator(String path) { + this.path = path; + } + + /** + * Compare two patterns to determine which should match first, i.e. which + * is the most specific regarding the current path. + * @return a negative integer, zero, or a positive integer as pattern1 is + * more specific, equally specific, or less specific than pattern2. + */ + @Override + public int compare(String pattern1, String pattern2) { + PatternInfo info1 = new PatternInfo(pattern1); + PatternInfo info2 = new PatternInfo(pattern2); + + if (info1.isLeastSpecific() && info2.isLeastSpecific()) { + return 0; + } + else if (info1.isLeastSpecific()) { + return 1; + } + else if (info2.isLeastSpecific()) { + return -1; + } + + boolean pattern1EqualsPath = pattern1.equals(path); + boolean pattern2EqualsPath = pattern2.equals(path); + if (pattern1EqualsPath && pattern2EqualsPath) { + return 0; + } + else if (pattern1EqualsPath) { + return -1; + } + else if (pattern2EqualsPath) { + return 1; + } + + if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) { + return 1; + } + else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) { + return -1; + } + + if (info1.getTotalCount() != info2.getTotalCount()) { + return info1.getTotalCount() - info2.getTotalCount(); + } + + if (info1.getLength() != info2.getLength()) { + return info2.getLength() - info1.getLength(); + } + + if (info1.getSingleWildcards() < info2.getSingleWildcards()) { + return -1; + } + else if (info2.getSingleWildcards() < info1.getSingleWildcards()) { + return 1; + } + + if (info1.getUriVars() < info2.getUriVars()) { + return -1; + } + else if (info2.getUriVars() < info1.getUriVars()) { + return 1; + } + + return 0; + } + + + /** + * Value class that holds information about the pattern, e.g. number of + * occurrences of "*", "**", and "{" pattern elements. + */ + private static class PatternInfo { + + private final String pattern; + + private int uriVars; + + private int singleWildcards; + + private int doubleWildcards; + + private boolean catchAllPattern; + + private boolean prefixPattern; + + private Integer length; + + public PatternInfo(String pattern) { + this.pattern = pattern; + if (this.pattern != null) { + initCounters(); + this.catchAllPattern = this.pattern.equals("/**"); + this.prefixPattern = !this.catchAllPattern && this.pattern.endsWith("/**"); + } + if (this.uriVars == 0) { + this.length = (this.pattern != null ? this.pattern.length() : 0); + } + } + + protected void initCounters() { + int pos = 0; + while (pos < this.pattern.length()) { + if (this.pattern.charAt(pos) == '{') { + this.uriVars++; + pos++; + } + else if (this.pattern.charAt(pos) == '*') { + if (pos + 1 < this.pattern.length() && this.pattern.charAt(pos + 1) == '*') { + this.doubleWildcards++; + pos += 2; + } + else if (pos > 0 && !this.pattern.substring(pos - 1).equals(".*")) { + this.singleWildcards++; + pos++; + } + else { + pos++; + } + } + else { + pos++; + } + } + } + + public int getUriVars() { + return this.uriVars; + } + + public int getSingleWildcards() { + return this.singleWildcards; + } + + public int getDoubleWildcards() { + return this.doubleWildcards; + } + + public boolean isLeastSpecific() { + return (this.pattern == null || this.catchAllPattern); + } + + public boolean isPrefixPattern() { + return this.prefixPattern; + } + + public int getTotalCount() { + return this.uriVars + this.singleWildcards + (2 * this.doubleWildcards); + } + + /** + * Returns the length of the given pattern, where template variables are considered to be 1 long. + */ + public int getLength() { + if (this.length == null) { + this.length = VARIABLE_PATTERN.matcher(this.pattern).replaceAll("#").length(); + } + return this.length; + } + } + } + + + /** + * A simple cache for patterns that depend on the configured path separator. + */ + private static class PathSeparatorPatternCache { + + private final String endsOnWildCard; + + private final String endsOnDoubleWildCard; + + public PathSeparatorPatternCache(String pathSeparator) { + this.endsOnWildCard = pathSeparator + "*"; + this.endsOnDoubleWildCard = pathSeparator + "**"; + } + + public String getEndsOnWildCard() { + return this.endsOnWildCard; + } + + public String getEndsOnDoubleWildCard() { + return this.endsOnDoubleWildCard; + } + } + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/store/SsoLoginStore.java b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/store/SsoLoginStore.java new file mode 100644 index 0000000000000000000000000000000000000000..4eef7e15ff3345237c535814a03f9b39ea749f4f --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/store/SsoLoginStore.java @@ -0,0 +1,72 @@ +package com.itools.core.ucenter.store; + + +import com.itools.core.ucenter.conf.Conf; +import com.itools.core.ucenter.user.XxlSsoUser; +import com.itools.core.ucenter.util.JedisUtil; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 12:51 + */ +public class SsoLoginStore { + /** + * 1440 minite, 24 hour + */ + private static int redisExpireMinite = 1440; + public static void setRedisExpireMinite(int redisExpireMinite) { + if (redisExpireMinite < 30) { + redisExpireMinite = 30; + } + SsoLoginStore.redisExpireMinite = redisExpireMinite; + } + public static int getRedisExpireMinite() { + return redisExpireMinite; + } + + /** + * get + * + * @param storeKey + * @return + */ + public static XxlSsoUser get(String storeKey) { + + String redisKey = redisKey(storeKey); + Object objectValue = JedisUtil.getObjectValue(redisKey); + if (objectValue != null) { + XxlSsoUser xxlUser = (XxlSsoUser) objectValue; + return xxlUser; + } + return null; + } + + /** + * remove + * + * @param storeKey + */ + public static void remove(String storeKey) { + String redisKey = redisKey(storeKey); + JedisUtil.del(redisKey); + } + + /** + * put + * + * @param storeKey + * @param xxlUser + */ + public static void put(String storeKey, XxlSsoUser xxlUser) { + String redisKey = redisKey(storeKey); + // minite to second + JedisUtil.setObjectValue(redisKey, xxlUser, redisExpireMinite * 60); + } + + private static String redisKey(String sessionId){ + return Conf.SSO_SESSIONID.concat("#").concat(sessionId); + } + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/store/SsoSessionIdHelper.java b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/store/SsoSessionIdHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..96beb77b6621cc516b9817d1539e41d29d98de51 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/store/SsoSessionIdHelper.java @@ -0,0 +1,64 @@ +package com.itools.core.ucenter.store; + + +import com.itools.core.ucenter.user.XxlSsoUser; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 12:51 + */ +public class SsoSessionIdHelper { + + + /** + * make client sessionId + * + * @param xxlSsoUser + * @return + */ + public static String makeSessionId(XxlSsoUser xxlSsoUser){ + String sessionId = xxlSsoUser.getUserid().concat("_").concat(xxlSsoUser.getVersion()); + return sessionId; + } + + /** + * parse storeKey from sessionId + * + * @param sessionId + * @return + */ + public static String parseStoreKey(String sessionId) { + if (sessionId!=null && sessionId.indexOf("_")>-1) { + String[] sessionIdArr = sessionId.split("_"); + if (sessionIdArr.length==2 + && sessionIdArr[0]!=null + && sessionIdArr[0].trim().length()>0) { + String userId = sessionIdArr[0].trim(); + return userId; + } + } + return null; + } + + /** + * parse version from sessionId + * + * @param sessionId + * @return + */ + public static String parseVersion(String sessionId) { + if (sessionId!=null && sessionId.indexOf("_")>-1) { + String[] sessionIdArr = sessionId.split("_"); + if (sessionIdArr.length==2 + && sessionIdArr[1]!=null + && sessionIdArr[1].trim().length()>0) { + String version = sessionIdArr[1].trim(); + return version; + } + } + return null; + } + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/user/XxlSsoUser.java b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/user/XxlSsoUser.java new file mode 100644 index 0000000000000000000000000000000000000000..3ae5a2ebd5ce9548b984c935672577c505a1bc26 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/user/XxlSsoUser.java @@ -0,0 +1,74 @@ +package com.itools.core.ucenter.user; + +import java.io.Serializable; +import java.util.Map; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 12:51 + */ +public class XxlSsoUser implements Serializable { + private static final long serialVersionUID = 42L; + + // field + private String userid; + private String username; + private Map plugininfo; + + private String version; + private int expireMinite; + private long expireFreshTime; + + + // set get + public String getUserid() { + return userid; + } + + public void setUserid(String userid) { + this.userid = userid; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public Map getPlugininfo() { + return plugininfo; + } + + public void setPlugininfo(Map plugininfo) { + this.plugininfo = plugininfo; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public int getExpireMinite() { + return expireMinite; + } + + public void setExpireMinite(int expireMinite) { + this.expireMinite = expireMinite; + } + + public long getExpireFreshTime() { + return expireFreshTime; + } + + public void setExpireFreshTime(long expireFreshTime) { + this.expireFreshTime = expireFreshTime; + } + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/util/CookieUtil.java b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/util/CookieUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..effcc423fd452a966a7e43dd4e39200d65b149be --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/util/CookieUtil.java @@ -0,0 +1,99 @@ +package com.itools.core.ucenter.util; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 12:51 + */ +public class CookieUtil { + + // 默认缓存时间,单位/秒, 2H + private static final int COOKIE_MAX_AGE = Integer.MAX_VALUE; + // 保存路径,根路径 + private static final String COOKIE_PATH = "/"; + + /** + * 保存 + * + * @param response + * @param key + * @param value + * @param ifRemember + */ + public static void set(HttpServletResponse response, String key, String value, boolean ifRemember) { + int age = ifRemember?COOKIE_MAX_AGE:-1; + set(response, key, value, null, COOKIE_PATH, age, true); + } + + /** + * 保存 + * + * @param response + * @param key + * @param value + * @param maxAge + */ + private static void set(HttpServletResponse response, String key, String value, String domain, String path, int maxAge, boolean isHttpOnly) { + Cookie cookie = new Cookie(key, value); + if (domain != null) { + cookie.setDomain(domain); + } + cookie.setPath(path); + cookie.setMaxAge(maxAge); + cookie.setHttpOnly(isHttpOnly); + response.addCookie(cookie); + } + + /** + * 查询value + * + * @param request + * @param key + * @return + */ + public static String getValue(HttpServletRequest request, String key) { + Cookie cookie = get(request, key); + if (cookie != null) { + return cookie.getValue(); + } + return null; + } + + /** + * 查询Cookie + * + * @param request + * @param key + */ + private static Cookie get(HttpServletRequest request, String key) { + Cookie[] arr_cookie = request.getCookies(); + if (arr_cookie != null && arr_cookie.length > 0) { + for (Cookie cookie : arr_cookie) { + if (cookie.getName().equals(key)) { + return cookie; + } + } + } + return null; + } + + /** + * 删除Cookie + * + * @param request + * @param response + * @param key + */ + public static void remove(HttpServletRequest request, HttpServletResponse response, String key) { + Cookie cookie = get(request, key); + if (cookie != null) { + set(response, key, "", null, COOKIE_PATH, 0, true); + } + } + +} \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/util/JedisUtil.java b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/util/JedisUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..2c4fb4a44ba7aa9abfb9c784ae0f81cb4a122425 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/util/JedisUtil.java @@ -0,0 +1,392 @@ +package com.itools.core.ucenter.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisShardInfo; +import redis.clients.jedis.ShardedJedis; +import redis.clients.jedis.ShardedJedisPool; + +import java.io.*; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 12:51 + */ +public class JedisUtil { + private static Logger logger = LoggerFactory.getLogger(JedisUtil.class); + + /** + * redis address, like "{ip}"、"{ip}:{port}"、"{redis/rediss}://xxl-sso:{password}@{ip}:{port:6379}/{db}";Multiple "," separated + */ + private static String address; + + public static void init(String address) { + JedisUtil.address = address; + + getInstance(); + } + + // ------------------------ ShardedJedisPool ------------------------ + /** + * 方式01: Redis单节点 + Jedis单例 : Redis单节点压力过重, Jedis单例存在并发瓶颈 》》不可用于线上 + * new Jedis("127.0.0.1", 6379).get("cache_key"); + * 方式02: Redis单节点 + JedisPool单节点连接池 》》 Redis单节点压力过重,负载和容灾比较差 + * new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6379, 10000).getResource().get("cache_key"); + * 方式03: Redis分片(通过client端集群,一致性哈希方式实现) + Jedis多节点连接池 》》Redis集群,负载和容灾较好, ShardedJedisPool一致性哈希分片,读写均匀,动态扩充 + * new ShardedJedisPool(new JedisPoolConfig(), new LinkedList()); + * 方式03: Redis集群; + * new JedisCluster(jedisClusterNodes); // TODO + */ + + private static ShardedJedisPool shardedJedisPool; + private static ReentrantLock INSTANCE_INIT_LOCL = new ReentrantLock(false); + + /** + * 获取ShardedJedis实例 + * + * @return + */ + private static ShardedJedis getInstance() { + if (shardedJedisPool == null) { + try { + if (INSTANCE_INIT_LOCL.tryLock(2, TimeUnit.SECONDS)) { + + try { + + if (shardedJedisPool == null) { + + // JedisPoolConfig + JedisPoolConfig config = new JedisPoolConfig(); + config.setMaxTotal(200); + config.setMaxIdle(50); + config.setMinIdle(8); + config.setMaxWaitMillis(10000); // 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, 默认-1 + config.setTestOnBorrow(true); // 在获取连接的时候检查有效性, 默认false + config.setTestOnReturn(false); // 调用returnObject方法时,是否进行有效检查 + config.setTestWhileIdle(true); // Idle时进行连接扫描 + config.setTimeBetweenEvictionRunsMillis(30000); // 表示idle object evitor两次扫描之间要sleep的毫秒数 + config.setNumTestsPerEvictionRun(10); // 表示idle object evitor每次扫描的最多的对象数 + config.setMinEvictableIdleTimeMillis(60000); // 表示一个对象至少停留在idle状态的最短时间,然后才能被idle object evitor扫描并驱逐;这一项只有在timeBetweenEvictionRunsMillis大于0时才有意义 + + + // JedisShardInfo List + List jedisShardInfos = new LinkedList(); + + String[] addressArr = address.split(","); + for (int i = 0; i < addressArr.length; i++) { + JedisShardInfo jedisShardInfo = new JedisShardInfo(addressArr[i]); + jedisShardInfos.add(jedisShardInfo); + } + shardedJedisPool = new ShardedJedisPool(config, jedisShardInfos); + logger.info(">>>>>>>>>>> xxl-sso, JedisUtil.ShardedJedisPool init success."); + } + + } finally { + INSTANCE_INIT_LOCL.unlock(); + } + } + + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + if (shardedJedisPool == null) { + throw new NullPointerException(">>>>>>>>>>> xxl-sso, JedisUtil.ShardedJedisPool is null."); + } + + ShardedJedis shardedJedis = shardedJedisPool.getResource(); + return shardedJedis; + } + + public static void close() throws IOException { + if(shardedJedisPool != null) { + shardedJedisPool.close(); + } + } + + + // ------------------------ serialize and unserialize ------------------------ + + /** + * 将对象-->byte[] (由于jedis中不支持直接存储object所以转换成byte[]存入) + * + * @param object + * @return + */ + private static byte[] serialize(Object object) { + ObjectOutputStream oos = null; + ByteArrayOutputStream baos = null; + try { + // 序列化 + baos = new ByteArrayOutputStream(); + oos = new ObjectOutputStream(baos); + oos.writeObject(object); + byte[] bytes = baos.toByteArray(); + return bytes; + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + try { + oos.close(); + baos.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + return null; + } + + /** + * 将byte[] -->Object + * + * @param bytes + * @return + */ + private static Object unserialize(byte[] bytes) { + ByteArrayInputStream bais = null; + try { + // 反序列化 + bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais); + return ois.readObject(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + try { + bais.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + return null; + } + + // ------------------------ jedis util ------------------------ + /** + * 存储简单的字符串或者是Object 因为jedis没有分装直接存储Object的方法,所以在存储对象需斟酌下 + * 存储对象的字段是不是非常多而且是不是每个字段都用到,如果是的话那建议直接存储对象, + * 否则建议用集合的方式存储,因为redis可以针对集合进行日常的操作很方便而且还可以节省空间 + */ + + /** + * Set String + * + * @param key + * @param value + * @param seconds 存活时间,单位/秒 + * @return + */ + public static String setStringValue(String key, String value, int seconds) { + String result = null; + ShardedJedis client = getInstance(); + try { + result = client.setex(key, seconds, value); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return result; + } + + /** + * Set Object + * + * @param key + * @param obj + * @param seconds 存活时间,单位/秒 + */ + public static String setObjectValue(String key, Object obj, int seconds) { + String result = null; + ShardedJedis client = getInstance(); + try { + result = client.setex(key.getBytes(), seconds, serialize(obj)); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return result; + } + + /** + * Get String + * + * @param key + * @return + */ + public static String getStringValue(String key) { + String value = null; + ShardedJedis client = getInstance(); + try { + value = client.get(key); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return value; + } + + /** + * Get Object + * + * @param key + * @return + */ + public static Object getObjectValue(String key) { + Object obj = null; + ShardedJedis client = getInstance(); + try { + byte[] bytes = client.get(key.getBytes()); + if (bytes != null && bytes.length > 0) { + obj = unserialize(bytes); + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return obj; + } + + /** + * Delete key + * + * @param key + * @return Integer reply, specifically: + * an integer greater than 0 if one or more keys were removed + * 0 if none of the specified key existed + */ + public static Long del(String key) { + Long result = null; + ShardedJedis client = getInstance(); + try { + result = client.del(key); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return result; + } + + /** + * incrBy i(+i) + * + * @param key + * @param i + * @return new value after incr + */ + public static Long incrBy(String key, int i) { + Long result = null; + ShardedJedis client = getInstance(); + try { + result = client.incrBy(key, i); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return result; + } + + /** + * exists valid + * + * @param key + * @return Boolean reply, true if the key exists, otherwise false + */ + public static boolean exists(String key) { + Boolean result = null; + ShardedJedis client = getInstance(); + try { + result = client.exists(key); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return result; + } + + /** + * expire reset + * + * @param key + * @param seconds 存活时间,单位/秒 + * @return Integer reply, specifically: + * 1: the timeout was set. + * 0: the timeout was not set since the key already has an associated timeout (versions lt 2.1.3), or the key does not exist. + */ + public static long expire(String key, int seconds) { + Long result = null; + ShardedJedis client = getInstance(); + try { + result = client.expire(key, seconds); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return result; + } + + /** + * expire at unixTime + * + * @param key + * @param unixTime + * @return + */ + public static long expireAt(String key, long unixTime) { + Long result = null; + ShardedJedis client = getInstance(); + try { + result = client.expireAt(key, unixTime); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + return result; + } + + public static void main(String[] args) { + String xxlSsoRedisAddress = "redis://xxl-sso:password@127.0.0.1:6379/0"; + xxlSsoRedisAddress = "redis://127.0.0.1:6379/0"; + init(xxlSsoRedisAddress); + + setObjectValue("key", "666", 2*60*60); + System.out.println(getObjectValue("key")); + + } + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/util/StringUtils.java b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/util/StringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..e1523d6260f6cfc2f90fdf906c22dce545ed2e45 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-core/src/main/java/com/itools/core/ucenter/util/StringUtils.java @@ -0,0 +1,84 @@ +package com.itools.core.ucenter.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.StringTokenizer; +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 12:51 + */ +public abstract class StringUtils { + + /** + * + * @param str + * @return + */ + public static boolean hasLength(String str) { + return (str != null && !str.isEmpty()); + } + + /** + * + * @param str + * @return + */ + public static boolean hasText(String str) { + return (hasLength(str) && containsText(str)); + } + + private static boolean containsText(CharSequence str) { + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * + * @param str + * @param delimiters + * @param trimTokens + * @param ignoreEmptyTokens + * @return + */ + public static String[] tokenizeToStringArray( + String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { + + if (str == null) { + return null; + } + + StringTokenizer st = new StringTokenizer(str, delimiters); + List tokens = new ArrayList(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (trimTokens) { + token = token.trim(); + } + if (!ignoreEmptyTokens || token.length() > 0) { + tokens.add(token); + } + } + return toStringArray(tokens); + } + + /** + * + * @param collection + * @return + */ + public static String[] toStringArray(Collection collection) { + if (collection == null) { + return null; + } + return collection.toArray(new String[collection.size()]); + } + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-model/pom.xml b/itools-ucenter-sso/itools-ucenter-sso-model/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..14e979f9cf670a49615afc7b980efc0f7a044a67 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-model/pom.xml @@ -0,0 +1,80 @@ + + + + itools-ucenter-sso + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-ucenter-sso-model + + + com.itools.core + itools-common + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + + + com.baomidou + mybatis-plus-boot-starter + + + + + mysql + mysql-connector-java + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + + src/main/resources + true + + **/*.jks + + + + src/main/resources + false + + **/*.jks + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/ApplicationInfo.java b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/ApplicationInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..5686e3322d3a6fabb272b7a612a660a62f86d325 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/ApplicationInfo.java @@ -0,0 +1,68 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_application_info") +@ApiModel(value="ApplicationInfo对象", description="") +public class ApplicationInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键") + @TableId(value = "id", type = IdType.ID_WORKER_STR) + private String id; + + @ApiModelProperty(value = "系统名称") + private String appName; + + @ApiModelProperty(value = "系统唯一id") + private String appInstanceId; + + @ApiModelProperty(value = "系统所有者") + private String appOwner; + + @ApiModelProperty(value = "状态") + private Integer status; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建人") + private String createUserId; + + @ApiModelProperty(value = "最近更新时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "最近更新人") + private String lastUpdateUserId; + + @ApiModelProperty(value = "系统编码") + private String appCode; + + @ApiModelProperty(value = "系统所有者邮箱") + private String appOwnerEmail; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "类型【内部应用:1;外部应用:2】") + private Integer appType; + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/AuthResourceRef.java b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/AuthResourceRef.java new file mode 100644 index 0000000000000000000000000000000000000000..d469e83a00f5e4754974a8297cacbf43c794c91f --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/AuthResourceRef.java @@ -0,0 +1,59 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_auth_resource_ref") +@ApiModel(value="AuthResourceRef对象", description="") +public class AuthResourceRef implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "ID") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "权限ID") + private String authorizationId; + + @ApiModelProperty(value = "资源ID") + private String resourceId; + + @ApiModelProperty(value = "权限资源类型【数据权限项:1;资源:2】") + private Integer type; + + @ApiModelProperty(value = "角色权限状态【1:有效;2:超级管理员或父级置为失效;3:权限拥有者置为失效】") + private Integer status; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户") + private String lastUpdateUserId; + + @ApiModelProperty(value = "应用ID") + private String applicationId; + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/AuthorizationInfo.java b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/AuthorizationInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..bbf3ea35809726f40a722258a4b65b3164677b24 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/AuthorizationInfo.java @@ -0,0 +1,65 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_authorization_info") +@ApiModel(value="AuthorizationInfo对象", description="") +public class AuthorizationInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "ID") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "名称") + private String name; + + @ApiModelProperty(value = "父ID") + private String parentId; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "状态【启用:1;未启用:2】") + private Integer status; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户") + private String lastUpdateUserId; + + @ApiModelProperty(value = "应用ID") + private String applicationId; + + @ApiModelProperty(value = "code") + private String code; + + @ApiModelProperty(value = "排序字段") + private Integer orderFiled; + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/GroupInfo.java b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/GroupInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..80324c9f89e90dad9d9ca0b67a599f628029e0f1 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/GroupInfo.java @@ -0,0 +1,59 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_group_info") +@ApiModel(value="GroupInfo对象", description="") +public class GroupInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键id") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户id") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户id") + private String lastUpdateUserId; + + @ApiModelProperty(value = "应用id") + private String applicationId; + + @ApiModelProperty(value = "编号") + private String code; + + @ApiModelProperty(value = "组名") + private String name; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "状态【启用:1;未启用:2】") + private Integer status; + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/OauthClientDetails.java b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/OauthClientDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..aac192dfa12524a4e752c3184104781a464b0f19 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/OauthClientDetails.java @@ -0,0 +1,49 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import lombok.Data; + +import java.io.Serializable; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("oauth_client_details") +@ApiModel(value="OauthClientDetails对象", description="") +public class OauthClientDetails implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "client_id", type = IdType.AUTO) + private String clientId; + + private String resourceIds; + + private String clientSecret; + + private String scope; + + private String authorizedGrantTypes; + + private String webServerRedirectUri; + + private String authorities; + + private Integer accessTokenValidity; + + private Integer refreshTokenValidity; + + private String additionalInformation; + + private String autoapprove; + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/OrganizationInfo.java b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/OrganizationInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..f9cfccd284ae70ed7075b4ea531367d385d5b4f5 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/OrganizationInfo.java @@ -0,0 +1,94 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_organization_info") +@ApiModel(value="OrganizationInfo对象", description="") +public class OrganizationInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "ID") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "机构名称") + private String name; + + @ApiModelProperty(value = "父ID") + private String parentId; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户") + private String lastUpdateUserId; + + @ApiModelProperty(value = "地址") + private String address; + + @ApiModelProperty(value = "应用ID") + private String applicationId; + + @ApiModelProperty(value = "code") + private String code; + + @ApiModelProperty(value = "联系人部门") + private String linkManDept; + + @ApiModelProperty(value = "联系人邮箱") + private String linkManEmail; + + @ApiModelProperty(value = "联系人名称") + private String linkManName; + + @ApiModelProperty(value = "联系人手机号") + private String linkManPhone; + + @ApiModelProperty(value = "等级") + private Integer rank; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "状态【启用:1;未启用:2】") + private Integer status; + + @ApiModelProperty(value = "组织电话") + private String phone; + + @ApiModelProperty(value = "组织类型【部门:1;区:2;机构:3】") + private Integer type; + + @ApiModelProperty(value = "机构编号") + private String organizationNumber; + + @ApiModelProperty(value = "管理员id") + private String adminUserId; + + @ApiModelProperty(value = "排序字段") + private Integer orderField; +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/ResourceInfo.java b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/ResourceInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..68a0cd3e8c9091fe074d9742c4ed1b97ef7da885 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/ResourceInfo.java @@ -0,0 +1,74 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_resource_info") +@ApiModel(value="ResourceInfo对象", description="") +public class ResourceInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "ID") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "资源名称") + private String name; + + @ApiModelProperty(value = "父ID") + private String parentId; + + @ApiModelProperty(value = "状态【启用:1;未启用:2】") + private Integer status; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户ID") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户") + private String lastUpdateUserId; + + @ApiModelProperty(value = "应用ID") + private String applicationId; + + @ApiModelProperty(value = "code") + private String code; + + @ApiModelProperty(value = "排序") + private Integer orderField; + + @ApiModelProperty(value = "图标") + private String pictureName; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "类型") + private Integer type; + + @ApiModelProperty(value = "路由") + private String url; + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/RoleAuthorizationRef.java b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/RoleAuthorizationRef.java new file mode 100644 index 0000000000000000000000000000000000000000..cc98367c3ac77bf44746c8fd7fd0fbe3eeef9793 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/RoleAuthorizationRef.java @@ -0,0 +1,56 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_role_authorization_ref") +@ApiModel(value="RoleAuthorizationRef对象", description="") +public class RoleAuthorizationRef implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键id") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户id") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户id") + private String lastUpdateUserId; + + @ApiModelProperty(value = "应用id") + private String applicationId; + + @ApiModelProperty(value = "权限id") + private String authorizationId; + + @ApiModelProperty(value = "角色id") + private String roleId; + + @ApiModelProperty(value = "角色权限状态【1:有效;2:超级管理员或父级置为失效;3:权限拥有者置为失效】") + private Integer status; + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/RoleInfo.java b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/RoleInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..c5bd388ac0960e41bc2c0ff50250f01403f36e3d --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/RoleInfo.java @@ -0,0 +1,68 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_role_info") +@ApiModel(value="RoleInfo对象", description="") +public class RoleInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键ID") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "父ID") + private String parentId; + + @ApiModelProperty(value = "角色名称") + private String name; + + @ApiModelProperty(value = "应用ID") + private String applicationId; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "code") + private String code; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户") + private String lastUpdateUserId; + + @ApiModelProperty(value = "角色状态【启用:1;未启用:2】") + private Integer status; + + @ApiModelProperty(value = "排序字段") + private Integer orderField; + + @ApiModelProperty(value = "角色类型【0:功能角色,1:数据角色】") + private Integer type; + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/RoleResourceRef.java b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/RoleResourceRef.java new file mode 100644 index 0000000000000000000000000000000000000000..1cb99db4dce6358261a0226867ae479c6f980661 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/RoleResourceRef.java @@ -0,0 +1,59 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_role_resource_ref") +@ApiModel(value="RoleResourceRef对象", description="") +public class RoleResourceRef implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "用户ID") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户") + private String lastUpdateUserId; + + @ApiModelProperty(value = "应用ID") + private String applicationId; + + @ApiModelProperty(value = "角色名称") + private String roleId; + + @ApiModelProperty(value = "资源ID") + private String resourceId; + + @ApiModelProperty(value = "角色资源状态【1:有效;2:超级管理员或父级置为失效;3:权限拥有者置为失效】") + private Integer status; + + @ApiModelProperty(value = "角色资源类型【1:数据权限项;2:资源】") + private Integer type; + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/UserInfo.java b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/UserInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..0970ed49e5b988fda453c00383ac076ad8296c25 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/UserInfo.java @@ -0,0 +1,99 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.sql.Blob; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_user_info") +@ApiModel(value="UserInfo对象", description="") +public class UserInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键id") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户") + private String lastUpdateUserId; + + @ApiModelProperty(value = "公司id") + private String companyId; + + @ApiModelProperty(value = "部门id") + private String orgId; + + @ApiModelProperty(value = "用户名") + private String name; + + @ApiModelProperty(value = "用户类型") + private Integer userType; + + @ApiModelProperty(value = "性别") + private Integer sex; + + @ApiModelProperty(value = "生日") + private String birthday; + + @ApiModelProperty(value = "邮箱") + private String email; + + @ApiModelProperty(value = "位置") + private String position; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "手机号") + private String phone; + + @ApiModelProperty(value = "编号") + private String userCode; + + @ApiModelProperty(value = "等级") + private Integer userLevel; + + @ApiModelProperty(value = "头像") + private Blob faceUrl; + + @ApiModelProperty(value = "工号") + private String jobNumber; + + @ApiModelProperty(value = "账号") + private String identifier; + + @ApiModelProperty(value = "账号类型【手机号:1;邮箱:2,;账号:3】") + private Integer identityType; + + @ApiModelProperty(value = "账号密码") + private String password; + + @ApiModelProperty(value = "上次登录时间") + private LocalDateTime lastLoginTime; + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/UserRoleRef.java b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/UserRoleRef.java new file mode 100644 index 0000000000000000000000000000000000000000..1da2a50550de7c21eee761fbabe95e5d7a795c8a --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/entity/UserRoleRef.java @@ -0,0 +1,56 @@ +package com.itools.core.ucenter.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@TableName("itools_user_role_ref") +@ApiModel(value="UserRoleRef对象", description="") +public class UserRoleRef implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键id") + @TableId(value = "id", type = IdType.UUID) + private String id; + + @ApiModelProperty(value = "应用id") + private String applicationId; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户") + private String createUserId; + + @ApiModelProperty(value = "上次修改时间") + private LocalDateTime lastUpdateTime; + + @ApiModelProperty(value = "上次修改用户") + private String lastUpdateUserId; + + @ApiModelProperty(value = "角色ID") + private String roleId; + + @ApiModelProperty(value = "用户ID") + private String userId; + + @ApiModelProperty(value = "用户角色类型【1:功能角色;2:数据角色】") + private Integer type; + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/request/user/UserInfoAddRequest.java b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/request/user/UserInfoAddRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..4b4e60a5bef405ad768da962d27ba07288bdc2e0 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-model/src/main/java/com/itools/core/ucenter/request/user/UserInfoAddRequest.java @@ -0,0 +1,76 @@ +package com.itools.core.ucenter.request.user; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.sql.Blob; + +/** + *

+ * + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Data +@ApiModel(value="添加用户视图层对象", description="") +public class UserInfoAddRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "公司id") + private String companyId; + + @ApiModelProperty(value = "部门id") + private String orgId; + + @ApiModelProperty(value = "用户名") + private String name; + + @ApiModelProperty(value = "用户类型") + private Integer userType; + + @ApiModelProperty(value = "性别") + private Integer sex; + + @ApiModelProperty(value = "生日") + private String birthday; + + @ApiModelProperty(value = "邮箱") + private String email; + + @ApiModelProperty(value = "位置") + private String position; + + @ApiModelProperty(value = "描述") + private String remark; + + @ApiModelProperty(value = "手机号") + private String phone; + + @ApiModelProperty(value = "编号") + private String userCode; + + @ApiModelProperty(value = "等级") + private Integer userLevel; + + @ApiModelProperty(value = "头像") + private Blob faceUrl; + + @ApiModelProperty(value = "工号") + private String jobNumber; + + @ApiModelProperty(value = "账号") + private String identifier; + + @ApiModelProperty(value = "账号类型【手机号:1;邮箱:2,;账号:3】") + private Integer identityType; + + @ApiModelProperty(value = "账号密码") + private String password; + + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/pom.xml b/itools-ucenter-sso/itools-ucenter-sso-server/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..991798f019000955760bcc9170a3a476ea48a679 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/pom.xml @@ -0,0 +1,91 @@ + + + + itools-ucenter-sso + com.itools.core + 1.0-SNAPSHOT + + 4.0.0 + + itools-ucenter-sso-server + + + com.itools.core + itools-ucenter-sso-model + 1.0-SNAPSHOT + + + com.itools.core + itools-ucenter-sso-core + 1.0-SNAPSHOT + + + com.itools.core + itools-common + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + + + com.baomidou + mybatis-plus-boot-starter + + + + + mysql + mysql-connector-java + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + + src/main/resources + true + + **/*.jks + + + + src/main/resources + false + + **/*.jks + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/UcenterSsoServerApplication.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/UcenterSsoServerApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..42d3ee06e4f7869ab2ad4d59df56e0fe26cfd974 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/UcenterSsoServerApplication.java @@ -0,0 +1,57 @@ +package com.itools.core.ucenter; + +import com.itools.core.snowflake.config.EnableSequenceService; +import com.itools.core.validate.EnableValidator; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.Banner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 13:20 + */ +@SpringBootApplication(scanBasePackages = {"com.itools.core"}) +@EnableSequenceService +@EnableValidator +@MapperScan("com.itools.core.ucenter.mapper") +@EnableAspectJAutoProxy(exposeProxy = true)//exposeProxy类内部可以获取到当前类的代理对象 +public class UcenterSsoServerApplication { + + public static void main(String[] args) { + SpringApplication springApplication = new SpringApplication(UcenterSsoServerApplication.class); + springApplication.setBannerMode(Banner.Mode.CONSOLE); + springApplication.run(args); + } + @Bean + public CorsFilter corsFilter() { + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + final CorsConfiguration config = new CorsConfiguration(); + // 允许cookies跨域 + config.setAllowCredentials(true); + // 允许向该服务器提交请求的URI,*表示全部允许。。这里尽量限制来源域,比如http://xxxx:8080 ,以降低安全风险。。 + config.addAllowedOrigin("*"); + // 允许访问的头信息,*表示全部 + config.addAllowedHeader("*"); + // 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了 + config.setMaxAge(18000L); + // 允许提交请求的方法,*表示全部允许,也可以单独设置GET、PUT等 + config.addAllowedMethod("*"); + config.addAllowedMethod("HEAD"); + // 允许Get的请求方法 + config.addAllowedMethod("GET"); + config.addAllowedMethod("PUT"); + config.addAllowedMethod("POST"); + config.addAllowedMethod("DELETE"); + config.addAllowedMethod("PATCH"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/config/XxlSsoConfig.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/config/XxlSsoConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..e13c827f06817864e184921f239fce93cc1b9be9 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/config/XxlSsoConfig.java @@ -0,0 +1,36 @@ +package com.itools.core.ucenter.config; + +import com.itools.core.ucenter.store.SsoLoginStore; +import com.itools.core.ucenter.util.JedisUtil; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 13:20 + */ +@Configuration +public class XxlSsoConfig implements InitializingBean, DisposableBean { + + @Value("${xxl.sso.redis.address}") + private String redisAddress; + + @Value("${xxl.sso.redis.expire.minite}") + private int redisExpireMinite; + + @Override + public void afterPropertiesSet() throws Exception { + SsoLoginStore.setRedisExpireMinite(redisExpireMinite); + JedisUtil.init(redisAddress); + } + + @Override + public void destroy() throws Exception { + JedisUtil.close(); + } + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/AppController.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/AppController.java new file mode 100644 index 0000000000000000000000000000000000000000..72f748f0456781af1d902f282d562b9e31cb973d --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/AppController.java @@ -0,0 +1,101 @@ +package com.itools.core.ucenter.controller; + + +import com.itools.core.ucenter.core.model.UserInfo; +import com.itools.core.ucenter.core.result.ReturnT; +import com.itools.core.ucenter.login.SsoTokenLoginHelper; +import com.itools.core.ucenter.service.UserService; +import com.itools.core.ucenter.store.SsoLoginStore; +import com.itools.core.ucenter.store.SsoSessionIdHelper; +import com.itools.core.ucenter.user.XxlSsoUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.UUID; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 13:20 + */ +@Controller +@RequestMapping("/app") +public class AppController { + + @Autowired + private UserService userService; + + + /** + * Login + * + * @param username + * @param password + * @return + */ + @RequestMapping("/login") + @ResponseBody + public ReturnT login(String username, String password) { + + // valid login + ReturnT result = userService.findUser(username, password); + if (result.getCode() != ReturnT.SUCCESS_CODE) { + return new ReturnT(result.getCode(), result.getMsg()); + } + + // 1、make xxl-sso user + XxlSsoUser xxlUser = new XxlSsoUser(); + xxlUser.setUserid(String.valueOf(result.getData().getUserid())); + xxlUser.setUsername(result.getData().getUsername()); + xxlUser.setVersion(UUID.randomUUID().toString().replaceAll("-", "")); + xxlUser.setExpireMinite(SsoLoginStore.getRedisExpireMinite()); + xxlUser.setExpireFreshTime(System.currentTimeMillis()); + + + // 2、generate sessionId + storeKey + String sessionId = SsoSessionIdHelper.makeSessionId(xxlUser); + + // 3、login, store storeKey + SsoTokenLoginHelper.login(sessionId, xxlUser); + + // 4、return sessionId + return new ReturnT(sessionId); + } + + + /** + * Logout + * + * @param sessionId + * @return + */ + @RequestMapping("/logout") + @ResponseBody + public ReturnT logout(String sessionId) { + // logout, remove storeKey + SsoTokenLoginHelper.logout(sessionId); + return ReturnT.SUCCESS; + } + + /** + * logincheck + * + * @param sessionId + * @return + */ + @RequestMapping("/logincheck") + @ResponseBody + public ReturnT logincheck(String sessionId) { + + // logout + XxlSsoUser xxlUser = SsoTokenLoginHelper.loginCheck(sessionId); + if (xxlUser == null) { + return new ReturnT(ReturnT.FAIL_CODE, "sso not login."); + } + return new ReturnT(xxlUser); + } + +} \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/ApplicationInfoController.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/ApplicationInfoController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9c43d228fdc40cdfa17b55ea50a8e0e843c9f8c --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/ApplicationInfoController.java @@ -0,0 +1,25 @@ +package com.itools.core.ucenter.controller; + + +import com.itools.core.ucenter.service.ApplicationInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/application") +public class ApplicationInfoController { + @Autowired + private ApplicationInfoService applicationInfoService; + +} + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/AuthResourceRefController.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/AuthResourceRefController.java new file mode 100644 index 0000000000000000000000000000000000000000..177e9e445f1cc9fd07470f134f47fe88bc0fd79c --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/AuthResourceRefController.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/authResource") +public class AuthResourceRefController { + +} + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/AuthorizationInfoController.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/AuthorizationInfoController.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccc1a6e4e967390267e8f77e335bc8bd922bc9d --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/AuthorizationInfoController.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/authorization") +public class AuthorizationInfoController { + +} + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/GroupInfoController.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/GroupInfoController.java new file mode 100644 index 0000000000000000000000000000000000000000..cb585318bb65f50e9b720aab86406b536bdb42c9 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/GroupInfoController.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/group") +public class GroupInfoController { + +} + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/OrganizationInfoController.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/OrganizationInfoController.java new file mode 100644 index 0000000000000000000000000000000000000000..eec6d15cb913a7c2e6945623d5d9e7b0c5aaab50 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/OrganizationInfoController.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/organization") +public class OrganizationInfoController { + +} + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/ResourceInfoController.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/ResourceInfoController.java new file mode 100644 index 0000000000000000000000000000000000000000..60a6970cfdeabda31ba2f736b9d3f5933909b92e --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/ResourceInfoController.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/resource") +public class ResourceInfoController { + +} + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/RoleAuthorizationRefController.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/RoleAuthorizationRefController.java new file mode 100644 index 0000000000000000000000000000000000000000..c9b3362209472621ac72cac9ba3b3cc1d098754b --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/RoleAuthorizationRefController.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/roleAuth") +public class RoleAuthorizationRefController { + +} + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/RoleInfoController.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/RoleInfoController.java new file mode 100644 index 0000000000000000000000000000000000000000..92491760bcfc5abc66b584c40d79a2dcc9729227 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/RoleInfoController.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/role") +public class RoleInfoController { + +} + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/RoleResourceRefController.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/RoleResourceRefController.java new file mode 100644 index 0000000000000000000000000000000000000000..1304d39d14a7bc5ad3a2c9735ae9ba391b64f74f --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/RoleResourceRefController.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/roleResource") +public class RoleResourceRefController { + +} + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/UserInfoController.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/UserInfoController.java new file mode 100644 index 0000000000000000000000000000000000000000..69ae1d538abb4bd62c133b6565c23e7af2d97022 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/UserInfoController.java @@ -0,0 +1,34 @@ +package com.itools.core.ucenter.controller; + + +import com.itools.core.base.CommonResult; +import com.itools.core.ucenter.request.user.UserInfoAddRequest; +import com.itools.core.ucenter.service.UserInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/user") +public class UserInfoController { + @Autowired + private UserInfoService userInfoService; + + @PostMapping("/add") + public CommonResult add(@RequestBody UserInfoAddRequest userInfoAddRequest){ + return null; + } + + +} + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/UserRoleRefController.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/UserRoleRefController.java new file mode 100644 index 0000000000000000000000000000000000000000..b54f2ffdeded6a40b436c04a62ae8a3c838b4a6d --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/UserRoleRefController.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + *

+ * 前端控制器 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@RestController +@RequestMapping("/res/userRole") +public class UserRoleRefController { + +} + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/WebController.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/WebController.java new file mode 100644 index 0000000000000000000000000000000000000000..7286d1b9624103d01316ad5e2f652fbff4eb94fa --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/WebController.java @@ -0,0 +1,153 @@ +package com.itools.core.ucenter.controller; + + +import com.itools.core.ucenter.conf.Conf; +import com.itools.core.ucenter.core.model.UserInfo; +import com.itools.core.ucenter.core.result.ReturnT; +import com.itools.core.ucenter.login.SsoWebLoginHelper; +import com.itools.core.ucenter.service.UserService; +import com.itools.core.ucenter.store.SsoLoginStore; +import com.itools.core.ucenter.store.SsoSessionIdHelper; +import com.itools.core.ucenter.user.XxlSsoUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.UUID; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 13:20 + */ +@Controller +public class WebController { + + @Autowired + private UserService userService; + + @RequestMapping("/") + public String index(Model model, HttpServletRequest request, HttpServletResponse response) { + + // login check + XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(request, response); + + if (xxlUser == null) { + return "redirect:/login"; + } else { + model.addAttribute("xxlUser", xxlUser); + return "index"; + } + } + + /** + * Login page + * + * @param model + * @param request + * @return + */ + @RequestMapping(Conf.SSO_LOGIN) + public String login(Model model, HttpServletRequest request, HttpServletResponse response) { + + // login check + XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(request, response); + + if (xxlUser != null) { + + // success redirect + String redirectUrl = request.getParameter(Conf.REDIRECT_URL); + if (redirectUrl!=null && redirectUrl.trim().length()>0) { + + String sessionId = SsoWebLoginHelper.getSessionIdByCookie(request); + String redirectUrlFinal = redirectUrl + "?" + Conf.SSO_SESSIONID + "=" + sessionId;; + + return "redirect:" + redirectUrlFinal; + } else { + return "redirect:/"; + } + } + + model.addAttribute("errorMsg", request.getParameter("errorMsg")); + model.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL)); + return "login"; + } + + /** + * Login + * + * @param request + * @param redirectAttributes + * @param username + * @param password + * @return + */ + @RequestMapping("/doLogin") + public String doLogin(HttpServletRequest request, + HttpServletResponse response, + RedirectAttributes redirectAttributes, + String username, + String password, + String ifRemember) { + + boolean ifRem = (ifRemember!=null&&"on".equals(ifRemember))?true:false; + + // valid login + ReturnT result = userService.findUser(username, password); + if (result.getCode() != ReturnT.SUCCESS_CODE) { + redirectAttributes.addAttribute("errorMsg", result.getMsg()); + + redirectAttributes.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL)); + return "redirect:/login"; + } + + // 1、make xxl-sso user + XxlSsoUser xxlUser = new XxlSsoUser(); + xxlUser.setUserid(String.valueOf(result.getData().getUserid())); + xxlUser.setUsername(result.getData().getUsername()); + xxlUser.setVersion(UUID.randomUUID().toString().replaceAll("-", "")); + xxlUser.setExpireMinite(SsoLoginStore.getRedisExpireMinite()); + xxlUser.setExpireFreshTime(System.currentTimeMillis()); + + + // 2、make session id + String sessionId = SsoSessionIdHelper.makeSessionId(xxlUser); + + // 3、login, store storeKey + cookie sessionId + SsoWebLoginHelper.login(response, sessionId, xxlUser, ifRem); + + // 4、return, redirect sessionId + String redirectUrl = request.getParameter(Conf.REDIRECT_URL); + if (redirectUrl!=null && redirectUrl.trim().length()>0) { + String redirectUrlFinal = redirectUrl + "?" + Conf.SSO_SESSIONID + "=" + sessionId; + return "redirect:" + redirectUrlFinal; + } else { + return "redirect:/"; + } + + } + + /** + * Logout + * + * @param request + * @param redirectAttributes + * @return + */ + @RequestMapping(Conf.SSO_LOGOUT) + public String logout(HttpServletRequest request, HttpServletResponse response, RedirectAttributes redirectAttributes) { + + // logout + SsoWebLoginHelper.logout(request, response); + + redirectAttributes.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL)); + return "redirect:/login"; + } + + +} \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/interceptor/MyWebMvcConfigurer.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/interceptor/MyWebMvcConfigurer.java new file mode 100644 index 0000000000000000000000000000000000000000..97f1a6c106ecb4a7ec3e0f2ed2f2339407b75375 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/interceptor/MyWebMvcConfigurer.java @@ -0,0 +1,22 @@ +package com.itools.core.ucenter.controller.interceptor; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 13:20 + */ +//@Configuration +//public class MyWebMvcConfigurer extends WebMvcConfigurerAdapter { +// +// @Override +// public void addInterceptors(InterceptorRegistry registry) { +// registry.addInterceptor(new PermissionInterceptor()).addPathPatterns("/**"); +// super.addInterceptors(registry); +// } +// +//} \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/interceptor/PermissionInterceptor.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/interceptor/PermissionInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..49ccd79330ef24893474f8a7be3851beb9eb4e4b --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/interceptor/PermissionInterceptor.java @@ -0,0 +1,41 @@ +package com.itools.core.ucenter.controller.interceptor; + +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 13:20 + */ +public class PermissionInterceptor implements HandlerInterceptor { + + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + if (!(handler instanceof HandlerMethod)) { + return true; + } + + // TODO + + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + + } + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/interceptor/UcenterWebMvcConfigurer.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/interceptor/UcenterWebMvcConfigurer.java new file mode 100644 index 0000000000000000000000000000000000000000..3d723820fab723cd301269fea53155540becf544 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/interceptor/UcenterWebMvcConfigurer.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.controller.interceptor; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 16:39 + */ +@Configuration +public class UcenterWebMvcConfigurer implements WebMvcConfigurer { + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new PermissionInterceptor()).addPathPatterns("/**"); + + } +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/resolver/WebExceptionResolver.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/resolver/WebExceptionResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..817b4190f8263c13228e3a6d33f9a7161bc3dd9b --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/controller/resolver/WebExceptionResolver.java @@ -0,0 +1,74 @@ +package com.itools.core.ucenter.controller.resolver; + +import com.itools.core.ucenter.core.result.ReturnT; +import com.itools.core.ucenter.exception.XxlSsoException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @project: itools-backend + * @description: + * 统一异常处理(Controller切面方式实现) + * + * 1、@ControllerAdvice:扫描所有Controller; + * 2、@ControllerAdvice(annotations=RestController.class):扫描指定注解类型的Controller; + * 3、@ControllerAdvice(basePackages={"com.aaa","com.bbb"}):扫描指定package下的Controller + * @author: XUCHANG + * @create: 2021-08-08 13:20 + */ +@Component +public class WebExceptionResolver implements HandlerExceptionResolver { + private static transient Logger logger = LoggerFactory.getLogger(WebExceptionResolver.class); + + @Override + public ModelAndView resolveException(HttpServletRequest request, + HttpServletResponse response, + Object handler, + Exception ex) { + + logger.error("WebExceptionResolver:{}", ex); + + // if json + boolean isJson = false; + HandlerMethod method = (HandlerMethod)handler; + ResponseBody responseBody = method.getMethodAnnotation(ResponseBody.class); + if (responseBody != null) { + isJson = true; + } + + // error result + ReturnT errorResult = null; + if (ex instanceof XxlSsoException) { + errorResult = new ReturnT(ReturnT.FAIL_CODE, ex.getMessage()); + } else { + errorResult = new ReturnT(ReturnT.FAIL_CODE, ex.toString().replaceAll("\n", "
")); + } + + // response + ModelAndView mv = new ModelAndView(); + if (isJson) { + try { + response.setContentType("application/json;charset=utf-8"); + response.getWriter().print("{\"code\":"+errorResult.getCode()+", \"msg\":\""+ errorResult.getMsg() +"\"}"); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return mv; + } else { + + mv.addObject("exceptionMsg", errorResult.getMsg()); + mv.setViewName("/common/common.exception"); + return mv; + } + } + +} \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/core/model/UserInfo.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/core/model/UserInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..c7a30951a70e82b25e2debfe7927768bb1d9530a --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/core/model/UserInfo.java @@ -0,0 +1,39 @@ +package com.itools.core.ucenter.core.model; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 13:20 + */ +public class UserInfo { + + private int userid; + private String username; + private String password; + + public int getUserid() { + return userid; + } + + public void setUserid(int userid) { + this.userid = userid; + } + + 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; + } + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/core/result/ReturnT.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/core/result/ReturnT.java new file mode 100644 index 0000000000000000000000000000000000000000..59ebb9048988fc150a73c00010a74ed7c37958c0 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/core/result/ReturnT.java @@ -0,0 +1,51 @@ +package com.itools.core.ucenter.core.result; + +import java.io.Serializable; + +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 13:20 + */ +public class ReturnT implements Serializable { + public static final long serialVersionUID = 42L; + + public static final int SUCCESS_CODE = 200; + public static final int FAIL_CODE = 500; + public static final ReturnT SUCCESS = new ReturnT(null); + public static final ReturnT FAIL = new ReturnT(FAIL_CODE, null); + + private int code; + private String msg; + private T data; + + public ReturnT(int code, String msg) { + this.code = code; + this.msg = msg; + } + public ReturnT(T data) { + this.code = SUCCESS_CODE; + this.data = data; + } + + public int getCode() { + return code; + } + public void setCode(int code) { + this.code = code; + } + public String getMsg() { + return msg; + } + public void setMsg(String msg) { + this.msg = msg; + } + public T getData() { + return data; + } + public void setData(T data) { + this.data = data; + } + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/em/UcenterCodeBean.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/em/UcenterCodeBean.java new file mode 100644 index 0000000000000000000000000000000000000000..71acb26714f1bd3a9589d41be219883fcbb0f51b --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/em/UcenterCodeBean.java @@ -0,0 +1,35 @@ +package com.itools.core.ucenter.em; + + +import com.itools.core.code.SystemCode; + +/** + * @description: + * @author: XUCHANG + * @create: 2021-03-28 15:51 + */ +@SystemCode +public class UcenterCodeBean { + + public enum UcenterCode { + + FAIL_SYSTEM("CODE00", "系统内部错误,请联系管理员!"), + ; + + public final String code; + public final String message; + + UcenterCode(String code, String message) { + this.code = code; + this.message = message; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } + } +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/ApplicationInfoMapper.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/ApplicationInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..c1c10997be2b6f60ffd7b80cc36711ca3669c5b7 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/ApplicationInfoMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.ApplicationInfo; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface ApplicationInfoMapper extends BaseMapper { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/AuthResourceRefMapper.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/AuthResourceRefMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..ca4c37866395bdc30287961c0ae32866ef9f441b --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/AuthResourceRefMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.AuthResourceRef; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface AuthResourceRefMapper extends BaseMapper { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/AuthorizationInfoMapper.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/AuthorizationInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..aab13cc2ee2c5852006cedc35e9187371eb3659e --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/AuthorizationInfoMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.AuthorizationInfo; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface AuthorizationInfoMapper extends BaseMapper { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/GroupInfoMapper.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/GroupInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..29cf40d73f8a5cc4aa0f209389a42805386938da --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/GroupInfoMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.GroupInfo; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface GroupInfoMapper extends BaseMapper { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/OauthClientDetailsMapper.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/OauthClientDetailsMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..99e1639bac6ac2996a45ae527dfcc5035b13ae52 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/OauthClientDetailsMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.OauthClientDetails; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface OauthClientDetailsMapper extends BaseMapper { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/OrganizationInfoMapper.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/OrganizationInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..c8e3e41af238cf352d70b729f3d03e483d0096eb --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/OrganizationInfoMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.OrganizationInfo; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface OrganizationInfoMapper extends BaseMapper { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/ResourceInfoMapper.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/ResourceInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..422f072a48a9fad3e1b651920d9149b33c220e5f --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/ResourceInfoMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.ResourceInfo; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface ResourceInfoMapper extends BaseMapper { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/RoleAuthorizationRefMapper.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/RoleAuthorizationRefMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..e5a812822746e8fc161582e20a500cd098e54ae6 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/RoleAuthorizationRefMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.RoleAuthorizationRef; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface RoleAuthorizationRefMapper extends BaseMapper { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/RoleInfoMapper.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/RoleInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..7bf6ae2a9f43bb55101fe84c69a1567a1c213004 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/RoleInfoMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.RoleInfo; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface RoleInfoMapper extends BaseMapper { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/RoleResourceRefMapper.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/RoleResourceRefMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..e72b56b62e7e0a3d9e1421b5c04fff46b3e296ee --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/RoleResourceRefMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.RoleResourceRef; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface RoleResourceRefMapper extends BaseMapper { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/UserInfoMapper.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/UserInfoMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..4901cf9416e3c5f3dacc9510d65972c5f948a6c2 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/UserInfoMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.UserInfo; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface UserInfoMapper extends BaseMapper { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/UserRoleRefMapper.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/UserRoleRefMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..17cdc1d83f80f6e14b0af08952a839e48cc0d725 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/mapper/UserRoleRefMapper.java @@ -0,0 +1,18 @@ +package com.itools.core.ucenter.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itools.core.ucenter.entity.UserRoleRef; +import org.springframework.stereotype.Repository; + +/** + *

+ * Mapper 接口 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Repository +public interface UserRoleRefMapper extends BaseMapper { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/ApplicationInfoService.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/ApplicationInfoService.java new file mode 100644 index 0000000000000000000000000000000000000000..62fcd92d4fcd043b5e614721bbe6e9ab080064ef --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/ApplicationInfoService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.ApplicationInfo; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface ApplicationInfoService extends IService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/AuthResourceRefService.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/AuthResourceRefService.java new file mode 100644 index 0000000000000000000000000000000000000000..c46676969721732c5458f4a009df807443f6625c --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/AuthResourceRefService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.AuthResourceRef; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface AuthResourceRefService extends IService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/AuthorizationInfoService.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/AuthorizationInfoService.java new file mode 100644 index 0000000000000000000000000000000000000000..14d3939ac0e67a4ddf3a14abf356903193305c9d --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/AuthorizationInfoService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.AuthorizationInfo; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface AuthorizationInfoService extends IService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/GroupInfoService.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/GroupInfoService.java new file mode 100644 index 0000000000000000000000000000000000000000..b102b6e7cfa7edda53fded53e9a41aa38b8b76a2 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/GroupInfoService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.GroupInfo; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface GroupInfoService extends IService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/OauthClientDetailsService.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/OauthClientDetailsService.java new file mode 100644 index 0000000000000000000000000000000000000000..c1f5d4d8c0e4bf8f09210c82380b978073ac545d --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/OauthClientDetailsService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.OauthClientDetails; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface OauthClientDetailsService extends IService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/OrganizationInfoService.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/OrganizationInfoService.java new file mode 100644 index 0000000000000000000000000000000000000000..6ddd423af90e39f6f614ce1a28739e87a69cfa64 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/OrganizationInfoService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.OrganizationInfo; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface OrganizationInfoService extends IService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/ResourceInfoService.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/ResourceInfoService.java new file mode 100644 index 0000000000000000000000000000000000000000..bd22c098bb542e5ebd1e9b00f9680741f062e329 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/ResourceInfoService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.ResourceInfo; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface ResourceInfoService extends IService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/RoleAuthorizationRefService.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/RoleAuthorizationRefService.java new file mode 100644 index 0000000000000000000000000000000000000000..72acdfa243393c9484fb9181fa42ddedd372d125 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/RoleAuthorizationRefService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.RoleAuthorizationRef; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface RoleAuthorizationRefService extends IService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/RoleInfoService.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/RoleInfoService.java new file mode 100644 index 0000000000000000000000000000000000000000..d98171cf62cc0c0cb4930143df0d01ab0bd60505 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/RoleInfoService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.RoleInfo; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface RoleInfoService extends IService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/RoleResourceRefService.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/RoleResourceRefService.java new file mode 100644 index 0000000000000000000000000000000000000000..80ec9853561867d82da81180cf5686c4613f66f0 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/RoleResourceRefService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.RoleResourceRef; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface RoleResourceRefService extends IService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/UserInfoService.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/UserInfoService.java new file mode 100644 index 0000000000000000000000000000000000000000..91d0e9c4f843e800556bf4c1ca1881645d2dfad6 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/UserInfoService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.UserInfo; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface UserInfoService extends IService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/UserRoleRefService.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/UserRoleRefService.java new file mode 100644 index 0000000000000000000000000000000000000000..88da731d614b07db80028f8512d9deddc88ee589 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/UserRoleRefService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itools.core.ucenter.entity.UserRoleRef; + +/** + *

+ * 服务类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +public interface UserRoleRefService extends IService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/UserService.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/UserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6b56912db4fe35bfa3d002d0ed213e55c1d493b2 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/UserService.java @@ -0,0 +1,16 @@ +package com.itools.core.ucenter.service; + + +import com.itools.core.ucenter.core.model.UserInfo; +import com.itools.core.ucenter.core.result.ReturnT; +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 13:20 + */ +public interface UserService { + + ReturnT findUser(String username, String password); + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/ApplicationInfoServiceImpl.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/ApplicationInfoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..05c81edb094462a95103725978cc92888ceff480 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/ApplicationInfoServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.ApplicationInfo; +import com.itools.core.ucenter.mapper.ApplicationInfoMapper; +import com.itools.core.ucenter.service.ApplicationInfoService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class ApplicationInfoServiceImpl extends ServiceImpl implements ApplicationInfoService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/AuthResourceRefServiceImpl.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/AuthResourceRefServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..90420f86ec6fb82cbe9f13cfd23e0efa5ffec7c2 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/AuthResourceRefServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.AuthResourceRef; +import com.itools.core.ucenter.mapper.AuthResourceRefMapper; +import com.itools.core.ucenter.service.AuthResourceRefService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class AuthResourceRefServiceImpl extends ServiceImpl implements AuthResourceRefService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/AuthorizationInfoServiceImpl.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/AuthorizationInfoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..f8424de0f497dea6738a1d618dab1f02a920b471 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/AuthorizationInfoServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.AuthorizationInfo; +import com.itools.core.ucenter.mapper.AuthorizationInfoMapper; +import com.itools.core.ucenter.service.AuthorizationInfoService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class AuthorizationInfoServiceImpl extends ServiceImpl implements AuthorizationInfoService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/GroupInfoServiceImpl.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/GroupInfoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..2e4d74a1aea0671b109af2e9d9591cafdfc6d8cc --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/GroupInfoServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.GroupInfo; +import com.itools.core.ucenter.mapper.GroupInfoMapper; +import com.itools.core.ucenter.service.GroupInfoService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class GroupInfoServiceImpl extends ServiceImpl implements GroupInfoService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/OauthClientDetailsServiceImpl.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/OauthClientDetailsServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..271cfae3f8a1af4b5e805c6b8d1248792874a353 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/OauthClientDetailsServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.OauthClientDetails; +import com.itools.core.ucenter.mapper.OauthClientDetailsMapper; +import com.itools.core.ucenter.service.OauthClientDetailsService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class OauthClientDetailsServiceImpl extends ServiceImpl implements OauthClientDetailsService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/OrganizationInfoServiceImpl.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/OrganizationInfoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..da726b888af9ea2653464f25d75db71d37c158d1 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/OrganizationInfoServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.OrganizationInfo; +import com.itools.core.ucenter.mapper.OrganizationInfoMapper; +import com.itools.core.ucenter.service.OrganizationInfoService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class OrganizationInfoServiceImpl extends ServiceImpl implements OrganizationInfoService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/ResourceInfoServiceImpl.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/ResourceInfoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..454c3beed8b1b6859c8d8ac2883ecfec6da32e27 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/ResourceInfoServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.ResourceInfo; +import com.itools.core.ucenter.mapper.ResourceInfoMapper; +import com.itools.core.ucenter.service.ResourceInfoService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class ResourceInfoServiceImpl extends ServiceImpl implements ResourceInfoService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/RoleAuthorizationRefServiceImpl.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/RoleAuthorizationRefServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..c60b2b7d948aeece148e28e6df2c1bf77c2b47f2 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/RoleAuthorizationRefServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.RoleAuthorizationRef; +import com.itools.core.ucenter.mapper.RoleAuthorizationRefMapper; +import com.itools.core.ucenter.service.RoleAuthorizationRefService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class RoleAuthorizationRefServiceImpl extends ServiceImpl implements RoleAuthorizationRefService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/RoleInfoServiceImpl.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/RoleInfoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..982fb2a721d4e3c499a909f152844a3cc2755474 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/RoleInfoServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.RoleInfo; +import com.itools.core.ucenter.mapper.RoleInfoMapper; +import com.itools.core.ucenter.service.RoleInfoService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class RoleInfoServiceImpl extends ServiceImpl implements RoleInfoService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/RoleResourceRefServiceImpl.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/RoleResourceRefServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..ada1080ae68c0f6a82496ce2800aac9d97029f7e --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/RoleResourceRefServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.RoleResourceRef; +import com.itools.core.ucenter.mapper.RoleResourceRefMapper; +import com.itools.core.ucenter.service.RoleResourceRefService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class RoleResourceRefServiceImpl extends ServiceImpl implements RoleResourceRefService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/UserInfoServiceImpl.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/UserInfoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..04a9000d5a46902d93869e40d71ce578fbeaffee --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/UserInfoServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.UserInfo; +import com.itools.core.ucenter.mapper.UserInfoMapper; +import com.itools.core.ucenter.service.UserInfoService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class UserInfoServiceImpl extends ServiceImpl implements UserInfoService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/UserRoleRefServiceImpl.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/UserRoleRefServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..b02270a8213c3c7d6a4353eb21900b905ebcf291 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/UserRoleRefServiceImpl.java @@ -0,0 +1,20 @@ +package com.itools.core.ucenter.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itools.core.ucenter.entity.UserRoleRef; +import com.itools.core.ucenter.mapper.UserRoleRefMapper; +import com.itools.core.ucenter.service.UserRoleRefService; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author XUCHANG + * @since 2021-07-23 + */ +@Service +public class UserRoleRefServiceImpl extends ServiceImpl implements UserRoleRefService { + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/UserServiceImpl.java b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/UserServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..d21217dcdbc54d1e9751bef2f20b8f7c286a3ff0 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/java/com/itools/core/ucenter/service/impl/UserServiceImpl.java @@ -0,0 +1,52 @@ +package com.itools.core.ucenter.service.impl; + + +import com.itools.core.ucenter.core.model.UserInfo; +import com.itools.core.ucenter.core.result.ReturnT; +import com.itools.core.ucenter.service.UserService; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +/** + * @project: itools-backend + * @description: + * @author: XUCHANG + * @create: 2021-08-08 13:20 + */ +@Service +public class UserServiceImpl implements UserService { + + private static List mockUserList = new ArrayList<>(); + static { + for (int i = 0; i <5; i++) { + UserInfo userInfo = new UserInfo(); + userInfo.setUserid(1000+i); + userInfo.setUsername("user" + (i>0?String.valueOf(i):"")); + userInfo.setPassword("123456"); + mockUserList.add(userInfo); + } + } + + @Override + public ReturnT findUser(String username, String password) { + + if (username==null || username.trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, "Please input username."); + } + if (password==null || password.trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, "Please input password."); + } + + // mock user + for (UserInfo mockUser: mockUserList) { + if (mockUser.getUsername().equals(username) && mockUser.getPassword().equals(password)) { + return new ReturnT(mockUser); + } + } + + return new ReturnT(ReturnT.FAIL_CODE, "username or password is invalid."); + } + + +} diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/application-dev.yml b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/application-dev.yml new file mode 100644 index 0000000000000000000000000000000000000000..d1e9074d44b6124f15c8bee38696d7f8e33cf3ca --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/application-dev.yml @@ -0,0 +1,50 @@ + +########################################## 组件配置 ###################################################### +system: + errorCode: Code001 +#分布式雪花算法策略 +sequence: + enable: true + type: snowflake + generate: simple +########################################## 组件配置 ###################################################### + + +########################################## mybatis-plus相关配置 ###################################################### +mybatis-plus: + mapper-locations: classpath:/mapper/**/*Mapper.xml + typeAliasesPackage: com.itools.core.ucenter.entity + global-config: + db-column-underline: true + db-config: + logic-delete-field: del_flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + configuration: + default-enum-type-handler: com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +########################################## mybatis-plus相关配置 ###################################################### + + +########################################### 用户中心放行接口 ##################################################### +#secure: +# ignored: +# shouldSkipUrls: +# - /user/test +#jwt: +# tokenHeader: Authorization #JWT存储的请求头 +# secret: 123123 #JWT加解密使用的密钥 +# expiration: 604800 #JWT的超期限时间(60*60*24) +# tokenHead: bearer #JWT负载中拿到开头 +# +#security: +# oauth2: +# client: +# access-token-uri: http://localhost:17003/oauth/token +# user-authorization-uri: http://localhost:17003/oauth/authorize +# # client-id: webapp +# resource: +# user-info-uri: http://localhost:17003/user +# prefer-token-info: false +########################################### 用户中心放行接口 ##################################################### \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/application.properties b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..d102a9b9b376e62ce0e7a26904b0accbefd6cf27 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/application.properties @@ -0,0 +1,16 @@ + +### resources +spring.mvc.static-path-pattern=/static/** +spring.resources.static-locations=classpath:/static/ + +### freemarker +spring.freemarker.templateLoaderPath=classpath:/templates/ +spring.freemarker.suffix=.ftl +spring.freemarker.charset=UTF-8 +spring.freemarker.request-context-attribute=request +spring.freemarker.settings.number_format=0.########## + +#redis 地址: 如 "{ip}"、"{ip}:{port}"、"{redis/rediss}://xxl-sso:{password}@{ip}:{port:6379}/{db}";多地址逗号分隔 +### xxl-sso +xxl.sso.redis.address=redis://127.0.0.1:6379 +xxl.sso.redis.expire.minite=1440 diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/application.yml b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..84c775dd1440070a3ff300c69964ac7da0f8749f --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/application.yml @@ -0,0 +1,30 @@ +server: + port: 20001 + servlet: + context-path: /xxl-sso-server +spring: + application: + name: itools-ucnter-sso-server + + redis: + host: 127.0.0.1 + database: 0 + + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/itools?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC + username: root + password: root + hikari: + minimum-idle: 5 + idle-timeout: 600000 + maximum-pool-size: 10 + auto-commit: true + pool-name: MyHikariCP + max-lifetime: 1800000 + connection-timeout: 30000 + connection-test-query: SELECT 1 + main: + allow-bean-definition-overriding: true + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/banner.txt b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/banner.txt new file mode 100644 index 0000000000000000000000000000000000000000..7d5e0ad3c055086f93400d83bb030f2251f137ac --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/banner.txt @@ -0,0 +1,7 @@ + _____ U ___ u U ___ u _ ____ + ___ |_ " _| \/"_ \/ \/"_ \/ |"| / __"| u + |_"_| | | | | | | | | | |U | | u<\___ \/ + | | /| |\.-,_| |_| |.-,_| |_| | \| |/__u___) | + U/| |\u u |_|U \_)-\___/ \_)-\___/ |_____|____/>> +.-,_|___|_,-._// \\_ \\ \\ // \\ )( (__) + \_)-' '-(_/(__) (__) (__) (__) (_")("_)__) diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/bootstrap.yml b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/bootstrap.yml new file mode 100644 index 0000000000000000000000000000000000000000..56e3eb83288da40dfdacd24ca651a7e16ca7ebbd --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/bootstrap.yml @@ -0,0 +1,18 @@ +# Nacos Server 的地址 +spring: + profiles: + active: dev + cloud: + nacos: + config: + server-addr: 106.54.85.156:8848 + file-extension: yaml + prefix: itools-oms-server + remote-first: true + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev + discovery: + server-addr: 106.54.85.156:8848 + namespace: e32f7110-9472-427b-8b1a-a71cdebdfdb8 + group: dev + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/logback-spring.xml b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/logback-spring.xml new file mode 100644 index 0000000000000000000000000000000000000000..84b713e214bf711e7ac0cbe16a32ad2b3b061569 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/logback-spring.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/ApplicationInfoMapper.xml b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/ApplicationInfoMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..2bf3e42b383a43c231ca57fc5dcdd1e4fae42ce3 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/ApplicationInfoMapper.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + id, app_name, app_instance_id, app_owner, status, create_time, create_user_id, last_update_time, last_update_user_id, app_code, app_owner_email, remark, app_type + + + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/AuthResourceRefMapper.xml b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/AuthResourceRefMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..ad003d280453b8db3be647cc0335ee4111bc6b51 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/AuthResourceRefMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + id, authorization_id, resource_id, type, status, create_time, create_user_id, last_update_time, last_update_user_id, application_id + + + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/AuthorizationInfoMapper.xml b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/AuthorizationInfoMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a1bde8eb82772a2dd79097d4dc55f7a7bd78e66f --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/AuthorizationInfoMapper.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + id, name, parent_id, remark, status, create_time, create_user_id, last_update_time, last_update_user_id, application_id, code, order_filed + + + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/GroupInfoMapper.xml b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/GroupInfoMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..b138e0d6c5f33fe7444fb2c6e51e3de551faece1 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/GroupInfoMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + id, create_time, create_user_id, last_update_time, last_update_user_id, application_id, code, name, remark, status + + + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/OauthClientDetailsMapper.xml b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/OauthClientDetailsMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f8130239a9c49d88b02fa33591e4a9e33a4f447 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/OauthClientDetailsMapper.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + client_id, resource_ids, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove + + + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/OrganizationInfoMapper.xml b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/OrganizationInfoMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..61202bc10475c37615f25012043c28d0f7a18846 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/OrganizationInfoMapper.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id, name, parent_id, create_time, create_user_id, last_update_time, last_update_user_id, address, application_id, code, link_man_dept, link_man_email, link_man_name, link_man_phone, rank, remark, status, phone, type, organization_number, admin_user_id, order_field + + + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/ResourceInfoMapper.xml b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/ResourceInfoMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..53ce05770ef92fefb902f596a583fdd649b3b204 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/ResourceInfoMapper.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + id, name, parent_id, status, create_time, create_user_id, last_update_time, last_update_user_id, application_id, code, order_field, picture_name, remark, type, url + + + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/RoleAuthorizationRefMapper.xml b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/RoleAuthorizationRefMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..6bc3bfe367ec07f47206cf38365a2d18e0fac7a4 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/RoleAuthorizationRefMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + id, create_time, create_user_id, last_update_time, last_update_user_id, application_id, authorization_id, role_id, status + + + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/RoleInfoMapper.xml b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/RoleInfoMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..0580a4eac879e357690861cbc8ee94e65da292a0 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/RoleInfoMapper.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + id, parent_id, name, application_id, remark, code, create_time, create_user_id, last_update_time, last_update_user_id, status, order_field, type + + + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/RoleResourceRefMapper.xml b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/RoleResourceRefMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..383ace40bc9369f0f246cc476eac7ae322601453 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/RoleResourceRefMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + id, create_time, create_user_id, last_update_time, last_update_user_id, application_id, role_id, resource_id, status, type + + + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/UserInfoMapper.xml b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/UserInfoMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..31d6bd590f29ce67a91e3ac19ac6c7b0865fb920 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/UserInfoMapper.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id, create_time, create_user_id, last_update_time, last_update_user_id, company_id, org_id, name, user_type, sex, birthday, email, position, remark, phone, user_code, user_level, face_url, job_number, identifier, identity_type, password, last_login_time + + + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/UserRoleRefMapper.xml b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/UserRoleRefMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..faee6a60015f8b2699ec0b9d1b9e362655bfe2ba --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/mapper/UserRoleRefMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + id, application_id, create_time, create_user_id, last_update_time, last_update_user_id, role_id, user_id, type + + + diff --git a/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/static/adminlte/bootstrap/css/bootstrap.css.map b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/static/adminlte/bootstrap/css/bootstrap.css.map new file mode 100644 index 0000000000000000000000000000000000000000..f010c82d113c25336f5ae7df32ce5de0caaddbd2 --- /dev/null +++ b/itools-ucenter-sso/itools-ucenter-sso-server/src/main/resources/static/adminlte/bootstrap/css/bootstrap.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["bootstrap.css","less/normalize.less","less/print.less","less/glyphicons.less","less/scaffolding.less","less/mixins/vendor-prefixes.less","less/mixins/tab-focus.less","less/mixins/image.less","less/type.less","less/mixins/text-emphasis.less","less/mixins/background-variant.less","less/mixins/text-overflow.less","less/code.less","less/grid.less","less/mixins/grid.less","less/mixins/grid-framework.less","less/tables.less","less/mixins/table-row.less","less/forms.less","less/mixins/forms.less","less/buttons.less","less/mixins/buttons.less","less/mixins/opacity.less","less/component-animations.less","less/dropdowns.less","less/mixins/nav-divider.less","less/mixins/reset-filter.less","less/button-groups.less","less/mixins/border-radius.less","less/input-groups.less","less/navs.less","less/navbar.less","less/mixins/nav-vertical-align.less","less/utilities.less","less/breadcrumbs.less","less/pagination.less","less/mixins/pagination.less","less/pager.less","less/labels.less","less/mixins/labels.less","less/badges.less","less/jumbotron.less","less/thumbnails.less","less/alerts.less","less/mixins/alerts.less","less/progress-bars.less","less/mixins/gradients.less","less/mixins/progress-bar.less","less/media.less","less/list-group.less","less/mixins/list-group.less","less/panels.less","less/mixins/panels.less","less/responsive-embed.less","less/wells.less","less/close.less","less/modals.less","less/tooltip.less","less/mixins/reset-text.less","less/popovers.less","less/carousel.less","less/mixins/clearfix.less","less/mixins/center-block.less","less/mixins/hide-text.less","less/responsive-utilities.less","less/mixins/responsive-visibility.less"],"names":[],"mappings":"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AW,iBAAA;CHsR9C;AGrRmC;EAAW,iBAAA;CHwR9C;AGvRmC;EAAW,iBAAA;CH0R9C;AGzRmC;EAAW,iBAAA;CH4R9C;AG3RmC;EAAW,iBAAA;CH8R9C;AG7RmC;EAAW,iBAAA;CHgS9C;AG/RmC;EAAW,iBAAA;CHkS9C;AGjSmC;EAAW,iBAAA;CHoS9C;AGnSmC;EAAW,iBAAA;CHsS9C;AGrSmC;EAAW,iBAAA;CHwS9C;AGvSmC;EAAW,iBAAA;CH0S9C;AGzSmC;EAAW,iBAAA;CH4S9C;AG3SmC;EAAW,iBAAA;CH8S9C;AG7SmC;EAAW,iBAAA;CHgT9C;AG/SmC;EAAW,iBAAA;CHkT9C;AGjTmC;EAAW,iBAAA;CHoT9C;AGnTmC;EAAW,iBAAA;CHsT9C;AGrTmC;EAAW,iBAAA;CHwT9C;AGvTmC;EAAW,iBAAA;CH0T9C;AGzTmC;EAAW,iBAAA;CH4T9C;AG3TmC;EAAW,iBAAA;CH8T9C;AG7TmC;EAAW,iBAAA;CHgU9C;AG/TmC;EAAW,iBAAA;CHkU9C;AGjUmC;EAAW,iBAAA;CHoU9C;AGnUmC;EAAW,iBAAA;CHsU9C;AGrUmC;EAAW,iBAAA;CHwU9C;AGvUmC;EAAW,iBAAA;CH0U9C;AGzUmC;EAAW,iBAAA;CH4U9C;AG3UmC;EAAW,iBAAA;CH8U9C;AG7UmC;EAAW,iBAAA;CHgV9C;AG/UmC;EAAW,iBAAA;CHkV9C;AGjVmC;EAAW,iBAAA;CHoV9C;AGnVmC;EAAW,iBAAA;CHsV9C;AGrVmC;EAAW,iBAAA;CHwV9C;AGvVmC;EAAW,iBAAA;CH0V9C;AGzVmC;EAAW,iBAAA;CH4V9C;AG3VmC;EAAW,iBAAA;CH8V9C;AG7VmC;EAAW,iBAAA;CHgW9C;AG/VmC;EAAW,iBAAA;CHkW9C;AGjWmC;EAAW,iBAAA;CHoW9C;AGnWmC;EAAW,iBAAA;CHsW9C;AGrWmC;EAAW,iBAAA;CHwW9C;AGvWmC;EAAW,iBAAA;CH0W9C;AGzWmC;EAAW,iBAAA;CH4W9C;AG3WmC;EAAW,iBAAA;CH8W9C;AG7WmC;EAAW,iBAAA;CHgX9C;AG/WmC;EAAW,iBAAA;CHkX9C;AGjXmC;EAAW,iBAAA;CHoX9C;AGnXmC;EAAW,iBAAA;CHsX9C;AGrXmC;EAAW,iBAAA;CHwX9C;AGvXmC;EAAW,iBAAA;CH0X9C;AGzXmC;EAAW,iBAAA;CH4X9C;AG3XmC;EAAW,iBAAA;CH8X9C;AG7XmC;EAAW,iBAAA;CHgY9C;AG/XmC;EAAW,iBAAA;CHkY9C;AGjYmC;EAAW,iBAAA;CHoY9C;AGnYmC;EAAW,iBAAA;CHsY9C;AGrYmC;EAAW,iBAAA;CHwY9C;AGvYmC;EAAW,iBAAA;CH0Y9C;AGzYmC;EAAW,iBAAA;CH4Y9C;AG3YmC;EAAW,iBAAA;CH8Y9C;AG7YmC;EAAW,iBAAA;CHgZ9C;AG/YmC;EAAW,iBAAA;CHkZ9C;AGjZmC;EAAW,iBAAA;CHoZ9C;AGnZmC;EAAW,iBAAA;CHsZ9C;AGrZmC;EAAW,iBAAA;CHwZ9C;AGvZmC;EAAW,iBAAA;CH0Z9C;AGzZmC;EAAW,iBAAA;CH4Z9C;AG3ZmC;EAAW,iBAAA;CH8Z9C;AG7ZmC;EAAW,iBAAA;CHga9C;AG/ZmC;EAAW,iBAAA;CHka9C;AGjamC;EAAW,iBAAA;CHoa9C;AGnamC;EAAW,iBAAA;CHsa9C;AGramC;EAAW,iBAAA;CHwa9C;AGvamC;EAAW,iBAAA;CH0a9C;AGzamC;EAAW,iBAAA;CH4a9C;AG3amC;EAAW,iBAAA;CH8a9C;AG7amC;EAAW,iBAAA;CHgb9C;AG/amC;EAAW,iBAAA;CHkb9C;AGjbmC;EAAW,iBAAA;CHob9C;AGnbmC;EAAW,iBAAA;CHsb9C;AGrbmC;EAAW,iBAAA;CHwb9C;AGvbmC;EAAW,iBAAA;CH0b9C;AGzbmC;EAAW,iBAAA;CH4b9C;AG3bmC;EAAW,iBAAA;CH8b9C;AG7bmC;EAAW,iBAAA;CHgc9C;AG/bmC;EAAW,iBAAA;CHkc9C;AGjcmC;EAAW,iBAAA;CHoc9C;AGncmC;EAAW,iBAAA;CHsc9C;AGrcmC;EAAW,iBAAA;CHwc9C;AGvcmC;EAAW,iBAAA;CH0c9C;AGzcmC;EAAW,iBAAA;CH4c9C;AG3cmC;EAAW,iBAAA;CH8c9C;AG7cmC;EAAW,iBAAA;CHgd9C;AG/cmC;EAAW,iBAAA;CHkd9C;AGjdmC;EAAW,iBAAA;CHod9C;AGndmC;EAAW,iBAAA;CHsd9C;AGrdmC;EAAW,iBAAA;CHwd9C;AGvdmC;EAAW,iBAAA;CH0d9C;AGzdmC;EAAW,iBAAA;CH4d9C;AG3dmC;EAAW,iBAAA;CH8d9C;AG7dmC;EAAW,iBAAA;CHge9C;AG/dmC;EAAW,iBAAA;CHke9C;AGjemC;EAAW,iBAAA;CHoe9C;AGnemC;EAAW,iBAAA;CHse9C;AGremC;EAAW,iBAAA;CHwe9C;AGvemC;EAAW,iBAAA;CH0e9C;AGzemC;EAAW,iBAAA;CH4e9C;AG3emC;EAAW,iBAAA;CH8e9C;AG7emC;EAAW,iBAAA;CHgf9C;AG/emC;EAAW,iBAAA;CHkf9C;AGjfmC;EAAW,iBAAA;CHof9C;AGnfmC;EAAW,iBAAA;CHsf9C;AGrfmC;EAAW,iBAAA;CHwf9C;AGvfmC;EAAW,iBAAA;CH0f9C;AGzfmC;EAAW,iBAAA;CH4f9C;AG3fmC;EAAW,iBAAA;CH8f9C;AG7fmC;EAAW,iBAAA;CHggB9C;AG/fmC;EAAW,iBAAA;CHkgB9C;AGjgBmC;EAAW,iBAAA;CHogB9C;AGngBmC;EAAW,iBAAA;CHsgB9C;AGrgBmC;EAAW,iBAAA;CHwgB9C;AGvgBmC;EAAW,iBAAA;CH0gB9C;AGzgBmC;EAAW,iBAAA;CH4gB9C;AG3gBmC;EAAW,iBAAA;CH8gB9C;AG7gBmC;EAAW,iBAAA;CHghB9C;AG/gBmC;EAAW,iBAAA;CHkhB9C;AGjhBmC;EAAW,iBAAA;CHohB9C;AGnhBmC;EAAW,iBAAA;CHshB9C;AGrhBmC;EAAW,iBAAA;CHwhB9C;AGvhBmC;EAAW,iBAAA;CH0hB9C;AGzhBmC;EAAW,iBAAA;CH4hB9C;AG3hBmC;EAAW,iBAAA;CH8hB9C;AG7hBmC;EAAW,iBAAA;CHgiB9C;AG/hBmC;EAAW,iBAAA;CHkiB9C;AGjiBmC;EAAW,iBAAA;CHoiB9C;AGniBmC;EAAW,iBAAA;CHsiB9C;AGriBmC;EAAW,iBAAA;CHwiB9C;AGviBmC;EAAW,iBAAA;CH0iB9C;AGziBmC;EAAW,iBAAA;CH4iB9C;AG3iBmC;EAAW,iBAAA;CH8iB9C;AG7iBmC;EAAW,iBAAA;CHgjB9C;AG/iBmC;EAAW,iBAAA;CHkjB9C;AGjjBmC;EAAW,iBAAA;CHojB9C;AGnjBmC;EAAW,iBAAA;CHsjB9C;AGrjBmC;EAAW,iBAAA;CHwjB9C;AGvjBmC;EAAW,iBAAA;CH0jB9C;AGzjBmC;EAAW,iBAAA;CH4jB9C;AG3jBmC;EAAW,iBAAA;CH8jB9C;AG7jBmC;EAAW,iBAAA;CHgkB9C;AG/jBmC;EAAW,iBAAA;CHkkB9C;AGjkBmC;EAAW,iBAAA;CHokB9C;AGnkBmC;EAAW,iBAAA;CHskB9C;AGrkBmC;EAAW,iBAAA;CHwkB9C;AGvkBmC;EAAW,iBAAA;CH0kB9C;AGzkBmC;EAAW,iBAAA;CH4kB9C;AG3kBmC;EAAW,iBAAA;CH8kB9C;AG7kBmC;EAAW,iBAAA;CHglB9C;AG/kBmC;EAAW,iBAAA;CHklB9C;AGjlBmC;EAAW,iBAAA;CHolB9C;AGnlBmC;EAAW,iBAAA;CHslB9C;AGrlBmC;EAAW,iBAAA;CHwlB9C;AGvlBmC;EAAW,iBAAA;CH0lB9C;AGzlBmC;EAAW,iBAAA;CH4lB9C;AG3lBmC;EAAW,iBAAA;CH8lB9C;AG7lBmC;EAAW,iBAAA;CHgmB9C;AG/lBmC;EAAW,iBAAA;CHkmB9C;AGjmBmC;EAAW,iBAAA;CHomB9C;AGnmBmC;EAAW,iBAAA;CHsmB9C;AGrmBmC;EAAW,iBAAA;CHwmB9C;AGvmBmC;EAAW,iBAAA;CH0mB9C;AGzmBmC;EAAW,iBAAA;CH4mB9C;AG3mBmC;EAAW,iBAAA;CH8mB9C;AG7mBmC;EAAW,iBAAA;CHgnB9C;AG/mBmC;EAAW,iBAAA;CHknB9C;AGjnBmC;EAAW,iBAAA;CHonB9C;AGnnBmC;EAAW,iBAAA;CHsnB9C;AGrnBmC;EAAW,iBAAA;CHwnB9C;AGvnBmC;EAAW,iBAAA;CH0nB9C;AGznBmC;EAAW,iBAAA;CH4nB9C;AG3nBmC;EAAW,iBAAA;CH8nB9C;AG7nBmC;EAAW,iBAAA;CHgoB9C;AG/nBmC;EAAW,iBAAA;CHkoB9C;AGjoBmC;EAAW,iBAAA;CHooB9C;AGnoBmC;EAAW,iBAAA;CHsoB9C;AGroBmC;EAAW,iBAAA;CHwoB9C;AG/nBmC;EAAW,iBAAA;CHkoB9C;AGjoBmC;EAAW,iBAAA;CHooB9C;AGnoBmC;EAAW,iBAAA;CHsoB9C;AGroBmC;EAAW,iBAAA;CHwoB9C;AGvoBmC;EAAW,iBAAA;CH0oB9C;AGzoBmC;EAAW,iBAAA;CH4oB9C;AG3oBmC;EAAW,iBAAA;CH8oB9C;AG7oBmC;EAAW,iBAAA;CHgpB9C;AG/oBmC;EAAW,iBAAA;CHkpB9C;AGjpBmC;EAAW,iBAAA;CHopB9C;AGnpBmC;EAAW,iBAAA;CHspB9C;AGrpBmC;EAAW,iBAAA;CHwpB9C;AGvpBmC;EAAW,iBAAA;CH0pB9C;AGzpBmC;EAAW,iBAAA;CH4pB9C;AG3pBmC;EAAW,iBAAA;CH8pB9C;AG7pBmC;EAAW,iBAAA;CHgqB9C;AG/pBmC;EAAW,iBAAA;CHkqB9C;AGjqBmC;EAAW,iBAAA;CHoqB9C;AGnqBmC;EAAW,iBAAA;CHsqB9C;AGrqBmC;EAAW,iBAAA;CHwqB9C;AGvqBmC;EAAW,iBAAA;CH0qB9C;AGzqBmC;EAAW,iBAAA;CH4qB9C;AG3qBmC;EAAW,iBAAA;CH8qB9C;AG7qBmC;EAAW,iBAAA;CHgrB9C;AG/qBmC;EAAW,iBAAA;CHkrB9C;AGjrBmC;EAAW,iBAAA;CHorB9C;AGnrBmC;EAAW,iBAAA;CHsrB9C;AGrrBmC;EAAW,iBAAA;CHwrB9C;AGvrBmC;EAAW,iBAAA;CH0rB9C;AGzrBmC;EAAW,iBAAA;CH4rB9C;AG3rBmC;EAAW,iBAAA;CH8rB9C;AG7rBmC;EAAW,iBAAA;CHgsB9C;AG/rBmC;EAAW,iBAAA;CHksB9C;AGjsBmC;EAAW,iBAAA;CHosB9C;AGnsBmC;EAAW,iBAAA;CHssB9C;AGrsBmC;EAAW,iBAAA;CHwsB9C;AGvsBmC;EAAW,iBAAA;CH0sB9C;AGzsBmC;EAAW,iBAAA;CH4sB9C;AG3sBmC;EAAW,iBAAA;CH8sB9C;AG7sBmC;EAAW,iBAAA;CHgtB9C;AG/sBmC;EAAW,iBAAA;CHktB9C;AGjtBmC;EAAW,iBAAA;CHotB9C;AGntBmC;EAAW,iBAAA;CHstB9C;AGrtBmC;EAAW,iBAAA;CHwtB9C;AGvtBmC;EAAW,iBAAA;CH0tB9C;AGztBmC;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ukDH;AexkDC;EACE,oBAAA;Cf0kDH;Ae3kDC;EACE,WAAA;Cf6kDH;Ae9kDC;EACE,oBAAA;CfglDH;AejlDC;EACE,mBAAA;CfmlDH;Ae/kDC;EACE,YAAA;CfilDH;AejmDC;EACE,WAAA;CfmmDH;AepmDC;EACE,mBAAA;CfsmDH;AevmDC;EACE,mBAAA;CfymDH;Ae1mDC;EACE,UAAA;Cf4mDH;Ae7mDC;EACE,mBAAA;Cf+mDH;AehnDC;EACE,mBAAA;CfknDH;AennDC;EACE,UAAA;CfqnDH;AetnDC;EACE,mBAAA;CfwnDH;AeznDC;EACE,mBAAA;Cf2nDH;Ae5nDC;EACE,UAAA;Cf8nDH;Ae/nDC;EACE,mBAAA;CfioDH;AeloDC;EACE,kBAAA;CfooDH;AehoDC;EACE,WAAA;CfkoDH;AepnDC;EACE,kBAAA;CfsnDH;AevnDC;EACE,0BAAA;CfynDH;Ae1nDC;EACE,0BAAA;Cf4nDH;Ae7nDC;EACE,iBAAA;Cf+nDH;AehoDC;EACE,0BAAA;CfkoDH;AenoDC;EACE,0BAAA;CfqoDH;AetoDC;EACE,iBAAA;CfwoDH;AezoDC;EACE,0BAAA;Cf2oDH;Ae5oDC;EACE,0BAAA;Cf8oDH;Ae/oDC;EACE,iBAAA;CfipDH;AelpDC;EACE,0BAAA;CfopDH;AerpDC;EACE,yBAAA;CfupDH;AexpDC;EACE,gBAAA;Cf0pDH;Aa1pDD;EElCI;IACE,YAAA;Gf+rDH;EexrDD;IACE,YAAA;Gf0rDD;Ee3rDD;IACE,oBAAA;Gf6rDD;Ee9rDD;IACE,oBAAA;GfgsDD;EejsDD;IACE,WAAA;GfmsDD;EepsDD;IACE,oBAAA;GfssDD;EevsDD;IACE,oBAAA;GfysDD;Ee1sDD;IACE,WAAA;Gf4sDD;Ee7sDD;IACE,oBAAA;Gf+sDD;EehtDD;IACE,oBAAA;GfktDD;EentDD;IACE,WAAA;GfqtDD;EettDD;IACE,oBAAA;GfwtDD;EeztDD;IACE,mBAAA;Gf2tDD;Ee7sDD;IACE,YAAA;Gf+sDD;EehtDD;IACE,oBAAA;GfktDD;EentDD;IACE,oBAAA;GfqtDD;EettDD;IACE,WAAA;GfwtDD;EeztDD;IACE,oBAAA;Gf2tDD;Ee5tDD;IACE,oBAAA;Gf8tDD;Ee/tDD;IACE,WAAA;GfiuDD;EeluDD;IACE,oBAAA;GfouDD;EeruDD;IACE,oBAAA;GfuuDD;EexuDD;IACE,WAAA;Gf0uDD;Ee3uDD;IACE,oBAAA;Gf6uDD;Ee9uDD;IACE,mBAAA;GfgvDD;Ee5uDD;IACE,YAAA;Gf8uDD;Ee9vDD;IACE,WAAA;GfgwDD;EejwDD;IACE,mBAAA;GfmwDD;EepwDD;IACE,mBAAA;GfswDD;EevwDD;IACE,UAAA;GfywDD;Ee1wDD;IACE,mBAAA;Gf4wDD;Ee7wDD;IACE,mBAAA;Gf+wDD;EehxDD;IACE,UAAA;GfkxDD;EenxDD;IACE,mBAAA;GfqxDD;EetxDD;IACE,mBAAA;GfwxDD;EezxDD;IACE,UAAA;Gf2xDD;Ee5xDD;IACE,mBAAA;Gf8xDD;Ee/xDD;IACE,kBAAA;GfiyDD;Ee7xDD;IACE,WAAA;Gf+xDD;EejxDD;IACE,kBAAA;GfmxDD;EepxDD;IACE,0BAAA;GfsxDD;EevxDD;IACE,0BAAA;GfyxDD;Ee1xDD;IACE,iBAAA;Gf4xDD;Ee7xDD;IACE,0BAAA;Gf+xDD;EehyDD;IACE,0BAAA;GfkyDD;EenyDD;IACE,iBAAA;GfqyDD;EetyDD;IACE,0BAAA;GfwyDD;EezyDD;IACE,0BAAA;Gf2yDD;Ee5yDD;IACE,iBAAA;Gf8yDD;Ee/yDD;IACE,0BAAA;GfizDD;EelzDD;IACE,yBAAA;GfozDD;EerzDD;IACE,gBAAA;GfuzDD;CACF;Aa/yDD;EE3CI;IACE,YAAA;Gf61DH;Eet1DD;IACE,YAAA;Gfw1DD;Eez1DD;IACE,oBAAA;Gf21DD;Ee51DD;IACE,oBAAA;Gf81DD;Ee/1DD;IACE,WAAA;Gfi2DD;Eel2DD;IACE,oBAAA;Gfo2DD;Eer2DD;IACE,oBAAA;Gfu2DD;Eex2DD;IACE,WAAA;Gf02DD;Ee32DD;IACE,oBAAA;Gf62DD;Ee92DD;IACE,oBAAA;Gfg3DD;Eej3DD;IACE,WAAA;Gfm3DD;Eep3DD;IACE,oBAAA;Gfs3DD;Eev3DD;IACE,mBAAA;Gfy3DD;Ee32DD;IACE,YAAA;Gf62DD;Ee92DD;IACE,oBAAA;Gfg3DD;Eej3DD;IACE,oBAAA;Gfm3DD;Eep3DD;IACE,WAAA;Gfs3DD;Eev3DD;IACE,oBAAA;Gfy3DD;Ee13DD;IACE,oBAAA;Gf43DD;Ee73DD;IACE,WAAA;Gf+3DD;Eeh4DD;IACE,oBAAA;Gfk4DD;Een4DD;IACE,oBAAA;Gfq4DD;Eet4DD;IACE,WAAA;Gfw4DD;Eez4DD;IACE,oBAAA;Gf24DD;Ee54DD;IACE,mBAAA;Gf84DD;Ee14DD;IACE,YAAA;Gf44DD;Ee55DD;IACE,WAAA;Gf85DD;Ee/5DD;IACE,mBAAA;Gfi6DD;Eel6DD;IACE,mBAAA;Gfo6DD;Eer6DD;IACE,UAAA;Gfu6DD;Eex6DD;IACE,mBAAA;Gf06DD;Ee36DD;IACE,mBAAA;Gf66DD;Ee96DD;IACE,UAAA;Gfg7DD;Eej7DD;IACE,mBAAA;Gfm7DD;Eep7DD;IACE,mBAAA;Gfs7DD;Eev7DD;IACE,UAAA;Gfy7DD;Ee17DD;IACE,mBAAA;Gf47DD;Ee77DD;IACE,kBAAA;Gf+7DD;Ee37DD;IACE,WAAA;Gf67DD;Ee/6DD;IACE,kBAAA;Gfi7DD;Eel7DD;IACE,0BAAA;Gfo7DD;Eer7DD;IACE,0BAAA;Gfu7DD;Eex7DD;IACE,iBAAA;Gf07DD;Ee37DD;IACE,0BAAA;Gf67DD;Ee97DD;IACE,0BAAA;Gfg8DD;Eej8DD;IACE,iBAAA;Gfm8DD;Eep8DD;IACE,0BAAA;Gfs8DD;Eev8DD;IACE,0BAAA;Gfy8DD;Ee18DD;IACE,iBAAA;Gf48DD;Ee78DD;IACE,0BAAA;Gf+8DD;Eeh9DD;IACE,yBAAA;Gfk9DD;Een9DD;IACE,gBAAA;Gfq9DD;CACF;Aa18DD;EE9CI;IACE,YAAA;Gf2/DH;Eep/DD;IACE,YAAA;Gfs/DD;Eev/DD;IACE,oBAAA;Gfy/DD;Ee1/DD;IACE,oBAAA;Gf4/DD;Ee7/DD;IACE,WAAA;Gf+/DD;EehgED;IACE,oBAAA;GfkgED;EengED;IACE,oBAAA;GfqgED;EetgED;IACE,WAAA;GfwgED;EezgED;IACE,oBAAA;Gf2gED;Ee5gED;IACE,oBAAA;Gf8gED;Ee/gED;IACE,WAAA;GfihED;EelhED;IACE,oBAAA;GfohED;EerhED;IACE,mBAAA;GfuhED;EezgED;IACE,YAAA;Gf2gED;Ee5gED;IACE,oBAAA;Gf8gED;Ee/gED;IACE,oBAAA;GfihED;EelhED;IACE,WAAA;GfohED;EerhED;IACE,oBAAA;GfuhED;EexhED;IACE,oBAAA;Gf0hED;Ee3hED;IACE,WAAA;Gf6hED;Ee9hED;IACE,oBAAA;GfgiED;EejiED;IACE,oBAAA;GfmiED;EepiED;IACE,WAAA;GfsiED;EeviED;IACE,oBAAA;GfyiED;Ee1iED;IACE,mBAAA;Gf4iED;EexiED;IACE,YAAA;Gf0iED;Ee1jED;IACE,WAAA;Gf4jED;Ee7jED;IACE,mBAAA;Gf+jED;EehkED;IACE,mBAAA;GfkkED;EenkED;IACE,UAAA;GfqkED;EetkED;IACE,mBAAA;GfwkED;EezkED;IACE,mBAAA;Gf2kED;Ee5kED;IACE,UAAA;Gf8kED;Ee/kED;IACE,mBAAA;GfilED;EellED;IACE,mBAAA;GfolED;EerlED;IACE,UAAA;GfulED;EexlED;IACE,mBAAA;Gf0lED;Ee3lED;IACE,kBAAA;Gf6lED;EezlED;IACE,WAAA;Gf2lED;Ee7kED;IACE,kBAAA;Gf+kED;EehlED;IACE,0BAAA;GfklED;EenlED;IACE,0BAAA;GfqlED;EetlED;IACE,iBAAA;GfwlED;EezlED;IACE,0BAAA;Gf2lED;Ee5lED;IACE,0BAAA;Gf8lED;Ee/lED;IACE,iBAAA;GfimED;EelmED;IACE,0BAAA;GfomED;EermED;IACE,0BAAA;GfumED;EexmED;IACE,iBAAA;Gf0mED;Ee3mED;IACE,0BAAA;Gf6mED;Ee9mED;IACE,yBAAA;GfgnED;EejnED;IACE,gBAAA;GfmnED;CACF;AgBvrED;EACE,8BAAA;ChByrED;AgBvrED;EACE,iBAAA;EACA,oBAAA;EACA,eAAA;EACA,iBAAA;ChByrED;AgBvrED;EACE,iBAAA;ChByrED;AgBnrED;EACE,YAAA;EACA,gBA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","file":"bootstrap.css","sourcesContent":["/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\nhtml {\n font-family: sans-serif;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n}\nbody {\n margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block;\n vertical-align: baseline;\n}\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n[hidden],\ntemplate {\n display: none;\n}\na {\n background-color: transparent;\n}\na:active,\na:hover {\n outline: 0;\n}\nabbr[title] {\n border-bottom: 1px dotted;\n}\nb,\nstrong {\n font-weight: bold;\n}\ndfn {\n font-style: italic;\n}\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\nmark {\n background: #ff0;\n color: #000;\n}\nsmall {\n font-size: 80%;\n}\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsup {\n top: -0.5em;\n}\nsub {\n bottom: -0.25em;\n}\nimg {\n border: 0;\n}\nsvg:not(:root) {\n overflow: hidden;\n}\nfigure {\n margin: 1em 40px;\n}\nhr {\n box-sizing: content-box;\n height: 0;\n}\npre {\n overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit;\n font: inherit;\n margin: 0;\n}\nbutton {\n overflow: visible;\n}\nbutton,\nselect {\n text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button;\n cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\ninput {\n line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box;\n padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: textfield;\n box-sizing: content-box;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n border: 0;\n padding: 0;\n}\ntextarea {\n overflow: auto;\n}\noptgroup {\n font-weight: bold;\n}\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\ntd,\nth {\n padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n *,\n *:before,\n *:after {\n background: transparent !important;\n color: #000 !important;\n box-shadow: none !important;\n text-shadow: none !important;\n }\n a,\n a:visited {\n text-decoration: underline;\n }\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n img {\n max-width: 100% !important;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n .navbar {\n display: none;\n }\n .btn > .caret,\n .dropup > .btn > .caret {\n border-top-color: #000 !important;\n }\n .label {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #ddd !important;\n }\n}\n@font-face {\n font-family: 'Glyphicons Halflings';\n src: url('../fonts/glyphicons-halflings-regular.eot');\n src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');\n}\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: 'Glyphicons Halflings';\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n content: \"\\002a\";\n}\n.glyphicon-plus:before {\n content: \"\\002b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n content: \"\\270f\";\n}\n.glyphicon-glass:before {\n content: \"\\e001\";\n}\n.glyphicon-music:before {\n content: \"\\e002\";\n}\n.glyphicon-search:before {\n content: \"\\e003\";\n}\n.glyphicon-heart:before {\n content: \"\\e005\";\n}\n.glyphicon-star:before {\n content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n content: \"\\e007\";\n}\n.glyphicon-user:before {\n content: \"\\e008\";\n}\n.glyphicon-film:before {\n content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n content: \"\\e010\";\n}\n.glyphicon-th:before {\n content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n content: \"\\e012\";\n}\n.glyphicon-ok:before {\n content: \"\\e013\";\n}\n.glyphicon-remove:before {\n content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n content: \"\\e016\";\n}\n.glyphicon-off:before {\n content: \"\\e017\";\n}\n.glyphicon-signal:before {\n content: \"\\e018\";\n}\n.glyphicon-cog:before {\n content: \"\\e019\";\n}\n.glyphicon-trash:before {\n content: \"\\e020\";\n}\n.glyphicon-home:before {\n content: \"\\e021\";\n}\n.glyphicon-file:before {\n content: \"\\e022\";\n}\n.glyphicon-time:before {\n content: \"\\e023\";\n}\n.glyphicon-road:before {\n content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n content: \"\\e025\";\n}\n.glyphicon-download:before {\n content: \"\\e026\";\n}\n.glyphicon-upload:before {\n content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n content: \"\\e032\";\n}\n.glyphicon-lock:before {\n content: \"\\e033\";\n}\n.glyphicon-flag:before {\n content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n content: \"\\e040\";\n}\n.glyphicon-tag:before {\n content: \"\\e041\";\n}\n.glyphicon-tags:before {\n content: \"\\e042\";\n}\n.glyphicon-book:before {\n content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n content: \"\\e044\";\n}\n.glyphicon-print:before {\n content: \"\\e045\";\n}\n.glyphicon-camera:before {\n content: \"\\e046\";\n}\n.glyphicon-font:before {\n content: \"\\e047\";\n}\n.glyphicon-bold:before {\n content: \"\\e048\";\n}\n.glyphicon-italic:before {\n content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n content: \"\\e055\";\n}\n.glyphicon-list:before {\n content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n content: \"\\e059\";\n}\n.glyphicon-picture:before {\n content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n content: \"\\e063\";\n}\n.glyphicon-tint:before {\n content: \"\\e064\";\n}\n.glyphicon-edit:before {\n content: \"\\e065\";\n}\n.glyphicon-share:before {\n content: \"\\e066\";\n}\n.glyphicon-check:before {\n content: \"\\e067\";\n}\n.glyphicon-move:before {\n content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n content: \"\\e070\";\n}\n.glyphicon-backward:before {\n content: \"\\e071\";\n}\n.glyphicon-play:before {\n content: \"\\e072\";\n}\n.glyphicon-pause:before {\n content: \"\\e073\";\n}\n.glyphicon-stop:before {\n content: \"\\e074\";\n}\n.glyphicon-forward:before {\n content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n content: \"\\e077\";\n}\n.glyphicon-eject:before {\n content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n content: \"\\e101\";\n}\n.glyphicon-gift:before {\n content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n content: \"\\e103\";\n}\n.glyphicon-fire:before {\n content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n content: \"\\e107\";\n}\n.glyphicon-plane:before {\n content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n content: \"\\e109\";\n}\n.glyphicon-random:before {\n content: \"\\e110\";\n}\n.glyphicon-comment:before {\n content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n content: \"\\e122\";\n}\n.glyphicon-bell:before {\n content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n content: \"\\e134\";\n}\n.glyphicon-globe:before {\n content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n content: \"\\e137\";\n}\n.glyphicon-filter:before {\n content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n content: \"\\e143\";\n}\n.glyphicon-link:before {\n content: \"\\e144\";\n}\n.glyphicon-phone:before {\n content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n content: \"\\e146\";\n}\n.glyphicon-usd:before {\n content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n content: \"\\e149\";\n}\n.glyphicon-sort:before {\n content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n content: \"\\e157\";\n}\n.glyphicon-expand:before {\n content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n content: \"\\e161\";\n}\n.glyphicon-flash:before {\n content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n content: \"\\e164\";\n}\n.glyphicon-record:before {\n content: \"\\e165\";\n}\n.glyphicon-save:before {\n content: \"\\e166\";\n}\n.glyphicon-open:before {\n content: \"\\e167\";\n}\n.glyphicon-saved:before {\n content: \"\\e168\";\n}\n.glyphicon-import:before {\n content: \"\\e169\";\n}\n.glyphicon-export:before {\n content: \"\\e170\";\n}\n.glyphicon-send:before {\n content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n content: \"\\e179\";\n}\n.glyphicon-header:before {\n content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n content: \"\\e183\";\n}\n.glyphicon-tower:before {\n content: \"\\e184\";\n}\n.glyphicon-stats:before {\n content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n content: \"\\e200\";\n}\n.glyphicon-cd:before {\n content: \"\\e201\";\n}\n.glyphicon-save-file:before {\n content: \"\\e202\";\n}\n.glyphicon-open-file:before {\n content: \"\\e203\";\n}\n.glyphicon-level-up:before {\n content: \"\\e204\";\n}\n.glyphicon-copy:before {\n content: \"\\e205\";\n}\n.glyphicon-paste:before {\n content: \"\\e206\";\n}\n.glyphicon-alert:before {\n content: \"\\e209\";\n}\n.glyphicon-equalizer:before {\n content: \"\\e210\";\n}\n.glyphicon-king:before {\n content: \"\\e211\";\n}\n.glyphicon-queen:before {\n content: \"\\e212\";\n}\n.glyphicon-pawn:before {\n content: \"\\e213\";\n}\n.glyphicon-bishop:before {\n content: \"\\e214\";\n}\n.glyphicon-knight:before {\n content: \"\\e215\";\n}\n.glyphicon-baby-formula:before {\n content: \"\\e216\";\n}\n.glyphicon-tent:before {\n content: \"\\26fa\";\n}\n.glyphicon-blackboard:before {\n content: \"\\e218\";\n}\n.glyphicon-bed:before {\n content: \"\\e219\";\n}\n.glyphicon-apple:before {\n content: \"\\f8ff\";\n}\n.glyphicon-erase:before {\n content: \"\\e221\";\n}\n.glyphicon-hourglass:before {\n content: \"\\231b\";\n}\n.glyphicon-lamp:before {\n content: \"\\e223\";\n}\n.glyphicon-duplicate:before {\n content: \"\\e224\";\n}\n.glyphicon-piggy-bank:before {\n content: \"\\e225\";\n}\n.glyphicon-scissors:before {\n content: \"\\e226\";\n}\n.glyphicon-bitcoin:before {\n content: \"\\e227\";\n}\n.glyphicon-btc:before {\n content: \"\\e227\";\n}\n.glyphicon-xbt:before {\n content: \"\\e227\";\n}\n.glyphicon-yen:before {\n content: \"\\00a5\";\n}\n.glyphicon-jpy:before {\n content: \"\\00a5\";\n}\n.glyphicon-ruble:before {\n content: \"\\20bd\";\n}\n.glyphicon-rub:before {\n content: \"\\20bd\";\n}\n.glyphicon-scale:before {\n content: \"\\e230\";\n}\n.glyphicon-ice-lolly:before {\n content: \"\\e231\";\n}\n.glyphicon-ice-lolly-tasted:before {\n content: \"\\e232\";\n}\n.glyphicon-education:before {\n content: \"\\e233\";\n}\n.glyphicon-option-horizontal:before {\n content: \"\\e234\";\n}\n.glyphicon-option-vertical:before {\n content: \"\\e235\";\n}\n.glyphicon-menu-hamburger:before {\n content: \"\\e236\";\n}\n.glyphicon-modal-window:before {\n content: \"\\e237\";\n}\n.glyphicon-oil:before {\n content: \"\\e238\";\n}\n.glyphicon-grain:before {\n content: \"\\e239\";\n}\n.glyphicon-sunglasses:before {\n content: \"\\e240\";\n}\n.glyphicon-text-size:before {\n content: \"\\e241\";\n}\n.glyphicon-text-color:before {\n content: \"\\e242\";\n}\n.glyphicon-text-background:before {\n content: \"\\e243\";\n}\n.glyphicon-object-align-top:before {\n content: \"\\e244\";\n}\n.glyphicon-object-align-bottom:before {\n content: \"\\e245\";\n}\n.glyphicon-object-align-horizontal:before {\n content: \"\\e246\";\n}\n.glyphicon-object-align-left:before {\n content: \"\\e247\";\n}\n.glyphicon-object-align-vertical:before {\n content: \"\\e248\";\n}\n.glyphicon-object-align-right:before {\n content: \"\\e249\";\n}\n.glyphicon-triangle-right:before {\n content: \"\\e250\";\n}\n.glyphicon-triangle-left:before {\n content: \"\\e251\";\n}\n.glyphicon-triangle-bottom:before {\n content: \"\\e252\";\n}\n.glyphicon-triangle-top:before {\n content: \"\\e253\";\n}\n.glyphicon-console:before {\n content: \"\\e254\";\n}\n.glyphicon-superscript:before {\n content: \"\\e255\";\n}\n.glyphicon-subscript:before {\n content: \"\\e256\";\n}\n.glyphicon-menu-left:before {\n content: \"\\e257\";\n}\n.glyphicon-menu-right:before {\n content: \"\\e258\";\n}\n.glyphicon-menu-down:before {\n content: \"\\e259\";\n}\n.glyphicon-menu-up:before {\n content: \"\\e260\";\n}\n* {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n*:before,\n*:after {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.42857143;\n color: #333333;\n background-color: #fff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\na {\n color: #337ab7;\n text-decoration: none;\n}\na:hover,\na:focus {\n color: #23527c;\n text-decoration: underline;\n}\na:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\nfigure {\n margin: 0;\n}\nimg {\n vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n display: block;\n max-width: 100%;\n height: auto;\n}\n.img-rounded {\n border-radius: 6px;\n}\n.img-thumbnail {\n padding: 4px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: all 0.2s ease-in-out;\n -o-transition: all 0.2s ease-in-out;\n transition: all 0.2s ease-in-out;\n display: inline-block;\n max-width: 100%;\n height: auto;\n}\n.img-circle {\n border-radius: 50%;\n}\nhr {\n margin-top: 20px;\n margin-bottom: 20px;\n border: 0;\n border-top: 1px solid #eeeeee;\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n}\n[role=\"button\"] {\n cursor: pointer;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n font-family: inherit;\n font-weight: 500;\n line-height: 1.1;\n color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n font-weight: normal;\n line-height: 1;\n color: #777777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n margin-top: 20px;\n margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n margin-top: 10px;\n margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n font-size: 75%;\n}\nh1,\n.h1 {\n font-size: 36px;\n}\nh2,\n.h2 {\n font-size: 30px;\n}\nh3,\n.h3 {\n font-size: 24px;\n}\nh4,\n.h4 {\n font-size: 18px;\n}\nh5,\n.h5 {\n font-size: 14px;\n}\nh6,\n.h6 {\n font-size: 12px;\n}\np {\n margin: 0 0 10px;\n}\n.lead {\n margin-bottom: 20px;\n font-size: 16px;\n font-weight: 300;\n line-height: 1.4;\n}\n@media (min-width: 768px) {\n .lead {\n font-size: 21px;\n }\n}\nsmall,\n.small {\n font-size: 85%;\n}\nmark,\n.mark {\n background-color: #fcf8e3;\n padding: .2em;\n}\n.text-left {\n text-align: left;\n}\n.text-right {\n text-align: right;\n}\n.text-center {\n text-align: center;\n}\n.text-justify {\n text-align: justify;\n}\n.text-nowrap {\n white-space: nowrap;\n}\n.text-lowercase {\n text-transform: lowercase;\n}\n.text-uppercase {\n text-transform: uppercase;\n}\n.text-capitalize {\n text-transform: capitalize;\n}\n.text-muted {\n color: #777777;\n}\n.text-primary {\n color: #337ab7;\n}\na.text-primary:hover,\na.text-primary:focus {\n color: #286090;\n}\n.text-success {\n color: #3c763d;\n}\na.text-success:hover,\na.text-success:focus {\n color: #2b542c;\n}\n.text-info {\n color: #31708f;\n}\na.text-info:hover,\na.text-info:focus {\n color: #245269;\n}\n.text-warning {\n color: #8a6d3b;\n}\na.text-warning:hover,\na.text-warning:focus {\n color: #66512c;\n}\n.text-danger {\n color: #a94442;\n}\na.text-danger:hover,\na.text-danger:focus {\n color: #843534;\n}\n.bg-primary {\n color: #fff;\n background-color: #337ab7;\n}\na.bg-primary:hover,\na.bg-primary:focus {\n background-color: #286090;\n}\n.bg-success {\n background-color: #dff0d8;\n}\na.bg-success:hover,\na.bg-success:focus {\n background-color: #c1e2b3;\n}\n.bg-info {\n background-color: #d9edf7;\n}\na.bg-info:hover,\na.bg-info:focus {\n background-color: #afd9ee;\n}\n.bg-warning {\n background-color: #fcf8e3;\n}\na.bg-warning:hover,\na.bg-warning:focus {\n background-color: #f7ecb5;\n}\n.bg-danger {\n background-color: #f2dede;\n}\na.bg-danger:hover,\na.bg-danger:focus {\n background-color: #e4b9b9;\n}\n.page-header {\n padding-bottom: 9px;\n margin: 40px 0 20px;\n border-bottom: 1px solid #eeeeee;\n}\nul,\nol {\n margin-top: 0;\n margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n margin-bottom: 0;\n}\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n.list-inline {\n padding-left: 0;\n list-style: none;\n margin-left: -5px;\n}\n.list-inline > li {\n display: inline-block;\n padding-left: 5px;\n padding-right: 5px;\n}\ndl {\n margin-top: 0;\n margin-bottom: 20px;\n}\ndt,\ndd {\n line-height: 1.42857143;\n}\ndt {\n font-weight: bold;\n}\ndd {\n margin-left: 0;\n}\n@media (min-width: 768px) {\n .dl-horizontal dt {\n float: left;\n width: 160px;\n clear: left;\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .dl-horizontal dd {\n margin-left: 180px;\n }\n}\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n border-bottom: 1px dotted #777777;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\nblockquote {\n padding: 10px 20px;\n margin: 0 0 20px;\n font-size: 17.5px;\n border-left: 5px solid #eeeeee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n display: block;\n font-size: 80%;\n line-height: 1.42857143;\n color: #777777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n content: '\\2014 \\00A0';\n}\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n border-right: 5px solid #eeeeee;\n border-left: 0;\n text-align: right;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n content: '';\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n content: '\\00A0 \\2014';\n}\naddress {\n margin-bottom: 20px;\n font-style: normal;\n line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: #c7254e;\n background-color: #f9f2f4;\n border-radius: 4px;\n}\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: #fff;\n background-color: #333;\n border-radius: 3px;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: bold;\n box-shadow: none;\n}\npre {\n display: block;\n padding: 9.5px;\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 1.42857143;\n word-break: break-all;\n word-wrap: break-word;\n color: #333333;\n background-color: #f5f5f5;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\npre code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n}\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n.container {\n margin-right: auto;\n margin-left: auto;\n padding-left: 15px;\n padding-right: 15px;\n}\n@media (min-width: 768px) {\n .container {\n width: 750px;\n }\n}\n@media (min-width: 992px) {\n .container {\n width: 970px;\n }\n}\n@media (min-width: 1200px) {\n .container {\n width: 1170px;\n }\n}\n.container-fluid {\n margin-right: auto;\n margin-left: auto;\n padding-left: 15px;\n padding-right: 15px;\n}\n.row {\n margin-left: -15px;\n margin-right: -15px;\n}\n.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {\n position: relative;\n min-height: 1px;\n padding-left: 15px;\n padding-right: 15px;\n}\n.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {\n float: left;\n}\n.col-xs-12 {\n width: 100%;\n}\n.col-xs-11 {\n width: 91.66666667%;\n}\n.col-xs-10 {\n width: 83.33333333%;\n}\n.col-xs-9 {\n width: 75%;\n}\n.col-xs-8 {\n width: 66.66666667%;\n}\n.col-xs-7 {\n width: 58.33333333%;\n}\n.col-xs-6 {\n width: 50%;\n}\n.col-xs-5 {\n width: 41.66666667%;\n}\n.col-xs-4 {\n width: 33.33333333%;\n}\n.col-xs-3 {\n width: 25%;\n}\n.col-xs-2 {\n width: 16.66666667%;\n}\n.col-xs-1 {\n width: 8.33333333%;\n}\n.col-xs-pull-12 {\n right: 100%;\n}\n.col-xs-pull-11 {\n right: 91.66666667%;\n}\n.col-xs-pull-10 {\n right: 83.33333333%;\n}\n.col-xs-pull-9 {\n right: 75%;\n}\n.col-xs-pull-8 {\n right: 66.66666667%;\n}\n.col-xs-pull-7 {\n right: 58.33333333%;\n}\n.col-xs-pull-6 {\n right: 50%;\n}\n.col-xs-pull-5 {\n right: 41.66666667%;\n}\n.col-xs-pull-4 {\n right: 33.33333333%;\n}\n.col-xs-pull-3 {\n right: 25%;\n}\n.col-xs-pull-2 {\n right: 16.66666667%;\n}\n.col-xs-pull-1 {\n right: 8.33333333%;\n}\n.col-xs-pull-0 {\n right: auto;\n}\n.col-xs-push-12 {\n left: 100%;\n}\n.col-xs-push-11 {\n left: 91.66666667%;\n}\n.col-xs-push-10 {\n left: 83.33333333%;\n}\n.col-xs-push-9 {\n left: 75%;\n}\n.col-xs-push-8 {\n left: 66.66666667%;\n}\n.col-xs-push-7 {\n left: 58.33333333%;\n}\n.col-xs-push-6 {\n left: 50%;\n}\n.col-xs-push-5 {\n left: 41.66666667%;\n}\n.col-xs-push-4 {\n left: 33.33333333%;\n}\n.col-xs-push-3 {\n left: 25%;\n}\n.col-xs-push-2 {\n left: 16.66666667%;\n}\n.col-xs-push-1 {\n left: 8.33333333%;\n}\n.col-xs-push-0 {\n left: auto;\n}\n.col-xs-offset-12 {\n margin-left: 100%;\n}\n.col-xs-offset-11 {\n margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n margin-left: 75%;\n}\n.col-xs-offset-8 {\n margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n margin-left: 50%;\n}\n.col-xs-offset-5 {\n margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n margin-left: 25%;\n}\n.col-xs-offset-2 {\n margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n margin-left: 0%;\n}\n@media (min-width: 768px) {\n .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {\n float: left;\n }\n .col-sm-12 {\n width: 100%;\n }\n .col-sm-11 {\n width: 91.66666667%;\n }\n .col-sm-10 {\n width: 83.33333333%;\n }\n .col-sm-9 {\n width: 75%;\n }\n .col-sm-8 {\n width: 66.66666667%;\n }\n .col-sm-7 {\n width: 58.33333333%;\n }\n .col-sm-6 {\n width: 50%;\n }\n .col-sm-5 {\n width: 41.66666667%;\n }\n .col-sm-4 {\n width: 33.33333333%;\n }\n .col-sm-3 {\n width: 25%;\n }\n .col-sm-2 {\n width: 16.66666667%;\n }\n .col-sm-1 {\n width: 8.33333333%;\n }\n .col-sm-pull-12 {\n right: 100%;\n }\n .col-sm-pull-11 {\n right: 91.66666667%;\n }\n .col-sm-pull-10 {\n right: 83.33333333%;\n }\n .col-sm-pull-9 {\n right: 75%;\n }\n .col-sm-pull-8 {\n right: 66.66666667%;\n }\n .col-sm-pull-7 {\n right: 58.33333333%;\n }\n .col-sm-pull-6 {\n right: 50%;\n }\n .col-sm-pull-5 {\n right: 41.66666667%;\n }\n .col-sm-pull-4 {\n right: 33.33333333%;\n }\n .col-sm-pull-3 {\n right: 25%;\n }\n .col-sm-pull-2 {\n right: 16.66666667%;\n }\n .col-sm-pull-1 {\n right: 8.33333333%;\n }\n .col-sm-pull-0 {\n right: auto;\n }\n .col-sm-push-12 {\n left: 100%;\n }\n .col-sm-push-11 {\n left: 91.66666667%;\n }\n .col-sm-push-10 {\n left: 83.33333333%;\n }\n .col-sm-push-9 {\n left: 75%;\n }\n .col-sm-push-8 {\n left: 66.66666667%;\n }\n .col-sm-push-7 {\n left: 58.33333333%;\n }\n .col-sm-push-6 {\n left: 50%;\n }\n .col-sm-push-5 {\n left: 41.66666667%;\n }\n .col-sm-push-4 {\n left: 33.33333333%;\n }\n .col-sm-push-3 {\n left: 25%;\n }\n .col-sm-push-2 {\n left: 16.66666667%;\n }\n .col-sm-push-1 {\n left: 8.33333333%;\n }\n .col-sm-push-0 {\n left: auto;\n }\n .col-sm-offset-12 {\n margin-left: 100%;\n }\n .col-sm-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-sm-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-sm-offset-9 {\n margin-left: 75%;\n }\n .col-sm-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-sm-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-sm-offset-6 {\n margin-left: 50%;\n }\n .col-sm-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-sm-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-sm-offset-3 {\n margin-left: 25%;\n }\n .col-sm-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-sm-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-sm-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 992px) {\n .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {\n float: left;\n }\n .col-md-12 {\n width: 100%;\n }\n .col-md-11 {\n width: 91.66666667%;\n }\n .col-md-10 {\n width: 83.33333333%;\n }\n .col-md-9 {\n width: 75%;\n }\n .col-md-8 {\n width: 66.66666667%;\n }\n .col-md-7 {\n width: 58.33333333%;\n }\n .col-md-6 {\n width: 50%;\n }\n .col-md-5 {\n width: 41.66666667%;\n }\n .col-md-4 {\n width: 33.33333333%;\n }\n .col-md-3 {\n width: 25%;\n }\n .col-md-2 {\n width: 16.66666667%;\n }\n .col-md-1 {\n width: 8.33333333%;\n }\n .col-md-pull-12 {\n right: 100%;\n }\n .col-md-pull-11 {\n right: 91.66666667%;\n }\n .col-md-pull-10 {\n right: 83.33333333%;\n }\n .col-md-pull-9 {\n right: 75%;\n }\n .col-md-pull-8 {\n right: 66.66666667%;\n }\n .col-md-pull-7 {\n right: 58.33333333%;\n }\n .col-md-pull-6 {\n right: 50%;\n }\n .col-md-pull-5 {\n right: 41.66666667%;\n }\n .col-md-pull-4 {\n right: 33.33333333%;\n }\n .col-md-pull-3 {\n right: 25%;\n }\n .col-md-pull-2 {\n right: 16.66666667%;\n }\n .col-md-pull-1 {\n right: 8.33333333%;\n }\n .col-md-pull-0 {\n right: auto;\n }\n .col-md-push-12 {\n left: 100%;\n }\n .col-md-push-11 {\n left: 91.66666667%;\n }\n .col-md-push-10 {\n left: 83.33333333%;\n }\n .col-md-push-9 {\n left: 75%;\n }\n .col-md-push-8 {\n left: 66.66666667%;\n }\n .col-md-push-7 {\n left: 58.33333333%;\n }\n .col-md-push-6 {\n left: 50%;\n }\n .col-md-push-5 {\n left: 41.66666667%;\n }\n .col-md-push-4 {\n left: 33.33333333%;\n }\n .col-md-push-3 {\n left: 25%;\n }\n .col-md-push-2 {\n left: 16.66666667%;\n }\n .col-md-push-1 {\n left: 8.33333333%;\n }\n .col-md-push-0 {\n left: auto;\n }\n .col-md-offset-12 {\n margin-left: 100%;\n }\n .col-md-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-md-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-md-offset-9 {\n margin-left: 75%;\n }\n .col-md-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-md-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-md-offset-6 {\n margin-left: 50%;\n }\n .col-md-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-md-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-md-offset-3 {\n margin-left: 25%;\n }\n .col-md-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-md-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-md-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 1200px) {\n .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {\n float: left;\n }\n .col-lg-12 {\n width: 100%;\n }\n .col-lg-11 {\n width: 91.66666667%;\n }\n .col-lg-10 {\n width: 83.33333333%;\n }\n .col-lg-9 {\n width: 75%;\n }\n .col-lg-8 {\n width: 66.66666667%;\n }\n .col-lg-7 {\n width: 58.33333333%;\n }\n .col-lg-6 {\n width: 50%;\n }\n .col-lg-5 {\n width: 41.66666667%;\n }\n .col-lg-4 {\n width: 33.33333333%;\n }\n .col-lg-3 {\n width: 25%;\n }\n .col-lg-2 {\n width: 16.66666667%;\n }\n .col-lg-1 {\n width: 8.33333333%;\n }\n .col-lg-pull-12 {\n right: 100%;\n }\n .col-lg-pull-11 {\n right: 91.66666667%;\n }\n .col-lg-pull-10 {\n right: 83.33333333%;\n }\n .col-lg-pull-9 {\n right: 75%;\n }\n .col-lg-pull-8 {\n right: 66.66666667%;\n }\n .col-lg-pull-7 {\n right: 58.33333333%;\n }\n .col-lg-pull-6 {\n right: 50%;\n }\n .col-lg-pull-5 {\n right: 41.66666667%;\n }\n .col-lg-pull-4 {\n right: 33.33333333%;\n }\n .col-lg-pull-3 {\n right: 25%;\n }\n .col-lg-pull-2 {\n right: 16.66666667%;\n }\n .col-lg-pull-1 {\n right: 8.33333333%;\n }\n .col-lg-pull-0 {\n right: auto;\n }\n .col-lg-push-12 {\n left: 100%;\n }\n .col-lg-push-11 {\n left: 91.66666667%;\n }\n .col-lg-push-10 {\n left: 83.33333333%;\n }\n .col-lg-push-9 {\n left: 75%;\n }\n .col-lg-push-8 {\n left: 66.66666667%;\n }\n .col-lg-push-7 {\n left: 58.33333333%;\n }\n .col-lg-push-6 {\n left: 50%;\n }\n .col-lg-push-5 {\n left: 41.66666667%;\n }\n .col-lg-push-4 {\n left: 33.33333333%;\n }\n .col-lg-push-3 {\n left: 25%;\n }\n .col-lg-push-2 {\n left: 16.66666667%;\n }\n .col-lg-push-1 {\n left: 8.33333333%;\n }\n .col-lg-push-0 {\n left: auto;\n }\n .col-lg-offset-12 {\n margin-left: 100%;\n }\n .col-lg-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-lg-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-lg-offset-9 {\n margin-left: 75%;\n }\n .col-lg-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-lg-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-lg-offset-6 {\n margin-left: 50%;\n }\n .col-lg-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-lg-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-lg-offset-3 {\n margin-left: 25%;\n }\n .col-lg-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-lg-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-lg-offset-0 {\n margin-left: 0%;\n }\n}\ntable {\n background-color: transparent;\n}\ncaption {\n padding-top: 8px;\n padding-bottom: 8px;\n color: #777777;\n text-align: left;\n}\nth {\n text-align: left;\n}\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n padding: 8px;\n line-height: 1.42857143;\n vertical-align: top;\n border-top: 1px solid #ddd;\n}\n.table > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid #ddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n border-top: 0;\n}\n.table > tbody + tbody {\n border-top: 2px solid #ddd;\n}\n.table .table {\n background-color: #fff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n padding: 5px;\n}\n.table-bordered {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-of-type(odd) {\n background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n background-color: #f5f5f5;\n}\ntable col[class*=\"col-\"] {\n position: static;\n float: none;\n display: table-column;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n position: static;\n float: none;\n display: table-cell;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n background-color: #ebcccc;\n}\n.table-responsive {\n overflow-x: auto;\n min-height: 0.01%;\n}\n@media screen and (max-width: 767px) {\n .table-responsive {\n width: 100%;\n margin-bottom: 15px;\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid #ddd;\n }\n .table-responsive > .table {\n margin-bottom: 0;\n }\n .table-responsive > .table > thead > tr > th,\n .table-responsive > .table > tbody > tr > th,\n .table-responsive > .table > tfoot > tr > th,\n .table-responsive > .table > thead > tr > td,\n .table-responsive > .table > tbody > tr > td,\n .table-responsive > .table > tfoot > tr > td {\n white-space: nowrap;\n }\n .table-responsive > .table-bordered {\n border: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:first-child,\n .table-responsive > .table-bordered > tbody > tr > th:first-child,\n .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n .table-responsive > .table-bordered > thead > tr > td:first-child,\n .table-responsive > .table-bordered > tbody > tr > td:first-child,\n .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:last-child,\n .table-responsive > .table-bordered > tbody > tr > th:last-child,\n .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n .table-responsive > .table-bordered > thead > tr > td:last-child,\n .table-responsive > .table-bordered > tbody > tr > td:last-child,\n .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n }\n .table-responsive > .table-bordered > tbody > tr:last-child > th,\n .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n .table-responsive > .table-bordered > tbody > tr:last-child > td,\n .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n border-bottom: 0;\n }\n}\nfieldset {\n padding: 0;\n margin: 0;\n border: 0;\n min-width: 0;\n}\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: 20px;\n font-size: 21px;\n line-height: inherit;\n color: #333333;\n border: 0;\n border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n display: inline-block;\n max-width: 100%;\n margin-bottom: 5px;\n font-weight: bold;\n}\ninput[type=\"search\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9;\n line-height: normal;\n}\ninput[type=\"file\"] {\n display: block;\n}\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\nselect[multiple],\nselect[size] {\n height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\noutput {\n display: block;\n padding-top: 7px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n}\n.form-control {\n display: block;\n width: 100%;\n height: 34px;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n background-color: #fff;\n background-image: none;\n border: 1px solid #ccc;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n border-color: #66afe9;\n outline: 0;\n -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.form-control::-moz-placeholder {\n color: #999;\n opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n color: #999;\n}\n.form-control::-webkit-input-placeholder {\n color: #999;\n}\n.form-control::-ms-expand {\n border: 0;\n background-color: transparent;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n background-color: #eeeeee;\n opacity: 1;\n}\n.form-control[disabled],\nfieldset[disabled] .form-control {\n cursor: not-allowed;\n}\ntextarea.form-control {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: none;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"].form-control,\n input[type=\"time\"].form-control,\n input[type=\"datetime-local\"].form-control,\n input[type=\"month\"].form-control {\n line-height: 34px;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm,\n .input-group-sm input[type=\"date\"],\n .input-group-sm input[type=\"time\"],\n .input-group-sm input[type=\"datetime-local\"],\n .input-group-sm input[type=\"month\"] {\n line-height: 30px;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg,\n .input-group-lg input[type=\"date\"],\n .input-group-lg input[type=\"time\"],\n .input-group-lg input[type=\"datetime-local\"],\n .input-group-lg input[type=\"month\"] {\n line-height: 46px;\n }\n}\n.form-group {\n margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.radio label,\n.checkbox label {\n min-height: 20px;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: normal;\n cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-left: -20px;\n margin-top: 4px \\9;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n position: relative;\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n vertical-align: middle;\n font-weight: normal;\n cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n cursor: not-allowed;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n cursor: not-allowed;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n cursor: not-allowed;\n}\n.form-control-static {\n padding-top: 7px;\n padding-bottom: 7px;\n margin-bottom: 0;\n min-height: 34px;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n padding-left: 0;\n padding-right: 0;\n}\n.input-sm {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-sm {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n height: auto;\n}\n.form-group-sm .form-control {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.form-group-sm select.form-control {\n height: 30px;\n line-height: 30px;\n}\n.form-group-sm textarea.form-control,\n.form-group-sm select[multiple].form-control {\n height: auto;\n}\n.form-group-sm .form-control-static {\n height: 30px;\n min-height: 32px;\n padding: 6px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.input-lg {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-lg {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n height: auto;\n}\n.form-group-lg .form-control {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.form-group-lg select.form-control {\n height: 46px;\n line-height: 46px;\n}\n.form-group-lg textarea.form-control,\n.form-group-lg select[multiple].form-control {\n height: auto;\n}\n.form-group-lg .form-control-static {\n height: 46px;\n min-height: 38px;\n padding: 11px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.has-feedback {\n position: relative;\n}\n.has-feedback .form-control {\n padding-right: 42.5px;\n}\n.form-control-feedback {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n display: block;\n width: 34px;\n height: 34px;\n line-height: 34px;\n text-align: center;\n pointer-events: none;\n}\n.input-lg + .form-control-feedback,\n.input-group-lg + .form-control-feedback,\n.form-group-lg .form-control + .form-control-feedback {\n width: 46px;\n height: 46px;\n line-height: 46px;\n}\n.input-sm + .form-control-feedback,\n.input-group-sm + .form-control-feedback,\n.form-group-sm .form-control + .form-control-feedback {\n width: 30px;\n height: 30px;\n line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n color: #3c763d;\n}\n.has-success .form-control {\n border-color: #3c763d;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-success .form-control:focus {\n border-color: #2b542c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n color: #3c763d;\n border-color: #3c763d;\n background-color: #dff0d8;\n}\n.has-success .form-control-feedback {\n color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n color: #8a6d3b;\n}\n.has-warning .form-control {\n border-color: #8a6d3b;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-warning .form-control:focus {\n border-color: #66512c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n color: #8a6d3b;\n border-color: #8a6d3b;\n background-color: #fcf8e3;\n}\n.has-warning .form-control-feedback {\n color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n color: #a94442;\n}\n.has-error .form-control {\n border-color: #a94442;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-error .form-control:focus {\n border-color: #843534;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n color: #a94442;\n border-color: #a94442;\n background-color: #f2dede;\n}\n.has-error .form-control-feedback {\n color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n top: 25px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n top: 0;\n}\n.help-block {\n display: block;\n margin-top: 5px;\n margin-bottom: 10px;\n color: #737373;\n}\n@media (min-width: 768px) {\n .form-inline .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-static {\n display: inline-block;\n }\n .form-inline .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .form-inline .input-group .input-group-addon,\n .form-inline .input-group .input-group-btn,\n .form-inline .input-group .form-control {\n width: auto;\n }\n .form-inline .input-group > .form-control {\n width: 100%;\n }\n .form-inline .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio,\n .form-inline .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio label,\n .form-inline .checkbox label {\n padding-left: 0;\n }\n .form-inline .radio input[type=\"radio\"],\n .form-inline .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .form-inline .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n margin-top: 0;\n margin-bottom: 0;\n padding-top: 7px;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n min-height: 27px;\n}\n.form-horizontal .form-group {\n margin-left: -15px;\n margin-right: -15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .control-label {\n text-align: right;\n margin-bottom: 0;\n padding-top: 7px;\n }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n right: 15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-lg .control-label {\n padding-top: 11px;\n font-size: 18px;\n }\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-sm .control-label {\n padding-top: 6px;\n font-size: 12px;\n }\n}\n.btn {\n display: inline-block;\n margin-bottom: 0;\n font-weight: normal;\n text-align: center;\n vertical-align: middle;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none;\n border: 1px solid transparent;\n white-space: nowrap;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n border-radius: 4px;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n color: #333;\n text-decoration: none;\n}\n.btn:active,\n.btn.active {\n outline: 0;\n background-image: none;\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n cursor: not-allowed;\n opacity: 0.65;\n filter: alpha(opacity=65);\n -webkit-box-shadow: none;\n box-shadow: none;\n}\na.btn.disabled,\nfieldset[disabled] a.btn {\n pointer-events: none;\n}\n.btn-default {\n color: #333;\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default:focus,\n.btn-default.focus {\n color: #333;\n background-color: #e6e6e6;\n border-color: #8c8c8c;\n}\n.btn-default:hover {\n color: #333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n color: #333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active:hover,\n.btn-default.active:hover,\n.open > .dropdown-toggle.btn-default:hover,\n.btn-default:active:focus,\n.btn-default.active:focus,\n.open > .dropdown-toggle.btn-default:focus,\n.btn-default:active.focus,\n.btn-default.active.focus,\n.open > .dropdown-toggle.btn-default.focus {\n color: #333;\n background-color: #d4d4d4;\n border-color: #8c8c8c;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n background-image: none;\n}\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus {\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default .badge {\n color: #fff;\n background-color: #333;\n}\n.btn-primary {\n color: #fff;\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary:focus,\n.btn-primary.focus {\n color: #fff;\n background-color: #286090;\n border-color: #122b40;\n}\n.btn-primary:hover {\n color: #fff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n color: #fff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active:hover,\n.btn-primary.active:hover,\n.open > .dropdown-toggle.btn-primary:hover,\n.btn-primary:active:focus,\n.btn-primary.active:focus,\n.open > .dropdown-toggle.btn-primary:focus,\n.btn-primary:active.focus,\n.btn-primary.active.focus,\n.open > .dropdown-toggle.btn-primary.focus {\n color: #fff;\n background-color: #204d74;\n border-color: #122b40;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n background-image: none;\n}\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus {\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.btn-success {\n color: #fff;\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success:focus,\n.btn-success.focus {\n color: #fff;\n background-color: #449d44;\n border-color: #255625;\n}\n.btn-success:hover {\n color: #fff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n color: #fff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active:hover,\n.btn-success.active:hover,\n.open > .dropdown-toggle.btn-success:hover,\n.btn-success:active:focus,\n.btn-success.active:focus,\n.open > .dropdown-toggle.btn-success:focus,\n.btn-success:active.focus,\n.btn-success.active.focus,\n.open > .dropdown-toggle.btn-success.focus {\n color: #fff;\n background-color: #398439;\n border-color: #255625;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n background-image: none;\n}\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus {\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success .badge {\n color: #5cb85c;\n background-color: #fff;\n}\n.btn-info {\n color: #fff;\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info:focus,\n.btn-info.focus {\n color: #fff;\n background-color: #31b0d5;\n border-color: #1b6d85;\n}\n.btn-info:hover {\n color: #fff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n color: #fff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active:hover,\n.btn-info.active:hover,\n.open > .dropdown-toggle.btn-info:hover,\n.btn-info:active:focus,\n.btn-info.active:focus,\n.open > .dropdown-toggle.btn-info:focus,\n.btn-info:active.focus,\n.btn-info.active.focus,\n.open > .dropdown-toggle.btn-info.focus {\n color: #fff;\n background-color: #269abc;\n border-color: #1b6d85;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n background-image: none;\n}\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus {\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info .badge {\n color: #5bc0de;\n background-color: #fff;\n}\n.btn-warning {\n color: #fff;\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning:focus,\n.btn-warning.focus {\n color: #fff;\n background-color: #ec971f;\n border-color: #985f0d;\n}\n.btn-warning:hover {\n color: #fff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n color: #fff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active:hover,\n.btn-warning.active:hover,\n.open > .dropdown-toggle.btn-warning:hover,\n.btn-warning:active:focus,\n.btn-warning.active:focus,\n.open > .dropdown-toggle.btn-warning:focus,\n.btn-warning:active.focus,\n.btn-warning.active.focus,\n.open > .dropdown-toggle.btn-warning.focus {\n color: #fff;\n background-color: #d58512;\n border-color: #985f0d;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n background-image: none;\n}\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus {\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning .badge {\n color: #f0ad4e;\n background-color: #fff;\n}\n.btn-danger {\n color: #fff;\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger:focus,\n.btn-danger.focus {\n color: #fff;\n background-color: #c9302c;\n border-color: #761c19;\n}\n.btn-danger:hover {\n color: #fff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n color: #fff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active:hover,\n.btn-danger.active:hover,\n.open > .dropdown-toggle.btn-danger:hover,\n.btn-danger:active:focus,\n.btn-danger.active:focus,\n.open > .dropdown-toggle.btn-danger:focus,\n.btn-danger:active.focus,\n.btn-danger.active.focus,\n.open > .dropdown-toggle.btn-danger.focus {\n color: #fff;\n background-color: #ac2925;\n border-color: #761c19;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n background-image: none;\n}\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus {\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger .badge {\n color: #d9534f;\n background-color: #fff;\n}\n.btn-link {\n color: #337ab7;\n font-weight: normal;\n border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n background-color: transparent;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n color: #23527c;\n text-decoration: underline;\n background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n color: #777777;\n text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n padding: 1px 5px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-block {\n display: block;\n width: 100%;\n}\n.btn-block + .btn-block {\n margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n.fade {\n opacity: 0;\n -webkit-transition: opacity 0.15s linear;\n -o-transition: opacity 0.15s linear;\n transition: opacity 0.15s linear;\n}\n.fade.in {\n opacity: 1;\n}\n.collapse {\n display: none;\n}\n.collapse.in {\n display: block;\n}\ntr.collapse.in {\n display: table-row;\n}\ntbody.collapse.in {\n display: table-row-group;\n}\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n -webkit-transition-property: height, visibility;\n transition-property: height, visibility;\n -webkit-transition-duration: 0.35s;\n transition-duration: 0.35s;\n -webkit-transition-timing-function: ease;\n transition-timing-function: ease;\n}\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: 4px dashed;\n border-top: 4px solid \\9;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n}\n.dropup,\n.dropdown {\n position: relative;\n}\n.dropdown-toggle:focus {\n outline: 0;\n}\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0;\n list-style: none;\n font-size: 14px;\n text-align: left;\n background-color: #fff;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 4px;\n -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n background-clip: padding-box;\n}\n.dropdown-menu.pull-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu .divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: normal;\n line-height: 1.42857143;\n color: #333333;\n white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n text-decoration: none;\n color: #262626;\n background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n background-color: #337ab7;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n color: #777777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n text-decoration: none;\n background-color: transparent;\n background-image: none;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n cursor: not-allowed;\n}\n.open > .dropdown-menu {\n display: block;\n}\n.open > a {\n outline: 0;\n}\n.dropdown-menu-right {\n left: auto;\n right: 0;\n}\n.dropdown-menu-left {\n left: 0;\n right: auto;\n}\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: 12px;\n line-height: 1.42857143;\n color: #777777;\n white-space: nowrap;\n}\n.dropdown-backdrop {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n top: 0;\n z-index: 990;\n}\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n border-top: 0;\n border-bottom: 4px dashed;\n border-bottom: 4px solid \\9;\n content: \"\";\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 2px;\n}\n@media (min-width: 768px) {\n .navbar-right .dropdown-menu {\n left: auto;\n right: 0;\n }\n .navbar-right .dropdown-menu-left {\n left: 0;\n right: auto;\n }\n}\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n margin-left: -1px;\n}\n.btn-toolbar {\n margin-left: -5px;\n}\n.btn-toolbar .btn,\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n padding-left: 8px;\n padding-right: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-left: 12px;\n padding-right: 12px;\n}\n.btn-group.open .dropdown-toggle {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn .caret {\n margin-left: 0;\n}\n.btn-lg .caret {\n border-width: 5px 5px 0;\n border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n float: none;\n display: table-cell;\n width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.input-group {\n position: relative;\n display: table;\n border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n float: none;\n padding-left: 0;\n padding-right: 0;\n}\n.input-group .form-control {\n position: relative;\n z-index: 2;\n float: left;\n width: 100%;\n margin-bottom: 0;\n}\n.input-group .form-control:focus {\n z-index: 3;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle;\n}\n.input-group-addon {\n padding: 6px 12px;\n font-size: 14px;\n font-weight: normal;\n line-height: 1;\n color: #555555;\n text-align: center;\n background-color: #eeeeee;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\n.input-group-addon.input-sm {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 3px;\n}\n.input-group-addon.input-lg {\n padding: 10px 16px;\n font-size: 18px;\n border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n.input-group-btn {\n position: relative;\n font-size: 0;\n white-space: nowrap;\n}\n.input-group-btn > .btn {\n position: relative;\n}\n.input-group-btn > .btn + .btn {\n margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n z-index: 2;\n margin-left: -1px;\n}\n.nav {\n margin-bottom: 0;\n padding-left: 0;\n list-style: none;\n}\n.nav > li {\n position: relative;\n display: block;\n}\n.nav > li > a {\n position: relative;\n display: block;\n padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.nav > li.disabled > a {\n color: #777777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n color: #777777;\n text-decoration: none;\n background-color: transparent;\n cursor: not-allowed;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n background-color: #eeeeee;\n border-color: #337ab7;\n}\n.nav .nav-divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.nav > li > a > img {\n max-width: none;\n}\n.nav-tabs {\n border-bottom: 1px solid #ddd;\n}\n.nav-tabs > li {\n float: left;\n margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n margin-right: 2px;\n line-height: 1.42857143;\n border: 1px solid transparent;\n border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n border-color: #eeeeee #eeeeee #ddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n color: #555555;\n background-color: #fff;\n border: 1px solid #ddd;\n border-bottom-color: transparent;\n cursor: default;\n}\n.nav-tabs.nav-justified {\n width: 100%;\n border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n float: none;\n}\n.nav-tabs.nav-justified > li > a {\n text-align: center;\n margin-bottom: 5px;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-tabs.nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs.nav-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs.nav-justified > .active > a,\n .nav-tabs.nav-justified > .active > a:hover,\n .nav-tabs.nav-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.nav-pills > li {\n float: left;\n}\n.nav-pills > li > a {\n border-radius: 4px;\n}\n.nav-pills > li + li {\n margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n color: #fff;\n background-color: #337ab7;\n}\n.nav-stacked > li {\n float: none;\n}\n.nav-stacked > li + li {\n margin-top: 2px;\n margin-left: 0;\n}\n.nav-justified {\n width: 100%;\n}\n.nav-justified > li {\n float: none;\n}\n.nav-justified > li > a {\n text-align: center;\n margin-bottom: 5px;\n}\n.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs-justified {\n border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs-justified > .active > a,\n .nav-tabs-justified > .active > a:hover,\n .nav-tabs-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.navbar {\n position: relative;\n min-height: 50px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n .navbar {\n border-radius: 4px;\n }\n}\n@media (min-width: 768px) {\n .navbar-header {\n float: left;\n }\n}\n.navbar-collapse {\n overflow-x: visible;\n padding-right: 15px;\n padding-left: 15px;\n border-top: 1px solid transparent;\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n -webkit-overflow-scrolling: touch;\n}\n.navbar-collapse.in {\n overflow-y: auto;\n}\n@media (min-width: 768px) {\n .navbar-collapse {\n width: auto;\n border-top: 0;\n box-shadow: none;\n }\n .navbar-collapse.collapse {\n display: block !important;\n height: auto !important;\n padding-bottom: 0;\n overflow: visible !important;\n }\n .navbar-collapse.in {\n overflow-y: visible;\n }\n .navbar-fixed-top .navbar-collapse,\n .navbar-static-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n padding-left: 0;\n padding-right: 0;\n }\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n .navbar-fixed-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n max-height: 200px;\n }\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .container > .navbar-header,\n .container-fluid > .navbar-header,\n .container > .navbar-collapse,\n .container-fluid > .navbar-collapse {\n margin-right: 0;\n margin-left: 0;\n }\n}\n.navbar-static-top {\n z-index: 1000;\n border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n .navbar-static-top {\n border-radius: 0;\n }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n@media (min-width: 768px) {\n .navbar-fixed-top,\n .navbar-fixed-bottom {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0;\n border-width: 1px 0 0;\n}\n.navbar-brand {\n float: left;\n padding: 15px 15px;\n font-size: 18px;\n line-height: 20px;\n height: 50px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n text-decoration: none;\n}\n.navbar-brand > img {\n display: block;\n}\n@media (min-width: 768px) {\n .navbar > .container .navbar-brand,\n .navbar > .container-fluid .navbar-brand {\n margin-left: -15px;\n }\n}\n.navbar-toggle {\n position: relative;\n float: right;\n margin-right: 15px;\n padding: 9px 10px;\n margin-top: 8px;\n margin-bottom: 8px;\n background-color: transparent;\n background-image: none;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.navbar-toggle:focus {\n outline: 0;\n}\n.navbar-toggle .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n margin-top: 4px;\n}\n@media (min-width: 768px) {\n .navbar-toggle {\n display: none;\n }\n}\n.navbar-nav {\n margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: 20px;\n}\n@media (max-width: 767px) {\n .navbar-nav .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n box-shadow: none;\n }\n .navbar-nav .open .dropdown-menu > li > a,\n .navbar-nav .open .dropdown-menu .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n .navbar-nav .open .dropdown-menu > li > a {\n line-height: 20px;\n }\n .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-nav .open .dropdown-menu > li > a:focus {\n background-image: none;\n }\n}\n@media (min-width: 768px) {\n .navbar-nav {\n float: left;\n margin: 0;\n }\n .navbar-nav > li {\n float: left;\n }\n .navbar-nav > li > a {\n padding-top: 15px;\n padding-bottom: 15px;\n }\n}\n.navbar-form {\n margin-left: -15px;\n margin-right: -15px;\n padding: 10px 15px;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n margin-top: 8px;\n margin-bottom: 8px;\n}\n@media (min-width: 768px) {\n .navbar-form .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .navbar-form .form-control-static {\n display: inline-block;\n }\n .navbar-form .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .navbar-form .input-group .input-group-addon,\n .navbar-form .input-group .input-group-btn,\n .navbar-form .input-group .form-control {\n width: auto;\n }\n .navbar-form .input-group > .form-control {\n width: 100%;\n }\n .navbar-form .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio,\n .navbar-form .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio label,\n .navbar-form .checkbox label {\n padding-left: 0;\n }\n .navbar-form .radio input[type=\"radio\"],\n .navbar-form .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .navbar-form .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n@media (max-width: 767px) {\n .navbar-form .form-group {\n margin-bottom: 5px;\n }\n .navbar-form .form-group:last-child {\n margin-bottom: 0;\n }\n}\n@media (min-width: 768px) {\n .navbar-form {\n width: auto;\n border: 0;\n margin-left: 0;\n margin-right: 0;\n padding-top: 0;\n padding-bottom: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n}\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n margin-bottom: 0;\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.navbar-btn {\n margin-top: 8px;\n margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n margin-top: 14px;\n margin-bottom: 14px;\n}\n.navbar-text {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n .navbar-text {\n float: left;\n margin-left: 15px;\n margin-right: 15px;\n }\n}\n@media (min-width: 768px) {\n .navbar-left {\n float: left !important;\n }\n .navbar-right {\n float: right !important;\n margin-right: -15px;\n }\n .navbar-right ~ .navbar-right {\n margin-right: 0;\n }\n}\n.navbar-default {\n background-color: #f8f8f8;\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n color: #777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n color: #5e5e5e;\n background-color: transparent;\n}\n.navbar-default .navbar-text {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n color: #333;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n}\n.navbar-default .navbar-toggle {\n border-color: #ddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n background-color: #ddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n background-color: #888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n background-color: #e7e7e7;\n color: #555;\n}\n@media (max-width: 767px) {\n .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n color: #777;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #333;\n background-color: transparent;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n }\n}\n.navbar-default .navbar-link {\n color: #777;\n}\n.navbar-default .navbar-link:hover {\n color: #333;\n}\n.navbar-default .btn-link {\n color: #777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n color: #333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n color: #ccc;\n}\n.navbar-inverse {\n background-color: #222;\n border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n color: #fff;\n background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n}\n.navbar-inverse .navbar-toggle {\n border-color: #333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n background-color: #333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n background-color: #fff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n border-color: #101010;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n background-color: #080808;\n color: #fff;\n}\n@media (max-width: 767px) {\n .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n border-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n color: #9d9d9d;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #fff;\n background-color: transparent;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n }\n}\n.navbar-inverse .navbar-link {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n color: #fff;\n}\n.navbar-inverse .btn-link {\n color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n color: #fff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n color: #444;\n}\n.breadcrumb {\n padding: 8px 15px;\n margin-bottom: 20px;\n list-style: none;\n background-color: #f5f5f5;\n border-radius: 4px;\n}\n.breadcrumb > li {\n display: inline-block;\n}\n.breadcrumb > li + li:before {\n content: \"/\\00a0\";\n padding: 0 5px;\n color: #ccc;\n}\n.breadcrumb > .active {\n color: #777777;\n}\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: 20px 0;\n border-radius: 4px;\n}\n.pagination > li {\n display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n position: relative;\n float: left;\n padding: 6px 12px;\n line-height: 1.42857143;\n text-decoration: none;\n color: #337ab7;\n background-color: #fff;\n border: 1px solid #ddd;\n margin-left: -1px;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n margin-left: 0;\n border-bottom-left-radius: 4px;\n border-top-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n border-bottom-right-radius: 4px;\n border-top-right-radius: 4px;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n z-index: 2;\n color: #23527c;\n background-color: #eeeeee;\n border-color: #ddd;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n z-index: 3;\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n cursor: default;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n color: #777777;\n background-color: #fff;\n border-color: #ddd;\n cursor: not-allowed;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n border-bottom-left-radius: 6px;\n border-top-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n border-bottom-right-radius: 6px;\n border-top-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n border-bottom-left-radius: 3px;\n border-top-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n border-bottom-right-radius: 3px;\n border-top-right-radius: 3px;\n}\n.pager {\n padding-left: 0;\n margin: 20px 0;\n list-style: none;\n text-align: center;\n}\n.pager li {\n display: inline;\n}\n.pager li > a,\n.pager li > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.pager .next > a,\n.pager .next > span {\n float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n color: #777777;\n background-color: #fff;\n cursor: not-allowed;\n}\n.label {\n display: inline;\n padding: .2em .6em .3em;\n font-size: 75%;\n font-weight: bold;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: .25em;\n}\na.label:hover,\na.label:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.label:empty {\n display: none;\n}\n.btn .label {\n position: relative;\n top: -1px;\n}\n.label-default {\n background-color: #777777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n background-color: #5e5e5e;\n}\n.label-primary {\n background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n background-color: #286090;\n}\n.label-success {\n background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n background-color: #449d44;\n}\n.label-info {\n background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n background-color: #31b0d5;\n}\n.label-warning {\n background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n background-color: #ec971f;\n}\n.label-danger {\n background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n background-color: #c9302c;\n}\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: 12px;\n font-weight: bold;\n color: #fff;\n line-height: 1;\n vertical-align: middle;\n white-space: nowrap;\n text-align: center;\n background-color: #777777;\n border-radius: 10px;\n}\n.badge:empty {\n display: none;\n}\n.btn .badge {\n position: relative;\n top: -1px;\n}\n.btn-xs .badge,\n.btn-group-xs > .btn .badge {\n top: 0;\n padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.list-group-item > .badge {\n float: right;\n}\n.list-group-item > .badge + .badge {\n margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n margin-left: 3px;\n}\n.jumbotron {\n padding-top: 30px;\n padding-bottom: 30px;\n margin-bottom: 30px;\n color: inherit;\n background-color: #eeeeee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n color: inherit;\n}\n.jumbotron p {\n margin-bottom: 15px;\n font-size: 21px;\n font-weight: 200;\n}\n.jumbotron > hr {\n border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n border-radius: 6px;\n padding-left: 15px;\n padding-right: 15px;\n}\n.jumbotron .container {\n max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n .jumbotron {\n padding-top: 48px;\n padding-bottom: 48px;\n }\n .container .jumbotron,\n .container-fluid .jumbotron {\n padding-left: 60px;\n padding-right: 60px;\n }\n .jumbotron h1,\n .jumbotron .h1 {\n font-size: 63px;\n }\n}\n.thumbnail {\n display: block;\n padding: 4px;\n margin-bottom: 20px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: border 0.2s ease-in-out;\n -o-transition: border 0.2s ease-in-out;\n transition: border 0.2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n margin-left: auto;\n margin-right: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n border-color: #337ab7;\n}\n.thumbnail .caption {\n padding: 9px;\n color: #333333;\n}\n.alert {\n padding: 15px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.alert h4 {\n margin-top: 0;\n color: inherit;\n}\n.alert .alert-link {\n font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n margin-bottom: 0;\n}\n.alert > p + p {\n margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n}\n.alert-success {\n background-color: #dff0d8;\n border-color: #d6e9c6;\n color: #3c763d;\n}\n.alert-success hr {\n border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n color: #2b542c;\n}\n.alert-info {\n background-color: #d9edf7;\n border-color: #bce8f1;\n color: #31708f;\n}\n.alert-info hr {\n border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n color: #245269;\n}\n.alert-warning {\n background-color: #fcf8e3;\n border-color: #faebcc;\n color: #8a6d3b;\n}\n.alert-warning hr {\n border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n color: #66512c;\n}\n.alert-danger {\n background-color: #f2dede;\n border-color: #ebccd1;\n color: #a94442;\n}\n.alert-danger hr {\n border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n.progress {\n overflow: hidden;\n height: 20px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: 12px;\n line-height: 20px;\n color: #fff;\n text-align: center;\n background-color: #337ab7;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n -webkit-transition: width 0.6s ease;\n -o-transition: width 0.6s ease;\n transition: width 0.6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n -webkit-animation: progress-bar-stripes 2s linear infinite;\n -o-animation: progress-bar-stripes 2s linear infinite;\n animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.media {\n margin-top: 15px;\n}\n.media:first-child {\n margin-top: 0;\n}\n.media,\n.media-body {\n zoom: 1;\n overflow: hidden;\n}\n.media-body {\n width: 10000px;\n}\n.media-object {\n display: block;\n}\n.media-object.img-thumbnail {\n max-width: none;\n}\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n.media-middle {\n vertical-align: middle;\n}\n.media-bottom {\n vertical-align: bottom;\n}\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n.list-group {\n margin-bottom: 20px;\n padding-left: 0;\n}\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.list-group-item:first-child {\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n}\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\na.list-group-item,\nbutton.list-group-item {\n color: #555;\n}\na.list-group-item .list-group-item-heading,\nbutton.list-group-item .list-group-item-heading {\n color: #333;\n}\na.list-group-item:hover,\nbutton.list-group-item:hover,\na.list-group-item:focus,\nbutton.list-group-item:focus {\n text-decoration: none;\n color: #555;\n background-color: #f5f5f5;\n}\nbutton.list-group-item {\n width: 100%;\n text-align: left;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n background-color: #eeeeee;\n color: #777777;\n cursor: not-allowed;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n color: #777777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n z-index: 2;\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n color: #c7ddef;\n}\n.list-group-item-success {\n color: #3c763d;\n background-color: #dff0d8;\n}\na.list-group-item-success,\nbutton.list-group-item-success {\n color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading,\nbutton.list-group-item-success .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-success:hover,\nbutton.list-group-item-success:hover,\na.list-group-item-success:focus,\nbutton.list-group-item-success:focus {\n color: #3c763d;\n background-color: #d0e9c6;\n}\na.list-group-item-success.active,\nbutton.list-group-item-success.active,\na.list-group-item-success.active:hover,\nbutton.list-group-item-success.active:hover,\na.list-group-item-success.active:focus,\nbutton.list-group-item-success.active:focus {\n color: #fff;\n background-color: #3c763d;\n border-color: #3c763d;\n}\n.list-group-item-info {\n color: #31708f;\n background-color: #d9edf7;\n}\na.list-group-item-info,\nbutton.list-group-item-info {\n color: #31708f;\n}\na.list-group-item-info .list-group-item-heading,\nbutton.list-group-item-info .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-info:hover,\nbutton.list-group-item-info:hover,\na.list-group-item-info:focus,\nbutton.list-group-item-info:focus {\n color: #31708f;\n background-color: #c4e3f3;\n}\na.list-group-item-info.active,\nbutton.list-group-item-info.active,\na.list-group-item-info.active:hover,\nbutton.list-group-item-info.active:hover,\na.list-group-item-info.active:focus,\nbutton.list-group-item-info.active:focus {\n color: #fff;\n background-color: #31708f;\n border-color: #31708f;\n}\n.list-group-item-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n}\na.list-group-item-warning,\nbutton.list-group-item-warning {\n color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading,\nbutton.list-group-item-warning .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-warning:hover,\nbutton.list-group-item-warning:hover,\na.list-group-item-warning:focus,\nbutton.list-group-item-warning:focus {\n color: #8a6d3b;\n background-color: #faf2cc;\n}\na.list-group-item-warning.active,\nbutton.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\nbutton.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus,\nbutton.list-group-item-warning.active:focus {\n color: #fff;\n background-color: #8a6d3b;\n border-color: #8a6d3b;\n}\n.list-group-item-danger {\n color: #a94442;\n background-color: #f2dede;\n}\na.list-group-item-danger,\nbutton.list-group-item-danger {\n color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading,\nbutton.list-group-item-danger .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-danger:hover,\nbutton.list-group-item-danger:hover,\na.list-group-item-danger:focus,\nbutton.list-group-item-danger:focus {\n color: #a94442;\n background-color: #ebcccc;\n}\na.list-group-item-danger.active,\nbutton.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\nbutton.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus,\nbutton.list-group-item-danger.active:focus {\n color: #fff;\n background-color: #a94442;\n border-color: #a94442;\n}\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n.panel {\n margin-bottom: 20px;\n background-color: #fff;\n border: 1px solid transparent;\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.panel-body {\n padding: 15px;\n}\n.panel-heading {\n padding: 10px 15px;\n border-bottom: 1px solid transparent;\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n color: inherit;\n}\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: 16px;\n color: inherit;\n}\n.panel-title > a,\n.panel-title > small,\n.panel-title > .small,\n.panel-title > small > a,\n.panel-title > .small > a {\n color: inherit;\n}\n.panel-footer {\n padding: 10px 15px;\n background-color: #f5f5f5;\n border-top: 1px solid #ddd;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n border-top: 0;\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n border-bottom: 0;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child {\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n border-top-width: 0;\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n padding-left: 15px;\n padding-right: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n border-bottom-left-radius: 3px;\n border-bottom-right-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n border-top: 1px solid #ddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n border-bottom: 0;\n}\n.panel > .table-responsive {\n border: 0;\n margin-bottom: 0;\n}\n.panel-group {\n margin-bottom: 20px;\n}\n.panel-group .panel {\n margin-bottom: 0;\n border-radius: 4px;\n}\n.panel-group .panel + .panel {\n margin-top: 5px;\n}\n.panel-group .panel-heading {\n border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n border-top: 1px solid #ddd;\n}\n.panel-group .panel-footer {\n border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n border-bottom: 1px solid #ddd;\n}\n.panel-default {\n border-color: #ddd;\n}\n.panel-default > .panel-heading {\n color: #333333;\n background-color: #f5f5f5;\n border-color: #ddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ddd;\n}\n.panel-default > .panel-heading .badge {\n color: #f5f5f5;\n background-color: #333333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ddd;\n}\n.panel-primary {\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #337ab7;\n}\n.panel-success {\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n color: #dff0d8;\n background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #d6e9c6;\n}\n.panel-info {\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n color: #d9edf7;\n background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #bce8f1;\n}\n.panel-warning {\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n color: #fcf8e3;\n background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #faebcc;\n}\n.panel-danger {\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n color: #f2dede;\n background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n height: 100%;\n width: 100%;\n border: 0;\n}\n.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n}\n.embed-responsive-4by3 {\n padding-bottom: 75%;\n}\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.well blockquote {\n border-color: #ddd;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.well-lg {\n padding: 24px;\n border-radius: 6px;\n}\n.well-sm {\n padding: 9px;\n border-radius: 3px;\n}\n.close {\n float: right;\n font-size: 21px;\n font-weight: bold;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n opacity: 0.2;\n filter: alpha(opacity=20);\n}\n.close:hover,\n.close:focus {\n color: #000;\n text-decoration: none;\n cursor: pointer;\n opacity: 0.5;\n filter: alpha(opacity=50);\n}\nbutton.close {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n}\n.modal-open {\n overflow: hidden;\n}\n.modal {\n display: none;\n overflow: hidden;\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1050;\n -webkit-overflow-scrolling: touch;\n outline: 0;\n}\n.modal.fade .modal-dialog {\n -webkit-transform: translate(0, -25%);\n -ms-transform: translate(0, -25%);\n -o-transform: translate(0, -25%);\n transform: translate(0, -25%);\n -webkit-transition: -webkit-transform 0.3s ease-out;\n -moz-transition: -moz-transform 0.3s ease-out;\n -o-transition: -o-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n -webkit-transform: translate(0, 0);\n -ms-transform: translate(0, 0);\n -o-transform: translate(0, 0);\n transform: translate(0, 0);\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n.modal-content {\n position: relative;\n background-color: #fff;\n border: 1px solid #999;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n background-clip: padding-box;\n outline: 0;\n}\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n background-color: #000;\n}\n.modal-backdrop.fade {\n opacity: 0;\n filter: alpha(opacity=0);\n}\n.modal-backdrop.in {\n opacity: 0.5;\n filter: alpha(opacity=50);\n}\n.modal-header {\n padding: 15px;\n border-bottom: 1px solid #e5e5e5;\n}\n.modal-header .close {\n margin-top: -2px;\n}\n.modal-title {\n margin: 0;\n line-height: 1.42857143;\n}\n.modal-body {\n position: relative;\n padding: 15px;\n}\n.modal-footer {\n padding: 15px;\n text-align: right;\n border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n margin-left: 5px;\n margin-bottom: 0;\n}\n.modal-footer .btn-group .btn + .btn {\n margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n margin-left: 0;\n}\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n@media (min-width: 768px) {\n .modal-dialog {\n width: 600px;\n margin: 30px auto;\n }\n .modal-content {\n -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n }\n .modal-sm {\n width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg {\n width: 900px;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-break: auto;\n line-height: 1.42857143;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n white-space: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n font-size: 12px;\n opacity: 0;\n filter: alpha(opacity=0);\n}\n.tooltip.in {\n opacity: 0.9;\n filter: alpha(opacity=90);\n}\n.tooltip.top {\n margin-top: -3px;\n padding: 5px 0;\n}\n.tooltip.right {\n margin-left: 3px;\n padding: 0 5px;\n}\n.tooltip.bottom {\n margin-top: 3px;\n padding: 5px 0;\n}\n.tooltip.left {\n margin-left: -3px;\n padding: 0 5px;\n}\n.tooltip-inner {\n max-width: 200px;\n padding: 3px 8px;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 4px;\n}\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.tooltip.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-left .tooltip-arrow {\n bottom: 0;\n right: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-right .tooltip-arrow {\n bottom: 0;\n left: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -5px;\n border-width: 5px 5px 5px 0;\n border-right-color: #000;\n}\n.tooltip.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -5px;\n border-width: 5px 0 5px 5px;\n border-left-color: #000;\n}\n.tooltip.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n top: 0;\n right: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n top: 0;\n left: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: none;\n max-width: 276px;\n padding: 1px;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-break: auto;\n line-height: 1.42857143;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n white-space: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n font-size: 14px;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n}\n.popover.top {\n margin-top: -10px;\n}\n.popover.right {\n margin-left: 10px;\n}\n.popover.bottom {\n margin-top: 10px;\n}\n.popover.left {\n margin-left: -10px;\n}\n.popover-title {\n margin: 0;\n padding: 8px 14px;\n font-size: 14px;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-radius: 5px 5px 0 0;\n}\n.popover-content {\n padding: 9px 14px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover > .arrow {\n border-width: 11px;\n}\n.popover > .arrow:after {\n border-width: 10px;\n content: \"\";\n}\n.popover.top > .arrow {\n left: 50%;\n margin-left: -11px;\n border-bottom-width: 0;\n border-top-color: #999999;\n border-top-color: rgba(0, 0, 0, 0.25);\n bottom: -11px;\n}\n.popover.top > .arrow:after {\n content: \" \";\n bottom: 1px;\n margin-left: -10px;\n border-bottom-width: 0;\n border-top-color: #fff;\n}\n.popover.right > .arrow {\n top: 50%;\n left: -11px;\n margin-top: -11px;\n border-left-width: 0;\n border-right-color: #999999;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n.popover.right > .arrow:after {\n content: \" \";\n left: 1px;\n bottom: -10px;\n border-left-width: 0;\n border-right-color: #fff;\n}\n.popover.bottom > .arrow {\n left: 50%;\n margin-left: -11px;\n border-top-width: 0;\n border-bottom-color: #999999;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n top: -11px;\n}\n.popover.bottom > .arrow:after {\n content: \" \";\n top: 1px;\n margin-left: -10px;\n border-top-width: 0;\n border-bottom-color: #fff;\n}\n.popover.left > .arrow {\n top: 50%;\n right: -11px;\n margin-top: -11px;\n border-right-width: 0;\n border-left-color: #999999;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.popover.left > .arrow:after {\n content: \" \";\n right: 1px;\n border-right-width: 0;\n border-left-color: #fff;\n bottom: -10px;\n}\n.carousel {\n position: relative;\n}\n.carousel-inner {\n position: relative;\n overflow: hidden;\n width: 100%;\n}\n.carousel-inner > .item {\n display: none;\n position: relative;\n -webkit-transition: 0.6s ease-in-out left;\n -o-transition: 0.6s ease-in-out left;\n transition: 0.6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n .carousel-inner > .item {\n -webkit-transition: -webkit-transform 0.6s ease-in-out;\n -moz-transition: -moz-transform 0.6s ease-in-out;\n -o-transition: -o-transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out;\n -webkit-backface-visibility: hidden;\n -moz-backface-visibility: hidden;\n backface-visibility: hidden;\n -webkit-perspective: 1000px;\n -moz-perspective: 1000px;\n perspective: 1000px;\n }\n .carousel-inner > .item.next,\n .carousel-inner > .item.active.right {\n -webkit-transform: translate3d(100%, 0, 0);\n transform: translate3d(100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.prev,\n .carousel-inner > .item.active.left {\n -webkit-transform: translate3d(-100%, 0, 0);\n transform: translate3d(-100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.next.left,\n .carousel-inner > .item.prev.right,\n .carousel-inner > .item.active {\n -webkit-transform: translate3d(0, 0, 0);\n transform: translate3d(0, 0, 0);\n left: 0;\n }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n display: block;\n}\n.carousel-inner > .active {\n left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n}\n.carousel-inner > .next {\n left: 100%;\n}\n.carousel-inner > .prev {\n left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n left: 0;\n}\n.carousel-inner > .active.left {\n left: -100%;\n}\n.carousel-inner > .active.right {\n left: 100%;\n}\n.carousel-control {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n width: 15%;\n opacity: 0.5;\n filter: alpha(opacity=50);\n font-size: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n background-color: rgba(0, 0, 0, 0);\n}\n.carousel-control.left {\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n}\n.carousel-control.right {\n left: auto;\n right: 0;\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n}\n.carousel-control:hover,\n.carousel-control:focus {\n outline: 0;\n color: #fff;\n text-decoration: none;\n opacity: 0.9;\n filter: alpha(opacity=90);\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n margin-top: -10px;\n z-index: 5;\n display: inline-block;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n width: 20px;\n height: 20px;\n line-height: 1;\n font-family: serif;\n}\n.carousel-control .icon-prev:before {\n content: '\\2039';\n}\n.carousel-control .icon-next:before {\n content: '\\203a';\n}\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n margin-left: -30%;\n padding-left: 0;\n list-style: none;\n text-align: center;\n}\n.carousel-indicators li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n border: 1px solid #fff;\n border-radius: 10px;\n cursor: pointer;\n background-color: #000 \\9;\n background-color: rgba(0, 0, 0, 0);\n}\n.carousel-indicators .active {\n margin: 0;\n width: 12px;\n height: 12px;\n background-color: #fff;\n}\n.carousel-caption {\n position: absolute;\n left: 15%;\n right: 15%;\n bottom: 20px;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-caption .btn {\n text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-prev,\n .carousel-control .icon-next {\n width: 30px;\n height: 30px;\n margin-top: -10px;\n font-size: 30px;\n }\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .icon-prev {\n margin-left: -10px;\n }\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-next {\n margin-right: -10px;\n }\n .carousel-caption {\n left: 20%;\n right: 20%;\n padding-bottom: 30px;\n }\n .carousel-indicators {\n bottom: 20px;\n }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-header:before,\n.modal-header:after,\n.modal-footer:before,\n.modal-footer:after {\n content: \" \";\n display: table;\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-header:after,\n.modal-footer:after {\n clear: both;\n}\n.center-block {\n display: block;\n margin-left: auto;\n margin-right: auto;\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n.hidden {\n display: none !important;\n}\n.affix {\n position: fixed;\n}\n@-ms-viewport {\n width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n@media (max-width: 767px) {\n .visible-xs {\n display: block !important;\n }\n table.visible-xs {\n display: table !important;\n }\n tr.visible-xs {\n display: table-row !important;\n }\n th.visible-xs,\n td.visible-xs {\n display: table-cell !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-block {\n display: block !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline {\n display: inline !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm {\n display: block !important;\n }\n table.visible-sm {\n display: table !important;\n }\n tr.visible-sm {\n display: table-row !important;\n }\n th.visible-sm,\n td.visible-sm {\n display: table-cell !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-block {\n display: block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline {\n display: inline !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md {\n display: block !important;\n }\n table.visible-md {\n display: table !important;\n }\n tr.visible-md {\n display: table-row !important;\n }\n th.visible-md,\n td.visible-md {\n display: table-cell !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-block {\n display: block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline {\n display: inline !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg {\n display: block !important;\n }\n table.visible-lg {\n display: table !important;\n }\n tr.visible-lg {\n display: table-row !important;\n }\n th.visible-lg,\n td.visible-lg {\n display: table-cell !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-block {\n display: block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline {\n display: inline !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline-block {\n display: inline-block !important;\n }\n}\n@media (max-width: 767px) {\n .hidden-xs {\n display: none !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .hidden-sm {\n display: none !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .hidden-md {\n display: none !important;\n }\n}\n@media (min-width: 1200px) {\n .hidden-lg {\n display: none !important;\n }\n}\n.visible-print {\n display: none !important;\n}\n@media print {\n .visible-print {\n display: block !important;\n }\n table.visible-print {\n display: table !important;\n }\n tr.visible-print {\n display: table-row !important;\n }\n th.visible-print,\n td.visible-print {\n display: table-cell !important;\n }\n}\n.visible-print-block {\n display: none !important;\n}\n@media print {\n .visible-print-block {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n}\n@media print {\n .visible-print-inline {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n}\n@media print {\n .visible-print-inline-block {\n display: inline-block !important;\n }\n}\n@media print {\n .hidden-print {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\n\n//\n// 1. Set default font family to sans-serif.\n// 2. Prevent iOS and IE text size adjust after device orientation change,\n// without disabling user zoom.\n//\n\nhtml {\n font-family: sans-serif; // 1\n -ms-text-size-adjust: 100%; // 2\n -webkit-text-size-adjust: 100%; // 2\n}\n\n//\n// Remove default margin.\n//\n\nbody {\n margin: 0;\n}\n\n// HTML5 display definitions\n// ==========================================================================\n\n//\n// Correct `block` display not defined for any HTML5 element in IE 8/9.\n// Correct `block` display not defined for `details` or `summary` in IE 10/11\n// and Firefox.\n// Correct `block` display not defined for `main` in IE 11.\n//\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\n\n//\n// 1. Correct `inline-block` display not defined in IE 8/9.\n// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.\n//\n\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block; // 1\n vertical-align: baseline; // 2\n}\n\n//\n// Prevent modern browsers from displaying `audio` without controls.\n// Remove excess height in iOS 5 devices.\n//\n\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n\n//\n// Address `[hidden]` styling not present in IE 8/9/10.\n// Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.\n//\n\n[hidden],\ntemplate {\n display: none;\n}\n\n// Links\n// ==========================================================================\n\n//\n// Remove the gray background color from active links in IE 10.\n//\n\na {\n background-color: transparent;\n}\n\n//\n// Improve readability of focused elements when they are also in an\n// active/hover state.\n//\n\na:active,\na:hover {\n outline: 0;\n}\n\n// Text-level semantics\n// ==========================================================================\n\n//\n// Address styling not present in IE 8/9/10/11, Safari, and Chrome.\n//\n\nabbr[title] {\n border-bottom: 1px dotted;\n}\n\n//\n// Address style set to `bolder` in Firefox 4+, Safari, and Chrome.\n//\n\nb,\nstrong {\n font-weight: bold;\n}\n\n//\n// Address styling not present in Safari and Chrome.\n//\n\ndfn {\n font-style: italic;\n}\n\n//\n// Address variable `h1` font-size and margin within `section` and `article`\n// contexts in Firefox 4+, Safari, and Chrome.\n//\n\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n//\n// Address styling not present in IE 8/9.\n//\n\nmark {\n background: #ff0;\n color: #000;\n}\n\n//\n// Address inconsistent and variable font size in all browsers.\n//\n\nsmall {\n font-size: 80%;\n}\n\n//\n// Prevent `sub` and `sup` affecting `line-height` in all browsers.\n//\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsup {\n top: -0.5em;\n}\n\nsub {\n bottom: -0.25em;\n}\n\n// Embedded content\n// ==========================================================================\n\n//\n// Remove border when inside `a` element in IE 8/9/10.\n//\n\nimg {\n border: 0;\n}\n\n//\n// Correct overflow not hidden in IE 9/10/11.\n//\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\n// Grouping content\n// ==========================================================================\n\n//\n// Address margin not present in IE 8/9 and Safari.\n//\n\nfigure {\n margin: 1em 40px;\n}\n\n//\n// Address differences between Firefox and other browsers.\n//\n\nhr {\n box-sizing: content-box;\n height: 0;\n}\n\n//\n// Contain overflow in all browsers.\n//\n\npre {\n overflow: auto;\n}\n\n//\n// Address odd `em`-unit font size rendering in all browsers.\n//\n\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\n// Forms\n// ==========================================================================\n\n//\n// Known limitation: by default, Chrome and Safari on OS X allow very limited\n// styling of `select`, unless a `border` property is set.\n//\n\n//\n// 1. Correct color not being inherited.\n// Known issue: affects color of disabled elements.\n// 2. Correct font properties not being inherited.\n// 3. Address margins set differently in Firefox 4+, Safari, and Chrome.\n//\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit; // 1\n font: inherit; // 2\n margin: 0; // 3\n}\n\n//\n// Address `overflow` set to `hidden` in IE 8/9/10/11.\n//\n\nbutton {\n overflow: visible;\n}\n\n//\n// Address inconsistent `text-transform` inheritance for `button` and `select`.\n// All other form control elements do not inherit `text-transform` values.\n// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.\n// Correct `select` style inheritance in Firefox.\n//\n\nbutton,\nselect {\n text-transform: none;\n}\n\n//\n// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`\n// and `video` controls.\n// 2. Correct inability to style clickable `input` types in iOS.\n// 3. Improve usability and consistency of cursor style between image-type\n// `input` and others.\n//\n\nbutton,\nhtml input[type=\"button\"], // 1\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button; // 2\n cursor: pointer; // 3\n}\n\n//\n// Re-set default cursor for disabled elements.\n//\n\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\n\n//\n// Remove inner padding and border in Firefox 4+.\n//\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\n\n//\n// Address Firefox 4+ setting `line-height` on `input` using `!important` in\n// the UA stylesheet.\n//\n\ninput {\n line-height: normal;\n}\n\n//\n// It's recommended that you don't attempt to style these elements.\n// Firefox's implementation doesn't respect box-sizing, padding, or width.\n//\n// 1. Address box sizing set to `content-box` in IE 8/9/10.\n// 2. Remove excess padding in IE 8/9/10.\n//\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box; // 1\n padding: 0; // 2\n}\n\n//\n// Fix the cursor style for Chrome's increment/decrement buttons. For certain\n// `font-size` values of the `input`, it causes the cursor style of the\n// decrement button to change from `default` to `text`.\n//\n\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n//\n// 1. Address `appearance` set to `searchfield` in Safari and Chrome.\n// 2. Address `box-sizing` set to `border-box` in Safari and Chrome.\n//\n\ninput[type=\"search\"] {\n -webkit-appearance: textfield; // 1\n box-sizing: content-box; //2\n}\n\n//\n// Remove inner padding and search cancel button in Safari and Chrome on OS X.\n// Safari (but not Chrome) clips the cancel button when the search input has\n// padding (and `textfield` appearance).\n//\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// Define consistent border, margin, and padding.\n//\n\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\n\n//\n// 1. Correct `color` not being inherited in IE 8/9/10/11.\n// 2. Remove padding so people aren't caught out if they zero out fieldsets.\n//\n\nlegend {\n border: 0; // 1\n padding: 0; // 2\n}\n\n//\n// Remove default vertical scrollbar in IE 8/9/10/11.\n//\n\ntextarea {\n overflow: auto;\n}\n\n//\n// Don't inherit the `font-weight` (applied by a rule above).\n// NOTE: the default cannot safely be changed in Chrome and Safari on OS X.\n//\n\noptgroup {\n font-weight: bold;\n}\n\n// Tables\n// ==========================================================================\n\n//\n// Remove most spacing between table cells.\n//\n\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\n\ntd,\nth {\n padding: 0;\n}\n","/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n\n// ==========================================================================\n// Print styles.\n// Inlined to avoid the additional HTTP request: h5bp.com/r\n// ==========================================================================\n\n@media print {\n *,\n *:before,\n *:after {\n background: transparent !important;\n color: #000 !important; // Black prints faster: h5bp.com/s\n box-shadow: none !important;\n text-shadow: none !important;\n }\n\n a,\n a:visited {\n text-decoration: underline;\n }\n\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n\n // Don't show links that are fragment identifiers,\n // or use the `javascript:` pseudo protocol\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n\n thead {\n display: table-header-group; // h5bp.com/t\n }\n\n tr,\n img {\n page-break-inside: avoid;\n }\n\n img {\n max-width: 100% !important;\n }\n\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n\n h2,\n h3 {\n page-break-after: avoid;\n }\n\n // Bootstrap specific changes start\n\n // Bootstrap components\n .navbar {\n display: none;\n }\n .btn,\n .dropup > .btn {\n > .caret {\n border-top-color: #000 !important;\n }\n }\n .label {\n border: 1px solid #000;\n }\n\n .table {\n border-collapse: collapse !important;\n\n td,\n th {\n background-color: #fff !important;\n }\n }\n .table-bordered {\n th,\n td {\n border: 1px solid #ddd !important;\n }\n }\n\n // Bootstrap specific changes end\n}\n","//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus automatically sized to match the surrounding child. To use, create an\n// inline element with the appropriate classes, like so:\n//\n// Star\n\n// Import the fonts\n@font-face {\n font-family: 'Glyphicons Halflings';\n src: url('@{icon-font-path}@{icon-font-name}.eot');\n src: url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype'),\n url('@{icon-font-path}@{icon-font-name}.woff2') format('woff2'),\n url('@{icon-font-path}@{icon-font-name}.woff') format('woff'),\n url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype'),\n url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg');\n}\n\n// Catchall baseclass\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: 'Glyphicons Halflings';\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n// Individual icons\n.glyphicon-asterisk { &:before { content: \"\\002a\"; } }\n.glyphicon-plus { &:before { content: \"\\002b\"; } }\n.glyphicon-euro,\n.glyphicon-eur { &:before { content: \"\\20ac\"; } }\n.glyphicon-minus { &:before { content: \"\\2212\"; } }\n.glyphicon-cloud { &:before { content: \"\\2601\"; } }\n.glyphicon-envelope { &:before { content: \"\\2709\"; } }\n.glyphicon-pencil { &:before { content: \"\\270f\"; } }\n.glyphicon-glass { &:before { content: \"\\e001\"; } }\n.glyphicon-music { &:before { content: \"\\e002\"; } }\n.glyphicon-search { &:before { content: \"\\e003\"; } }\n.glyphicon-heart { &:before { content: \"\\e005\"; } }\n.glyphicon-star { &:before { content: \"\\e006\"; } }\n.glyphicon-star-empty { &:before { content: \"\\e007\"; } }\n.glyphicon-user { &:before { content: \"\\e008\"; } }\n.glyphicon-film { &:before { content: \"\\e009\"; } }\n.glyphicon-th-large { &:before { content: \"\\e010\"; } }\n.glyphicon-th { &:before { content: \"\\e011\"; } }\n.glyphicon-th-list { &:before { content: \"\\e012\"; } }\n.glyphicon-ok { &:before { content: \"\\e013\"; } }\n.glyphicon-remove { &:before { content: \"\\e014\"; } }\n.glyphicon-zoom-in { &:before { content: \"\\e015\"; } }\n.glyphicon-zoom-out { &:before { content: \"\\e016\"; } }\n.glyphicon-off { &:before { content: \"\\e017\"; } }\n.glyphicon-signal { &:before { content: \"\\e018\"; } }\n.glyphicon-cog { &:before { content: \"\\e019\"; } }\n.glyphicon-trash { &:before { content: \"\\e020\"; } }\n.glyphicon-home { &:before { content: \"\\e021\"; } }\n.glyphicon-file { &:before { content: \"\\e022\"; } }\n.glyphicon-time { &:before { content: \"\\e023\"; } }\n.glyphicon-road { &:before { content: \"\\e024\"; } }\n.glyphicon-download-alt { &:before { content: \"\\e025\"; } }\n.glyphicon-download { &:before { content: \"\\e026\"; } }\n.glyphicon-upload { &:before { content: \"\\e027\"; } }\n.glyphicon-inbox { &:before { content: \"\\e028\"; } }\n.glyphicon-play-circle { &:before { content: \"\\e029\"; } }\n.glyphicon-repeat { &:before { content: \"\\e030\"; } }\n.glyphicon-refresh { &:before { content: \"\\e031\"; } }\n.glyphicon-list-alt { &:before { content: \"\\e032\"; } }\n.glyphicon-lock { &:before { content: \"\\e033\"; } }\n.glyphicon-flag { &:before { content: \"\\e034\"; } }\n.glyphicon-headphones { &:before { content: \"\\e035\"; } }\n.glyphicon-volume-off { &:before { content: \"\\e036\"; } }\n.glyphicon-volume-down { &:before { content: \"\\e037\"; } }\n.glyphicon-volume-up { &:before { content: \"\\e038\"; } }\n.glyphicon-qrcode { &:before { content: \"\\e039\"; } }\n.glyphicon-barcode { &:before { content: \"\\e040\"; } }\n.glyphicon-tag { &:before { content: \"\\e041\"; } }\n.glyphicon-tags { &:before { content: \"\\e042\"; } }\n.glyphicon-book { &:before { content: \"\\e043\"; } }\n.glyphicon-bookmark { &:before { content: \"\\e044\"; } }\n.glyphicon-print { &:before { content: \"\\e045\"; } }\n.glyphicon-camera { &:before { content: \"\\e046\"; } }\n.glyphicon-font { &:before { content: \"\\e047\"; } }\n.glyphicon-bold { &:before { content: \"\\e048\"; } }\n.glyphicon-italic { &:before { content: \"\\e049\"; } }\n.glyphicon-text-height { &:before { content: \"\\e050\"; } }\n.glyphicon-text-width { &:before { content: \"\\e051\"; } }\n.glyphicon-align-left { &:before { content: \"\\e052\"; } }\n.glyphicon-align-center { &:before { content: \"\\e053\"; } }\n.glyphicon-align-right { &:before { content: \"\\e054\"; } }\n.glyphicon-align-justify { &:before { content: \"\\e055\"; } }\n.glyphicon-list { &:before { content: \"\\e056\"; } }\n.glyphicon-indent-left { &:before { content: \"\\e057\"; } }\n.glyphicon-indent-right { &:before { content: \"\\e058\"; } }\n.glyphicon-facetime-video { &:before { content: \"\\e059\"; } }\n.glyphicon-picture { &:before { content: \"\\e060\"; } }\n.glyphicon-map-marker { &:before { content: \"\\e062\"; } }\n.glyphicon-adjust { &:before { content: \"\\e063\"; } }\n.glyphicon-tint { &:before { content: \"\\e064\"; } }\n.glyphicon-edit { &:before { content: \"\\e065\"; } }\n.glyphicon-share { &:before { content: \"\\e066\"; } }\n.glyphicon-check { &:before { content: \"\\e067\"; } }\n.glyphicon-move { &:before { content: \"\\e068\"; } }\n.glyphicon-step-backward { &:before { content: \"\\e069\"; } }\n.glyphicon-fast-backward { &:before { content: \"\\e070\"; } }\n.glyphicon-backward { &:before { content: \"\\e071\"; } }\n.glyphicon-play { &:before { content: \"\\e072\"; } }\n.glyphicon-pause { &:before { content: \"\\e073\"; } }\n.glyphicon-stop { &:before { content: \"\\e074\"; } }\n.glyphicon-forward { &:before { content: \"\\e075\"; } }\n.glyphicon-fast-forward { &:before { content: \"\\e076\"; } }\n.glyphicon-step-forward { &:before { content: \"\\e077\"; } }\n.glyphicon-eject { &:before { content: \"\\e078\"; } }\n.glyphicon-chevron-left { &:before { content: \"\\e079\"; } }\n.glyphicon-chevron-right { &:before { content: \"\\e080\"; } }\n.glyphicon-plus-sign { &:before { content: \"\\e081\"; } }\n.glyphicon-minus-sign { &:before { content: \"\\e082\"; } }\n.glyphicon-remove-sign { &:before { content: \"\\e083\"; } }\n.glyphicon-ok-sign { &:before { content: \"\\e084\"; } }\n.glyphicon-question-sign { &:before { content: \"\\e085\"; } }\n.glyphicon-info-sign { &:before { content: \"\\e086\"; } }\n.glyphicon-screenshot { &:before { content: \"\\e087\"; } }\n.glyphicon-remove-circle { &:before { content: \"\\e088\"; } }\n.glyphicon-ok-circle { &:before { content: \"\\e089\"; } }\n.glyphicon-ban-circle { &:before { content: \"\\e090\"; } }\n.glyphicon-arrow-left { &:before { content: \"\\e091\"; } }\n.glyphicon-arrow-right { &:before { content: \"\\e092\"; } }\n.glyphicon-arrow-up { &:before { content: \"\\e093\"; } }\n.glyphicon-arrow-down { &:before { content: \"\\e094\"; } }\n.glyphicon-share-alt { &:before { content: \"\\e095\"; } }\n.glyphicon-resize-full { &:before { content: \"\\e096\"; } }\n.glyphicon-resize-small { &:before { content: \"\\e097\"; } }\n.glyphicon-exclamation-sign { &:before { content: \"\\e101\"; } }\n.glyphicon-gift { &:before { content: \"\\e102\"; } }\n.glyphicon-leaf { &:before { content: \"\\e103\"; } }\n.glyphicon-fire { &:before { content: \"\\e104\"; } }\n.glyphicon-eye-open { &:before { content: \"\\e105\"; } }\n.glyphicon-eye-close { &:before { content: \"\\e106\"; } }\n.glyphicon-warning-sign { &:before { content: \"\\e107\"; } }\n.glyphicon-plane { &:before { content: \"\\e108\"; } }\n.glyphicon-calendar { &:before { content: \"\\e109\"; } }\n.glyphicon-random { &:before { content: \"\\e110\"; } }\n.glyphicon-comment { &:before { content: \"\\e111\"; } }\n.glyphicon-magnet { &:before { content: \"\\e112\"; } }\n.glyphicon-chevron-up { &:before { content: \"\\e113\"; } }\n.glyphicon-chevron-down { &:before { content: \"\\e114\"; } }\n.glyphicon-retweet { &:before { content: \"\\e115\"; } }\n.glyphicon-shopping-cart { &:before { content: \"\\e116\"; } }\n.glyphicon-folder-close { &:before { content: \"\\e117\"; } }\n.glyphicon-folder-open { &:before { content: \"\\e118\"; } }\n.glyphicon-resize-vertical { &:before { content: \"\\e119\"; } }\n.glyphicon-resize-horizontal { &:before { content: \"\\e120\"; } }\n.glyphicon-hdd { &:before { content: \"\\e121\"; } }\n.glyphicon-bullhorn { &:before { content: \"\\e122\"; } }\n.glyphicon-bell { &:before { content: \"\\e123\"; } }\n.glyphicon-certificate { &:before { content: \"\\e124\"; } }\n.glyphicon-thumbs-up { &:before { content: \"\\e125\"; } }\n.glyphicon-thumbs-down { &:before { content: \"\\e126\"; } }\n.glyphicon-hand-right { &:before { content: \"\\e127\"; } }\n.glyphicon-hand-left { &:before { content: \"\\e128\"; } }\n.glyphicon-hand-up { &:before { content: \"\\e129\"; } }\n.glyphicon-hand-down { &:before { content: \"\\e130\"; } }\n.glyphicon-circle-arrow-right { &:before { content: \"\\e131\"; } }\n.glyphicon-circle-arrow-left { &:before { content: \"\\e132\"; } }\n.glyphicon-circle-arrow-up { &:before { content: \"\\e133\"; } }\n.glyphicon-circle-arrow-down { &:before { content: \"\\e134\"; } }\n.glyphicon-globe { &:before { content: \"\\e135\"; } }\n.glyphicon-wrench { &:before { content: \"\\e136\"; } }\n.glyphicon-tasks { &:before { content: \"\\e137\"; } }\n.glyphicon-filter { &:before { content: \"\\e138\"; } }\n.glyphicon-briefcase { &:before { content: \"\\e139\"; } }\n.glyphicon-fullscreen { &:before { content: \"\\e140\"; } }\n.glyphicon-dashboard { &:before { content: \"\\e141\"; } }\n.glyphicon-paperclip { &:before { content: \"\\e142\"; } }\n.glyphicon-heart-empty { &:before { content: \"\\e143\"; } }\n.glyphicon-link { &:before { content: \"\\e144\"; } }\n.glyphicon-phone { &:before { content: \"\\e145\"; } }\n.glyphicon-pushpin { &:before { content: \"\\e146\"; } }\n.glyphicon-usd { &:before { content: \"\\e148\"; } }\n.glyphicon-gbp { &:before { content: \"\\e149\"; } }\n.glyphicon-sort { &:before { content: \"\\e150\"; } }\n.glyphicon-sort-by-alphabet { &:before { content: \"\\e151\"; } }\n.glyphicon-sort-by-alphabet-alt { &:before { content: \"\\e152\"; } }\n.glyphicon-sort-by-order { &:before { content: \"\\e153\"; } }\n.glyphicon-sort-by-order-alt { &:before { content: \"\\e154\"; } }\n.glyphicon-sort-by-attributes { &:before { content: \"\\e155\"; } }\n.glyphicon-sort-by-attributes-alt { &:before { content: \"\\e156\"; } }\n.glyphicon-unchecked { &:before { content: \"\\e157\"; } }\n.glyphicon-expand { &:before { content: \"\\e158\"; } }\n.glyphicon-collapse-down { &:before { content: \"\\e159\"; } }\n.glyphicon-collapse-up { &:before { content: \"\\e160\"; } }\n.glyphicon-log-in { &:before { content: \"\\e161\"; } }\n.glyphicon-flash { &:before { content: \"\\e162\"; } }\n.glyphicon-log-out { &:before { content: \"\\e163\"; } }\n.glyphicon-new-window { &:before { content: \"\\e164\"; } }\n.glyphicon-record { &:before { content: \"\\e165\"; } }\n.glyphicon-save { &:before { content: \"\\e166\"; } }\n.glyphicon-open { &:before { content: \"\\e167\"; } }\n.glyphicon-saved { &:before { content: \"\\e168\"; } }\n.glyphicon-import { &:before { content: \"\\e169\"; } }\n.glyphicon-export { &:before { content: \"\\e170\"; } }\n.glyphicon-send { &:before { content: \"\\e171\"; } }\n.glyphicon-floppy-disk { &:before { content: \"\\e172\"; } }\n.glyphicon-floppy-saved { &:before { content: \"\\e173\"; } }\n.glyphicon-floppy-remove { &:before { content: \"\\e174\"; } }\n.glyphicon-floppy-save { &:before { content: \"\\e175\"; } }\n.glyphicon-floppy-open { &:before { content: \"\\e176\"; } }\n.glyphicon-credit-card { &:before { content: \"\\e177\"; } }\n.glyphicon-transfer { &:before { content: \"\\e178\"; } }\n.glyphicon-cutlery { &:before { content: \"\\e179\"; } }\n.glyphicon-header { &:before { content: \"\\e180\"; } }\n.glyphicon-compressed { &:before { content: \"\\e181\"; } }\n.glyphicon-earphone { &:before { content: \"\\e182\"; } }\n.glyphicon-phone-alt { &:before { content: \"\\e183\"; } }\n.glyphicon-tower { &:before { content: \"\\e184\"; } }\n.glyphicon-stats { &:before { content: \"\\e185\"; } }\n.glyphicon-sd-video { &:before { content: \"\\e186\"; } }\n.glyphicon-hd-video { &:before { content: \"\\e187\"; } }\n.glyphicon-subtitles { &:before { content: \"\\e188\"; } }\n.glyphicon-sound-stereo { &:before { content: \"\\e189\"; } }\n.glyphicon-sound-dolby { &:before { content: \"\\e190\"; } }\n.glyphicon-sound-5-1 { &:before { content: \"\\e191\"; } }\n.glyphicon-sound-6-1 { &:before { content: \"\\e192\"; } }\n.glyphicon-sound-7-1 { &:before { content: \"\\e193\"; } }\n.glyphicon-copyright-mark { &:before { content: \"\\e194\"; } }\n.glyphicon-registration-mark { &:before { content: \"\\e195\"; } }\n.glyphicon-cloud-download { &:before { content: \"\\e197\"; } }\n.glyphicon-cloud-upload { &:before { content: \"\\e198\"; } }\n.glyphicon-tree-conifer { &:before { content: \"\\e199\"; } }\n.glyphicon-tree-deciduous { &:before { content: \"\\e200\"; } }\n.glyphicon-cd { &:before { content: \"\\e201\"; } }\n.glyphicon-save-file { &:before { content: \"\\e202\"; } }\n.glyphicon-open-file { &:before { content: \"\\e203\"; } }\n.glyphicon-level-up { &:before { content: \"\\e204\"; } }\n.glyphicon-copy { &:before { content: \"\\e205\"; } }\n.glyphicon-paste { &:before { content: \"\\e206\"; } }\n// The following 2 Glyphicons are omitted for the time being because\n// they currently use Unicode codepoints that are outside the\n// Basic Multilingual Plane (BMP). Older buggy versions of WebKit can't handle\n// non-BMP codepoints in CSS string escapes, and thus can't display these two icons.\n// Notably, the bug affects some older versions of the Android Browser.\n// More info: https://github.com/twbs/bootstrap/issues/10106\n// .glyphicon-door { &:before { content: \"\\1f6aa\"; } }\n// .glyphicon-key { &:before { content: \"\\1f511\"; } }\n.glyphicon-alert { &:before { content: \"\\e209\"; } }\n.glyphicon-equalizer { &:before { content: \"\\e210\"; } }\n.glyphicon-king { &:before { content: \"\\e211\"; } }\n.glyphicon-queen { &:before { content: \"\\e212\"; } }\n.glyphicon-pawn { &:before { content: \"\\e213\"; } }\n.glyphicon-bishop { &:before { content: \"\\e214\"; } }\n.glyphicon-knight { &:before { content: \"\\e215\"; } }\n.glyphicon-baby-formula { &:before { content: \"\\e216\"; } }\n.glyphicon-tent { &:before { content: \"\\26fa\"; } }\n.glyphicon-blackboard { &:before { content: \"\\e218\"; } }\n.glyphicon-bed { &:before { content: \"\\e219\"; } }\n.glyphicon-apple { &:before { content: \"\\f8ff\"; } }\n.glyphicon-erase { &:before { content: \"\\e221\"; } }\n.glyphicon-hourglass { &:before { content: \"\\231b\"; } }\n.glyphicon-lamp { &:before { content: \"\\e223\"; } }\n.glyphicon-duplicate { &:before { content: \"\\e224\"; } }\n.glyphicon-piggy-bank { &:before { content: \"\\e225\"; } }\n.glyphicon-scissors { &:before { content: \"\\e226\"; } }\n.glyphicon-bitcoin { &:before { content: \"\\e227\"; } }\n.glyphicon-btc { &:before { content: \"\\e227\"; } }\n.glyphicon-xbt { &:before { content: \"\\e227\"; } }\n.glyphicon-yen { &:before { content: \"\\00a5\"; } }\n.glyphicon-jpy { &:before { content: \"\\00a5\"; } }\n.glyphicon-ruble { &:before { content: \"\\20bd\"; } }\n.glyphicon-rub { &:before { content: \"\\20bd\"; } }\n.glyphicon-scale { &:before { content: \"\\e230\"; } }\n.glyphicon-ice-lolly { &:before { content: \"\\e231\"; } }\n.glyphicon-ice-lolly-tasted { &:before { content: \"\\e232\"; } }\n.glyphicon-education { &:before { content: \"\\e233\"; } }\n.glyphicon-option-horizontal { &:before { content: \"\\e234\"; } }\n.glyphicon-option-vertical { &:before { content: \"\\e235\"; } }\n.glyphicon-menu-hamburger { &:before { content: \"\\e236\"; } }\n.glyphicon-modal-window { &:before { content: \"\\e237\"; } }\n.glyphicon-oil { &:before { content: \"\\e238\"; } }\n.glyphicon-grain { &:before { content: \"\\e239\"; } }\n.glyphicon-sunglasses { &:before { content: \"\\e240\"; } }\n.glyphicon-text-size { &:before { content: \"\\e241\"; } }\n.glyphicon-text-color { &:before { content: \"\\e242\"; } }\n.glyphicon-text-background { &:before { content: \"\\e243\"; } }\n.glyphicon-object-align-top { &:before { content: \"\\e244\"; } }\n.glyphicon-object-align-bottom { &:before { content: \"\\e245\"; } }\n.glyphicon-object-align-horizontal{ &:before { content: \"\\e246\"; } }\n.glyphicon-object-align-left { &:before { content: \"\\e247\"; } }\n.glyphicon-object-align-vertical { &:before { content: \"\\e248\"; } }\n.glyphicon-object-align-right { &:before { content: \"\\e249\"; } }\n.glyphicon-triangle-right { &:before { content: \"\\e250\"; } }\n.glyphicon-triangle-left { &:before { content: \"\\e251\"; } }\n.glyphicon-triangle-bottom { &:before { content: \"\\e252\"; } }\n.glyphicon-triangle-top { &:before { content: \"\\e253\"; } }\n.glyphicon-console { &:before { content: \"\\e254\"; } }\n.glyphicon-superscript { &:before { content: \"\\e255\"; } }\n.glyphicon-subscript { &:before { content: \"\\e256\"; } }\n.glyphicon-menu-left { &:before { content: \"\\e257\"; } }\n.glyphicon-menu-right { &:before { content: \"\\e258\"; } }\n.glyphicon-menu-down { &:before { content: \"\\e259\"; } }\n.glyphicon-menu-up { &:before { content: \"\\e260\"; } }\n","//\n// Scaffolding\n// --------------------------------------------------\n\n\n// Reset the box-sizing\n//\n// Heads up! This reset may cause conflicts with some third-party widgets.\n// For recommendations on resolving such conflicts, see\n// http://getbootstrap.com/getting-started/#third-box-sizing\n* {\n .box-sizing(border-box);\n}\n*:before,\n*:after {\n .box-sizing(border-box);\n}\n\n\n// Body reset\n\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0,0,0,0);\n}\n\nbody {\n font-family: @font-family-base;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @text-color;\n background-color: @body-bg;\n}\n\n// Reset fonts for relevant elements\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\n\n// Links\n\na {\n color: @link-color;\n text-decoration: none;\n\n &:hover,\n &:focus {\n color: @link-hover-color;\n text-decoration: @link-hover-decoration;\n }\n\n &:focus {\n .tab-focus();\n }\n}\n\n\n// Figures\n//\n// We reset this here because previously Normalize had no `figure` margins. This\n// ensures we don't break anyone's use of the element.\n\nfigure {\n margin: 0;\n}\n\n\n// Images\n\nimg {\n vertical-align: middle;\n}\n\n// Responsive images (ensure images don't scale beyond their parents)\n.img-responsive {\n .img-responsive();\n}\n\n// Rounded corners\n.img-rounded {\n border-radius: @border-radius-large;\n}\n\n// Image thumbnails\n//\n// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`.\n.img-thumbnail {\n padding: @thumbnail-padding;\n line-height: @line-height-base;\n background-color: @thumbnail-bg;\n border: 1px solid @thumbnail-border;\n border-radius: @thumbnail-border-radius;\n .transition(all .2s ease-in-out);\n\n // Keep them at most 100% wide\n .img-responsive(inline-block);\n}\n\n// Perfect circle\n.img-circle {\n border-radius: 50%; // set radius in percents\n}\n\n\n// Horizontal rules\n\nhr {\n margin-top: @line-height-computed;\n margin-bottom: @line-height-computed;\n border: 0;\n border-top: 1px solid @hr-border;\n}\n\n\n// Only display content to screen readers\n//\n// See: http://a11yproject.com/posts/how-to-hide-content\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0,0,0,0);\n border: 0;\n}\n\n// Use in conjunction with .sr-only to only display content when it's focused.\n// Useful for \"Skip to main content\" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n// Credit: HTML5 Boilerplate\n\n.sr-only-focusable {\n &:active,\n &:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n }\n}\n\n\n// iOS \"clickable elements\" fix for role=\"button\"\n//\n// Fixes \"clickability\" issue (and more generally, the firing of events such as focus as well)\n// for traditionally non-focusable elements with role=\"button\"\n// see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile\n\n[role=\"button\"] {\n cursor: pointer;\n}\n","// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They have been removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility) {\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// WebKit-style focus\n\n.tab-focus() {\n // WebKit-specific. Other browsers will keep their default outline style.\n // (Initially tried to also force default via `outline: initial`,\n // but that seems to erroneously remove the outline in Firefox altogether.)\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n","// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n.img-responsive(@display: block) {\n display: @display;\n max-width: 100%; // Part 1: Set a maximum relative to the parent\n height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size. Note that the\n// spelling of `min--moz-device-pixel-ratio` is intentional.\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n background-image: url(\"@{file-1x}\");\n\n @media\n only screen and (-webkit-min-device-pixel-ratio: 2),\n only screen and ( min--moz-device-pixel-ratio: 2),\n only screen and ( -o-min-device-pixel-ratio: 2/1),\n only screen and ( min-device-pixel-ratio: 2),\n only screen and ( min-resolution: 192dpi),\n only screen and ( min-resolution: 2dppx) {\n background-image: url(\"@{file-2x}\");\n background-size: @width-1x @height-1x;\n }\n}\n","//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n font-family: @headings-font-family;\n font-weight: @headings-font-weight;\n line-height: @headings-line-height;\n color: @headings-color;\n\n small,\n .small {\n font-weight: normal;\n line-height: 1;\n color: @headings-small-color;\n }\n}\n\nh1, .h1,\nh2, .h2,\nh3, .h3 {\n margin-top: @line-height-computed;\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 65%;\n }\n}\nh4, .h4,\nh5, .h5,\nh6, .h6 {\n margin-top: (@line-height-computed / 2);\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 75%;\n }\n}\n\nh1, .h1 { font-size: @font-size-h1; }\nh2, .h2 { font-size: @font-size-h2; }\nh3, .h3 { font-size: @font-size-h3; }\nh4, .h4 { font-size: @font-size-h4; }\nh5, .h5 { font-size: @font-size-h5; }\nh6, .h6 { font-size: @font-size-h6; }\n\n\n// Body text\n// -------------------------\n\np {\n margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n margin-bottom: @line-height-computed;\n font-size: floor((@font-size-base * 1.15));\n font-weight: 300;\n line-height: 1.4;\n\n @media (min-width: @screen-sm-min) {\n font-size: (@font-size-base * 1.5);\n }\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: (12px small font / 14px base font) * 100% = about 85%\nsmall,\n.small {\n font-size: floor((100% * @font-size-small / @font-size-base));\n}\n\nmark,\n.mark {\n background-color: @state-warning-bg;\n padding: .2em;\n}\n\n// Alignment\n.text-left { text-align: left; }\n.text-right { text-align: right; }\n.text-center { text-align: center; }\n.text-justify { text-align: justify; }\n.text-nowrap { white-space: nowrap; }\n\n// Transformation\n.text-lowercase { text-transform: lowercase; }\n.text-uppercase { text-transform: uppercase; }\n.text-capitalize { text-transform: capitalize; }\n\n// Contextual colors\n.text-muted {\n color: @text-muted;\n}\n.text-primary {\n .text-emphasis-variant(@brand-primary);\n}\n.text-success {\n .text-emphasis-variant(@state-success-text);\n}\n.text-info {\n .text-emphasis-variant(@state-info-text);\n}\n.text-warning {\n .text-emphasis-variant(@state-warning-text);\n}\n.text-danger {\n .text-emphasis-variant(@state-danger-text);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n // Given the contrast here, this is the only class to have its color inverted\n // automatically.\n color: #fff;\n .bg-variant(@brand-primary);\n}\n.bg-success {\n .bg-variant(@state-success-bg);\n}\n.bg-info {\n .bg-variant(@state-info-bg);\n}\n.bg-warning {\n .bg-variant(@state-warning-bg);\n}\n.bg-danger {\n .bg-variant(@state-danger-bg);\n}\n\n\n// Page header\n// -------------------------\n\n.page-header {\n padding-bottom: ((@line-height-computed / 2) - 1);\n margin: (@line-height-computed * 2) 0 @line-height-computed;\n border-bottom: 1px solid @page-header-border-color;\n}\n\n\n// Lists\n// -------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n margin-top: 0;\n margin-bottom: (@line-height-computed / 2);\n ul,\n ol {\n margin-bottom: 0;\n }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n .list-unstyled();\n margin-left: -5px;\n\n > li {\n display: inline-block;\n padding-left: 5px;\n padding-right: 5px;\n }\n}\n\n// Description Lists\ndl {\n margin-top: 0; // Remove browser default\n margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n line-height: @line-height-base;\n}\ndt {\n font-weight: bold;\n}\ndd {\n margin-left: 0; // Undo browser default\n}\n\n// Horizontal description lists\n//\n// Defaults to being stacked without any of the below styles applied, until the\n// grid breakpoint is reached (default of ~768px).\n\n.dl-horizontal {\n dd {\n &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present\n }\n\n @media (min-width: @dl-horizontal-breakpoint) {\n dt {\n float: left;\n width: (@dl-horizontal-offset - 20);\n clear: left;\n text-align: right;\n .text-overflow();\n }\n dd {\n margin-left: @dl-horizontal-offset;\n }\n }\n}\n\n\n// Misc\n// -------------------------\n\n// Abbreviations and acronyms\nabbr[title],\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[data-original-title] {\n cursor: help;\n border-bottom: 1px dotted @abbr-border-color;\n}\n.initialism {\n font-size: 90%;\n .text-uppercase();\n}\n\n// Blockquotes\nblockquote {\n padding: (@line-height-computed / 2) @line-height-computed;\n margin: 0 0 @line-height-computed;\n font-size: @blockquote-font-size;\n border-left: 5px solid @blockquote-border-color;\n\n p,\n ul,\n ol {\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n // Note: Deprecated small and .small as of v3.1.0\n // Context: https://github.com/twbs/bootstrap/issues/11660\n footer,\n small,\n .small {\n display: block;\n font-size: 80%; // back to default font-size\n line-height: @line-height-base;\n color: @blockquote-small-color;\n\n &:before {\n content: '\\2014 \\00A0'; // em dash, nbsp\n }\n }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n border-right: 5px solid @blockquote-border-color;\n border-left: 0;\n text-align: right;\n\n // Account for citation\n footer,\n small,\n .small {\n &:before { content: ''; }\n &:after {\n content: '\\00A0 \\2014'; // nbsp, em dash\n }\n }\n}\n\n// Addresses\naddress {\n margin-bottom: @line-height-computed;\n font-style: normal;\n line-height: @line-height-base;\n}\n","// Typography\n\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover,\n a&:focus {\n color: darken(@color, 10%);\n }\n}\n","// Contextual backgrounds\n\n.bg-variant(@color) {\n background-color: @color;\n a&:hover,\n a&:focus {\n background-color: darken(@color, 10%);\n }\n}\n","// Text overflow\n// Requires inline-block or block for proper styling\n\n.text-overflow() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n","//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n font-family: @font-family-monospace;\n}\n\n// Inline code\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: @code-color;\n background-color: @code-bg;\n border-radius: @border-radius-base;\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: @kbd-color;\n background-color: @kbd-bg;\n border-radius: @border-radius-small;\n box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);\n\n kbd {\n padding: 0;\n font-size: 100%;\n font-weight: bold;\n box-shadow: none;\n }\n}\n\n// Blocks of code\npre {\n display: block;\n padding: ((@line-height-computed - 1) / 2);\n margin: 0 0 (@line-height-computed / 2);\n font-size: (@font-size-base - 1); // 14px to 13px\n line-height: @line-height-base;\n word-break: break-all;\n word-wrap: break-word;\n color: @pre-color;\n background-color: @pre-bg;\n border: 1px solid @pre-border-color;\n border-radius: @border-radius-base;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: @pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n.container {\n .container-fixed();\n\n @media (min-width: @screen-sm-min) {\n width: @container-sm;\n }\n @media (min-width: @screen-md-min) {\n width: @container-md;\n }\n @media (min-width: @screen-lg-min) {\n width: @container-lg;\n }\n}\n\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but without any defined\n// width for fluid, full width layouts.\n\n.container-fluid {\n .container-fixed();\n}\n\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n.row {\n .make-row();\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n.make-grid-columns();\n\n\n// Extra small grid\n//\n// Columns, offsets, pushes, and pulls for extra small devices like\n// smartphones.\n\n.make-grid(xs);\n\n\n// Small grid\n//\n// Columns, offsets, pushes, and pulls for the small device range, from phones\n// to tablets.\n\n@media (min-width: @screen-sm-min) {\n .make-grid(sm);\n}\n\n\n// Medium grid\n//\n// Columns, offsets, pushes, and pulls for the desktop device range.\n\n@media (min-width: @screen-md-min) {\n .make-grid(md);\n}\n\n\n// Large grid\n//\n// Columns, offsets, pushes, and pulls for the large desktop device range.\n\n@media (min-width: @screen-lg-min) {\n .make-grid(lg);\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n// Centered container element\n.container-fixed(@gutter: @grid-gutter-width) {\n margin-right: auto;\n margin-left: auto;\n padding-left: floor((@gutter / 2));\n padding-right: ceil((@gutter / 2));\n &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n margin-left: ceil((@gutter / -2));\n margin-right: floor((@gutter / -2));\n &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n float: left;\n width: percentage((@columns / @grid-columns));\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n margin-left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-push(@columns) {\n left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-pull(@columns) {\n right: percentage((@columns / @grid-columns));\n}\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-sm-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-offset(@columns) {\n @media (min-width: @screen-sm-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-push(@columns) {\n @media (min-width: @screen-sm-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-pull(@columns) {\n @media (min-width: @screen-sm-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-md-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-offset(@columns) {\n @media (min-width: @screen-md-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-push(@columns) {\n @media (min-width: @screen-md-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-pull(@columns) {\n @media (min-width: @screen-md-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-lg-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-offset(@columns) {\n @media (min-width: @screen-lg-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-push(@columns) {\n @media (min-width: @screen-lg-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-pull(@columns) {\n @media (min-width: @screen-lg-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n // Common styles for all sizes of grid columns, widths 1-12\n .col(@index) { // initial\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n position: relative;\n // Prevent columns from collapsing when empty\n min-height: 1px;\n // Inner gutter via padding\n padding-left: ceil((@grid-gutter-width / 2));\n padding-right: floor((@grid-gutter-width / 2));\n }\n }\n .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n .col(@index) { // initial\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n float: left;\n }\n }\n .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n .col-@{class}-@{index} {\n width: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) {\n .col-@{class}-push-@{index} {\n left: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) {\n .col-@{class}-push-0 {\n left: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) {\n .col-@{class}-pull-@{index} {\n right: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) {\n .col-@{class}-pull-0 {\n right: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n .col-@{class}-offset-@{index} {\n margin-left: percentage((@index / @grid-columns));\n }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n .calc-grid-column(@index, @class, @type);\n // next iteration\n .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n .float-grid-columns(@class);\n .loop-grid-columns(@grid-columns, @class, width);\n .loop-grid-columns(@grid-columns, @class, pull);\n .loop-grid-columns(@grid-columns, @class, push);\n .loop-grid-columns(@grid-columns, @class, offset);\n}\n","//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n background-color: @table-bg;\n}\ncaption {\n padding-top: @table-cell-padding;\n padding-bottom: @table-cell-padding;\n color: @text-muted;\n text-align: left;\n}\nth {\n text-align: left;\n}\n\n\n// Baseline styles\n\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: @line-height-computed;\n // Cells\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-cell-padding;\n line-height: @line-height-base;\n vertical-align: top;\n border-top: 1px solid @table-border-color;\n }\n }\n }\n // Bottom align for column headings\n > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid @table-border-color;\n }\n // Remove top border from thead by default\n > caption + thead,\n > colgroup + thead,\n > thead:first-child {\n > tr:first-child {\n > th,\n > td {\n border-top: 0;\n }\n }\n }\n // Account for multiple tbody instances\n > tbody + tbody {\n border-top: 2px solid @table-border-color;\n }\n\n // Nesting\n .table {\n background-color: @body-bg;\n }\n}\n\n\n// Condensed table w/ half padding\n\n.table-condensed {\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-condensed-cell-padding;\n }\n }\n }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n border: 1px solid @table-border-color;\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n border: 1px solid @table-border-color;\n }\n }\n }\n > thead > tr {\n > th,\n > td {\n border-bottom-width: 2px;\n }\n }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n > tbody > tr:nth-of-type(odd) {\n background-color: @table-bg-accent;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n > tbody > tr:hover {\n background-color: @table-bg-hover;\n }\n}\n\n\n// Table cell sizing\n//\n// Reset default table behavior\n\ntable col[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n float: none;\n display: table-column;\n}\ntable {\n td,\n th {\n &[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n float: none;\n display: table-cell;\n }\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n// Generate the contextual variants\n.table-row-variant(active; @table-bg-active);\n.table-row-variant(success; @state-success-bg);\n.table-row-variant(info; @state-info-bg);\n.table-row-variant(warning; @state-warning-bg);\n.table-row-variant(danger; @state-danger-bg);\n\n\n// Responsive tables\n//\n// Wrap your tables in `.table-responsive` and we'll make them mobile friendly\n// by enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n.table-responsive {\n overflow-x: auto;\n min-height: 0.01%; // Workaround for IE9 bug (see https://github.com/twbs/bootstrap/issues/14837)\n\n @media screen and (max-width: @screen-xs-max) {\n width: 100%;\n margin-bottom: (@line-height-computed * 0.75);\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid @table-border-color;\n\n // Tighten up spacing\n > .table {\n margin-bottom: 0;\n\n // Ensure the content doesn't wrap\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n white-space: nowrap;\n }\n }\n }\n }\n\n // Special overrides for the bordered tables\n > .table-bordered {\n border: 0;\n\n // Nuke the appropriate borders so that the parent can handle them\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th:first-child,\n > td:first-child {\n border-left: 0;\n }\n > th:last-child,\n > td:last-child {\n border-right: 0;\n }\n }\n }\n\n // Only nuke the last row's bottom-border in `tbody` and `tfoot` since\n // chances are there will be only one `tr` in a `thead` and that would\n // remove the border altogether.\n > tbody,\n > tfoot {\n > tr:last-child {\n > th,\n > td {\n border-bottom: 0;\n }\n }\n }\n\n }\n }\n}\n","// Tables\n\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table > thead > tr,\n .table > tbody > tr,\n .table > tfoot > tr {\n > td.@{state},\n > th.@{state},\n &.@{state} > td,\n &.@{state} > th {\n background-color: @background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover > tbody > tr {\n > td.@{state}:hover,\n > th.@{state}:hover,\n &.@{state}:hover > td,\n &:hover > .@{state},\n &.@{state}:hover > th {\n background-color: darken(@background, 5%);\n }\n }\n}\n","//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline non-control form elements.\n\nfieldset {\n padding: 0;\n margin: 0;\n border: 0;\n // Chrome and Firefox set a `min-width: min-content;` on fieldsets,\n // so we reset that to ensure it behaves more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359.\n min-width: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: @line-height-computed;\n font-size: (@font-size-base * 1.5);\n line-height: inherit;\n color: @legend-color;\n border: 0;\n border-bottom: 1px solid @legend-border-color;\n}\n\nlabel {\n display: inline-block;\n max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)\n margin-bottom: 5px;\n font-weight: bold;\n}\n\n\n// Normalize form controls\n//\n// While most of our form styles require extra classes, some basic normalization\n// is required to ensure optimum display with or without those classes to better\n// address browser inconsistencies.\n\n// Override content-box in Normalize (* isn't specific enough)\ninput[type=\"search\"] {\n .box-sizing(border-box);\n}\n\n// Position radios and checkboxes better\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9; // IE8-9\n line-height: normal;\n}\n\ninput[type=\"file\"] {\n display: block;\n}\n\n// Make range inputs behave like textual form controls\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\n\n// Make multiple select elements height not fixed\nselect[multiple],\nselect[size] {\n height: auto;\n}\n\n// Focus for file, radio, and checkbox\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n .tab-focus();\n}\n\n// Adjust output element\noutput {\n display: block;\n padding-top: (@padding-base-vertical + 1);\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @input-color;\n}\n\n\n// Common form controls\n//\n// Shared size and type resets for form controls. Apply `.form-control` to any\n// of the following form controls:\n//\n// select\n// textarea\n// input[type=\"text\"]\n// input[type=\"password\"]\n// input[type=\"datetime\"]\n// input[type=\"datetime-local\"]\n// input[type=\"date\"]\n// input[type=\"month\"]\n// input[type=\"time\"]\n// input[type=\"week\"]\n// input[type=\"number\"]\n// input[type=\"email\"]\n// input[type=\"url\"]\n// input[type=\"search\"]\n// input[type=\"tel\"]\n// input[type=\"color\"]\n\n.form-control {\n display: block;\n width: 100%;\n height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)\n padding: @padding-base-vertical @padding-base-horizontal;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @input-color;\n background-color: @input-bg;\n background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n border: 1px solid @input-border;\n border-radius: @input-border-radius; // Note: This has no effect on s in CSS.\n .box-shadow(inset 0 1px 1px rgba(0,0,0,.075));\n .transition(~\"border-color ease-in-out .15s, box-shadow ease-in-out .15s\");\n\n // Customize the `:focus` state to imitate native WebKit styles.\n .form-control-focus();\n\n // Placeholder\n .placeholder();\n\n // Unstyle the caret on ``\n// element gets special love because it's special, and that's a fact!\n.input-size(@input-height; @padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n height: @input-height;\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n line-height: @line-height;\n border-radius: @border-radius;\n\n select& {\n height: @input-height;\n line-height: @input-height;\n }\n\n textarea&,\n select[multiple]& {\n height: auto;\n }\n}\n","//\n// Buttons\n// --------------------------------------------------\n\n\n// Base styles\n// --------------------------------------------------\n\n.btn {\n display: inline-block;\n margin-bottom: 0; // For input.btn\n font-weight: @btn-font-weight;\n text-align: center;\n vertical-align: middle;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n border: 1px solid transparent;\n white-space: nowrap;\n .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @btn-border-radius-base);\n .user-select(none);\n\n &,\n &:active,\n &.active {\n &:focus,\n &.focus {\n .tab-focus();\n }\n }\n\n &:hover,\n &:focus,\n &.focus {\n color: @btn-default-color;\n text-decoration: none;\n }\n\n &:active,\n &.active {\n outline: 0;\n background-image: none;\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n cursor: @cursor-disabled;\n .opacity(.65);\n .box-shadow(none);\n }\n\n a& {\n &.disabled,\n fieldset[disabled] & {\n pointer-events: none; // Future-proof disabling of clicks on `` elements\n }\n }\n}\n\n\n// Alternate buttons\n// --------------------------------------------------\n\n.btn-default {\n .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border);\n}\n.btn-primary {\n .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border);\n}\n// Success appears as green\n.btn-success {\n .button-variant(@btn-success-color; @btn-success-bg; @btn-success-border);\n}\n// Info appears as blue-green\n.btn-info {\n .button-variant(@btn-info-color; @btn-info-bg; @btn-info-border);\n}\n// Warning appears as orange\n.btn-warning {\n .button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border);\n}\n// Danger and error appear as red\n.btn-danger {\n .button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border);\n}\n\n\n// Link buttons\n// -------------------------\n\n// Make a button look and behave like a link\n.btn-link {\n color: @link-color;\n font-weight: normal;\n border-radius: 0;\n\n &,\n &:active,\n &.active,\n &[disabled],\n fieldset[disabled] & {\n background-color: transparent;\n .box-shadow(none);\n }\n &,\n &:hover,\n &:focus,\n &:active {\n border-color: transparent;\n }\n &:hover,\n &:focus {\n color: @link-hover-color;\n text-decoration: @link-hover-decoration;\n background-color: transparent;\n }\n &[disabled],\n fieldset[disabled] & {\n &:hover,\n &:focus {\n color: @btn-link-disabled-color;\n text-decoration: none;\n }\n }\n}\n\n\n// Button Sizes\n// --------------------------------------------------\n\n.btn-lg {\n // line-height: ensure even-numbered height of button next to large input\n .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @btn-border-radius-large);\n}\n.btn-sm {\n // line-height: ensure proper height of button next to small input\n .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @btn-border-radius-small);\n}\n.btn-xs {\n .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @btn-border-radius-small);\n}\n\n\n// Block button\n// --------------------------------------------------\n\n.btn-block {\n display: block;\n width: 100%;\n}\n\n// Vertically space out multiple block buttons\n.btn-block + .btn-block {\n margin-top: 5px;\n}\n\n// Specificity overrides\ninput[type=\"submit\"],\ninput[type=\"reset\"],\ninput[type=\"button\"] {\n &.btn-block {\n width: 100%;\n }\n}\n","// Button variants\n//\n// Easily pump out default styles, as well as :hover, :focus, :active,\n// and disabled options for all buttons\n\n.button-variant(@color; @background; @border) {\n color: @color;\n background-color: @background;\n border-color: @border;\n\n &:focus,\n &.focus {\n color: @color;\n background-color: darken(@background, 10%);\n border-color: darken(@border, 25%);\n }\n &:hover {\n color: @color;\n background-color: darken(@background, 10%);\n border-color: darken(@border, 12%);\n }\n &:active,\n &.active,\n .open > .dropdown-toggle& {\n color: @color;\n background-color: darken(@background, 10%);\n border-color: darken(@border, 12%);\n\n &:hover,\n &:focus,\n &.focus {\n color: @color;\n background-color: darken(@background, 17%);\n border-color: darken(@border, 25%);\n }\n }\n &:active,\n &.active,\n .open > .dropdown-toggle& {\n background-image: none;\n }\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n &:hover,\n &:focus,\n &.focus {\n background-color: @background;\n border-color: @border;\n }\n }\n\n .badge {\n color: @background;\n background-color: @color;\n }\n}\n\n// Button sizes\n.button-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n line-height: @line-height;\n border-radius: @border-radius;\n}\n","// Opacity\n\n.opacity(@opacity) {\n opacity: @opacity;\n // IE8 filter\n @opacity-ie: (@opacity * 100);\n filter: ~\"alpha(opacity=@{opacity-ie})\";\n}\n","//\n// Component animations\n// --------------------------------------------------\n\n// Heads up!\n//\n// We don't use the `.opacity()` mixin here since it causes a bug with text\n// fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552.\n\n.fade {\n opacity: 0;\n .transition(opacity .15s linear);\n &.in {\n opacity: 1;\n }\n}\n\n.collapse {\n display: none;\n\n &.in { display: block; }\n tr&.in { display: table-row; }\n tbody&.in { display: table-row-group; }\n}\n\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n .transition-property(~\"height, visibility\");\n .transition-duration(.35s);\n .transition-timing-function(ease);\n}\n","//\n// Dropdown menus\n// --------------------------------------------------\n\n\n// Dropdown arrow/caret\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: @caret-width-base dashed;\n border-top: @caret-width-base solid ~\"\\9\"; // IE8\n border-right: @caret-width-base solid transparent;\n border-left: @caret-width-base solid transparent;\n}\n\n// The dropdown wrapper (div)\n.dropup,\n.dropdown {\n position: relative;\n}\n\n// Prevent the focus on the dropdown toggle when closing dropdowns\n.dropdown-toggle:focus {\n outline: 0;\n}\n\n// The dropdown menu (ul)\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: @zindex-dropdown;\n display: none; // none by default, but block on \"open\" of the menu\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0; // override default ul\n list-style: none;\n font-size: @font-size-base;\n text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer)\n background-color: @dropdown-bg;\n border: 1px solid @dropdown-fallback-border; // IE8 fallback\n border: 1px solid @dropdown-border;\n border-radius: @border-radius-base;\n .box-shadow(0 6px 12px rgba(0,0,0,.175));\n background-clip: padding-box;\n\n // Aligns the dropdown menu to right\n //\n // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]`\n &.pull-right {\n right: 0;\n left: auto;\n }\n\n // Dividers (basically an hr) within the dropdown\n .divider {\n .nav-divider(@dropdown-divider-bg);\n }\n\n // Links within the dropdown menu\n > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: normal;\n line-height: @line-height-base;\n color: @dropdown-link-color;\n white-space: nowrap; // prevent links from randomly breaking onto new lines\n }\n}\n\n// Hover/Focus state\n.dropdown-menu > li > a {\n &:hover,\n &:focus {\n text-decoration: none;\n color: @dropdown-link-hover-color;\n background-color: @dropdown-link-hover-bg;\n }\n}\n\n// Active state\n.dropdown-menu > .active > a {\n &,\n &:hover,\n &:focus {\n color: @dropdown-link-active-color;\n text-decoration: none;\n outline: 0;\n background-color: @dropdown-link-active-bg;\n }\n}\n\n// Disabled state\n//\n// Gray out text and ensure the hover/focus state remains gray\n\n.dropdown-menu > .disabled > a {\n &,\n &:hover,\n &:focus {\n color: @dropdown-link-disabled-color;\n }\n\n // Nuke hover/focus effects\n &:hover,\n &:focus {\n text-decoration: none;\n background-color: transparent;\n background-image: none; // Remove CSS gradient\n .reset-filter();\n cursor: @cursor-disabled;\n }\n}\n\n// Open state for the dropdown\n.open {\n // Show the menu\n > .dropdown-menu {\n display: block;\n }\n\n // Remove the outline when :focus is triggered\n > a {\n outline: 0;\n }\n}\n\n// Menu positioning\n//\n// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown\n// menu with the parent.\n.dropdown-menu-right {\n left: auto; // Reset the default from `.dropdown-menu`\n right: 0;\n}\n// With v3, we enabled auto-flipping if you have a dropdown within a right\n// aligned nav component. To enable the undoing of that, we provide an override\n// to restore the default dropdown menu alignment.\n//\n// This is only for left-aligning a dropdown menu within a `.navbar-right` or\n// `.pull-right` nav component.\n.dropdown-menu-left {\n left: 0;\n right: auto;\n}\n\n// Dropdown section headers\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: @font-size-small;\n line-height: @line-height-base;\n color: @dropdown-header-color;\n white-space: nowrap; // as with > li > a\n}\n\n// Backdrop to catch body clicks on mobile, etc.\n.dropdown-backdrop {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n top: 0;\n z-index: (@zindex-dropdown - 10);\n}\n\n// Right aligned dropdowns\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n\n// Allow for dropdowns to go bottom up (aka, dropup-menu)\n//\n// Just add .dropup after the standard .dropdown class and you're set, bro.\n// TODO: abstract this so that the navbar fixed styles are not placed here?\n\n.dropup,\n.navbar-fixed-bottom .dropdown {\n // Reverse the caret\n .caret {\n border-top: 0;\n border-bottom: @caret-width-base dashed;\n border-bottom: @caret-width-base solid ~\"\\9\"; // IE8\n content: \"\";\n }\n // Different positioning for bottom up menu\n .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 2px;\n }\n}\n\n\n// Component alignment\n//\n// Reiterate per navbar.less and the modified component alignment there.\n\n@media (min-width: @grid-float-breakpoint) {\n .navbar-right {\n .dropdown-menu {\n .dropdown-menu-right();\n }\n // Necessary for overrides of the default right aligned menu.\n // Will remove come v4 in all likelihood.\n .dropdown-menu-left {\n .dropdown-menu-left();\n }\n }\n}\n","// Horizontal dividers\n//\n// Dividers (basically an hr) within dropdowns and nav lists\n\n.nav-divider(@color: #e5e5e5) {\n height: 1px;\n margin: ((@line-height-computed / 2) - 1) 0;\n overflow: hidden;\n background-color: @color;\n}\n","// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n","//\n// Button groups\n// --------------------------------------------------\n\n// Make the div behave like a button\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle; // match .btn alignment given font-size hack above\n > .btn {\n position: relative;\n float: left;\n // Bring the \"active\" button to the front\n &:hover,\n &:focus,\n &:active,\n &.active {\n z-index: 2;\n }\n }\n}\n\n// Prevent double borders when buttons are next to each other\n.btn-group {\n .btn + .btn,\n .btn + .btn-group,\n .btn-group + .btn,\n .btn-group + .btn-group {\n margin-left: -1px;\n }\n}\n\n// Optional: Group multiple button groups together for a toolbar\n.btn-toolbar {\n margin-left: -5px; // Offset the first child's margin\n &:extend(.clearfix all);\n\n .btn,\n .btn-group,\n .input-group {\n float: left;\n }\n > .btn,\n > .btn-group,\n > .input-group {\n margin-left: 5px;\n }\n}\n\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n\n// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match\n.btn-group > .btn:first-child {\n margin-left: 0;\n &:not(:last-child):not(.dropdown-toggle) {\n .border-right-radius(0);\n }\n}\n// Need .dropdown-toggle since :last-child doesn't apply, given that a .dropdown-menu is used immediately after it\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n .border-left-radius(0);\n}\n\n// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group)\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) {\n > .btn:last-child,\n > .dropdown-toggle {\n .border-right-radius(0);\n }\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n .border-left-radius(0);\n}\n\n// On active and open, don't show outline\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n\n\n// Sizing\n//\n// Remix the default button sizing classes into new ones for easier manipulation.\n\n.btn-group-xs > .btn { &:extend(.btn-xs); }\n.btn-group-sm > .btn { &:extend(.btn-sm); }\n.btn-group-lg > .btn { &:extend(.btn-lg); }\n\n\n// Split button dropdowns\n// ----------------------\n\n// Give the line between buttons some depth\n.btn-group > .btn + .dropdown-toggle {\n padding-left: 8px;\n padding-right: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-left: 12px;\n padding-right: 12px;\n}\n\n// The clickable button for toggling the menu\n// Remove the gradient and set the same inset shadow as the :active state\n.btn-group.open .dropdown-toggle {\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n\n // Show no shadow for `.btn-link` since it has no other button styles.\n &.btn-link {\n .box-shadow(none);\n }\n}\n\n\n// Reposition the caret\n.btn .caret {\n margin-left: 0;\n}\n// Carets in other button sizes\n.btn-lg .caret {\n border-width: @caret-width-large @caret-width-large 0;\n border-bottom-width: 0;\n}\n// Upside down carets for .dropup\n.dropup .btn-lg .caret {\n border-width: 0 @caret-width-large @caret-width-large;\n}\n\n\n// Vertical button groups\n// ----------------------\n\n.btn-group-vertical {\n > .btn,\n > .btn-group,\n > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n }\n\n // Clear floats so dropdown menus can be properly placed\n > .btn-group {\n &:extend(.clearfix all);\n > .btn {\n float: none;\n }\n }\n\n > .btn + .btn,\n > .btn + .btn-group,\n > .btn-group + .btn,\n > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n }\n}\n\n.btn-group-vertical > .btn {\n &:not(:first-child):not(:last-child) {\n border-radius: 0;\n }\n &:first-child:not(:last-child) {\n .border-top-radius(@btn-border-radius-base);\n .border-bottom-radius(0);\n }\n &:last-child:not(:first-child) {\n .border-top-radius(0);\n .border-bottom-radius(@btn-border-radius-base);\n }\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) {\n > .btn:last-child,\n > .dropdown-toggle {\n .border-bottom-radius(0);\n }\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n .border-top-radius(0);\n}\n\n\n// Justified button groups\n// ----------------------\n\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n > .btn,\n > .btn-group {\n float: none;\n display: table-cell;\n width: 1%;\n }\n > .btn-group .btn {\n width: 100%;\n }\n\n > .btn-group .dropdown-menu {\n left: auto;\n }\n}\n\n\n// Checkbox and radio options\n//\n// In order to support the browser's form validation feedback, powered by the\n// `required` attribute, we have to \"hide\" the inputs via `clip`. We cannot use\n// `display: none;` or `visibility: hidden;` as that also hides the popover.\n// Simply visually hiding the inputs via `opacity` would leave them clickable in\n// certain cases which is prevented by using `clip` and `pointer-events`.\n// This way, we ensure a DOM element is visible to position the popover from.\n//\n// See https://github.com/twbs/bootstrap/pull/12794 and\n// https://github.com/twbs/bootstrap/pull/14559 for more information.\n\n[data-toggle=\"buttons\"] {\n > .btn,\n > .btn-group > .btn {\n input[type=\"radio\"],\n input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0,0,0,0);\n pointer-events: none;\n }\n }\n}\n","// Single side border-radius\n\n.border-top-radius(@radius) {\n border-top-right-radius: @radius;\n border-top-left-radius: @radius;\n}\n.border-right-radius(@radius) {\n border-bottom-right-radius: @radius;\n border-top-right-radius: @radius;\n}\n.border-bottom-radius(@radius) {\n border-bottom-right-radius: @radius;\n border-bottom-left-radius: @radius;\n}\n.border-left-radius(@radius) {\n border-bottom-left-radius: @radius;\n border-top-left-radius: @radius;\n}\n","//\n// Input groups\n// --------------------------------------------------\n\n// Base styles\n// -------------------------\n.input-group {\n position: relative; // For dropdowns\n display: table;\n border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table\n\n // Undo padding and float of grid classes\n &[class*=\"col-\"] {\n float: none;\n padding-left: 0;\n padding-right: 0;\n }\n\n .form-control {\n // Ensure that the input is always above the *appended* addon button for\n // proper border colors.\n position: relative;\n z-index: 2;\n\n // IE9 fubars the placeholder attribute in text inputs and the arrows on\n // select elements in input groups. To fix it, we float the input. Details:\n // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855\n float: left;\n\n width: 100%;\n margin-bottom: 0;\n\n &:focus {\n z-index: 3;\n }\n }\n}\n\n// Sizing options\n//\n// Remix the default form control sizing classes into new ones for easier\n// manipulation.\n\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n .input-lg();\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n .input-sm();\n}\n\n\n// Display as table-cell\n// -------------------------\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n\n &:not(:first-child):not(:last-child) {\n border-radius: 0;\n }\n}\n// Addon and addon wrapper for buttons\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle; // Match the inputs\n}\n\n// Text input groups\n// -------------------------\n.input-group-addon {\n padding: @padding-base-vertical @padding-base-horizontal;\n font-size: @font-size-base;\n font-weight: normal;\n line-height: 1;\n color: @input-color;\n text-align: center;\n background-color: @input-group-addon-bg;\n border: 1px solid @input-group-addon-border-color;\n border-radius: @input-border-radius;\n\n // Sizing\n &.input-sm {\n padding: @padding-small-vertical @padding-small-horizontal;\n font-size: @font-size-small;\n border-radius: @input-border-radius-small;\n }\n &.input-lg {\n padding: @padding-large-vertical @padding-large-horizontal;\n font-size: @font-size-large;\n border-radius: @input-border-radius-large;\n }\n\n // Nuke default margins from checkboxes and radios to vertically center within.\n input[type=\"radio\"],\n input[type=\"checkbox\"] {\n margin-top: 0;\n }\n}\n\n// Reset rounded corners\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n .border-right-radius(0);\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n .border-left-radius(0);\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n\n// Button input groups\n// -------------------------\n.input-group-btn {\n position: relative;\n // Jankily prevent input button groups from wrapping with `white-space` and\n // `font-size` in combination with `inline-block` on buttons.\n font-size: 0;\n white-space: nowrap;\n\n // Negative margin for spacing, position for bringing hovered/focused/actived\n // element above the siblings.\n > .btn {\n position: relative;\n + .btn {\n margin-left: -1px;\n }\n // Bring the \"active\" button to the front\n &:hover,\n &:focus,\n &:active {\n z-index: 2;\n }\n }\n\n // Negative margin to only have a 1px border between the two\n &:first-child {\n > .btn,\n > .btn-group {\n margin-right: -1px;\n }\n }\n &:last-child {\n > .btn,\n > .btn-group {\n z-index: 2;\n margin-left: -1px;\n }\n }\n}\n","//\n// Navs\n// --------------------------------------------------\n\n\n// Base class\n// --------------------------------------------------\n\n.nav {\n margin-bottom: 0;\n padding-left: 0; // Override default ul/ol\n list-style: none;\n &:extend(.clearfix all);\n\n > li {\n position: relative;\n display: block;\n\n > a {\n position: relative;\n display: block;\n padding: @nav-link-padding;\n &:hover,\n &:focus {\n text-decoration: none;\n background-color: @nav-link-hover-bg;\n }\n }\n\n // Disabled state sets text to gray and nukes hover/tab effects\n &.disabled > a {\n color: @nav-disabled-link-color;\n\n &:hover,\n &:focus {\n color: @nav-disabled-link-hover-color;\n text-decoration: none;\n background-color: transparent;\n cursor: @cursor-disabled;\n }\n }\n }\n\n // Open dropdowns\n .open > a {\n &,\n &:hover,\n &:focus {\n background-color: @nav-link-hover-bg;\n border-color: @link-color;\n }\n }\n\n // Nav dividers (deprecated with v3.0.1)\n //\n // This should have been removed in v3 with the dropping of `.nav-list`, but\n // we missed it. We don't currently support this anywhere, but in the interest\n // of maintaining backward compatibility in case you use it, it's deprecated.\n .nav-divider {\n .nav-divider();\n }\n\n // Prevent IE8 from misplacing imgs\n //\n // See https://github.com/h5bp/html5-boilerplate/issues/984#issuecomment-3985989\n > li > a > img {\n max-width: none;\n }\n}\n\n\n// Tabs\n// -------------------------\n\n// Give the tabs something to sit on\n.nav-tabs {\n border-bottom: 1px solid @nav-tabs-border-color;\n > li {\n float: left;\n // Make the list-items overlay the bottom border\n margin-bottom: -1px;\n\n // Actual tabs (as links)\n > a {\n margin-right: 2px;\n line-height: @line-height-base;\n border: 1px solid transparent;\n border-radius: @border-radius-base @border-radius-base 0 0;\n &:hover {\n border-color: @nav-tabs-link-hover-border-color @nav-tabs-link-hover-border-color @nav-tabs-border-color;\n }\n }\n\n // Active state, and its :hover to override normal :hover\n &.active > a {\n &,\n &:hover,\n &:focus {\n color: @nav-tabs-active-link-hover-color;\n background-color: @nav-tabs-active-link-hover-bg;\n border: 1px solid @nav-tabs-active-link-hover-border-color;\n border-bottom-color: transparent;\n cursor: default;\n }\n }\n }\n // pulling this in mainly for less shorthand\n &.nav-justified {\n .nav-justified();\n .nav-tabs-justified();\n }\n}\n\n\n// Pills\n// -------------------------\n.nav-pills {\n > li {\n float: left;\n\n // Links rendered as pills\n > a {\n border-radius: @nav-pills-border-radius;\n }\n + li {\n margin-left: 2px;\n }\n\n // Active state\n &.active > a {\n &,\n &:hover,\n &:focus {\n color: @nav-pills-active-link-hover-color;\n background-color: @nav-pills-active-link-hover-bg;\n }\n }\n }\n}\n\n\n// Stacked pills\n.nav-stacked {\n > li {\n float: none;\n + li {\n margin-top: 2px;\n margin-left: 0; // no need for this gap between nav items\n }\n }\n}\n\n\n// Nav variations\n// --------------------------------------------------\n\n// Justified nav links\n// -------------------------\n\n.nav-justified {\n width: 100%;\n\n > li {\n float: none;\n > a {\n text-align: center;\n margin-bottom: 5px;\n }\n }\n\n > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n }\n\n @media (min-width: @screen-sm-min) {\n > li {\n display: table-cell;\n width: 1%;\n > a {\n margin-bottom: 0;\n }\n }\n }\n}\n\n// Move borders to anchors instead of bottom of list\n//\n// Mixin for adding on top the shared `.nav-justified` styles for our tabs\n.nav-tabs-justified {\n border-bottom: 0;\n\n > li > a {\n // Override margin from .nav-tabs\n margin-right: 0;\n border-radius: @border-radius-base;\n }\n\n > .active > a,\n > .active > a:hover,\n > .active > a:focus {\n border: 1px solid @nav-tabs-justified-link-border-color;\n }\n\n @media (min-width: @screen-sm-min) {\n > li > a {\n border-bottom: 1px solid @nav-tabs-justified-link-border-color;\n border-radius: @border-radius-base @border-radius-base 0 0;\n }\n > .active > a,\n > .active > a:hover,\n > .active > a:focus {\n border-bottom-color: @nav-tabs-justified-active-link-border-color;\n }\n }\n}\n\n\n// Tabbable tabs\n// -------------------------\n\n// Hide tabbable panes to start, show them when `.active`\n.tab-content {\n > .tab-pane {\n display: none;\n }\n > .active {\n display: block;\n }\n}\n\n\n// Dropdowns\n// -------------------------\n\n// Specific dropdowns\n.nav-tabs .dropdown-menu {\n // make dropdown border overlap tab border\n margin-top: -1px;\n // Remove the top rounded corners here since there is a hard edge above the menu\n .border-top-radius(0);\n}\n","//\n// Navbars\n// --------------------------------------------------\n\n\n// Wrapper and base class\n//\n// Provide a static navbar from which we expand to create full-width, fixed, and\n// other navbar variations.\n\n.navbar {\n position: relative;\n min-height: @navbar-height; // Ensure a navbar always shows (e.g., without a .navbar-brand in collapsed mode)\n margin-bottom: @navbar-margin-bottom;\n border: 1px solid transparent;\n\n // Prevent floats from breaking the navbar\n &:extend(.clearfix all);\n\n @media (min-width: @grid-float-breakpoint) {\n border-radius: @navbar-border-radius;\n }\n}\n\n\n// Navbar heading\n//\n// Groups `.navbar-brand` and `.navbar-toggle` into a single component for easy\n// styling of responsive aspects.\n\n.navbar-header {\n &:extend(.clearfix all);\n\n @media (min-width: @grid-float-breakpoint) {\n float: left;\n }\n}\n\n\n// Navbar collapse (body)\n//\n// Group your navbar content into this for easy collapsing and expanding across\n// various device sizes. By default, this content is collapsed when <768px, but\n// will expand past that for a horizontal display.\n//\n// To start (on mobile devices) the navbar links, forms, and buttons are stacked\n// vertically and include a `max-height` to overflow in case you have too much\n// content for the user's viewport.\n\n.navbar-collapse {\n overflow-x: visible;\n padding-right: @navbar-padding-horizontal;\n padding-left: @navbar-padding-horizontal;\n border-top: 1px solid transparent;\n box-shadow: inset 0 1px 0 rgba(255,255,255,.1);\n &:extend(.clearfix all);\n -webkit-overflow-scrolling: touch;\n\n &.in {\n overflow-y: auto;\n }\n\n @media (min-width: @grid-float-breakpoint) {\n width: auto;\n border-top: 0;\n box-shadow: none;\n\n &.collapse {\n display: block !important;\n height: auto !important;\n padding-bottom: 0; // Override default setting\n overflow: visible !important;\n }\n\n &.in {\n overflow-y: visible;\n }\n\n // Undo the collapse side padding for navbars with containers to ensure\n // alignment of right-aligned contents.\n .navbar-fixed-top &,\n .navbar-static-top &,\n .navbar-fixed-bottom & {\n padding-left: 0;\n padding-right: 0;\n }\n }\n}\n\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n .navbar-collapse {\n max-height: @navbar-collapse-max-height;\n\n @media (max-device-width: @screen-xs-min) and (orientation: landscape) {\n max-height: 200px;\n }\n }\n}\n\n\n// Both navbar header and collapse\n//\n// When a container is present, change the behavior of the header and collapse.\n\n.container,\n.container-fluid {\n > .navbar-header,\n > .navbar-collapse {\n margin-right: -@navbar-padding-horizontal;\n margin-left: -@navbar-padding-horizontal;\n\n @media (min-width: @grid-float-breakpoint) {\n margin-right: 0;\n margin-left: 0;\n }\n }\n}\n\n\n//\n// Navbar alignment options\n//\n// Display the navbar across the entirety of the page or fixed it to the top or\n// bottom of the page.\n\n// Static top (unfixed, but 100% wide) navbar\n.navbar-static-top {\n z-index: @zindex-navbar;\n border-width: 0 0 1px;\n\n @media (min-width: @grid-float-breakpoint) {\n border-radius: 0;\n }\n}\n\n// Fix the top/bottom navbars when screen real estate supports it\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: @zindex-navbar-fixed;\n\n // Undo the rounded corners\n @media (min-width: @grid-float-breakpoint) {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0; // override .navbar defaults\n border-width: 1px 0 0;\n}\n\n\n// Brand/project name\n\n.navbar-brand {\n float: left;\n padding: @navbar-padding-vertical @navbar-padding-horizontal;\n font-size: @font-size-large;\n line-height: @line-height-computed;\n height: @navbar-height;\n\n &:hover,\n &:focus {\n text-decoration: none;\n }\n\n > img {\n display: block;\n }\n\n @media (min-width: @grid-float-breakpoint) {\n .navbar > .container &,\n .navbar > .container-fluid & {\n margin-left: -@navbar-padding-horizontal;\n }\n }\n}\n\n\n// Navbar toggle\n//\n// Custom button for toggling the `.navbar-collapse`, powered by the collapse\n// JavaScript plugin.\n\n.navbar-toggle {\n position: relative;\n float: right;\n margin-right: @navbar-padding-horizontal;\n padding: 9px 10px;\n .navbar-vertical-align(34px);\n background-color: transparent;\n background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n border: 1px solid transparent;\n border-radius: @border-radius-base;\n\n // We remove the `outline` here, but later compensate by attaching `:hover`\n // styles to `:focus`.\n &:focus {\n outline: 0;\n }\n\n // Bars\n .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n }\n .icon-bar + .icon-bar {\n margin-top: 4px;\n }\n\n @media (min-width: @grid-float-breakpoint) {\n display: none;\n }\n}\n\n\n// Navbar nav links\n//\n// Builds on top of the `.nav` components with its own modifier class to make\n// the nav the full height of the horizontal nav (above 768px).\n\n.navbar-nav {\n margin: (@navbar-padding-vertical / 2) -@navbar-padding-horizontal;\n\n > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: @line-height-computed;\n }\n\n @media (max-width: @grid-float-breakpoint-max) {\n // Dropdowns get custom display when collapsed\n .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n box-shadow: none;\n > li > a,\n .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n > li > a {\n line-height: @line-height-computed;\n &:hover,\n &:focus {\n background-image: none;\n }\n }\n }\n }\n\n // Uncollapse the nav\n @media (min-width: @grid-float-breakpoint) {\n float: left;\n margin: 0;\n\n > li {\n float: left;\n > a {\n padding-top: @navbar-padding-vertical;\n padding-bottom: @navbar-padding-vertical;\n }\n }\n }\n}\n\n\n// Navbar form\n//\n// Extension of the `.form-inline` with some extra flavor for optimum display in\n// our navbars.\n\n.navbar-form {\n margin-left: -@navbar-padding-horizontal;\n margin-right: -@navbar-padding-horizontal;\n padding: 10px @navbar-padding-horizontal;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n @shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);\n .box-shadow(@shadow);\n\n // Mixin behavior for optimum display\n .form-inline();\n\n .form-group {\n @media (max-width: @grid-float-breakpoint-max) {\n margin-bottom: 5px;\n\n &:last-child {\n margin-bottom: 0;\n }\n }\n }\n\n // Vertically center in expanded, horizontal navbar\n .navbar-vertical-align(@input-height-base);\n\n // Undo 100% width for pull classes\n @media (min-width: @grid-float-breakpoint) {\n width: auto;\n border: 0;\n margin-left: 0;\n margin-right: 0;\n padding-top: 0;\n padding-bottom: 0;\n .box-shadow(none);\n }\n}\n\n\n// Dropdown menus\n\n// Menu position and menu carets\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n .border-top-radius(0);\n}\n// Menu position and menu caret support for dropups via extra dropup class\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n margin-bottom: 0;\n .border-top-radius(@navbar-border-radius);\n .border-bottom-radius(0);\n}\n\n\n// Buttons in navbars\n//\n// Vertically center a button within a navbar (when *not* in a form).\n\n.navbar-btn {\n .navbar-vertical-align(@input-height-base);\n\n &.btn-sm {\n .navbar-vertical-align(@input-height-small);\n }\n &.btn-xs {\n .navbar-vertical-align(22);\n }\n}\n\n\n// Text in navbars\n//\n// Add a class to make any element properly align itself vertically within the navbars.\n\n.navbar-text {\n .navbar-vertical-align(@line-height-computed);\n\n @media (min-width: @grid-float-breakpoint) {\n float: left;\n margin-left: @navbar-padding-horizontal;\n margin-right: @navbar-padding-horizontal;\n }\n}\n\n\n// Component alignment\n//\n// Repurpose the pull utilities as their own navbar utilities to avoid specificity\n// issues with parents and chaining. Only do this when the navbar is uncollapsed\n// though so that navbar contents properly stack and align in mobile.\n//\n// Declared after the navbar components to ensure more specificity on the margins.\n\n@media (min-width: @grid-float-breakpoint) {\n .navbar-left { .pull-left(); }\n .navbar-right {\n .pull-right();\n margin-right: -@navbar-padding-horizontal;\n\n ~ .navbar-right {\n margin-right: 0;\n }\n }\n}\n\n\n// Alternate navbars\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n background-color: @navbar-default-bg;\n border-color: @navbar-default-border;\n\n .navbar-brand {\n color: @navbar-default-brand-color;\n &:hover,\n &:focus {\n color: @navbar-default-brand-hover-color;\n background-color: @navbar-default-brand-hover-bg;\n }\n }\n\n .navbar-text {\n color: @navbar-default-color;\n }\n\n .navbar-nav {\n > li > a {\n color: @navbar-default-link-color;\n\n &:hover,\n &:focus {\n color: @navbar-default-link-hover-color;\n background-color: @navbar-default-link-hover-bg;\n }\n }\n > .active > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-default-link-active-color;\n background-color: @navbar-default-link-active-bg;\n }\n }\n > .disabled > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-default-link-disabled-color;\n background-color: @navbar-default-link-disabled-bg;\n }\n }\n }\n\n .navbar-toggle {\n border-color: @navbar-default-toggle-border-color;\n &:hover,\n &:focus {\n background-color: @navbar-default-toggle-hover-bg;\n }\n .icon-bar {\n background-color: @navbar-default-toggle-icon-bar-bg;\n }\n }\n\n .navbar-collapse,\n .navbar-form {\n border-color: @navbar-default-border;\n }\n\n // Dropdown menu items\n .navbar-nav {\n // Remove background color from open dropdown\n > .open > a {\n &,\n &:hover,\n &:focus {\n background-color: @navbar-default-link-active-bg;\n color: @navbar-default-link-active-color;\n }\n }\n\n @media (max-width: @grid-float-breakpoint-max) {\n // Dropdowns get custom display when collapsed\n .open .dropdown-menu {\n > li > a {\n color: @navbar-default-link-color;\n &:hover,\n &:focus {\n color: @navbar-default-link-hover-color;\n background-color: @navbar-default-link-hover-bg;\n }\n }\n > .active > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-default-link-active-color;\n background-color: @navbar-default-link-active-bg;\n }\n }\n > .disabled > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-default-link-disabled-color;\n background-color: @navbar-default-link-disabled-bg;\n }\n }\n }\n }\n }\n\n\n // Links in navbars\n //\n // Add a class to ensure links outside the navbar nav are colored correctly.\n\n .navbar-link {\n color: @navbar-default-link-color;\n &:hover {\n color: @navbar-default-link-hover-color;\n }\n }\n\n .btn-link {\n color: @navbar-default-link-color;\n &:hover,\n &:focus {\n color: @navbar-default-link-hover-color;\n }\n &[disabled],\n fieldset[disabled] & {\n &:hover,\n &:focus {\n color: @navbar-default-link-disabled-color;\n }\n }\n }\n}\n\n// Inverse navbar\n\n.navbar-inverse {\n background-color: @navbar-inverse-bg;\n border-color: @navbar-inverse-border;\n\n .navbar-brand {\n color: @navbar-inverse-brand-color;\n &:hover,\n &:focus {\n color: @navbar-inverse-brand-hover-color;\n background-color: @navbar-inverse-brand-hover-bg;\n }\n }\n\n .navbar-text {\n color: @navbar-inverse-color;\n }\n\n .navbar-nav {\n > li > a {\n color: @navbar-inverse-link-color;\n\n &:hover,\n &:focus {\n color: @navbar-inverse-link-hover-color;\n background-color: @navbar-inverse-link-hover-bg;\n }\n }\n > .active > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-inverse-link-active-color;\n background-color: @navbar-inverse-link-active-bg;\n }\n }\n > .disabled > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-inverse-link-disabled-color;\n background-color: @navbar-inverse-link-disabled-bg;\n }\n }\n }\n\n // Darken the responsive nav toggle\n .navbar-toggle {\n border-color: @navbar-inverse-toggle-border-color;\n &:hover,\n &:focus {\n background-color: @navbar-inverse-toggle-hover-bg;\n }\n .icon-bar {\n background-color: @navbar-inverse-toggle-icon-bar-bg;\n }\n }\n\n .navbar-collapse,\n .navbar-form {\n border-color: darken(@navbar-inverse-bg, 7%);\n }\n\n // Dropdowns\n .navbar-nav {\n > .open > a {\n &,\n &:hover,\n &:focus {\n background-color: @navbar-inverse-link-active-bg;\n color: @navbar-inverse-link-active-color;\n }\n }\n\n @media (max-width: @grid-float-breakpoint-max) {\n // Dropdowns get custom display\n .open .dropdown-menu {\n > .dropdown-header {\n border-color: @navbar-inverse-border;\n }\n .divider {\n background-color: @navbar-inverse-border;\n }\n > li > a {\n color: @navbar-inverse-link-color;\n &:hover,\n &:focus {\n color: @navbar-inverse-link-hover-color;\n background-color: @navbar-inverse-link-hover-bg;\n }\n }\n > .active > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-inverse-link-active-color;\n background-color: @navbar-inverse-link-active-bg;\n }\n }\n > .disabled > a {\n &,\n &:hover,\n &:focus {\n color: @navbar-inverse-link-disabled-color;\n background-color: @navbar-inverse-link-disabled-bg;\n }\n }\n }\n }\n }\n\n .navbar-link {\n color: @navbar-inverse-link-color;\n &:hover {\n color: @navbar-inverse-link-hover-color;\n }\n }\n\n .btn-link {\n color: @navbar-inverse-link-color;\n &:hover,\n &:focus {\n color: @navbar-inverse-link-hover-color;\n }\n &[disabled],\n fieldset[disabled] & {\n &:hover,\n &:focus {\n color: @navbar-inverse-link-disabled-color;\n }\n }\n }\n}\n","// Navbar vertical align\n//\n// Vertically center elements in the navbar.\n// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin.\n\n.navbar-vertical-align(@element-height) {\n margin-top: ((@navbar-height - @element-height) / 2);\n margin-bottom: ((@navbar-height - @element-height) / 2);\n}\n","//\n// Utility classes\n// --------------------------------------------------\n\n\n// Floats\n// -------------------------\n\n.clearfix {\n .clearfix();\n}\n.center-block {\n .center-block();\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n\n\n// Toggling content\n// -------------------------\n\n// Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n .text-hide();\n}\n\n\n// Hide from screenreaders and browsers\n//\n// Credit: HTML5 Boilerplate\n\n.hidden {\n display: none !important;\n}\n\n\n// For Affix plugin\n// -------------------------\n\n.affix {\n position: fixed;\n}\n","//\n// Breadcrumbs\n// --------------------------------------------------\n\n\n.breadcrumb {\n padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal;\n margin-bottom: @line-height-computed;\n list-style: none;\n background-color: @breadcrumb-bg;\n border-radius: @border-radius-base;\n\n > li {\n display: inline-block;\n\n + li:before {\n content: \"@{breadcrumb-separator}\\00a0\"; // Unicode space added since inline-block means non-collapsing white-space\n padding: 0 5px;\n color: @breadcrumb-color;\n }\n }\n\n > .active {\n color: @breadcrumb-active-color;\n }\n}\n","//\n// Pagination (multiple pages)\n// --------------------------------------------------\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: @line-height-computed 0;\n border-radius: @border-radius-base;\n\n > li {\n display: inline; // Remove list-style and block-level defaults\n > a,\n > span {\n position: relative;\n float: left; // Collapse white-space\n padding: @padding-base-vertical @padding-base-horizontal;\n line-height: @line-height-base;\n text-decoration: none;\n color: @pagination-color;\n background-color: @pagination-bg;\n border: 1px solid @pagination-border;\n margin-left: -1px;\n }\n &:first-child {\n > a,\n > span {\n margin-left: 0;\n .border-left-radius(@border-radius-base);\n }\n }\n &:last-child {\n > a,\n > span {\n .border-right-radius(@border-radius-base);\n }\n }\n }\n\n > li > a,\n > li > span {\n &:hover,\n &:focus {\n z-index: 2;\n color: @pagination-hover-color;\n background-color: @pagination-hover-bg;\n border-color: @pagination-hover-border;\n }\n }\n\n > .active > a,\n > .active > span {\n &,\n &:hover,\n &:focus {\n z-index: 3;\n color: @pagination-active-color;\n background-color: @pagination-active-bg;\n border-color: @pagination-active-border;\n cursor: default;\n }\n }\n\n > .disabled {\n > span,\n > span:hover,\n > span:focus,\n > a,\n > a:hover,\n > a:focus {\n color: @pagination-disabled-color;\n background-color: @pagination-disabled-bg;\n border-color: @pagination-disabled-border;\n cursor: @cursor-disabled;\n }\n }\n}\n\n// Sizing\n// --------------------------------------------------\n\n// Large\n.pagination-lg {\n .pagination-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large);\n}\n\n// Small\n.pagination-sm {\n .pagination-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small);\n}\n","// Pagination\n\n.pagination-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n > li {\n > a,\n > span {\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n line-height: @line-height;\n }\n &:first-child {\n > a,\n > span {\n .border-left-radius(@border-radius);\n }\n }\n &:last-child {\n > a,\n > span {\n .border-right-radius(@border-radius);\n }\n }\n }\n}\n","//\n// Pager pagination\n// --------------------------------------------------\n\n\n.pager {\n padding-left: 0;\n margin: @line-height-computed 0;\n list-style: none;\n text-align: center;\n &:extend(.clearfix all);\n li {\n display: inline;\n > a,\n > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: @pager-bg;\n border: 1px solid @pager-border;\n border-radius: @pager-border-radius;\n }\n\n > a:hover,\n > a:focus {\n text-decoration: none;\n background-color: @pager-hover-bg;\n }\n }\n\n .next {\n > a,\n > span {\n float: right;\n }\n }\n\n .previous {\n > a,\n > span {\n float: left;\n }\n }\n\n .disabled {\n > a,\n > a:hover,\n > a:focus,\n > span {\n color: @pager-disabled-color;\n background-color: @pager-bg;\n cursor: @cursor-disabled;\n }\n }\n}\n","//\n// Labels\n// --------------------------------------------------\n\n.label {\n display: inline;\n padding: .2em .6em .3em;\n font-size: 75%;\n font-weight: bold;\n line-height: 1;\n color: @label-color;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: .25em;\n\n // Add hover effects, but only for links\n a& {\n &:hover,\n &:focus {\n color: @label-link-hover-color;\n text-decoration: none;\n cursor: pointer;\n }\n }\n\n // Empty labels collapse automatically (not available in IE8)\n &:empty {\n display: none;\n }\n\n // Quick fix for labels in buttons\n .btn & {\n position: relative;\n top: -1px;\n }\n}\n\n// Colors\n// Contextual variations (linked labels get darker on :hover)\n\n.label-default {\n .label-variant(@label-default-bg);\n}\n\n.label-primary {\n .label-variant(@label-primary-bg);\n}\n\n.label-success {\n .label-variant(@label-success-bg);\n}\n\n.label-info {\n .label-variant(@label-info-bg);\n}\n\n.label-warning {\n .label-variant(@label-warning-bg);\n}\n\n.label-danger {\n .label-variant(@label-danger-bg);\n}\n","// Labels\n\n.label-variant(@color) {\n background-color: @color;\n\n &[href] {\n &:hover,\n &:focus {\n background-color: darken(@color, 10%);\n }\n }\n}\n","//\n// Badges\n// --------------------------------------------------\n\n\n// Base class\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: @font-size-small;\n font-weight: @badge-font-weight;\n color: @badge-color;\n line-height: @badge-line-height;\n vertical-align: middle;\n white-space: nowrap;\n text-align: center;\n background-color: @badge-bg;\n border-radius: @badge-border-radius;\n\n // Empty badges collapse automatically (not available in IE8)\n &:empty {\n display: none;\n }\n\n // Quick fix for badges in buttons\n .btn & {\n position: relative;\n top: -1px;\n }\n\n .btn-xs &,\n .btn-group-xs > .btn & {\n top: 0;\n padding: 1px 5px;\n }\n\n // Hover state, but only for links\n a& {\n &:hover,\n &:focus {\n color: @badge-link-hover-color;\n text-decoration: none;\n cursor: pointer;\n }\n }\n\n // Account for badges in navs\n .list-group-item.active > &,\n .nav-pills > .active > a > & {\n color: @badge-active-color;\n background-color: @badge-active-bg;\n }\n\n .list-group-item > & {\n float: right;\n }\n\n .list-group-item > & + & {\n margin-right: 5px;\n }\n\n .nav-pills > li > a > & {\n margin-left: 3px;\n }\n}\n","//\n// Jumbotron\n// --------------------------------------------------\n\n\n.jumbotron {\n padding-top: @jumbotron-padding;\n padding-bottom: @jumbotron-padding;\n margin-bottom: @jumbotron-padding;\n color: @jumbotron-color;\n background-color: @jumbotron-bg;\n\n h1,\n .h1 {\n color: @jumbotron-heading-color;\n }\n\n p {\n margin-bottom: (@jumbotron-padding / 2);\n font-size: @jumbotron-font-size;\n font-weight: 200;\n }\n\n > hr {\n border-top-color: darken(@jumbotron-bg, 10%);\n }\n\n .container &,\n .container-fluid & {\n border-radius: @border-radius-large; // Only round corners at higher resolutions if contained in a container\n padding-left: (@grid-gutter-width / 2);\n padding-right: (@grid-gutter-width / 2);\n }\n\n .container {\n max-width: 100%;\n }\n\n @media screen and (min-width: @screen-sm-min) {\n padding-top: (@jumbotron-padding * 1.6);\n padding-bottom: (@jumbotron-padding * 1.6);\n\n .container &,\n .container-fluid & {\n padding-left: (@jumbotron-padding * 2);\n padding-right: (@jumbotron-padding * 2);\n }\n\n h1,\n .h1 {\n font-size: @jumbotron-heading-font-size;\n }\n }\n}\n","//\n// Thumbnails\n// --------------------------------------------------\n\n\n// Mixin and adjust the regular image class\n.thumbnail {\n display: block;\n padding: @thumbnail-padding;\n margin-bottom: @line-height-computed;\n line-height: @line-height-base;\n background-color: @thumbnail-bg;\n border: 1px solid @thumbnail-border;\n border-radius: @thumbnail-border-radius;\n .transition(border .2s ease-in-out);\n\n > img,\n a > img {\n &:extend(.img-responsive);\n margin-left: auto;\n margin-right: auto;\n }\n\n // Add a hover state for linked versions only\n a&:hover,\n a&:focus,\n a&.active {\n border-color: @link-color;\n }\n\n // Image captions\n .caption {\n padding: @thumbnail-caption-padding;\n color: @thumbnail-caption-color;\n }\n}\n","//\n// Alerts\n// --------------------------------------------------\n\n\n// Base styles\n// -------------------------\n\n.alert {\n padding: @alert-padding;\n margin-bottom: @line-height-computed;\n border: 1px solid transparent;\n border-radius: @alert-border-radius;\n\n // Headings for larger alerts\n h4 {\n margin-top: 0;\n // Specified for the h4 to prevent conflicts of changing @headings-color\n color: inherit;\n }\n\n // Provide class for links that match alerts\n .alert-link {\n font-weight: @alert-link-font-weight;\n }\n\n // Improve alignment and spacing of inner content\n > p,\n > ul {\n margin-bottom: 0;\n }\n\n > p + p {\n margin-top: 5px;\n }\n}\n\n// Dismissible alerts\n//\n// Expand the right padding and account for the close button's positioning.\n\n.alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0.\n.alert-dismissible {\n padding-right: (@alert-padding + 20);\n\n // Adjust close link position\n .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n }\n}\n\n// Alternate styles\n//\n// Generate contextual modifier classes for colorizing the alert.\n\n.alert-success {\n .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text);\n}\n\n.alert-info {\n .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text);\n}\n\n.alert-warning {\n .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text);\n}\n\n.alert-danger {\n .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text);\n}\n","// Alerts\n\n.alert-variant(@background; @border; @text-color) {\n background-color: @background;\n border-color: @border;\n color: @text-color;\n\n hr {\n border-top-color: darken(@border, 5%);\n }\n .alert-link {\n color: darken(@text-color, 10%);\n }\n}\n","//\n// Progress bars\n// --------------------------------------------------\n\n\n// Bar animations\n// -------------------------\n\n// WebKit\n@-webkit-keyframes progress-bar-stripes {\n from { background-position: 40px 0; }\n to { background-position: 0 0; }\n}\n\n// Spec and IE10+\n@keyframes progress-bar-stripes {\n from { background-position: 40px 0; }\n to { background-position: 0 0; }\n}\n\n\n// Bar itself\n// -------------------------\n\n// Outer container\n.progress {\n overflow: hidden;\n height: @line-height-computed;\n margin-bottom: @line-height-computed;\n background-color: @progress-bg;\n border-radius: @progress-border-radius;\n .box-shadow(inset 0 1px 2px rgba(0,0,0,.1));\n}\n\n// Bar of progress\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: @font-size-small;\n line-height: @line-height-computed;\n color: @progress-bar-color;\n text-align: center;\n background-color: @progress-bar-bg;\n .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15));\n .transition(width .6s ease);\n}\n\n// Striped bars\n//\n// `.progress-striped .progress-bar` is deprecated as of v3.2.0 in favor of the\n// `.progress-bar-striped` class, which you just add to an existing\n// `.progress-bar`.\n.progress-striped .progress-bar,\n.progress-bar-striped {\n #gradient > .striped();\n background-size: 40px 40px;\n}\n\n// Call animation for the active one\n//\n// `.progress.active .progress-bar` is deprecated as of v3.2.0 in favor of the\n// `.progress-bar.active` approach.\n.progress.active .progress-bar,\n.progress-bar.active {\n .animation(progress-bar-stripes 2s linear infinite);\n}\n\n\n// Variations\n// -------------------------\n\n.progress-bar-success {\n .progress-bar-variant(@progress-bar-success-bg);\n}\n\n.progress-bar-info {\n .progress-bar-variant(@progress-bar-info-bg);\n}\n\n.progress-bar-warning {\n .progress-bar-variant(@progress-bar-warning-bg);\n}\n\n.progress-bar-danger {\n .progress-bar-variant(@progress-bar-danger-bg);\n}\n","// Gradients\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-repeat: repeat-x;\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255,255,255,.15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n","// Progress bars\n\n.progress-bar-variant(@color) {\n background-color: @color;\n\n // Deprecated parent class requirement as of v3.2.0\n .progress-striped & {\n #gradient > .striped();\n }\n}\n",".media {\n // Proper spacing between instances of .media\n margin-top: 15px;\n\n &:first-child {\n margin-top: 0;\n }\n}\n\n.media,\n.media-body {\n zoom: 1;\n overflow: hidden;\n}\n\n.media-body {\n width: 10000px;\n}\n\n.media-object {\n display: block;\n\n // Fix collapse in webkit from max-width: 100% and display: table-cell.\n &.img-thumbnail {\n max-width: none;\n }\n}\n\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n\n.media-middle {\n vertical-align: middle;\n}\n\n.media-bottom {\n vertical-align: bottom;\n}\n\n// Reset margins on headings for tighter default spacing\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n\n// Media list variation\n//\n// Undo default ul/ol styles\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n","//\n// List groups\n// --------------------------------------------------\n\n\n// Base class\n//\n// Easily usable on