diff --git a/kn-common/src/main/java/org/springblade/common/constant/CommonConstant.java b/kn-common/src/main/java/org/springblade/common/constant/CommonConstant.java
index a8c5697..fc583b4 100644
--- a/kn-common/src/main/java/org/springblade/common/constant/CommonConstant.java
+++ b/kn-common/src/main/java/org/springblade/common/constant/CommonConstant.java
@@ -52,5 +52,8 @@ public interface CommonConstant {
*/
String KN_VORDM_MODULE_NAME = "biz-vordm";
-
+ /**
+ * 系统管理模块 application name
+ */
+ String KN_SYSTEM_MANAGER_MODULE_NAME = "system-manager";
}
diff --git a/kn-sys-manager/Dockerfile b/kn-sys-manager/Dockerfile
new file mode 100644
index 0000000..2dd5760
--- /dev/null
+++ b/kn-sys-manager/Dockerfile
@@ -0,0 +1,15 @@
+FROM bladex/alpine-java:openjdk8-openj9_cn_slim
+
+LABEL maintainer=whq<460794335@qq.com>
+
+RUN mkdir -p /kn/auth
+
+WORKDIR /kn/auth
+
+EXPOSE 8100
+
+ADD ./target/kn-sys-manager.jar ./app.jar
+
+ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "app.jar"]
+
+CMD ["--spring.profiles.active=test"]
diff --git a/kn-sys-manager/pom.xml b/kn-sys-manager/pom.xml
new file mode 100644
index 0000000..1461c52
--- /dev/null
+++ b/kn-sys-manager/pom.xml
@@ -0,0 +1,159 @@
+
+
+
+ 4.0.0
+
+
+ com.kening.platform
+ kn-vordm
+ ${revision}
+
+
+ kn-sys-manager
+ ${project.artifactId}
+ ${revision}
+ jar
+
+
+
+ com.alibaba
+ fastjson
+ 1.2.83
+
+
+ com.kening.platform
+ kn-common
+
+
+ com.kening.platform
+ kn-launcher
+
+
+ org.springblade
+ blade-core-db
+
+
+ postgresql
+ org.postgresql
+
+
+
+
+ org.springblade
+ blade-starter-tenant
+
+
+ org.springblade
+ blade-starter-mybatis
+
+
+ org.javassist
+ javassist
+ 3.25.0-GA
+
+
+
+ io.prometheus
+ simpleclient
+ 0.9.0
+
+
+ org.springblade
+ blade-starter-cache
+
+
+ org.springblade
+ blade-core-cloud
+
+
+ HdrHistogram
+ org.hdrhistogram
+
+
+ javassist
+ org.javassist
+
+
+ simpleclient
+ io.prometheus
+
+
+ fastjson
+ com.alibaba
+
+
+
+
+ org.hdrhistogram
+ HdrHistogram
+ 2.1.12
+
+
+ org.springblade
+ blade-starter-metrics
+
+
+ org.springblade
+ blade-starter-redis
+
+
+ javassist
+ org.javassist
+
+
+
+
+ org.springblade
+ blade-starter-swagger
+
+
+ org.springframework.cloud
+ spring-cloud-starter-security
+
+
+ org.springframework.security.oauth
+ spring-security-oauth2
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+ org.springframework.security
+ spring-security-jwt
+
+
+
+ com.github.whvcse
+ easy-captcha
+
+
+ org.springblade
+ blade-core-boot
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ com.spotify
+ dockerfile-maven-plugin
+
+ ${dockerfile.skip}
+
+
+
+
+
+
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/SystemManagerApplication.java b/kn-sys-manager/src/main/java/org/springblade/auth/SystemManagerApplication.java
new file mode 100644
index 0000000..ec350bf
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/SystemManagerApplication.java
@@ -0,0 +1,24 @@
+package org.springblade.auth;
+
+
+import org.springblade.common.constant.CommonConstant;
+import org.springblade.core.cloud.feign.EnableBladeFeign;
+import org.springblade.core.launch.BladeApplication;
+import org.springframework.cloud.client.SpringCloudApplication;
+import org.springframework.context.annotation.ComponentScan;
+
+/**
+ * 用户认证服务器
+ *
+ * @author Chill
+ */
+@EnableBladeFeign
+@SpringCloudApplication
+@ComponentScan({"org.springblade.auth"})
+public class SystemManagerApplication {
+
+ public static void main(String[] args) {
+ BladeApplication.run(CommonConstant.KN_SYSTEM_MANAGER_MODULE_NAME, SystemManagerApplication.class, args);
+ }
+
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/config/BladeAuthorizationServerConfiguration.java b/kn-sys-manager/src/main/java/org/springblade/auth/config/BladeAuthorizationServerConfiguration.java
new file mode 100644
index 0000000..837d672
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/config/BladeAuthorizationServerConfiguration.java
@@ -0,0 +1,91 @@
+package org.springblade.auth.config;
+
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+import org.springblade.auth.granter.BladeTokenGranter;
+import org.springblade.auth.service.BladeClientDetailsServiceImpl;
+import org.springblade.core.redis.cache.BladeRedis;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+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.TokenGranter;
+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;
+
+/**
+ * 认证服务器配置
+ *
+ * @author Chill
+ */
+@Order
+@Configuration
+@AllArgsConstructor
+@EnableAuthorizationServer
+public class BladeAuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
+
+ private final DataSource dataSource;
+
+ private final AuthenticationManager authenticationManager;
+
+ private final UserDetailsService userDetailsService;
+
+ private final TokenStore tokenStore;
+
+ private final TokenEnhancer jwtTokenEnhancer;
+
+ private final JwtAccessTokenConverter jwtAccessTokenConverter;
+
+ private final BladeRedis bladeRedis;
+
+ @Override
+ public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
+ //获取自定义tokenGranter
+ TokenGranter tokenGranter = BladeTokenGranter.getTokenGranter(authenticationManager, endpoints, bladeRedis);
+
+ //配置端点
+ endpoints.tokenStore(tokenStore)
+ .authenticationManager(authenticationManager)
+ .userDetailsService(userDetailsService)
+ .tokenGranter(tokenGranter);
+
+ //扩展token返回结果
+ if (jwtAccessTokenConverter != null && jwtTokenEnhancer != null) {
+ TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
+ List enhancerList = new ArrayList<>();
+ enhancerList.add(jwtTokenEnhancer);
+ enhancerList.add(jwtAccessTokenConverter);
+ tokenEnhancerChain.setTokenEnhancers(enhancerList);
+ //jwt增强
+ endpoints.tokenEnhancer(tokenEnhancerChain).accessTokenConverter(jwtAccessTokenConverter);
+ }
+ }
+
+ /**
+ * 配置客户端信息
+ */
+ @Override
+ @SneakyThrows
+ public void configure(ClientDetailsServiceConfigurer clients) {
+ clients.withClientDetails(new BladeClientDetailsServiceImpl(dataSource));
+ }
+
+ @Override
+ public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
+ oauthServer
+ .allowFormAuthenticationForClients()
+ .tokenKeyAccess("permitAll()")
+ .checkTokenAccess("isAuthenticated()");
+ }
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/config/BladeResourceServerConfiguration.java b/kn-sys-manager/src/main/java/org/springblade/auth/config/BladeResourceServerConfiguration.java
new file mode 100644
index 0000000..d67ae82
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/config/BladeResourceServerConfiguration.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the dreamlu.net developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.springblade.auth.config;
+
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+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;
+
+/**
+ * 自定义登录成功配置
+ *
+ * @author Chill
+ */
+@Configuration
+@AllArgsConstructor
+@EnableResourceServer
+public class BladeResourceServerConfiguration extends ResourceServerConfigurerAdapter {
+
+ @Override
+ @SneakyThrows
+ public void configure(HttpSecurity http) {
+ http.headers().frameOptions().disable();
+ http.formLogin()
+ .and()
+ .authorizeRequests()
+ .antMatchers(
+ "/actuator/**",
+ "/oauth/captcha",
+ "/oauth/logout",
+ "/oauth/mobile",
+ "/oauth/clear-cache",
+ "/oauth/render/**",
+ "/oauth/callback/**",
+ "/oauth/revoke/**",
+ "/oauth/refresh/**",
+ "/token/**",
+ "/user/**",
+ "/role/**",
+ "/menu/**",
+ "/dict-biz",
+ "/v2/api-docs").permitAll()
+ .anyRequest().authenticated().and()
+ .csrf().disable();
+ }
+
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/config/JwtTokenStoreConfiguration.java b/kn-sys-manager/src/main/java/org/springblade/auth/config/JwtTokenStoreConfiguration.java
new file mode 100644
index 0000000..f7a00a8
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/config/JwtTokenStoreConfiguration.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the dreamlu.net developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.springblade.auth.config;
+
+import org.springblade.auth.support.BladeJwtTokenEnhancer;
+import org.springblade.core.jwt.props.JwtProperties;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.oauth2.provider.token.TokenEnhancer;
+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;
+
+/**
+ * JwtTokenStore
+ *
+ * @author Chill
+ */
+@Configuration
+@ConditionalOnProperty(prefix = "blade.security.oauth2", name = "storeType", havingValue = "jwt", matchIfMissing = true)
+public class JwtTokenStoreConfiguration {
+
+ /**
+ * 使用jwtTokenStore存储token
+ */
+ @Bean
+ public TokenStore jwtTokenStore(JwtProperties jwtProperties) {
+ return new JwtTokenStore(jwtAccessTokenConverter(jwtProperties));
+ }
+
+ /**
+ * 用于生成jwt
+ */
+ @Bean
+ public JwtAccessTokenConverter jwtAccessTokenConverter(JwtProperties jwtProperties) {
+ JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
+ accessTokenConverter.setSigningKey(jwtProperties.getSignKey());
+ return accessTokenConverter;
+ }
+
+ /**
+ * 用于扩展jwt
+ */
+ @Bean
+ @ConditionalOnMissingBean(name = "jwtTokenEnhancer")
+ public TokenEnhancer jwtTokenEnhancer(JwtAccessTokenConverter jwtAccessTokenConverter, JwtProperties jwtProperties) {
+ return new BladeJwtTokenEnhancer(jwtAccessTokenConverter, jwtProperties);
+ }
+
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/config/SecurityConfiguration.java b/kn-sys-manager/src/main/java/org/springblade/auth/config/SecurityConfiguration.java
new file mode 100644
index 0000000..41bfb46
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/config/SecurityConfiguration.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the dreamlu.net developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.springblade.auth.config;
+
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+import org.springblade.auth.support.BladePasswordEncoderFactories;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+/**
+ * Security配置
+ *
+ * @author Chill
+ */
+@Configuration
+@AllArgsConstructor
+public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
+
+ @Bean
+ @Override
+ @SneakyThrows
+ public AuthenticationManager authenticationManagerBean() {
+ return super.authenticationManagerBean();
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return BladePasswordEncoderFactories.createDelegatingPasswordEncoder();
+ }
+
+ @Override
+ @SneakyThrows
+ protected void configure(HttpSecurity http) {
+ http.httpBasic().and().csrf().disable().authorizeRequests().anyRequest().fullyAuthenticated();
+ }
+
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/constant/AuthConstant.java b/kn-sys-manager/src/main/java/org/springblade/auth/constant/AuthConstant.java
new file mode 100644
index 0000000..3f5edae
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/constant/AuthConstant.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the dreamlu.net developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.springblade.auth.constant;
+
+/**
+ * 授权校验常量
+ *
+ * @author Chill
+ */
+public interface AuthConstant {
+
+ /**
+ * 密码加密规则
+ */
+ String ENCRYPT = "{blade}";
+
+ /**
+ * blade_client表字段
+ */
+ String CLIENT_FIELDS = "client_id, CONCAT('{noop}',client_secret) as client_secret, resource_ids, scope, authorized_grant_types, " +
+ "web_server_redirect_uri, authorities, access_token_validity, " +
+ "refresh_token_validity, additional_information, autoapprove";
+
+ /**
+ * blade_client查询语句
+ */
+ String BASE_STATEMENT = "select " + CLIENT_FIELDS + " from blade_client";
+
+ /**
+ * blade_client查询排序
+ */
+ String DEFAULT_FIND_STATEMENT = BASE_STATEMENT + " order by client_id";
+
+ /**
+ * 查询client_id
+ */
+ String DEFAULT_SELECT_STATEMENT = BASE_STATEMENT + " where client_id = ?";
+
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/endpoint/BladeTokenEndPoint.java b/kn-sys-manager/src/main/java/org/springblade/auth/endpoint/BladeTokenEndPoint.java
new file mode 100644
index 0000000..0b41aa8
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/endpoint/BladeTokenEndPoint.java
@@ -0,0 +1,93 @@
+package org.springblade.auth.endpoint;
+
+import com.wf.captcha.SpecCaptcha;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.common.cache.CacheNames;
+import org.springblade.core.cache.utils.CacheUtil;
+import org.springblade.core.jwt.JwtUtil;
+import org.springblade.core.jwt.props.JwtProperties;
+import org.springblade.core.launch.constant.TokenConstant;
+import org.springblade.core.redis.cache.BladeRedis;
+import org.springblade.core.secure.BladeUser;
+import org.springblade.core.secure.utils.AuthUtil;
+import org.springblade.core.tenant.annotation.NonDS;
+import org.springblade.core.tool.api.R;
+import org.springblade.core.tool.support.Kv;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.core.tool.utils.WebUtil;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.Duration;
+
+import static org.springblade.core.cache.constant.CacheConstant.*;
+
+/**
+ * BladeEndPoint
+ *
+ * @author Chill
+ */
+@NonDS
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+public class BladeTokenEndPoint {
+
+ private final BladeRedis bladeRedis;
+ private final JwtProperties jwtProperties;
+ @Value("${captcha.image.width:130}")
+ private Integer imageWidth;
+ @Value("${captcha.image.height:48}")
+ private Integer imageHeight;
+ @Value("${captcha.image.code.length:5}")
+ private Integer codeLength;
+ @Value("${captcha.image.code.time:30}")
+ private Integer codeValidateTime;
+
+ @GetMapping("/oauth/user-info")
+ public R currentUser(Authentication authentication) {
+ return R.data(authentication);
+ }
+
+ @GetMapping("/oauth/captcha")
+ public Kv captcha() {
+ SpecCaptcha specCaptcha = new SpecCaptcha(imageWidth, imageHeight, codeLength);
+ String verCode = specCaptcha.text().toLowerCase();
+ String key = StringUtil.randomUUID();
+ // 存入redis并设置过期时间为30分钟
+ bladeRedis.setEx(CacheNames.CAPTCHA_KEY + key, verCode, Duration.ofMinutes(codeValidateTime));
+ // 将key和base64返回给前端
+ return Kv.create().set("key", key).set("image", specCaptcha.toBase64());
+ }
+
+ @GetMapping("/oauth/logout")
+ public Kv logout() {
+ BladeUser user = AuthUtil.getUser();
+ if (user != null && jwtProperties.getState()) {
+ String token = JwtUtil.getToken(WebUtil.getRequest().getHeader(TokenConstant.HEADER));
+ JwtUtil.removeAccessToken(user.getTenantId(), String.valueOf(user.getUserId()), token);
+ }
+ return Kv.create().set("success", "true").set("msg", "success");
+ }
+
+ @GetMapping("/oauth/clear-cache")
+ public Kv clearCache() {
+ CacheUtil.clear(BIZ_CACHE);
+ CacheUtil.clear(USER_CACHE);
+ CacheUtil.clear(DICT_CACHE);
+ CacheUtil.clear(FLOW_CACHE);
+ CacheUtil.clear(SYS_CACHE);
+ CacheUtil.clear(PARAM_CACHE);
+ CacheUtil.clear(RESOURCE_CACHE);
+ CacheUtil.clear(MENU_CACHE);
+ CacheUtil.clear(DICT_CACHE, Boolean.FALSE);
+ CacheUtil.clear(MENU_CACHE, Boolean.FALSE);
+ CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
+ CacheUtil.clear(PARAM_CACHE, Boolean.FALSE);
+ return Kv.create().set("success", "true").set("msg", "success");
+ }
+
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/granter/BladeTokenGranter.java b/kn-sys-manager/src/main/java/org/springblade/auth/granter/BladeTokenGranter.java
new file mode 100644
index 0000000..8c1fa08
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/granter/BladeTokenGranter.java
@@ -0,0 +1,33 @@
+package org.springblade.auth.granter;
+
+import org.springblade.core.redis.cache.BladeRedis;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
+import org.springframework.security.oauth2.provider.CompositeTokenGranter;
+import org.springframework.security.oauth2.provider.TokenGranter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 自定义拓展TokenGranter
+ *
+ * @author Chill
+ */
+public class BladeTokenGranter {
+
+ /**
+ * 自定义tokenGranter
+ */
+ public static TokenGranter getTokenGranter(final AuthenticationManager authenticationManager,
+ final AuthorizationServerEndpointsConfigurer endpoints,
+ BladeRedis bladeRedis) {
+ // 默认tokenGranter集合
+ List granters = new ArrayList<>(Collections.singletonList(endpoints.getTokenGranter()));
+ // 增加验证码模式
+ granters.add(new CaptchaTokenGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory(), bladeRedis));
+ return new CompositeTokenGranter(granters);
+ }
+
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/granter/CaptchaTokenGranter.java b/kn-sys-manager/src/main/java/org/springblade/auth/granter/CaptchaTokenGranter.java
new file mode 100644
index 0000000..4f33384
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/granter/CaptchaTokenGranter.java
@@ -0,0 +1,82 @@
+package org.springblade.auth.granter;
+
+import org.springblade.auth.utils.TokenUtil;
+import org.springblade.common.cache.CacheNames;
+import org.springblade.core.redis.cache.BladeRedis;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.core.tool.utils.WebUtil;
+import org.springframework.security.authentication.*;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
+import org.springframework.security.oauth2.common.exceptions.UserDeniedAuthorizationException;
+import org.springframework.security.oauth2.provider.*;
+import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
+import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 验证码TokenGranter
+ *
+ * @author Chill
+ */
+public class CaptchaTokenGranter extends AbstractTokenGranter {
+
+ private static final String GRANT_TYPE = "captcha";
+
+ private final AuthenticationManager authenticationManager;
+
+ private BladeRedis bladeRedis;
+
+ public CaptchaTokenGranter(AuthenticationManager authenticationManager,
+ AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, BladeRedis bladeRedis) {
+ this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
+ this.bladeRedis = bladeRedis;
+ }
+
+ protected CaptchaTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices,
+ ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
+ super(tokenServices, clientDetailsService, requestFactory, grantType);
+ this.authenticationManager = authenticationManager;
+ }
+
+ @Override
+ protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
+ HttpServletRequest request = WebUtil.getRequest();
+ // 增加验证码判断
+ String key = request.getHeader(TokenUtil.CAPTCHA_HEADER_KEY);
+ String code = request.getHeader(TokenUtil.CAPTCHA_HEADER_CODE);
+ // 获取验证码
+ String redisCode = bladeRedis.get(CacheNames.CAPTCHA_KEY + key);
+ // 判断验证码
+ if (code == null || !StringUtil.equalsIgnoreCase(redisCode, code)) {
+ throw new UserDeniedAuthorizationException(TokenUtil.CAPTCHA_NOT_CORRECT);
+ }
+
+ Map parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
+ String username = parameters.get("username");
+ String password = parameters.get("password");
+ // Protect from downstream leaks of password
+ parameters.remove("password");
+
+ Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
+ ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
+ try {
+ userAuth = authenticationManager.authenticate(userAuth);
+ }
+ catch (AccountStatusException | BadCredentialsException ase) {
+ //covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
+ throw new InvalidGrantException(ase.getMessage());
+ }
+ // If the username/password are wrong the spec says we should send 400/invalid grant
+
+ if (userAuth == null || !userAuth.isAuthenticated()) {
+ throw new InvalidGrantException("Could not authenticate user: " + username);
+ }
+
+ OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
+ return new OAuth2Authentication(storedOAuth2Request, userAuth);
+ }
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/service/BladeClientDetailsServiceImpl.java b/kn-sys-manager/src/main/java/org/springblade/auth/service/BladeClientDetailsServiceImpl.java
new file mode 100644
index 0000000..7bc66f6
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/service/BladeClientDetailsServiceImpl.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the dreamlu.net developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.springblade.auth.service;
+
+import org.springblade.auth.constant.AuthConstant;
+import org.springframework.security.oauth2.provider.ClientDetails;
+import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
+import org.springframework.stereotype.Component;
+
+import javax.sql.DataSource;
+
+/**
+ * 客户端信息
+ *
+ * @author Chill
+ */
+@Component
+public class BladeClientDetailsServiceImpl extends JdbcClientDetailsService {
+
+ public BladeClientDetailsServiceImpl(DataSource dataSource) {
+ super(dataSource);
+ setSelectClientDetailsSql(AuthConstant.DEFAULT_SELECT_STATEMENT);
+ setFindClientDetailsSql(AuthConstant.DEFAULT_FIND_STATEMENT);
+ }
+
+ /**
+ * 缓存客户端信息
+ *
+ * @param clientId 客户端id
+ */
+ @Override
+ public ClientDetails loadClientByClientId(String clientId) {
+ try {
+ return super.loadClientByClientId(clientId);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ return null;
+ }
+ }
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/service/BladeUserDetails.java b/kn-sys-manager/src/main/java/org/springblade/auth/service/BladeUserDetails.java
new file mode 100644
index 0000000..e4135e0
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/service/BladeUserDetails.java
@@ -0,0 +1,93 @@
+package org.springblade.auth.service;
+
+import lombok.Getter;
+import org.springblade.core.tool.support.Kv;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+
+import java.util.Collection;
+
+/**
+ * 用户信息拓展
+ *
+ * @author whq
+ */
+@Getter
+public class BladeUserDetails extends User {
+
+ /**
+ * 用户id
+ */
+ private final Long userId;
+
+ private final String email;
+
+ /**
+ * 租户ID
+ */
+ private final String tenantId;
+
+ /**
+ * 第三方认证ID
+ */
+ private final String oauthId;
+ /**
+ * 昵称
+ */
+ private final String name;
+ /**
+ * 真名
+ */
+ private final String realName;
+ /**
+ * 账号
+ */
+ private final String account;
+ /**
+ * 部门id
+ */
+ private final String deptId;
+ /**
+ * 岗位id
+ */
+ private final String postId;
+ /**
+ * 角色id
+ */
+ private final String roleId;
+ /**
+ * 角色名
+ */
+ private final String roleName;
+ /**
+ * 头像
+ */
+ private final String avatar;
+ /**
+ * 用户详情
+ */
+ private final Kv detail;
+
+ public BladeUserDetails(Long userId, String email,
+ String tenantId, String oauthId, String name,
+ String realName, String deptId, String postId, String roleId,
+ String roleName, String avatar, String username, String password, Kv detail,
+ boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired,
+ boolean accountNonLocked, Collection extends GrantedAuthority> authorities) {
+ super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
+ this.userId = userId;
+ this.email = email;
+ this.tenantId = tenantId;
+ this.oauthId = oauthId;
+ this.name = name;
+ this.realName = realName;
+ this.account = username;
+ this.deptId = deptId;
+ this.postId = postId;
+ this.roleId = roleId;
+ this.roleName = roleName;
+ this.avatar = avatar;
+ this.detail = detail;
+ }
+
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/service/BladeUserDetailsServiceImpl.java b/kn-sys-manager/src/main/java/org/springblade/auth/service/BladeUserDetailsServiceImpl.java
new file mode 100644
index 0000000..459011e
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/service/BladeUserDetailsServiceImpl.java
@@ -0,0 +1,124 @@
+package org.springblade.auth.service;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+import org.apache.commons.lang.StringUtils;
+import org.springblade.auth.constant.AuthConstant;
+import org.springblade.auth.system.cache.SysCache;
+import org.springblade.auth.system.cache.UserCache;
+import org.springblade.auth.system.entity.User;
+import org.springblade.auth.system.entity.UserInfo;
+import org.springblade.auth.utils.TokenUtil;
+import org.springblade.common.cache.CacheNames;
+import org.springblade.core.redis.cache.BladeRedis;
+import org.springblade.core.tool.utils.DigestUtil;
+import org.springblade.core.tool.utils.Func;
+import org.springblade.core.tool.utils.StringPool;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.core.tool.utils.WebUtil;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.oauth2.common.exceptions.UserDeniedAuthorizationException;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.http.HttpServletRequest;
+import java.time.Duration;
+import java.util.List;
+
+/**
+ * 用户信息
+ *
+ * @author whq
+ */
+@Service
+@AllArgsConstructor
+public class BladeUserDetailsServiceImpl implements UserDetailsService {
+
+ public static final Integer FAIL_COUNT = 5;
+
+ private final BladeRedis bladeRedis;
+
+ @Override
+ @SneakyThrows
+ public BladeUserDetails loadUserByUsername(String username) {
+ HttpServletRequest request = WebUtil.getRequest();
+ // 获取用户绑定ID
+ String headerDept = request.getHeader(TokenUtil.DEPT_HEADER_KEY);
+ String headerRole = request.getHeader(TokenUtil.ROLE_HEADER_KEY);
+ // 获取租户ID
+ String headerTenant = request.getHeader(TokenUtil.TENANT_HEADER_KEY);
+ String paramTenant = request.getParameter(TokenUtil.TENANT_PARAM_KEY);
+ String password = request.getParameter(TokenUtil.PASSWORD_KEY);
+ String grantType = request.getParameter(TokenUtil.GRANT_TYPE_KEY);
+ if (StringUtil.isAllBlank(headerTenant, paramTenant)) {
+ throw new UserDeniedAuthorizationException(TokenUtil.TENANT_NOT_FOUND);
+ }
+ String tenantId = StringUtils.isBlank(headerTenant) ? paramTenant : headerTenant;
+
+ // 判断登录是否锁定
+ // 新版本将增加:1.参数管理读取配置 2.用户管理增加解封按钮
+ int count = getFailCount(tenantId, username);
+ if (count >= FAIL_COUNT) {
+ throw new UserDeniedAuthorizationException(TokenUtil.USER_HAS_TOO_MANY_FAILS);
+ }
+
+ // 获取用户类型
+ String userType = Func.toStr(request.getHeader(TokenUtil.USER_TYPE_HEADER_KEY), TokenUtil.DEFAULT_USER_TYPE);
+
+ // 远程调用返回数据
+ UserInfo userInfo = UserCache.getUser(tenantId, username);
+ User user = userInfo.getUser();
+ // 判断返回信息
+ // 用户不存在,但提示用户名与密码错误并锁定账号
+ if (user == null || user.getId() == null) {
+ setFailCount(tenantId, username, count);
+ throw new UsernameNotFoundException(TokenUtil.USER_NOT_FOUND);
+ }
+ // 用户存在但密码错误,超过次数则锁定账号
+ if (!grantType.equals(TokenUtil.REFRESH_TOKEN_KEY) && !user.getPassword().equals(DigestUtil.hex(password))) {
+ setFailCount(tenantId, username, count);
+ throw new UsernameNotFoundException(TokenUtil.USER_NOT_FOUND);
+ }
+ // 用户角色不存在
+ if (Func.isEmpty(userInfo.getRoles())) {
+ throw new UserDeniedAuthorizationException(TokenUtil.USER_HAS_NO_ROLE);
+ }
+ // 多角色情况下指定单角色
+ if (Func.isNotEmpty(headerRole) && user.getRoleId().contains(headerRole)) {
+ List roleResult = SysCache.getRoleAliases(headerRole);
+ userInfo.setRoles(roleResult);
+ user.setRoleId(headerRole);
+ }
+ return new BladeUserDetails(user.getId(), user.getEmail(),
+ user.getTenantId(), StringPool.EMPTY, user.getName(), user.getRealName(), user.getDeptId(), user.getPostId(), user.getRoleId(), Func.join(userInfo.getRoles()), Func.toStr(user.getAvatar(), TokenUtil.DEFAULT_AVATAR),
+ username, AuthConstant.ENCRYPT + user.getPassword(), userInfo.getDetail(), true, true, true, true,
+ AuthorityUtils.commaSeparatedStringToAuthorityList(Func.join(userInfo.getRoles())));
+ }
+
+ /**
+ * 获取账号错误次数
+ *
+ * @param tenantId 租户id
+ * @param username 账号
+ * @return int
+ */
+ private int getFailCount(String tenantId, String username) {
+ return Func.toInt(bladeRedis.get(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, username)), 0);
+ }
+
+ /**
+ * 设置账号错误次数
+ *
+ * @param tenantId 租户id
+ * @param username 账号
+ * @param count 次数
+ */
+ private void setFailCount(String tenantId, String username, int count) {
+ bladeRedis.setEx(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, username), count + 1, Duration.ofMinutes(30));
+ }
+
+
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/support/BladeJwtTokenEnhancer.java b/kn-sys-manager/src/main/java/org/springblade/auth/support/BladeJwtTokenEnhancer.java
new file mode 100644
index 0000000..fa39f83
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/support/BladeJwtTokenEnhancer.java
@@ -0,0 +1,60 @@
+package org.springblade.auth.support;
+
+import lombok.RequiredArgsConstructor;
+import org.springblade.auth.service.BladeUserDetails;
+import org.springblade.auth.utils.TokenUtil;
+import org.springblade.core.jwt.JwtUtil;
+import org.springblade.core.jwt.props.JwtProperties;
+import org.springblade.core.tool.utils.Func;
+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 org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * jwt返回参数增强
+ *
+ * @author Chill
+ */
+@RequiredArgsConstructor
+public class BladeJwtTokenEnhancer implements TokenEnhancer {
+
+ private final JwtAccessTokenConverter jwtAccessTokenConverter;
+ private final JwtProperties jwtProperties;
+
+ @Override
+ public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
+ BladeUserDetails principal = (BladeUserDetails) authentication.getUserAuthentication().getPrincipal();
+ //token参数增强
+ Map info = new HashMap<>(16);
+ info.put(TokenUtil.CLIENT_ID, TokenUtil.getClientIdFromHeader());
+ info.put(TokenUtil.USER_ID, Func.toStr(principal.getUserId()));
+ info.put(TokenUtil.DEPT_ID, Func.toStr(principal.getDeptId()));
+ //info.put(TokenUtil.POST_ID, Func.toStr(principal.getPostId()));
+ info.put(TokenUtil.ROLE_ID, Func.toStr(principal.getRoleId()));
+ info.put(TokenUtil.TENANT_ID, principal.getTenantId());
+ info.put(TokenUtil.EMAIL, principal.getEmail());
+ //info.put(TokenUtil.OAUTH_ID, principal.getOauthId());
+ info.put(TokenUtil.ACCOUNT, principal.getAccount());
+ info.put(TokenUtil.USER_NAME, principal.getUsername());
+ info.put(TokenUtil.NICK_NAME, principal.getName());
+ info.put(TokenUtil.REAL_NAME, principal.getRealName());
+ info.put(TokenUtil.ROLE_NAME, principal.getRoleName());
+ info.put(TokenUtil.AVATAR, principal.getAvatar());
+ info.put(TokenUtil.DETAIL, principal.getDetail());
+ ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
+ //token状态设置, 有状态,默认无状态
+ if (jwtProperties.getState()) {
+ OAuth2AccessToken oAuth2AccessToken = jwtAccessTokenConverter.enhance(accessToken, authentication);
+ String tokenValue = oAuth2AccessToken.getValue();
+ String tenantId = principal.getTenantId();
+ String userId = Func.toStr(principal.getUserId());
+ JwtUtil.addAccessToken(tenantId, userId, tokenValue, accessToken.getExpiresIn());
+ }
+ return accessToken;
+ }
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/support/BladeNoOpPasswordEncoder.java b/kn-sys-manager/src/main/java/org/springblade/auth/support/BladeNoOpPasswordEncoder.java
new file mode 100644
index 0000000..9210fb8
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/support/BladeNoOpPasswordEncoder.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the dreamlu.net developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.springblade.auth.support;
+
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+/**
+ * 无密码加密
+ *
+ * @author Chill
+ */
+public class BladeNoOpPasswordEncoder implements PasswordEncoder {
+
+ @Override
+ public String encode(CharSequence rawPassword) {
+ return rawPassword.toString();
+ }
+
+ @Override
+ public boolean matches(CharSequence rawPassword, String encodedPassword) {
+ return rawPassword.toString().equals(encodedPassword);
+ }
+
+ /**
+ * Get the singleton {@link BladeNoOpPasswordEncoder}.
+ */
+ public static PasswordEncoder getInstance() {
+ return INSTANCE;
+ }
+
+ private static final PasswordEncoder INSTANCE = new BladeNoOpPasswordEncoder();
+
+ private BladeNoOpPasswordEncoder() {
+ }
+
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/support/BladePasswordEncoder.java b/kn-sys-manager/src/main/java/org/springblade/auth/support/BladePasswordEncoder.java
new file mode 100644
index 0000000..c226b93
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/support/BladePasswordEncoder.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the dreamlu.net developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.springblade.auth.support;
+
+import org.springblade.core.tool.utils.DigestUtil;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+/**
+ * 自定义密码加密
+ *
+ * @author Chill
+ */
+public class BladePasswordEncoder implements PasswordEncoder {
+
+ @Override
+ public String encode(CharSequence rawPassword) {
+ return DigestUtil.hex((String) rawPassword);
+ }
+
+ @Override
+ public boolean matches(CharSequence rawPassword, String encodedPassword) {
+ return encodedPassword.equals(encode(rawPassword));
+ }
+
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/support/BladePasswordEncoderFactories.java b/kn-sys-manager/src/main/java/org/springblade/auth/support/BladePasswordEncoderFactories.java
new file mode 100644
index 0000000..664b0ba
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/support/BladePasswordEncoderFactories.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springblade.auth.support;
+
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
+import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 自定义密码工厂
+ *
+ * @author Rob Winch, Chill
+ * @since 5.0
+ */
+public class BladePasswordEncoderFactories {
+
+ /**
+ * Creates a {@link DelegatingPasswordEncoder} with default mappings. Additional
+ * mappings may be added and the encoding will be updated to conform with best
+ * practices. However, due to the nature of {@link DelegatingPasswordEncoder} the
+ * updates should not impact users. The mappings current are:
+ *
+ *
+ * - blade - {@link BladePasswordEncoder} (sha1(md5("password")))
+ * - bcrypt - {@link BCryptPasswordEncoder} (Also used for encoding)
+ * - noop - {@link BladeNoOpPasswordEncoder}
+ * - pbkdf2 - {@link Pbkdf2PasswordEncoder}
+ * - scrypt - {@link SCryptPasswordEncoder}
+ *
+ *
+ * @return the {@link PasswordEncoder} to use
+ */
+ public static PasswordEncoder createDelegatingPasswordEncoder() {
+ String encodingId = "blade";
+ Map encoders = new HashMap<>(16);
+ encoders.put(encodingId, new BladePasswordEncoder());
+ encoders.put("bcrypt", new BCryptPasswordEncoder());
+ encoders.put("noop", BladeNoOpPasswordEncoder.getInstance());
+ encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
+ encoders.put("scrypt", new SCryptPasswordEncoder());
+
+ return new DelegatingPasswordEncoder(encodingId, encoders);
+ }
+
+ private BladePasswordEncoderFactories() {
+ }
+
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/system/cache/DictBizCache.java b/kn-sys-manager/src/main/java/org/springblade/auth/system/cache/DictBizCache.java
new file mode 100644
index 0000000..3fc68b5
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/system/cache/DictBizCache.java
@@ -0,0 +1,89 @@
+package org.springblade.auth.system.cache;
+
+import org.springblade.auth.system.service.IDictBizService;
+import org.springblade.core.cache.utils.CacheUtil;
+import org.springblade.core.secure.utils.AuthUtil;
+import org.springblade.core.tool.utils.SpringUtil;
+import org.springblade.core.tool.utils.StringPool;
+import org.springblade.auth.system.entity.DictBiz;
+
+import java.util.List;
+
+import static org.springblade.core.cache.constant.CacheConstant.DICT_CACHE;
+
+/**
+ * 业务字典缓存工具类
+ *
+ * @author Chill
+ */
+public class DictBizCache {
+
+ private static final String DICT_ID = "dictBiz:id";
+ private static final String DICT_VALUE = "dictBiz:value";
+ private static final String DICT_LIST = "dictBiz:list";
+
+ private static IDictBizService dictClient;
+
+ private static IDictBizService getDictClient() {
+ if (dictClient == null) {
+ dictClient = SpringUtil.getBean(IDictBizService.class);
+ }
+ return dictClient;
+ }
+
+ /**
+ * 获取字典实体
+ *
+ * @param id 主键
+ * @return DictBiz
+ */
+ public static DictBiz getById(Long id) {
+ String keyPrefix = DICT_ID.concat(StringPool.DASH).concat(AuthUtil.getTenantId()).concat(StringPool.COLON);
+ return CacheUtil.get(DICT_CACHE, keyPrefix, id, () -> {
+ return getDictClient().getById(id);
+ });
+ }
+
+
+ /**
+ * 获取字典值
+ *
+ * @param code 字典编号
+ * @param dictKey Integer型字典键
+ * @return String
+ */
+ public static String getValue(String code, Integer dictKey) {
+ String keyPrefix = DICT_VALUE.concat(StringPool.DASH).concat(AuthUtil.getTenantId()).concat(StringPool.COLON);
+ return CacheUtil.get(DICT_CACHE, keyPrefix + code + StringPool.COLON, String.valueOf(dictKey), () -> {
+ return getDictClient().getValue(code, String.valueOf(dictKey));
+ });
+ }
+
+ /**
+ * 获取字典值
+ *
+ * @param code 字典编号
+ * @param dictKey String型字典键
+ * @return String
+ */
+ public static String getValue(String code, String dictKey) {
+ String keyPrefix = DICT_VALUE.concat(StringPool.DASH).concat(AuthUtil.getTenantId()).concat(StringPool.COLON);
+ return CacheUtil.get(DICT_CACHE, keyPrefix + code + StringPool.COLON, dictKey, () -> {
+ return getDictClient().getValue(code, dictKey);
+ });
+ }
+
+ /**
+ * 获取字典集合
+ *
+ * @param code 字典编号
+ * @return List
+ */
+ public static List getList(String code) {
+ String keyPrefix = DICT_LIST.concat(StringPool.DASH).concat(AuthUtil.getTenantId()).concat(StringPool.COLON);
+ return CacheUtil.get(DICT_CACHE, keyPrefix, code, () -> {
+ return getDictClient().getList(code);
+ });
+ }
+
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/system/cache/SysCache.java b/kn-sys-manager/src/main/java/org/springblade/auth/system/cache/SysCache.java
new file mode 100644
index 0000000..24b00ed
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/system/cache/SysCache.java
@@ -0,0 +1,116 @@
+package org.springblade.auth.system.cache;
+
+import org.springblade.auth.system.entity.Menu;
+import org.springblade.auth.system.entity.Role;
+import org.springblade.auth.system.service.IMenuService;
+import org.springblade.auth.system.service.IRoleService;
+import org.springblade.core.cache.utils.CacheUtil;
+import org.springblade.core.tool.utils.SpringUtil;
+import org.springblade.core.tool.utils.StringPool;
+
+import java.util.List;
+
+import static org.springblade.core.cache.constant.CacheConstant.SYS_CACHE;
+
+/**
+ * 系统缓存
+ *
+ * @author Chill
+ */
+public class SysCache {
+ private static final String MENU_ID = "menu:id:";
+ private static final String ROLE_ID = "role:id:";
+ private static final String ROLE_NAME = "role:name:";
+ private static final String ROLE_NAME_ID = "roleName:id:";
+ private static final String ROLE_NAMES_ID = "roleNames:id:";
+ private static final String ROLE_ALIAS_ID = "roleAlias:id:";
+ private static final String ROLE_ALIASES_ID = "roleAliases:id:";
+
+ private static IMenuService menuService;
+ private static IRoleService roleService;
+
+ private static IMenuService getSysClient() {
+ if (menuService == null) {
+ menuService = SpringUtil.getBean(IMenuService.class);
+ }
+ return menuService;
+ }
+
+ private static IRoleService getRoleClient() {
+ if (roleService == null) {
+ roleService = SpringUtil.getBean(IRoleService.class);
+ }
+ return roleService;
+ }
+
+ /**
+ * 获取菜单
+ *
+ * @param id 主键
+ * @return 菜单
+ */
+ public static Menu getMenu(Long id) {
+ return CacheUtil.get(SYS_CACHE, MENU_ID, id, () -> getSysClient().getById(id));
+ }
+ /**
+ * 获取角色
+ *
+ * @param id 主键
+ * @return Role
+ */
+ public static Role getRole(Long id) {
+ return CacheUtil.get(SYS_CACHE, ROLE_ID, id, () -> getRoleClient().getById(id));
+ }
+
+ /**
+ * 获取角色id
+ *
+ * @param tenantId 租户id
+ * @param roleNames 角色名
+ * @return
+ */
+ public static String getRoleIds(String tenantId, String roleNames) {
+ return CacheUtil.get(SYS_CACHE, ROLE_NAME, tenantId + StringPool.DASH + roleNames, () -> getRoleClient().getRoleIds(tenantId, roleNames));
+ }
+
+ /**
+ * 获取角色名
+ *
+ * @param id 主键
+ * @return 角色名
+ */
+ public static String getRoleName(Long id) {
+ return CacheUtil.get(SYS_CACHE, ROLE_NAME_ID, id, () -> getRoleClient().getById(id).getRoleName());
+ }
+
+ /**
+ * 获取角色别名
+ *
+ * @param id 主键
+ * @return 角色别名
+ */
+ public static String getRoleAlias(Long id) {
+ return CacheUtil.get(SYS_CACHE, ROLE_ALIAS_ID, id, () -> getRoleClient().getById(id).getRoleAlias());
+ }
+
+ /**
+ * 获取角色名集合
+ *
+ * @param roleIds 主键集合
+ * @return 角色名
+ */
+ public static List getRoleNames(String roleIds) {
+ return CacheUtil.get(SYS_CACHE, ROLE_NAMES_ID, roleIds, () -> getRoleClient().getRoleNames(roleIds));
+ }
+
+ /**
+ * 获取角色别名集合
+ *
+ * @param roleIds 主键集合
+ * @return 角色别名
+ */
+ public static List getRoleAliases(String roleIds) {
+ return CacheUtil.get(SYS_CACHE, ROLE_ALIASES_ID, roleIds, () -> getRoleClient().getRoleAliases(roleIds));
+ }
+
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/system/cache/UserCache.java b/kn-sys-manager/src/main/java/org/springblade/auth/system/cache/UserCache.java
new file mode 100644
index 0000000..ac81ec6
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/system/cache/UserCache.java
@@ -0,0 +1,65 @@
+package org.springblade.auth.system.cache;
+
+import org.springblade.auth.system.entity.User;
+import org.springblade.auth.system.entity.UserInfo;
+import org.springblade.auth.system.service.IUserService;
+import org.springblade.core.cache.utils.CacheUtil;
+import org.springblade.core.tool.utils.Func;
+import org.springblade.core.tool.utils.SpringUtil;
+import org.springblade.core.tool.utils.StringPool;
+import org.springblade.core.tool.utils.StringUtil;
+
+import static org.springblade.core.cache.constant.CacheConstant.USER_CACHE;
+import static org.springblade.core.launch.constant.FlowConstant.TASK_USR_PREFIX;
+
+/**
+ * 系统缓存
+ *
+ * @author Chill
+ */
+public class UserCache {
+ private static final String USER_CACHE_ID = "user:id:";
+ private static final String USER_CACHE_ACCOUNT = "user:account:";
+
+ private static IUserService userClient;
+
+ private static IUserService getUserClient() {
+ if (userClient == null) {
+ userClient = SpringUtil.getBean(IUserService.class);
+ }
+ return userClient;
+ }
+
+ /**
+ * 根据任务用户id获取用户信息
+ *
+ * @param taskUserId 任务用户id
+ * @return
+ */
+ public static User getUserByTaskUser(String taskUserId) {
+ Long userId = Func.toLong(StringUtil.removePrefix(taskUserId, TASK_USR_PREFIX));
+ return getUser(userId);
+ }
+
+ /**
+ * 获取用户
+ *
+ * @param userId 用户id
+ * @return
+ */
+ public static User getUser(Long userId) {
+ return CacheUtil.get(USER_CACHE, USER_CACHE_ID, userId, () -> getUserClient().getById(userId));
+ }
+
+ /**
+ * 获取用户
+ *
+ * @param tenantId 租户id
+ * @param account 账号名
+ * @return
+ */
+ public static UserInfo getUser(String tenantId, String account) {
+ return CacheUtil.get(USER_CACHE, USER_CACHE_ACCOUNT, tenantId + StringPool.DASH + account, () -> getUserClient().userInfo(tenantId, account));
+ }
+
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/system/controller/DictBizController.java b/kn-sys-manager/src/main/java/org/springblade/auth/system/controller/DictBizController.java
new file mode 100644
index 0000000..2c40fbb
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/system/controller/DictBizController.java
@@ -0,0 +1,171 @@
+package org.springblade.auth.system.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.AllArgsConstructor;
+import org.springblade.auth.system.entity.DictBiz;
+import org.springblade.auth.system.service.IDictBizService;
+import org.springblade.auth.system.vo.DictBizVO;
+import org.springblade.auth.system.wrapper.DictBizWrapper;
+import org.springblade.core.cache.utils.CacheUtil;
+import org.springblade.core.mp.support.Condition;
+import org.springblade.core.mp.support.Query;
+import org.springblade.core.tenant.annotation.NonDS;
+import org.springblade.core.tool.api.R;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import springfox.documentation.annotations.ApiIgnore;
+
+import javax.validation.Valid;
+import java.util.List;
+import java.util.Map;
+
+import static org.springblade.core.cache.constant.CacheConstant.DICT_CACHE;
+
+/**
+ * 控制器
+ *
+ * @author Chill
+ */
+@NonDS
+@RestController
+@AllArgsConstructor
+@RequestMapping("/dict-biz")
+@Api(value = "业务字典", tags = "业务字典")
+public class DictBizController {
+
+ private final IDictBizService dictService;
+
+ /**
+ * 详情
+ */
+ @GetMapping("/detail")
+ @ApiOperationSupport(order = 1)
+ @ApiOperation(value = "详情", notes = "传入dict")
+ public R detail(DictBiz dict) {
+ DictBiz detail = dictService.getOne(Condition.getQueryWrapper(dict));
+ return R.data(DictBizWrapper.build().entityVO(detail));
+ }
+
+ /**
+ * 列表
+ */
+ @GetMapping("/list")
+ @ApiImplicitParams({
+ @ApiImplicitParam(name = "code", value = "字典编号", paramType = "query", dataType = "string"),
+ @ApiImplicitParam(name = "dictValue", value = "字典名称", paramType = "query", dataType = "string")
+ })
+ @ApiOperationSupport(order = 2)
+ @ApiOperation(value = "列表", notes = "传入dict")
+ public R> list(@ApiIgnore @RequestParam Map dict) {
+ List list = dictService.list(Condition.getQueryWrapper(dict, DictBiz.class).lambda().orderByAsc(DictBiz::getSort));
+ return R.data(DictBizWrapper.build().listNodeVO(list));
+ }
+
+ /**
+ * 顶级列表
+ */
+ @GetMapping("/parent-list")
+ @ApiImplicitParams({
+ @ApiImplicitParam(name = "code", value = "字典编号", paramType = "query", dataType = "string"),
+ @ApiImplicitParam(name = "dictValue", value = "字典名称", paramType = "query", dataType = "string")
+ })
+ @ApiOperationSupport(order = 3)
+ @ApiOperation(value = "列表", notes = "传入dict")
+ public R> parentList(@ApiIgnore @RequestParam Map dict, Query query) {
+ return R.data(dictService.parentList(dict, query));
+ }
+
+ /**
+ * 子列表
+ */
+ @GetMapping("/child-list")
+ @ApiImplicitParams({
+ @ApiImplicitParam(name = "code", value = "字典编号", paramType = "query", dataType = "string"),
+ @ApiImplicitParam(name = "dictValue", value = "字典名称", paramType = "query", dataType = "string"),
+ @ApiImplicitParam(name = "parentId", value = "字典名称", paramType = "query", dataType = "string")
+ })
+ @ApiOperationSupport(order = 4)
+ @ApiOperation(value = "列表", notes = "传入dict")
+ public R> childList(@ApiIgnore @RequestParam Map dict, @RequestParam(required = false, defaultValue = "-1") Long parentId) {
+ return R.data(dictService.childList(dict, parentId));
+ }
+
+ /**
+ * 获取字典树形结构
+ */
+ @GetMapping("/tree")
+ @ApiOperationSupport(order = 5)
+ @ApiOperation(value = "树形结构", notes = "树形结构")
+ public R> tree() {
+ List tree = dictService.tree();
+ return R.data(tree);
+ }
+
+ /**
+ * 获取字典树形结构
+ */
+ @GetMapping("/parent-tree")
+ @ApiOperationSupport(order = 5)
+ @ApiOperation(value = "树形结构", notes = "树形结构")
+ public R> parentTree() {
+ List tree = dictService.parentTree();
+ return R.data(tree);
+ }
+
+ /**
+ * 新增或修改
+ */
+ @PostMapping("/submit")
+ @ApiOperationSupport(order = 6)
+ @ApiOperation(value = "新增或修改", notes = "传入dict")
+ public R submit(@Valid @RequestBody DictBiz dict) {
+ CacheUtil.clear(DICT_CACHE);
+ return R.status(dictService.submit(dict));
+ }
+
+
+ /**
+ * 删除
+ */
+ @PostMapping("/remove")
+ @ApiOperationSupport(order = 7)
+ @ApiOperation(value = "删除", notes = "传入ids")
+ public R remove(@ApiParam(value = "主键集合", required = true) @RequestParam String ids) {
+ CacheUtil.clear(DICT_CACHE);
+ return R.status(dictService.removeDict(ids));
+ }
+
+ /**
+ * 获取字典
+ */
+ @GetMapping("/dictionary")
+ @ApiOperationSupport(order = 8)
+ @ApiOperation(value = "获取字典", notes = "获取字典")
+ public R> dictionary(String code) {
+ List tree = dictService.getList(code);
+ return R.data(tree);
+ }
+
+ /**
+ * 获取字典树
+ */
+ @GetMapping("/dictionary-tree")
+ @ApiOperationSupport(order = 9)
+ @ApiOperation(value = "获取字典树", notes = "获取字典树")
+ public R> dictionaryTree(String code) {
+ List tree = dictService.getList(code);
+ return R.data(DictBizWrapper.build().listNodeVO(tree));
+ }
+
+
+}
diff --git a/kn-sys-manager/src/main/java/org/springblade/auth/system/controller/MenuController.java b/kn-sys-manager/src/main/java/org/springblade/auth/system/controller/MenuController.java
new file mode 100644
index 0000000..f11a73b
--- /dev/null
+++ b/kn-sys-manager/src/main/java/org/springblade/auth/system/controller/MenuController.java
@@ -0,0 +1,206 @@
+package org.springblade.auth.system.controller;
+
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.AllArgsConstructor;
+import org.springblade.auth.system.entity.Menu;
+import org.springblade.auth.system.service.IMenuService;
+import org.springblade.auth.system.vo.MenuVO;
+import org.springblade.auth.system.wrapper.MenuWrapper;
+import org.springblade.core.boot.ctrl.BladeController;
+import org.springblade.core.cache.utils.CacheUtil;
+import org.springblade.core.mp.support.Condition;
+import org.springblade.core.secure.BladeUser;
+import org.springblade.core.secure.annotation.PreAuth;
+import org.springblade.core.tenant.annotation.NonDS;
+import org.springblade.core.tool.api.R;
+import org.springblade.core.tool.constant.RoleConstant;
+import org.springblade.core.tool.support.Kv;
+import org.springblade.core.tool.utils.Func;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import springfox.documentation.annotations.ApiIgnore;
+
+import javax.validation.Valid;
+import java.util.List;
+import java.util.Map;
+
+import static org.springblade.core.cache.constant.CacheConstant.MENU_CACHE;
+
+
+/**
+ * 控制器
+ *
+ * @author Chill
+ */
+@NonDS
+@RestController
+@AllArgsConstructor
+@RequestMapping("/menu")
+@Api(value = "菜单", tags = "菜单")
+public class MenuController extends BladeController {
+
+ private final IMenuService menuService;
+
+ /**
+ * 详情
+ */
+ @GetMapping("/detail")
+ @PreAuth(RoleConstant.HAS_ROLE_ADMINISTRATOR)
+ @ApiOperationSupport(order = 1)
+ @ApiOperation(value = "详情", notes = "传入menu")
+ public R detail(Menu menu) {
+ Menu detail = menuService.getOne(Condition.getQueryWrapper(menu));
+ return R.data(MenuWrapper.build().entityVO(detail));
+ }
+
+ /**
+ * 列表
+ */
+ @GetMapping("/list")
+ @ApiImplicitParams({
+ @ApiImplicitParam(name = "code", value = "菜单编号", paramType = "query", dataType = "string"),
+ @ApiImplicitParam(name = "name", value = "菜单名称", paramType = "query", dataType = "string")
+ })
+ @PreAuth(RoleConstant.HAS_ROLE_ADMINISTRATOR)
+ @ApiOperationSupport(order = 2)
+ @ApiOperation(value = "列表", notes = "传入menu")
+ public R> list(@ApiIgnore @RequestParam Map menu) {
+ List