Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
L
loginservice
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
lanmw
loginservice
Commits
94e075e9
Commit
94e075e9
authored
Aug 11, 2026
by
xieshaohua
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
inti 海南农商单点
parent
c51bc65a
Show whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
1453 additions
and
2 deletions
+1453
-2
pom.xml
pom.xml
+10
-0
SsoApplication.java
src/main/java/com/keymobile/sso/SsoApplication.java
+7
-2
PortalAuthController.java
...main/java/com/keymobile/sso/api/PortalAuthController.java
+38
-0
RedisConfig.java
src/main/java/com/keymobile/sso/conf/RedisConfig.java
+110
-0
SsoProperties.java
src/main/java/com/keymobile/sso/conf/SsoProperties.java
+46
-0
SsoSecurityConfig.java
src/main/java/com/keymobile/sso/conf/SsoSecurityConfig.java
+1
-0
PortalLoginException.java
...ava/com/keymobile/sso/exception/PortalLoginException.java
+44
-0
RestExceptionHandler.java
...ava/com/keymobile/sso/exception/RestExceptionHandler.java
+6
-0
UtilException.java
src/main/java/com/keymobile/sso/exception/UtilException.java
+44
-0
AuthService.java
src/main/java/com/keymobile/sso/service/AuthService.java
+26
-0
PortalAuthService.java
...ain/java/com/keymobile/sso/service/PortalAuthService.java
+195
-0
IpUtil.java
src/main/java/com/keymobile/sso/utils/IpUtil.java
+106
-0
PortalTokenGenerator.java
...in/java/com/keymobile/sso/utils/PortalTokenGenerator.java
+89
-0
RedisUtil.java
src/main/java/com/keymobile/sso/utils/RedisUtil.java
+242
-0
ResponseUtil.java
src/main/java/com/keymobile/sso/utils/ResponseUtil.java
+62
-0
SM4Util.java
src/main/java/com/keymobile/sso/utils/SM4Util.java
+104
-0
Sm4CbcUtil.java
src/main/java/com/keymobile/sso/utils/Sm4CbcUtil.java
+237
-0
PortalAuthReq.java
src/main/java/com/keymobile/sso/vo/PortalAuthReq.java
+18
-0
ResponseVO.java
src/main/java/com/keymobile/sso/vo/ResponseVO.java
+51
-0
application-default.yml
src/main/resources/application-default.yml
+13
-0
application.yml
src/main/resources/application.yml
+4
-0
No files found.
pom.xml
View file @
94e075e9
...
...
@@ -52,6 +52,16 @@
<artifactId>
jasypt-spring-boot-starter
</artifactId>
<version>
3.0.3
</version>
</dependency>
<dependency>
<groupId>
org.projectlombok
</groupId>
<artifactId>
lombok
</artifactId>
<optional>
true
</optional>
</dependency>
<dependency>
<groupId>
cn.hutool
</groupId>
<artifactId>
hutool-all
</artifactId>
<version>
5.8.28
</version>
</dependency>
</dependencies>
<build>
...
...
src/main/java/com/keymobile/sso/SsoApplication.java
View file @
94e075e9
...
...
@@ -4,15 +4,20 @@ import com.keymobile.authservice.component.SecurityConfig;
import
org.springframework.boot.SpringApplication
;
import
org.springframework.boot.autoconfigure.SpringBootApplication
;
import
org.springframework.cloud.client.discovery.EnableDiscoveryClient
;
import
org.springframework.cloud.openfeign.EnableFeignClients
;
import
org.springframework.context.annotation.ComponentScan
;
import
org.springframework.context.annotation.FilterType
;
import
org.springframework.context.annotation.PropertySource
;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@ComponentScan
(
basePackages
=
{
"com.keymobile.sso"
,
"com.keymobile.config.logging"
,
"com.keymobile.config.naming"
,
"com.keymobile.config.redisclient"
,
"com.keymobile.authservice.component"
},
excludeFilters
=
{
"com.keymobile.config.logging"
,
"com.keymobile.config.naming"
,
"com.keymobile.config.redisclient"
,
"com.keymobile.config.feignclient"
,
"com.keymobile.authservice.component"
},
excludeFilters
=
{
@ComponentScan
.
Filter
(
type
=
FilterType
.
ASSIGNABLE_TYPE
,
value
=
SecurityConfig
.
class
)
})
@PropertySource
(
value
=
"classpath:/application.yml"
)
...
...
src/main/java/com/keymobile/sso/api/PortalAuthController.java
0 → 100644
View file @
94e075e9
package
com
.
keymobile
.
sso
.
api
;
import
com.keymobile.sso.service.PortalAuthService
;
import
com.keymobile.sso.utils.ResponseUtil
;
import
com.keymobile.sso.vo.PortalAuthReq
;
import
com.keymobile.sso.vo.ResponseVO
;
import
jakarta.servlet.http.HttpServletRequest
;
import
jakarta.servlet.http.HttpServletResponse
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RequestMethod
;
import
org.springframework.web.bind.annotation.RequestParam
;
import
org.springframework.web.bind.annotation.RestController
;
/**
* 门户免登录验签。
*/
@RestController
@RequestMapping
(
"/portalAuth"
)
@Slf4j
public
class
PortalAuthController
{
@Autowired
private
PortalAuthService
portalAuthService
;
@RequestMapping
(
value
=
"/verifyToken"
,
method
=
{
RequestMethod
.
POST
,
RequestMethod
.
GET
})
public
ResponseVO
<?>
verifyToken
(
@RequestParam
(
required
=
false
)
String
sign
,
HttpServletRequest
request
,
HttpServletResponse
response
)
{
try
{
return
portalAuthService
.
verifyAndLogin
(
sign
,
request
,
response
);
}
catch
(
Exception
e
)
{
log
.
error
(
"门户免登录验签异常:{}"
,
e
.
getMessage
(),
e
);
return
ResponseUtil
.
error
(
e
.
getMessage
()
!=
null
?
e
.
getMessage
()
:
"门户免登录验签异常"
);
}
}
}
src/main/java/com/keymobile/sso/conf/RedisConfig.java
0 → 100644
View file @
94e075e9
package
com
.
keymobile
.
sso
.
conf
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.data.redis.connection.RedisConnectionFactory
;
import
org.springframework.data.redis.core.*
;
import
org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
;
import
org.springframework.data.redis.serializer.StringRedisSerializer
;
import
org.springframework.session.data.redis.config.ConfigureRedisAction
;
/**
* redis配置.
* @author mahx
* @version 1.0
* @date 2019/12/27 16:55
*/
@Configuration
public
class
RedisConfig
{
/**
* 实例化 RedisTemplate 对象.
*
* @return redisTemplate
*/
@Bean
public
RedisTemplate
<
String
,
Object
>
functionDomainRedisTemplate
(
RedisConnectionFactory
redisConnectionFactory
)
{
RedisTemplate
<
String
,
Object
>
redisTemplate
=
new
RedisTemplate
<>();
initDomainRedisTemplate
(
redisTemplate
,
redisConnectionFactory
);
return
redisTemplate
;
}
/**
* 设置数据存入 redis 的序列化方式.
*
* @param redisTemplate redisTemplate
* @param factory RedisConnectionFactory
*/
private
void
initDomainRedisTemplate
(
RedisTemplate
<
String
,
Object
>
redisTemplate
,
RedisConnectionFactory
factory
)
{
redisTemplate
.
setKeySerializer
(
new
StringRedisSerializer
());
redisTemplate
.
setHashKeySerializer
(
new
StringRedisSerializer
());
redisTemplate
.
setHashValueSerializer
(
new
GenericJackson2JsonRedisSerializer
());
redisTemplate
.
setValueSerializer
(
new
GenericJackson2JsonRedisSerializer
());
redisTemplate
.
setConnectionFactory
(
factory
);
}
/**
* 实例化 HashOperations 对象,可以使用 Hash 类型操作.
*
* @param redisTemplate redisTemplate
* @return HashOperations
*/
@Bean
public
HashOperations
<
String
,
String
,
Object
>
hashOperations
(
RedisTemplate
<
String
,
Object
>
redisTemplate
)
{
return
redisTemplate
.
opsForHash
();
}
/**
* 实例化 ValueOperations 对象,可以使用 String 操作.
*
* @param redisTemplate redisTemplate
* @return ValueOperations
*/
@Bean
public
ValueOperations
<
String
,
Object
>
valueOperations
(
RedisTemplate
<
String
,
Object
>
redisTemplate
)
{
return
redisTemplate
.
opsForValue
();
}
/**
* 实例化 ListOperations 对象,可以使用 List 操作.
*
* @param redisTemplate redisTemplate
* @return ListOperations
*/
@Bean
public
ListOperations
<
String
,
Object
>
listOperations
(
RedisTemplate
<
String
,
Object
>
redisTemplate
)
{
return
redisTemplate
.
opsForList
();
}
/**
* 实例化 SetOperations 对象,可以使用 Set 操作.
*
* @param redisTemplate redisTemplate
* @return SetOperations
*/
@Bean
public
SetOperations
<
String
,
Object
>
setOperations
(
RedisTemplate
<
String
,
Object
>
redisTemplate
)
{
return
redisTemplate
.
opsForSet
();
}
/**
* 实例化 ZSetOperations 对象,可以使用 ZSet 操作.
*
* @param redisTemplate redisTemplate
* @return ZSetOperations
*/
@Bean
public
ZSetOperations
<
String
,
Object
>
zsetOperations
(
RedisTemplate
<
String
,
Object
>
redisTemplate
)
{
return
redisTemplate
.
opsForZSet
();
}
}
src/main/java/com/keymobile/sso/conf/SsoProperties.java
0 → 100644
View file @
94e075e9
package
com
.
keymobile
.
sso
.
conf
;
import
lombok.Data
;
import
org.springframework.boot.context.properties.ConfigurationProperties
;
import
org.springframework.cloud.context.config.annotation.RefreshScope
;
import
org.springframework.stereotype.Component
;
/**
* 单点登录配置(单一门户对接,无需多应用列表)。
*/
@Data
@Component
@RefreshScope
@ConfigurationProperties
(
prefix
=
"sso"
)
public
class
SsoProperties
{
/**
* 是否启用 IP 白名单校验,默认开启
*/
private
boolean
ipWhitelistEnabled
=
false
;
/**
* 应用标识(用于校验票据中的 appId,可选)
*/
private
String
appId
;
/**
* 门户服务器 IP(支持多个,逗号分隔)
*/
private
String
portalIp
;
/**
* SM4 加密密钥(32 位十六进制字符串)
*/
private
String
sm4Key
;
/**
* 票据有效期(秒),默认 300 秒
*/
private
long
expireSeconds
=
300
;
/**
* 是否启用防重放校验
*/
private
boolean
replayProtection
=
true
;
}
src/main/java/com/keymobile/sso/conf/SsoSecurityConfig.java
View file @
94e075e9
...
...
@@ -42,6 +42,7 @@ public class SsoSecurityConfig {
@Bean
protected
SecurityFilterChain
securityFilterChain
(
HttpSecurity
http
)
throws
Exception
{
http
.
authorizeHttpRequests
((
request
)
->
{
request
.
requestMatchers
(
"/portalAuth/**"
).
permitAll
();
request
.
anyRequest
().
authenticated
();
});
http
.
csrf
((
httpSecurityCsrfConfigurer
)
->
{
...
...
src/main/java/com/keymobile/sso/exception/PortalLoginException.java
0 → 100644
View file @
94e075e9
package
com
.
keymobile
.
sso
.
exception
;
/**
* 工具类通用异常(非受检),用于加解密等工具方法中包装底层错误。
*/
public
class
PortalLoginException
extends
RuntimeException
{
private
static
final
long
serialVersionUID
=
1L
;
private
String
cnMessage
;
public
PortalLoginException
()
{
super
();
}
public
PortalLoginException
(
String
message
)
{
super
(
message
);
this
.
cnMessage
=
message
;
}
public
PortalLoginException
(
String
message
,
String
cnMessage
)
{
super
(
message
);
this
.
cnMessage
=
cnMessage
;
}
public
PortalLoginException
(
String
message
,
Throwable
cause
)
{
super
(
message
,
cause
);
this
.
cnMessage
=
message
;
}
public
PortalLoginException
(
String
message
,
String
cnMessage
,
Throwable
cause
)
{
super
(
message
,
cause
);
this
.
cnMessage
=
cnMessage
;
}
public
PortalLoginException
(
Throwable
cause
)
{
super
(
cause
);
this
.
cnMessage
=
cause
!=
null
?
cause
.
getMessage
()
:
null
;
}
public
String
getCnMessage
()
{
return
cnMessage
;
}
}
src/main/java/com/keymobile/sso/exception/RestExceptionHandler.java
View file @
94e075e9
...
...
@@ -28,6 +28,12 @@ public class RestExceptionHandler extends ResponseEntityExceptionHandler {
return
buildResponseEntity
(
apiError
);
}
@ExceptionHandler
(
UtilException
.
class
)
protected
ResponseEntity
<
Object
>
handleUtilException
(
UtilException
ex
,
WebRequest
request
)
{
ApiError
apiError
=
new
ApiError
(
HttpStatus
.
INTERNAL_SERVER_ERROR
,
ex
.
getMessage
(),
ex
.
getCnMessage
(),
ex
);
return
buildResponseEntity
(
apiError
);
}
@ExceptionHandler
(
Exception
.
class
)
protected
ResponseEntity
<
Object
>
handlException
(
Exception
ex
,
WebRequest
request
)
{
ApiError
apiError
;
...
...
src/main/java/com/keymobile/sso/exception/UtilException.java
0 → 100644
View file @
94e075e9
package
com
.
keymobile
.
sso
.
exception
;
/**
* 工具类通用异常(非受检),用于加解密等工具方法中包装底层错误。
*/
public
class
UtilException
extends
RuntimeException
{
private
static
final
long
serialVersionUID
=
1L
;
private
String
cnMessage
;
public
UtilException
()
{
super
();
}
public
UtilException
(
String
message
)
{
super
(
message
);
this
.
cnMessage
=
message
;
}
public
UtilException
(
String
message
,
String
cnMessage
)
{
super
(
message
);
this
.
cnMessage
=
cnMessage
;
}
public
UtilException
(
String
message
,
Throwable
cause
)
{
super
(
message
,
cause
);
this
.
cnMessage
=
message
;
}
public
UtilException
(
String
message
,
String
cnMessage
,
Throwable
cause
)
{
super
(
message
,
cause
);
this
.
cnMessage
=
cnMessage
;
}
public
UtilException
(
Throwable
cause
)
{
super
(
cause
);
this
.
cnMessage
=
cause
!=
null
?
cause
.
getMessage
()
:
null
;
}
public
String
getCnMessage
()
{
return
cnMessage
;
}
}
src/main/java/com/keymobile/sso/service/AuthService.java
0 → 100644
View file @
94e075e9
package
com
.
keymobile
.
sso
.
service
;
import
org.springframework.cloud.openfeign.FeignClient
;
import
org.springframework.web.bind.annotation.*
;
import
java.util.List
;
import
java.util.Map
;
@FeignClient
(
value
=
"authService"
)
public
interface
AuthService
{
@RequestMapping
(
value
=
"/users/findByName"
)
List
<
Map
<
String
,
Object
>>
getUserByName
(
@RequestParam
(
value
=
"match"
)
String
match
);
@PostMapping
(
value
=
"/users"
)
Map
<
String
,
Object
>
addUser
(
@RequestBody
Map
<
String
,
Object
>
user
);
@PostMapping
(
value
=
"/users/{userId}"
)
Map
<
String
,
Object
>
updateUser
(
@PathVariable
(
value
=
"userId"
)
Long
userId
,
@RequestBody
Map
<
String
,
Object
>
user
);
@GetMapping
(
"/users/{userId}"
)
Map
<
String
,
Object
>
getUserById
(
@PathVariable
(
"userId"
)
Long
userId
);
@GetMapping
(
"/users/getByName"
)
Map
<
String
,
Object
>
getByName
(
@RequestParam
(
"name"
)
String
name
);
}
src/main/java/com/keymobile/sso/service/PortalAuthService.java
0 → 100644
View file @
94e075e9
package
com
.
keymobile
.
sso
.
service
;
import
cn.hutool.core.date.TimeInterval
;
import
cn.hutool.json.JSONUtil
;
import
com.keymobile.sso.conf.SsoProperties
;
import
com.keymobile.sso.exception.UtilException
;
import
com.keymobile.sso.logging.LogConstants
;
import
com.keymobile.sso.logging.LogManager
;
import
com.keymobile.sso.utils.*
;
import
com.keymobile.sso.vo.PortalAuthReq
;
import
com.keymobile.sso.vo.ResponseVO
;
import
jakarta.servlet.http.HttpServletRequest
;
import
jakarta.servlet.http.HttpServletResponse
;
import
jakarta.servlet.http.HttpSession
;
import
lombok.extern.slf4j.Slf4j
;
import
org.apache.commons.lang.StringUtils
;
import
org.slf4j.MDC
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.security.authentication.UsernamePasswordAuthenticationToken
;
import
org.springframework.security.core.context.SecurityContextHolder
;
import
org.springframework.security.core.userdetails.UserDetails
;
import
org.springframework.security.provisioning.UserDetailsManager
;
import
org.springframework.security.web.authentication.WebAuthenticationDetails
;
import
org.springframework.stereotype.Service
;
import
java.io.IOException
;
import
java.net.URLDecoder
;
import
java.nio.charset.StandardCharsets
;
import
java.util.Map
;
import
java.util.concurrent.TimeUnit
;
@Slf4j
@Service
public
class
PortalAuthService
{
@Autowired
private
SsoProperties
ssoProperties
;
@Autowired
private
RedisUtil
redisUtil
;
@Autowired
private
AuthService
authService
;
@Autowired
private
UserDetailsManager
customizedUserDetailService
;
@Value
(
"${self.login.redirectUrl:http://139.198.127.54:18079/data-center/view/index}"
)
private
String
ssoRedirectUrl
;
private
static
final
String
REPLAY_KEY_PREFIX
=
"sso:auth:replay:"
;
/**
* 门户免登录验签。
* 成功:建立会话并重定向到门户页面;失败:返回错误响应。
*
* @return 失败时返回错误信息;成功重定向后返回 null
*/
public
ResponseVO
<?>
verifyAndLogin
(
String
sign
,
HttpServletRequest
request
,
HttpServletResponse
response
)
throws
IOException
{
TimeInterval
timer
=
cn
.
hutool
.
core
.
date
.
DateUtil
.
timer
();
log
.
info
(
"sso请求参数:{}"
,
sign
);
// ① 参数校验
if
(
StringUtils
.
isBlank
(
sign
))
{
return
ResponseUtil
.
error
(
"请求参数不完整"
);
}
log
.
info
(
"单点配置:{}"
,
JSONUtil
.
toJsonStr
(
ssoProperties
));
if
(
StringUtils
.
isBlank
(
ssoProperties
.
getSm4Key
()))
{
return
ResponseUtil
.
error
(
"单点登录未配置"
);
}
String
requestIp
=
IpUtil
.
getIp
();
log
.
info
(
"请求ip:{}"
,
requestIp
);
// ② IP 白名单校验(可通过 sso.ipWhitelistEnabled 关闭)
if
(
ssoProperties
.
isIpWhitelistEnabled
())
{
if
(!
checkIpWhitelist
(
requestIp
,
ssoProperties
.
getPortalIp
()))
{
log
.
warn
(
"非法请求来源IP:{},允许IP:{}"
,
requestIp
,
ssoProperties
.
getPortalIp
());
return
ResponseUtil
.
error
(
"非法请求来源IP"
);
}
}
else
{
log
.
info
(
"IP白名单校验已关闭,跳过校验"
);
}
// ③ SM4 解密
String
plainText
;
try
{
plainText
=
Sm4CbcUtil
.
decrypt
(
sign
,
ssoProperties
.
getSm4Key
());
log
.
info
(
"票据:{}"
,
plainText
);
}
catch
(
Exception
e
)
{
log
.
error
(
"票据解密失败:{}"
,
e
.
getMessage
());
try
{
String
urlDecoder
=
URLDecoder
.
decode
(
sign
,
StandardCharsets
.
UTF_8
);
log
.
info
(
"url解码签名:{}"
,
urlDecoder
);
plainText
=
Sm4CbcUtil
.
decrypt
(
urlDecoder
,
ssoProperties
.
getSm4Key
());
log
.
info
(
"url解码票据:{}"
,
plainText
);
}
catch
(
Exception
ex
)
{
log
.
error
(
"url解码票据解密失败:{}"
,
e
.
getMessage
());
return
ResponseUtil
.
error
(
"票据解密失败"
);
}
}
// ④ 票据完整性校验(明文:emplyId,timestamp[,appId])
String
[]
parts
=
plainText
.
split
(
"\\|"
);
if
(
parts
.
length
<
4
)
{
return
ResponseUtil
.
error
(
"票据格式错误"
);
}
String
emplyId
=
parts
[
3
];
String
timestampStr
=
parts
[
1
];
String
portalIp
=
parts
[
0
];
// 若票据含 appId,则与配置中的 appId 比对
if
(
parts
.
length
>=
3
&&
StringUtils
.
isNotBlank
(
ssoProperties
.
getAppId
()))
{
String
appIdInToken
=
parts
[
2
];
if
(!
ssoProperties
.
getAppId
().
equals
(
appIdInToken
))
{
log
.
warn
(
"应用标识不一致: 配置={}, 票据={}"
,
ssoProperties
.
getAppId
(),
appIdInToken
);
return
ResponseUtil
.
error
(
"应用标识不一致"
);
}
}
if
(!
checkIpWhitelist
(
portalIp
,
ssoProperties
.
getPortalIp
()))
{
log
.
warn
(
"非法请求来源IP:{},允许IP:{}"
,
requestIp
,
ssoProperties
.
getPortalIp
());
return
ResponseUtil
.
error
(
"非法请求来源IP"
);
}
// 时间戳时效校验
long
timestamp
;
try
{
timestamp
=
Long
.
parseLong
(
timestampStr
);
}
catch
(
NumberFormatException
e
)
{
return
ResponseUtil
.
error
(
"时间戳格式错误"
);
}
long
now
=
System
.
currentTimeMillis
();
long
diffSeconds
=
(
now
-
timestamp
)
/
1000
;
if
(
diffSeconds
>
ssoProperties
.
getExpireSeconds
())
{
log
.
warn
(
"票据已过期: 时间差{}秒, 有效期{}秒"
,
diffSeconds
,
ssoProperties
.
getExpireSeconds
());
return
ResponseUtil
.
error
(
"票据已过期"
);
}
if
(
diffSeconds
<
-
5
)
{
return
ResponseUtil
.
error
(
"票据时间异常"
);
}
// 防重放校验
if
(
ssoProperties
.
isReplayProtection
())
{
String
replayKey
=
REPLAY_KEY_PREFIX
+
emplyId
+
":"
+
timestampStr
;
boolean
notUsed
=
redisUtil
.
setIfAbsent
(
replayKey
,
"1"
,
ssoProperties
.
getExpireSeconds
(),
TimeUnit
.
SECONDS
);
if
(!
notUsed
)
{
log
.
warn
(
"票据已被使用: emplyId={}, timestamp={}"
,
emplyId
,
timestampStr
);
return
ResponseUtil
.
error
(
"票据已被使用"
);
}
}
log
.
info
(
"门户免登录验签通过: emplyId={}, 耗时:{}ms"
,
emplyId
,
timer
.
intervalRestart
());
// ⑤ 用户身份校验 + 建立会话,成功后重定向
return
ssoLogin
(
emplyId
,
request
,
response
);
}
private
ResponseVO
<?>
ssoLogin
(
String
userName
,
HttpServletRequest
request
,
HttpServletResponse
response
)
throws
IOException
{
Map
<
String
,
Object
>
user
=
authService
.
getByName
(
userName
);
if
(
user
==
null
)
{
return
ResponseUtil
.
error
(
userName
+
"员工编号不存在"
);
}
UserDetails
userDetails
=
customizedUserDetailService
.
loadUserByUsername
(
userName
);
UsernamePasswordAuthenticationToken
authentication
=
new
UsernamePasswordAuthenticationToken
(
userDetails
,
userDetails
.
getPassword
(),
userDetails
.
getAuthorities
());
authentication
.
setDetails
(
new
WebAuthenticationDetails
(
request
));
SecurityContextHolder
.
getContext
().
setAuthentication
(
authentication
);
HttpSession
session
=
request
.
getSession
(
true
);
session
.
setAttribute
(
"SPRING_SECURITY_CONTEXT"
,
SecurityContextHolder
.
getContext
());
log
.
info
(
"单点登录用户:{}"
,
userName
);
MDC
.
put
(
"user"
,
userName
);
MDC
.
put
(
"session"
,
session
.
getId
());
LogManager
.
logInfo
(
LogConstants
.
CTX_AUDIT
,
"登录"
);
response
.
sendRedirect
(
ssoRedirectUrl
);
return
null
;
}
private
boolean
checkIpWhitelist
(
String
requestIp
,
String
allowedIps
)
{
if
(
StringUtils
.
isBlank
(
allowedIps
))
{
return
false
;
}
String
[]
ipArray
=
allowedIps
.
split
(
","
);
for
(
String
ip
:
ipArray
)
{
if
(
ip
.
trim
().
equals
(
requestIp
))
{
return
true
;
}
}
return
false
;
}
}
src/main/java/com/keymobile/sso/utils/IpUtil.java
0 → 100644
View file @
94e075e9
package
com
.
keymobile
.
sso
.
utils
;
import
jakarta.servlet.http.HttpServletRequest
;
import
org.springframework.web.context.request.RequestAttributes
;
import
org.springframework.web.context.request.RequestContextHolder
;
import
org.springframework.web.context.request.ServletRequestAttributes
;
/**
* IP工具类,获取客户端真实IP
*/
public
class
IpUtil
{
/**
* 直接调用获取当前请求客户端IP(无需传request)
* @return 客户端IP地址
*/
public
static
String
getIp
()
{
HttpServletRequest
request
=
getHttpServletRequest
();
if
(
request
==
null
)
{
return
"unknown"
;
}
return
getIpAddr
(
request
);
}
/**
* 根据request获取真实IP核心方法
*/
public
static
String
getIpAddr
(
HttpServletRequest
request
)
{
// 1. 优先取反向代理转发IP
String
xForwardedFor
=
request
.
getHeader
(
"X-Forwarded-For"
);
if
(
xForwardedFor
!=
null
&&
!
xForwardedFor
.
isEmpty
()
&&
!
"unknown"
.
equalsIgnoreCase
(
xForwardedFor
))
{
// 多次反向代理后会有多个ip,第一个为真实ip
String
[]
ipArray
=
xForwardedFor
.
split
(
","
);
for
(
String
ip
:
ipArray
)
{
String
realIp
=
ip
.
trim
();
if
(!
realIp
.
isEmpty
()
&&
!
"unknown"
.
equalsIgnoreCase
(
realIp
))
{
return
realIp
;
}
}
}
// 2. Nginx代理真实IP
String
xRealIp
=
request
.
getHeader
(
"X-Real-IP"
);
if
(
xRealIp
!=
null
&&
!
xRealIp
.
isEmpty
()
&&
!
"unknown"
.
equalsIgnoreCase
(
xRealIp
))
{
return
xRealIp
;
}
// 3. Apache代理
String
proxyClientIp
=
request
.
getHeader
(
"Proxy-Client-IP"
);
if
(
proxyClientIp
!=
null
&&
!
proxyClientIp
.
isEmpty
()
&&
!
"unknown"
.
equalsIgnoreCase
(
proxyClientIp
))
{
return
proxyClientIp
;
}
// 4. WL代理
String
wlProxyClientIp
=
request
.
getHeader
(
"WL-Proxy-Client-IP"
);
if
(
wlProxyClientIp
!=
null
&&
!
wlProxyClientIp
.
isEmpty
()
&&
!
"unknown"
.
equalsIgnoreCase
(
wlProxyClientIp
))
{
return
wlProxyClientIp
;
}
// 5. 无代理,直接取原始请求地址
return
request
.
getRemoteAddr
();
}
/**
* 从Spring上下文获取HttpServletRequest
*/
private
static
HttpServletRequest
getHttpServletRequest
()
{
RequestAttributes
requestAttributes
=
RequestContextHolder
.
getRequestAttributes
();
if
(
requestAttributes
==
null
)
{
return
null
;
}
return
((
ServletRequestAttributes
)
requestAttributes
).
getRequest
();
}
/**
* 判断是否为内网IP
*/
public
static
boolean
isInnerIp
(
String
ip
)
{
if
(
ip
==
null
||
ip
.
isEmpty
())
{
return
false
;
}
// 127.0.0.1
if
(
ip
.
startsWith
(
"127."
))
{
return
true
;
}
// 10.0.0.0/8
if
(
ip
.
startsWith
(
"10."
))
{
return
true
;
}
// 172.16.0.0/12
if
(
ip
.
startsWith
(
"172."
))
{
String
[]
split
=
ip
.
split
(
"\\."
);
if
(
split
.
length
>=
2
)
{
try
{
int
second
=
Integer
.
parseInt
(
split
[
1
]);
if
(
second
>=
16
&&
second
<=
31
)
{
return
true
;
}
}
catch
(
NumberFormatException
ignored
)
{
}
}
}
// 192.168.0.0/16
return
ip
.
startsWith
(
"192.168."
);
}
}
src/main/java/com/keymobile/sso/utils/PortalTokenGenerator.java
0 → 100644
View file @
94e075e9
package
com
.
keymobile
.
sso
.
utils
;
import
cn.hutool.core.codec.Base64Encoder
;
import
java.net.URLEncoder
;
import
java.nio.charset.StandardCharsets
;
import
java.util.Map
;
/**
* @author xiesh
* @version 1.0.0
* @date 2026/8/4
* @desc
*/
public
class
PortalTokenGenerator
{
/**
* 生成免登录加密票据
* @param emplyId 员工工号
* @param appId 应用标识
* @param sm4Key SM4密钥(32位十六进制字符串)
* @return Base64编码的加密票据
*/
public
static
String
generateToken
(
String
emplyId
,
String
appId
,
String
sm4Key
)
{
// 1. 生成时间戳(毫秒)
String
timestamp
=
String
.
valueOf
(
System
.
currentTimeMillis
());
// 2. 拼接明文:工号,时间戳,应用标识
String
plaintext
=
emplyId
+
","
+
timestamp
+
","
+
appId
;
// 3. SM4-CBC 加密
String
encrypt
=
Sm4CbcUtil
.
encrypt
(
plaintext
,
sm4Key
);
return
encrypt
;
}
/**
* 生成 iframe 嵌入URL
*/
public
static
String
generateIframeUrl
(
String
autoLoginUrl
,
String
token
,
String
targetPath
,
Map
<
String
,
String
>
extraParams
)
{
StringBuilder
url
=
new
StringBuilder
(
autoLoginUrl
);
url
.
append
(
"?code="
).
append
(
URLEncoder
.
encode
(
token
,
StandardCharsets
.
UTF_8
));
url
.
append
(
"&path="
).
append
(
targetPath
);
if
(
extraParams
!=
null
)
{
for
(
Map
.
Entry
<
String
,
String
>
entry
:
extraParams
.
entrySet
())
{
url
.
append
(
"&"
).
append
(
entry
.
getKey
()).
append
(
"="
)
.
append
(
URLEncoder
.
encode
(
entry
.
getValue
(),
StandardCharsets
.
UTF_8
));
}
}
return
url
.
toString
();
}
public
static
void
main
(
String
[]
args
)
{
String
test1
=
generateToken
(
"test1"
,
"portal_asset"
,
"703AF86205DB25E6999CEAB2859D0FA1"
);
System
.
out
.
println
(
test1
);
String
test2
=
Sm4CbcUtil
.
decrypt
(
test1
,
"703AF86205DB25E6999CEAB2859D0FA1"
);
System
.
out
.
println
(
test2
);
}
}
\ No newline at end of file
src/main/java/com/keymobile/sso/utils/RedisUtil.java
0 → 100644
View file @
94e075e9
package
com
.
keymobile
.
sso
.
utils
;
import
org.apache.commons.lang.StringUtils
;
import
org.slf4j.Logger
;
import
org.slf4j.LoggerFactory
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.data.redis.core.RedisTemplate
;
import
org.springframework.data.redis.core.ValueOperations
;
import
org.springframework.data.redis.core.script.DefaultRedisScript
;
import
org.springframework.data.redis.core.script.RedisScript
;
import
org.springframework.stereotype.Component
;
import
org.springframework.util.CollectionUtils
;
import
java.util.*
;
import
java.util.concurrent.TimeUnit
;
@Component
public
class
RedisUtil
{
private
static
final
Long
SUCCESS
=
1L
;
private
static
final
String
EMPTY_MSG
=
" empty key"
;
Logger
logger
=
LoggerFactory
.
getLogger
(
RedisUtil
.
class
);
@Autowired
private
RedisTemplate
<
String
,
Object
>
redisTemplate
;
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public
Object
get
(
String
key
)
{
return
key
==
null
?
null
:
redisTemplate
.
opsForValue
().
get
(
key
);
}
/**
* 获取所有缓存
*
*/
public
Map
<
String
,
Object
>
getAll
(
String
pattern
)
{
Map
<
String
,
Object
>
result
=
new
HashMap
<>();
//获取所有key
Set
<
String
>
keys
=
redisTemplate
.
keys
(
pattern
);
for
(
String
key
:
keys
)
{
result
.
put
(
key
,
get
(
key
));
}
return
result
;
}
/**
* setIfAbsent:key不存在才设置,同时设置过期时间
* 对应你业务代码:redisUtil.setIfAbsent(key, val, expire, unit)
* @param key 键
* @param value 值
* @param expire 过期时长
* @param timeUnit 时间单位
* @return true=设置成功(key不存在) false=key已存在
*/
public
Boolean
setIfAbsent
(
String
key
,
Object
value
,
long
expire
,
TimeUnit
timeUnit
)
{
ValueOperations
<
String
,
Object
>
ops
=
redisTemplate
.
opsForValue
();
// setIfAbsent + 过期时间 原子操作(推荐用于防重复提交、分布式锁)
return
ops
.
setIfAbsent
(
key
,
value
,
expire
,
timeUnit
);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public
boolean
set
(
String
key
,
Object
value
)
{
try
{
redisTemplate
.
opsForValue
().
set
(
key
,
value
);
return
true
;
}
catch
(
Exception
e
)
{
logger
.
error
(
"set value error:"
,
e
);
return
false
;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public
boolean
set
(
String
key
,
Object
value
,
long
time
)
{
try
{
if
(
time
>
0
)
{
redisTemplate
.
opsForValue
().
set
(
key
,
value
,
time
,
TimeUnit
.
SECONDS
);
}
else
{
set
(
key
,
value
);
}
return
true
;
}
catch
(
Exception
e
)
{
logger
.
error
(
"set value with expire time error:"
,
e
);
return
false
;
}
}
/**
* 删除建
*/
public
Long
del
(
String
...
key
)
{
if
(
key
!=
null
&&
key
.
length
>
0
)
{
if
(
key
.
length
==
1
)
{
return
Boolean
.
TRUE
.
equals
(
redisTemplate
.
delete
(
key
[
0
]))
?
1L
:
0L
;
}
else
{
return
redisTemplate
.
delete
(
Arrays
.
stream
(
key
).
toList
());
}
}
return
0L
;
}
/**
* 删除建
*/
public
Long
delLike
(
String
key
)
{
Set
<
String
>
keys
=
redisTemplate
.
keys
(
key
+
"*"
);
if
(!
CollectionUtils
.
isEmpty
(
keys
))
{
return
redisTemplate
.
delete
(
keys
);
}
return
0L
;
}
/**
* 递增
*
* @param key 键
* @param delta 增幅
* @return
*/
public
Long
incr
(
String
key
,
Long
delta
)
{
if
(
delta
<
0
)
{
logger
.
error
(
"递增幅度必须大于0"
);
}
return
redisTemplate
.
opsForValue
().
increment
(
key
,
delta
);
}
/**
* 递减
*/
public
Long
decr
(
String
key
,
Long
delta
)
{
if
(
delta
<
0
)
{
logger
.
error
(
"幅度必须大于0"
);
}
return
redisTemplate
.
opsForValue
().
increment
(
key
,
-
delta
);
}
/**
* 获取锁
*
* @param lockKey
* @param value
* @param expireTime:单位-秒
* @return
*/
public
boolean
getLock
(
String
lockKey
,
Object
value
,
int
expireTime
)
{
try
{
String
script
=
"if (redis.call('exists',KEYS[1]) == 0) then redis.call('setNx',KEYS[1],ARGV[1]) return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end"
;
RedisScript
<
Long
>
redisScript
=
new
DefaultRedisScript
<>(
script
,
Long
.
class
);
Object
result
=
redisTemplate
.
execute
(
redisScript
,
Collections
.
singletonList
(
lockKey
),
value
,
expireTime
);
if
(
SUCCESS
.
equals
(
result
))
{
return
true
;
}
}
catch
(
Exception
e
)
{
logger
.
error
(
"get redis lock error:"
,
e
);
}
return
false
;
}
/**
* 释放锁
*
* @param lockKey
* @param value
* @return
*/
public
boolean
releaseLock
(
String
lockKey
,
Object
value
)
{
try
{
String
script
=
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
;
RedisScript
<
Long
>
redisScript
=
new
DefaultRedisScript
<>(
script
,
Long
.
class
);
Object
result
=
redisTemplate
.
execute
(
redisScript
,
Collections
.
singletonList
(
lockKey
),
value
);
if
(
SUCCESS
.
equals
(
result
))
{
return
true
;
}
}
catch
(
Exception
e
)
{
logger
.
error
(
"release redis lock key:"
+
lockKey
+
" error:"
,
e
);
}
return
false
;
}
public
void
sendTopicMessage
(
String
topic
,
String
messageJson
)
{
redisTemplate
.
convertAndSend
(
topic
,
messageJson
);
}
public
Long
sAdd
(
String
key
,
String
...
values
)
{
if
(
StringUtils
.
isBlank
(
key
))
{
logger
.
error
(
EMPTY_MSG
);
return
0L
;
}
return
redisTemplate
.
opsForSet
().
add
(
key
,
values
);
}
public
Long
sRemove
(
String
key
,
String
...
values
)
{
if
(
StringUtils
.
isBlank
(
key
))
{
logger
.
error
(
EMPTY_MSG
);
return
0L
;
}
return
redisTemplate
.
opsForSet
().
remove
(
key
,
values
);
}
public
Boolean
sIsMember
(
String
key
,
Object
value
)
{
if
(
StringUtils
.
isBlank
(
key
))
{
logger
.
error
(
EMPTY_MSG
);
return
false
;
}
return
redisTemplate
.
opsForSet
().
isMember
(
key
,
value
);
}
public
Boolean
hasKey
(
String
key
)
{
return
redisTemplate
.
hasKey
(
key
);
}
public
Long
getExpire
(
String
key
)
{
if
(
StringUtils
.
isBlank
(
key
))
{
return
null
;
}
return
redisTemplate
.
getExpire
(
key
);
}
public
Boolean
expire
(
String
key
,
int
time
)
{
if
(
StringUtils
.
isBlank
(
key
))
{
return
false
;
}
return
redisTemplate
.
expire
(
key
,
time
,
TimeUnit
.
SECONDS
);
}
}
src/main/java/com/keymobile/sso/utils/ResponseUtil.java
0 → 100644
View file @
94e075e9
package
com
.
keymobile
.
sso
.
utils
;
import
com.keymobile.sso.vo.ResponseVO
;
/**
* 统一响应构造工具。
* <pre>
* 成功:{"code":"200","message":"成功","data":{...}}
* 失败:{"code":"500","message":"票据已过期"}
* </pre>
*/
public
final
class
ResponseUtil
{
public
static
final
String
CODE_SUCCESS
=
"200"
;
public
static
final
String
CODE_ERROR
=
"500"
;
public
static
final
String
MSG_SUCCESS
=
"成功"
;
private
ResponseUtil
()
{
}
/**
* 成功响应(无 data)
*/
public
static
<
T
>
ResponseVO
<
T
>
success
()
{
return
new
ResponseVO
<>(
CODE_SUCCESS
,
MSG_SUCCESS
);
}
/**
* 成功响应(带 data)
*/
public
static
<
T
>
ResponseVO
<
T
>
success
(
T
data
)
{
return
new
ResponseVO
<>(
CODE_SUCCESS
,
MSG_SUCCESS
,
data
);
}
/**
* 成功响应(自定义 message + data)
*/
public
static
<
T
>
ResponseVO
<
T
>
success
(
String
message
,
T
data
)
{
return
new
ResponseVO
<>(
CODE_SUCCESS
,
message
,
data
);
}
/**
* 失败响应(默认 code=500)
*/
public
static
<
T
>
ResponseVO
<
T
>
error
(
String
message
)
{
return
new
ResponseVO
<>(
CODE_ERROR
,
message
);
}
/**
* 失败响应(自定义 code)
*/
public
static
<
T
>
ResponseVO
<
T
>
error
(
String
code
,
String
message
)
{
return
new
ResponseVO
<>(
code
,
message
);
}
/**
* 失败响应(带 data)
*/
public
static
<
T
>
ResponseVO
<
T
>
error
(
String
code
,
String
message
,
T
data
)
{
return
new
ResponseVO
<>(
code
,
message
,
data
);
}
}
src/main/java/com/keymobile/sso/utils/SM4Util.java
0 → 100644
View file @
94e075e9
package
com
.
keymobile
.
sso
.
utils
;
import
org.bouncycastle.crypto.engines.SM4Engine
;
import
org.bouncycastle.crypto.modes.CBCBlockCipher
;
import
org.bouncycastle.crypto.paddings.PKCS7Padding
;
import
org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher
;
import
org.bouncycastle.crypto.params.KeyParameter
;
import
org.bouncycastle.crypto.params.ParametersWithIV
;
import
org.springframework.util.Base64Utils
;
import
java.nio.charset.StandardCharsets
;
import
java.util.Arrays
;
import
java.util.Base64
;
/**
* SM4 国密对称加密工具类
* 支持 SM4-CBC 模式,PKCS7 填充
*/
public
class
SM4Util
{
private
static
final
int
BLOCK_SIZE
=
16
;
// SM4 块大小 16 字节
/**
* SM4-CBC 加密
* @param keyHex 32位十六进制密钥字符串
* @param plaintext 明文字节数组
* @return 密文字节数组
*/
public
static
byte
[]
encryptCBC
(
String
keyHex
,
byte
[]
plaintext
)
{
byte
[]
key
=
hexStringToBytes
(
keyHex
);
byte
[]
iv
=
new
byte
[
BLOCK_SIZE
];
// IV 全零(与门户侧约定)
return
encrypt
(
plaintext
,
key
,
iv
);
}
/**
* SM4-CBC 解密
* @param keyHex 32位十六进制密钥字符串
* @param ciphertext 密文字节数组
* @return 明文字节数组
*/
public
static
byte
[]
decryptCBC
(
String
keyHex
,
byte
[]
ciphertext
)
{
byte
[]
key
=
hexStringToBytes
(
keyHex
);
byte
[]
iv
=
new
byte
[
BLOCK_SIZE
];
// IV 全零(与门户侧约定)
return
decrypt
(
ciphertext
,
key
,
iv
);
}
private
static
byte
[]
encrypt
(
byte
[]
plaintext
,
byte
[]
key
,
byte
[]
iv
)
{
try
{
PaddedBufferedBlockCipher
cipher
=
new
PaddedBufferedBlockCipher
(
new
CBCBlockCipher
(
new
SM4Engine
()),
new
PKCS7Padding
());
cipher
.
init
(
true
,
new
ParametersWithIV
(
new
KeyParameter
(
key
),
iv
));
byte
[]
output
=
new
byte
[
cipher
.
getOutputSize
(
plaintext
.
length
)];
int
len
=
cipher
.
processBytes
(
plaintext
,
0
,
plaintext
.
length
,
output
,
0
);
len
+=
cipher
.
doFinal
(
output
,
len
);
return
Arrays
.
copyOf
(
output
,
len
);
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
"SM4加密失败"
,
e
);
}
}
private
static
byte
[]
decrypt
(
byte
[]
ciphertext
,
byte
[]
key
,
byte
[]
iv
)
{
try
{
PaddedBufferedBlockCipher
cipher
=
new
PaddedBufferedBlockCipher
(
new
CBCBlockCipher
(
new
SM4Engine
()),
new
PKCS7Padding
());
cipher
.
init
(
false
,
new
ParametersWithIV
(
new
KeyParameter
(
key
),
iv
));
byte
[]
output
=
new
byte
[
cipher
.
getOutputSize
(
ciphertext
.
length
)];
int
len
=
cipher
.
processBytes
(
ciphertext
,
0
,
ciphertext
.
length
,
output
,
0
);
len
+=
cipher
.
doFinal
(
output
,
len
);
return
Arrays
.
copyOf
(
output
,
len
);
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
"SM4解密失败"
,
e
);
}
}
private
static
byte
[]
hexStringToBytes
(
String
hexString
)
{
int
len
=
hexString
.
length
();
byte
[]
data
=
new
byte
[
len
/
2
];
for
(
int
i
=
0
;
i
<
len
;
i
+=
2
)
{
data
[
i
/
2
]
=
(
byte
)
((
Character
.
digit
(
hexString
.
charAt
(
i
),
16
)
<<
4
)
+
Character
.
digit
(
hexString
.
charAt
(
i
+
1
),
16
));
}
return
data
;
}
public
static
void
main
(
String
[]
arg
){
//
// String plainText = null;
// try {
// plainText = new String(SM4Util.decryptCBC("703AF86205DB25E6999CEAB2859D0FA1", mms), StandardCharsets.UTF_8);
// System.out.println(plainText);
//
// } catch (Exception e) {
// System.out.println(e);
// }
// System.out.println(plainText);
}
}
\ No newline at end of file
src/main/java/com/keymobile/sso/utils/Sm4CbcUtil.java
0 → 100644
View file @
94e075e9
package
com
.
keymobile
.
sso
.
utils
;
import
com.keymobile.sso.exception.UtilException
;
import
org.apache.commons.lang.StringUtils
;
import
org.bouncycastle.jce.provider.BouncyCastleProvider
;
import
org.springframework.util.Base64Utils
;
import
javax.crypto.BadPaddingException
;
import
javax.crypto.Cipher
;
import
javax.crypto.IllegalBlockSizeException
;
import
javax.crypto.NoSuchPaddingException
;
import
javax.crypto.spec.IvParameterSpec
;
import
javax.crypto.spec.SecretKeySpec
;
import
java.net.URLDecoder
;
import
java.nio.charset.StandardCharsets
;
import
java.security.InvalidAlgorithmParameterException
;
import
java.security.InvalidKeyException
;
import
java.security.NoSuchAlgorithmException
;
import
java.security.NoSuchProviderException
;
import
java.security.SecureRandom
;
import
java.security.Security
;
import
java.security.spec.InvalidKeySpecException
;
import
java.util.Base64
;
import
java.util.Optional
;
/**
* 功能描述:SM4-CBC 模式加解密工具类
* 密钥长度固定为16字节,IV随机生成并与密文一同存储(IV在前,密文在后)
*
* @author 张冠松
* @Classname Sm4CbcUtil
* @Date 2026/3/10 10:43
*/
public
class
Sm4CbcUtil
{
static
{
// 添加Bouncy Castle安全提供者,若失败则抛出异常(但类加载失败将导致后续所有操作不可用)
try
{
Security
.
addProvider
(
new
BouncyCastleProvider
());
}
catch
(
Exception
e
)
{
throw
new
UtilException
(
"Bouncy Castle 提供者注册失败"
,
e
);
}
}
private
static
final
String
ALGORITHM
=
"SM4"
;
private
static
final
String
TRANSFORMATION
=
"SM4/CBC/PKCS7Padding"
;
private
static
final
int
IV_LENGTH
=
16
;
private
static
final
int
SALT_LENGTH
=
16
;
private
static
final
int
KEY_LENGTH_BITS
=
128
;
private
static
final
int
PBKDF2_ITERATIONS
=
100000
;
/**
* 使用口令加密
*
* @param plainText 明文(UTF-8编码)
* @param password 口令
* @return Base64字符串(格式:盐(16字节) + IV(16字节) + 密文)
* @throws UtilException 加密过程中的错误
*/
public
static
String
encrypt
(
String
plainText
,
String
password
)
throws
UtilException
{
// 参数校验
if
(
plainText
==
null
||
password
==
null
)
{
throw
new
UtilException
(
"明文和口令不能为null"
);
}
// 1. 生成随机盐
byte
[]
salt
=
new
byte
[
SALT_LENGTH
];
try
{
new
SecureRandom
().
nextBytes
(
salt
);
}
catch
(
Exception
e
)
{
throw
new
UtilException
(
"生成随机盐失败"
,
e
);
}
// 2. 从口令派生密钥
byte
[]
keyBytes
=
deriveKey
(
password
,
salt
);
// 3. 生成随机IV
byte
[]
iv
=
new
byte
[
IV_LENGTH
];
try
{
new
SecureRandom
().
nextBytes
(
iv
);
}
catch
(
Exception
e
)
{
throw
new
UtilException
(
"生成随机IV失败"
,
e
);
}
// 4. 加密
try
{
Cipher
cipher
=
Cipher
.
getInstance
(
TRANSFORMATION
,
"BC"
);
SecretKeySpec
keySpec
=
new
SecretKeySpec
(
keyBytes
,
ALGORITHM
);
IvParameterSpec
ivSpec
=
new
IvParameterSpec
(
iv
);
cipher
.
init
(
Cipher
.
ENCRYPT_MODE
,
keySpec
,
ivSpec
);
byte
[]
plainBytes
=
plainText
.
getBytes
(
"UTF-8"
);
byte
[]
cipherBytes
=
cipher
.
doFinal
(
plainBytes
);
// 5. 拼接盐 + IV + 密文
byte
[]
result
=
new
byte
[
salt
.
length
+
iv
.
length
+
cipherBytes
.
length
];
System
.
arraycopy
(
salt
,
0
,
result
,
0
,
salt
.
length
);
System
.
arraycopy
(
iv
,
0
,
result
,
salt
.
length
,
iv
.
length
);
System
.
arraycopy
(
cipherBytes
,
0
,
result
,
salt
.
length
+
iv
.
length
,
cipherBytes
.
length
);
return
Base64
.
getEncoder
().
encodeToString
(
result
);
}
catch
(
NoSuchAlgorithmException
e
)
{
throw
new
UtilException
(
"不支持的算法(SM4)"
,
e
);
}
catch
(
NoSuchProviderException
e
)
{
throw
new
UtilException
(
"Bouncy Castle 提供者未找到"
,
e
);
}
catch
(
NoSuchPaddingException
e
)
{
throw
new
UtilException
(
"不支持的填充模式"
,
e
);
}
catch
(
InvalidKeyException
e
)
{
throw
new
UtilException
(
"无效的密钥(派生密钥可能不符合要求)"
,
e
);
}
catch
(
InvalidAlgorithmParameterException
e
)
{
throw
new
UtilException
(
"无效的算法参数(IV错误)"
,
e
);
}
catch
(
IllegalBlockSizeException
e
)
{
throw
new
UtilException
(
"加密块大小错误"
,
e
);
}
catch
(
BadPaddingException
e
)
{
throw
new
UtilException
(
"加密填充错误"
,
e
);
}
catch
(
Exception
e
)
{
throw
new
UtilException
(
"加密过程中发生未知错误"
,
e
);
}
}
/**
* 使用口令解密
*
* @param cipherTextBase64 Base64密文(包含盐和IV)
* @param password 口令
* @return 明文字符串
* @throws UtilException 解密过程中的错误
*/
public
static
String
decrypt
(
String
cipherTextBase64
,
String
password
)
throws
UtilException
{
// 参数校验
if
(
cipherTextBase64
==
null
||
password
==
null
)
{
throw
new
UtilException
(
"密文和口令不能为null"
);
}
// 1. Base64解码
byte
[]
input
;
try
{
input
=
Base64
.
getDecoder
().
decode
(
cipherTextBase64
);
}
catch
(
IllegalArgumentException
e
)
{
throw
new
UtilException
(
"Base64解码失败,密文格式不正确"
,
e
);
}
// 2. 检查长度是否至少包含盐和IV
if
(
input
.
length
<
SALT_LENGTH
+
IV_LENGTH
)
{
throw
new
UtilException
(
"密文长度不足,无法提取盐和IV"
);
}
// 3. 提取盐、IV和密文
byte
[]
salt
=
new
byte
[
SALT_LENGTH
];
byte
[]
iv
=
new
byte
[
IV_LENGTH
];
byte
[]
cipherBytes
=
new
byte
[
input
.
length
-
SALT_LENGTH
-
IV_LENGTH
];
System
.
arraycopy
(
input
,
0
,
salt
,
0
,
SALT_LENGTH
);
System
.
arraycopy
(
input
,
SALT_LENGTH
,
iv
,
0
,
IV_LENGTH
);
System
.
arraycopy
(
input
,
SALT_LENGTH
+
IV_LENGTH
,
cipherBytes
,
0
,
cipherBytes
.
length
);
// 4. 从口令派生密钥
byte
[]
keyBytes
=
deriveKey
(
password
,
salt
);
// 5. 解密
try
{
Cipher
cipher
=
Cipher
.
getInstance
(
TRANSFORMATION
,
"BC"
);
SecretKeySpec
keySpec
=
new
SecretKeySpec
(
keyBytes
,
ALGORITHM
);
IvParameterSpec
ivSpec
=
new
IvParameterSpec
(
iv
);
cipher
.
init
(
Cipher
.
DECRYPT_MODE
,
keySpec
,
ivSpec
);
byte
[]
plainBytes
=
cipher
.
doFinal
(
cipherBytes
);
return
new
String
(
plainBytes
,
"UTF-8"
);
}
catch
(
NoSuchAlgorithmException
e
)
{
throw
new
UtilException
(
"不支持的算法(SM4)"
,
e
);
}
catch
(
NoSuchProviderException
e
)
{
throw
new
UtilException
(
"Bouncy Castle 提供者未找到"
,
e
);
}
catch
(
NoSuchPaddingException
e
)
{
throw
new
UtilException
(
"不支持的填充模式"
,
e
);
}
catch
(
InvalidKeyException
e
)
{
throw
new
UtilException
(
"无效的密钥(口令错误或派生密钥无效)"
,
e
);
}
catch
(
InvalidAlgorithmParameterException
e
)
{
throw
new
UtilException
(
"无效的算法参数(IV可能被篡改)"
,
e
);
}
catch
(
IllegalBlockSizeException
e
)
{
throw
new
UtilException
(
"解密块大小错误(密文可能损坏)"
,
e
);
}
catch
(
BadPaddingException
e
)
{
throw
new
UtilException
(
"解密填充错误(口令错误或密文被篡改)"
,
e
);
}
catch
(
Exception
e
)
{
throw
new
UtilException
(
"解密过程中发生未知错误"
,
e
);
}
}
/**
* PBKDF2密钥派生(使用固定迭代次数和密钥长度)
*/
private
static
byte
[]
deriveKey
(
String
password
,
byte
[]
salt
)
{
try
{
javax
.
crypto
.
SecretKeyFactory
factory
=
javax
.
crypto
.
SecretKeyFactory
.
getInstance
(
"PBKDF2WithHmacSHA256"
);
javax
.
crypto
.
spec
.
PBEKeySpec
spec
=
new
javax
.
crypto
.
spec
.
PBEKeySpec
(
password
.
toCharArray
(),
salt
,
PBKDF2_ITERATIONS
,
KEY_LENGTH_BITS
);
return
factory
.
generateSecret
(
spec
).
getEncoded
();
}
catch
(
NoSuchAlgorithmException
e
)
{
throw
new
UtilException
(
"不支持的密钥派生算法(PBKDF2WithHmacSHA256)"
,
e
);
}
catch
(
InvalidKeySpecException
e
)
{
throw
new
UtilException
(
"无效的密钥规范(参数错误)"
,
e
);
}
catch
(
Exception
e
)
{
throw
new
UtilException
(
"密钥派生过程中发生未知错误"
,
e
);
}
}
public
static
void
main
(
String
[]
args
)
{
String
[]
parts
=
"123|12312|test"
.
split
(
"\\|"
);
System
.
out
.
println
(
parts
.
length
);
System
.
out
.
println
(
parts
[
2
]);
String
password
=
"703AF86205DB25E6999CEAB2859D0FA1"
;
String
sss
=
"测试:123:test"
;
String
sign
=
"08ESlqUElarGW6BjxfX%2F3L%2Fe4u9CbTTNmKhBKJqy1hK5%2FhX%2BUbdi4da7rd%2BqQCPiyx8YG5xF3kyu4Eiqkss3ggN8iWMrWpBn7yRjZMaTAIY%3D"
;
System
.
out
.
println
(
sign
);
String
rawTicket
=
URLDecoder
.
decode
(
sign
,
StandardCharsets
.
UTF_8
);
System
.
out
.
println
(
rawTicket
);
try
{
String
encryptStr
=
encrypt
(
sss
,
password
);
System
.
out
.
println
(
encryptStr
);
String
decryptStr
=
decrypt
(
encryptStr
,
password
);
System
.
out
.
println
(
decryptStr
);
String
decryptStr1
=
decrypt
(
rawTicket
,
password
);
System
.
out
.
println
(
decryptStr1
);
}
catch
(
UtilException
e
)
{
throw
new
RuntimeException
(
e
);
}
}
}
src/main/java/com/keymobile/sso/vo/PortalAuthReq.java
0 → 100644
View file @
94e075e9
package
com
.
keymobile
.
sso
.
vo
;
import
lombok.AllArgsConstructor
;
import
lombok.Data
;
import
lombok.NoArgsConstructor
;
/**
* 门户免登录验签请求。
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public
class
PortalAuthReq
{
/**
* SM4 加密票据(Base64 编码)
*/
private
String
token
;
}
src/main/java/com/keymobile/sso/vo/ResponseVO.java
0 → 100644
View file @
94e075e9
package
com
.
keymobile
.
sso
.
vo
;
/**
* 统一 API 响应体。
*
* @param <T> data 类型
*/
public
class
ResponseVO
<
T
>
{
private
String
code
;
private
String
message
;
private
T
data
;
public
ResponseVO
()
{
}
public
ResponseVO
(
String
code
,
String
message
)
{
this
.
code
=
code
;
this
.
message
=
message
;
}
public
ResponseVO
(
String
code
,
String
message
,
T
data
)
{
this
.
code
=
code
;
this
.
message
=
message
;
this
.
data
=
data
;
}
public
String
getCode
()
{
return
code
;
}
public
void
setCode
(
String
code
)
{
this
.
code
=
code
;
}
public
String
getMessage
()
{
return
message
;
}
public
void
setMessage
(
String
message
)
{
this
.
message
=
message
;
}
public
T
getData
()
{
return
data
;
}
public
void
setData
(
T
data
)
{
this
.
data
=
data
;
}
}
src/main/resources/application-default.yml
View file @
94e075e9
...
...
@@ -21,3 +21,16 @@ eureka:
serviceUrl
:
defaultZone
:
http://e0:8081/eureka/
enabled
:
true
self
:
login
:
redirectUrl
:
http://139.198.127.54:18079/data-center/view/index
sso
:
# 是否启用 IP 白名单校验(本地联调可设为 false)
ipWhitelistEnabled
:
true
appId
:
portal_asset
portalIp
:
18.1.115.225
sm4Key
:
703AF86205DB25E6999CEAB2859D0FA1
expireSeconds
:
300
replayProtection
:
true
src/main/resources/application.yml
View file @
94e075e9
...
...
@@ -2,6 +2,9 @@ server:
port
:
8764
spring
:
main
:
allow-bean-definition-overriding
:
true
allow-circular-references
:
true
application
:
name
:
auth
cloud
:
...
...
@@ -34,3 +37,4 @@ self:
max-attempts
:
5
lockout-duration
:
30
attempt-window
:
15
redirectUrl
:
http://139.198.127.54:18079/data-center/view/index
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment