no message
This commit is contained in:
15
README.md
Normal file
15
README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
mvn versions:display-dependency-updates
|
||||||
|
|
||||||
|
mvn versions:update-properties
|
||||||
|
|
||||||
|
mvn versions:set -DnewVersion=1.2.0.RELEASE
|
||||||
|
```
|
||||||
|
|
||||||
10
pom.xml
10
pom.xml
@@ -5,12 +5,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.5.3</version>
|
<version>4.0.0-M1</version>
|
||||||
<relativePath/> <!-- lookup parent from repository -->
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>com.arrokoth</groupId>
|
<groupId>com.arrokoth</groupId>
|
||||||
<artifactId>authorization-server-standalone</artifactId>
|
<artifactId>authorization-server-standalone</artifactId>
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>1.2.0.RELEASE</version>
|
||||||
<name>authorization-server-standalone</name>
|
<name>authorization-server-standalone</name>
|
||||||
<description>authorization-server-standalone</description>
|
<description>authorization-server-standalone</description>
|
||||||
<url/>
|
<url/>
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
<maven.compiler.target>17</maven.compiler.target>
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
<spring.boot.version>3.5.3</spring.boot.version>
|
<spring.boot.version>3.5.3</spring.boot.version>
|
||||||
|
|
||||||
<arrokoth.version>1.1.0.RELEASE</arrokoth.version>
|
<arrokoth.version>1.2.0.RELEASE</arrokoth.version>
|
||||||
<arrokoth.bom.version>1.1.0.RELEASE</arrokoth.bom.version>
|
<arrokoth.bom.version>1.2.0.RELEASE</arrokoth.bom.version>
|
||||||
</properties>
|
</properties>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.arrokoth.framework</groupId>
|
<groupId>com.arrokoth.framework</groupId>
|
||||||
<artifactId>basic-authorization-server</artifactId>
|
<artifactId>basic-authorization-server</artifactId>
|
||||||
<version>1.0.1-RELEASE</version>
|
<version>1.2.0.RELEASE</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ public class SecurityWebAutoConfigurer {
|
|||||||
|
|
||||||
.logout(logout -> logout
|
.logout(logout -> logout
|
||||||
.logoutUrl(SecurityWebProperties.AXIOS_LOGOUT_PROCESSING_URL)
|
.logoutUrl(SecurityWebProperties.AXIOS_LOGOUT_PROCESSING_URL)
|
||||||
.logoutSuccessUrl(securityWebProperties.getLogoutSuccessUrl())
|
// .logoutSuccessUrl(securityWebProperties.getLogoutSuccessUrl())
|
||||||
.invalidateHttpSession(true) // 注销时销毁 session
|
.invalidateHttpSession(true) // 注销时销毁 session
|
||||||
.deleteCookies("JSESSIONID", "Authorization")
|
.deleteCookies("JSESSIONID", "Authorization")
|
||||||
.permitAll()
|
.permitAll()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
|
|||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
@@ -28,92 +29,113 @@ public class JwtRequestFilter extends OncePerRequestFilter {
|
|||||||
private final RedisTokenService redisTokenService;
|
private final RedisTokenService redisTokenService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||||
|
throws ServletException, IOException {
|
||||||
logRequestDetails(request);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 1. 提取 JWT Token
|
||||||
String jwt = extractJwtFromRequest(request);
|
String jwt = extractJwtFromRequest(request);
|
||||||
if (jwt == null) {
|
if (jwt == null) {
|
||||||
chain.doFilter(request, response);
|
chain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String username = extractUsernameFromToken(jwt);
|
// 2. 解析用户名
|
||||||
if (username == null) {
|
String username = JwtUtils.extractUsername(jwt);
|
||||||
chain.doFilter(request, response);
|
if (username == null || username.isBlank()) {
|
||||||
|
log.warn("JWT token does not contain a valid username: {}", maskToken(jwt));
|
||||||
|
sendUnauthorizedResponse(response, "Invalid token");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTokenBlacklisted(jwt, response)) {
|
// 3. 检查 Token 是否被拉黑(退出登录状态)
|
||||||
|
if (redisTokenService.isBlacklisted(jwt)) {
|
||||||
|
log.warn("Token is blacklisted: {}", maskToken(jwt));
|
||||||
|
sendUnauthorizedResponse(response, "Token is blacklisted");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. 若用户未认证,则进行认证
|
||||||
if (isUserNotAuthenticated(username)) {
|
if (isUserNotAuthenticated(username)) {
|
||||||
authenticateUser(jwt, username);
|
authenticateUser(jwt, username, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. 继续过滤链
|
||||||
chain.doFilter(request, response);
|
chain.doFilter(request, response);
|
||||||
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
logger.warn("Error occurred during JWT filter processing", ex);
|
log.warn("JWT filter processing failed for request: {}", request.getRequestURI(), ex);
|
||||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
|
sendUnauthorizedResponse(response, "Unauthorized: " + ex.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从请求中提取 JWT Token
|
||||||
|
*/
|
||||||
private String extractJwtFromRequest(HttpServletRequest request) {
|
private String extractJwtFromRequest(HttpServletRequest request) {
|
||||||
|
// 优先从 Authorization: Bearer <token>
|
||||||
String jwt = null;
|
String bearerToken = request.getHeader("Authorization");
|
||||||
|
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
|
||||||
// 1. 先从 Authorization Header 中提取
|
return bearerToken.substring(7).trim();
|
||||||
String authorizationHeader = request.getHeader("Authorization");
|
|
||||||
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
|
|
||||||
jwt = authorizationHeader.substring(7);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 如果 Header 中没有,则尝试从 URL 参数中获取(如 X-Token)
|
// 其次尝试从 X-Token 参数获取(URL 或表单)
|
||||||
if (jwt == null || jwt.isEmpty()) {
|
String tokenParam = request.getParameter("X-Token");
|
||||||
jwt = request.getParameter("X-Token");
|
if (tokenParam != null && !tokenParam.isBlank()) {
|
||||||
|
return tokenParam.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
return jwt;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
private String extractUsernameFromToken(String jwt) {
|
|
||||||
try {
|
|
||||||
return JwtUtils.extractUsername(jwt);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTokenBlacklisted(String jwt, HttpServletResponse response) throws IOException {
|
|
||||||
if (redisTokenService.isBlacklisted(jwt)) {
|
|
||||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token is blacklisted");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前上下文是否已认证为指定用户
|
||||||
|
*/
|
||||||
private boolean isUserNotAuthenticated(String username) {
|
private boolean isUserNotAuthenticated(String username) {
|
||||||
return SecurityContextHolder.getContext().getAuthentication() == null ||
|
var authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
!SecurityContextHolder.getContext().getAuthentication().isAuthenticated() ||
|
return authentication == null
|
||||||
!SecurityContextHolder.getContext().getAuthentication().getName().equals(username);
|
|| !authentication.isAuthenticated()
|
||||||
|
|| !username.equals(authentication.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void authenticateUser(String jwt, String username) {
|
/**
|
||||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
* 对用户进行身份认证并设置 SecurityContext
|
||||||
|
*/
|
||||||
|
private void authenticateUser(String jwt, String username, HttpServletRequest request) {
|
||||||
|
UserDetails userDetails;
|
||||||
|
try {
|
||||||
|
userDetails = userDetailsService.loadUserByUsername(username);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("User not found during JWT authentication: {}", username);
|
||||||
|
throw new RuntimeException("Invalid token or user does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
if (JwtUtils.validateToken(jwt, userDetails.getUsername())) {
|
if (!JwtUtils.validateToken(jwt, userDetails.getUsername())) {
|
||||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
log.warn("JWT token validation failed for user: {}", username);
|
||||||
userDetails, null, userDetails.getAuthorities());
|
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException("Token is expired or invalid");
|
throw new RuntimeException("Token is expired or invalid");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
||||||
|
userDetails, null, userDetails.getAuthorities());
|
||||||
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
log.debug("Authenticated user: {} via JWT", username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送 401 响应
|
||||||
|
*/
|
||||||
|
private void sendUnauthorizedResponse(HttpServletResponse response, String message) throws IOException {
|
||||||
|
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志脱敏:隐藏长 Token 的中间部分
|
||||||
|
*/
|
||||||
|
private String maskToken(String token) {
|
||||||
|
if (token == null || token.length() <= 10) return token;
|
||||||
|
return token.substring(0, 5) + "..." + token.substring(token.length() - 5);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 封装的打印请求详情方法
|
* 封装的打印请求详情方法
|
||||||
@@ -123,10 +145,10 @@ public class JwtRequestFilter extends OncePerRequestFilter {
|
|||||||
String queryString = request.getQueryString();
|
String queryString = request.getQueryString();
|
||||||
|
|
||||||
// 打印基本信息
|
// 打印基本信息
|
||||||
if (log.isInfoEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
log.info("Request URL: {}", requestURL);
|
log.debug("Request URL: {}", requestURL);
|
||||||
if (queryString != null) {
|
if (queryString != null) {
|
||||||
log.info("Query Parameters: {}", queryString);
|
log.debug("Query Parameters: {}", queryString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,8 +157,10 @@ public class JwtRequestFilter extends OncePerRequestFilter {
|
|||||||
while (paramNames.hasMoreElements()) {
|
while (paramNames.hasMoreElements()) {
|
||||||
String paramName = paramNames.nextElement();
|
String paramName = paramNames.nextElement();
|
||||||
String[] paramValues = request.getParameterValues(paramName);
|
String[] paramValues = request.getParameterValues(paramName);
|
||||||
for (String value : paramValues) {
|
if (log.isDebugEnabled()) {
|
||||||
log.debug("Request Param: {} = {}", paramName, value);
|
for (String value : paramValues) {
|
||||||
|
log.debug("Request Param: {} = {}", paramName, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,15 @@ public class UserDetailsServiceStore {
|
|||||||
.roles("admin", "normal")
|
.roles("admin", "normal")
|
||||||
.authorities("app", "web")
|
.authorities("app", "web")
|
||||||
.build();
|
.build();
|
||||||
return new InMemoryUserDetailsManager(user);
|
|
||||||
|
|
||||||
|
|
||||||
|
UserDetails user2 = User.withUsername("guest")
|
||||||
|
.password(passwordEncoder.encode("yyds@8848"))
|
||||||
|
.roles("normal")
|
||||||
|
.authorities("app")
|
||||||
|
.build();
|
||||||
|
return new InMemoryUserDetailsManager(user,user2);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ mybatis:
|
|||||||
mapper-locations: classpath*:com.arrokoth/**/mapper/xml/*.xml
|
mapper-locations: classpath*:com.arrokoth/**/mapper/xml/*.xml
|
||||||
configuration:
|
configuration:
|
||||||
map-underscore-to-camel-case: true
|
map-underscore-to-camel-case: true
|
||||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||||
mybatis-plus:
|
mybatis-plus:
|
||||||
type-aliases-package: com.arrokoth.**.domain
|
type-aliases-package: com.arrokoth.**.domain
|
||||||
mapper-locations: classpath*:com.arrokoth/**/mapper/xml/*.xml
|
mapper-locations: classpath*:com.arrokoth/**/mapper/xml/*.xml
|
||||||
|
|||||||
Reference in New Issue
Block a user