update
This commit is contained in:
33
pom.xml
33
pom.xml
@@ -37,6 +37,18 @@
|
|||||||
</properties>
|
</properties>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
||||||
|
<!-- 通用认证模块 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.arrokoth.framework</groupId>
|
||||||
|
<artifactId>basic-authorization-server</artifactId>
|
||||||
|
<version>1.0.1-RELEASE</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
@@ -56,6 +68,18 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
|
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>nimbus-jose-jwt</artifactId>
|
||||||
|
<groupId>com.nimbusds</groupId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
@@ -70,7 +94,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-pool2</artifactId>
|
<artifactId>commons-pool2</artifactId>
|
||||||
<version>2.11.1</version> <!-- 根据需要选择最新版本 -->
|
<version>2.12.1</version> <!-- 根据需要选择最新版本 -->
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -84,13 +108,6 @@
|
|||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.xiaoymin</groupId>
|
|
||||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
|
||||||
<version>4.4.0</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Spring Session Data Redis -->
|
<!-- Spring Session Data Redis -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.session</groupId>
|
<groupId>org.springframework.session</groupId>
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package com.arrokoth.standalone.authorization;
|
package com.arrokoth.standalone.authorization;
|
||||||
|
|
||||||
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
|
||||||
|
@MapperScan("com.arrokoth.basic.mapper")
|
||||||
|
@ComponentScan(basePackages = "com.arrokoth")
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class StandaloneServerApplication {
|
public class StandaloneServerApplication {
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.arrokoth.standalone.authorization.config;
|
package com.arrokoth.standalone.authorization.config;
|
||||||
|
|
||||||
import com.arrokoth.standalone.authorization.properties.AuthorizationServerProperties;
|
import com.arrokoth.basic.properties.AuthorizationServerProperties;
|
||||||
import com.arrokoth.standalone.authorization.properties.SecurityWebProperties;
|
import com.arrokoth.basic.properties.SecurityWebProperties;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package com.arrokoth.standalone.authorization.config;
|
package com.arrokoth.standalone.authorization.config;
|
||||||
|
|
||||||
|
|
||||||
|
import com.arrokoth.basic.autoconfiguration.BasicAutoConfiguration;
|
||||||
|
import com.arrokoth.basic.properties.SecurityWebProperties;
|
||||||
import com.arrokoth.standalone.authorization.filter.JwtRequestFilter;
|
import com.arrokoth.standalone.authorization.filter.JwtRequestFilter;
|
||||||
import com.arrokoth.standalone.authorization.properties.AuthorizationServerProperties;
|
|
||||||
import com.arrokoth.standalone.authorization.properties.SecurityWebProperties;
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@@ -25,34 +27,12 @@ import java.util.List;
|
|||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@AutoConfigureBefore(BasicAutoConfiguration.class)
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@EnableConfigurationProperties(SecurityWebProperties.class)
|
@EnableConfigurationProperties(SecurityWebProperties.class)
|
||||||
public class SecurityWebAutoConfigurer {
|
public class SecurityWebAutoConfigurer {
|
||||||
|
|
||||||
private final SecurityWebProperties securityWebProperties;
|
private final SecurityWebProperties securityWebProperties;
|
||||||
private final AuthorizationServerProperties authorizationServerProperties;
|
|
||||||
|
|
||||||
// @Bean
|
|
||||||
// public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
|
|
||||||
// http
|
|
||||||
// .securityMatcher("/**")
|
|
||||||
// .authorizeHttpRequests(auth -> auth
|
|
||||||
// .requestMatchers(HttpMethod.OPTIONS).permitAll()
|
|
||||||
// .requestMatchers(securityWebProperties.getMergedPermittedUrls().toArray(new String[0])).permitAll()
|
|
||||||
// .anyRequest().authenticated()
|
|
||||||
// )
|
|
||||||
// .formLogin(form -> form
|
|
||||||
// .loginPage(authorizationServerProperties.getLoginPage())
|
|
||||||
// .permitAll()
|
|
||||||
// )
|
|
||||||
// .logout(logout -> logout
|
|
||||||
// .logoutSuccessUrl(securityWebProperties.getLogoutSuccessUrl())
|
|
||||||
// .permitAll()
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// return http.build();
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
private final JwtRequestFilter jwtRequestFilter; // 你的 Token 过滤器
|
private final JwtRequestFilter jwtRequestFilter; // 你的 Token 过滤器
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
package com.arrokoth.standalone.authorization.controller;
|
package com.arrokoth.standalone.authorization.controller;
|
||||||
|
|
||||||
import com.arrokoth.standalone.authorization.domain.request.LoginRequest;
|
|
||||||
import com.arrokoth.standalone.authorization.domain.response.BasicUser;
|
|
||||||
import com.arrokoth.standalone.authorization.domain.response.RestResponse;
|
|
||||||
import com.arrokoth.standalone.authorization.domain.response.Token;
|
|
||||||
import com.arrokoth.standalone.authorization.properties.SecurityWebProperties;
|
|
||||||
import com.arrokoth.standalone.authorization.service.AuthorizationService;
|
|
||||||
import com.arrokoth.standalone.authorization.service.OAuth2ConsentService;
|
import com.arrokoth.standalone.authorization.service.OAuth2ConsentService;
|
||||||
import com.arrokoth.standalone.authorization.util.JwtUtils;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
|
|
||||||
@@ -23,7 +15,7 @@ import java.security.Principal;
|
|||||||
public class AuthorizationController {
|
public class AuthorizationController {
|
||||||
|
|
||||||
|
|
||||||
private final AuthorizationService authorizationService;
|
|
||||||
private final OAuth2ConsentService oAuth2ConsentService;
|
private final OAuth2ConsentService oAuth2ConsentService;
|
||||||
|
|
||||||
|
|
||||||
@@ -32,30 +24,8 @@ public class AuthorizationController {
|
|||||||
return "login"; // 对应templates/login.html
|
return "login"; // 对应templates/login.html
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(SecurityWebProperties.AXIOS_LOGIN_PROCESSING_URL)
|
|
||||||
@ResponseBody
|
|
||||||
public RestResponse<Token> axiosLogin(@RequestBody LoginRequest loginRequest) {
|
|
||||||
return RestResponse.success(authorizationService.login(loginRequest));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/user/info")
|
|
||||||
@ResponseBody
|
|
||||||
public RestResponse<BasicUser> getBasicUser() {
|
|
||||||
return RestResponse.success(authorizationService.getBasicUser());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/logout")
|
|
||||||
@ResponseBody
|
|
||||||
public ResponseEntity<?> logout(HttpServletRequest request) throws Exception {
|
|
||||||
String token = JwtUtils.extractTokenFromHeader(request);
|
|
||||||
authorizationService.logout(JwtUtils.extractJti(token));
|
|
||||||
return ResponseEntity.ok("Logged out successfully");
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping(value = "/oauth2/consent")
|
@GetMapping(value = "/oauth2/consent")
|
||||||
public String consent(Principal principal, Model model,
|
public String consent(Principal principal, Model model,
|
||||||
@RequestParam("client_id") String clientId,
|
@RequestParam("client_id") String clientId,
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.arrokoth.standalone.authorization.domain.request;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class LoginRequest {
|
|
||||||
|
|
||||||
|
|
||||||
private String username;
|
|
||||||
private String password;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.arrokoth.standalone.authorization.domain.response;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class BasicUser {
|
|
||||||
|
|
||||||
private String username;
|
|
||||||
private String avatar;
|
|
||||||
private String introduction;
|
|
||||||
private List<String> roles;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.arrokoth.standalone.authorization.domain.response;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class RestResponse<T> {
|
|
||||||
|
|
||||||
private String code;
|
|
||||||
private String message;
|
|
||||||
private Long timestamp;
|
|
||||||
private T data;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 默认构造函数
|
|
||||||
public RestResponse() {
|
|
||||||
this.timestamp = System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全参构造函数
|
|
||||||
public RestResponse(String code, String message, T data) {
|
|
||||||
this.code = code;
|
|
||||||
this.message = message;
|
|
||||||
this.data = data;
|
|
||||||
this.timestamp = System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为了方便使用,可以添加一些静态方法来创建实例
|
|
||||||
public static <T> RestResponse<T> success(T data) {
|
|
||||||
return new RestResponse<>("200", "success", data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> RestResponse<T> error(String code, String message) {
|
|
||||||
return new RestResponse<>(code, message, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.arrokoth.standalone.authorization.domain.response;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class Token {
|
|
||||||
|
|
||||||
private String access_token;
|
|
||||||
private String token_type;
|
|
||||||
private Long expires_in;
|
|
||||||
private String refresh_token;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.arrokoth.standalone.authorization.filter;
|
package com.arrokoth.standalone.authorization.filter;
|
||||||
|
|
||||||
|
import com.arrokoth.basic.util.JwtUtils;
|
||||||
import com.arrokoth.standalone.authorization.store.redis.RedisTokenService;
|
import com.arrokoth.standalone.authorization.store.redis.RedisTokenService;
|
||||||
import com.arrokoth.standalone.authorization.util.JwtUtils;
|
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
@@ -51,9 +51,9 @@ public class JwtRequestFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||||
|
|
||||||
if (JwtUtils.validateToken(jwt, userDetails)) {
|
if (JwtUtils.validateToken(jwt, userDetails.getUsername())) {
|
||||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
UsernamePasswordAuthenticationToken authentication =
|
||||||
userDetails, null, userDetails.getAuthorities());
|
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
package com.arrokoth.standalone.authorization.properties;
|
|
||||||
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 授权服务器相关配置属性
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@ConfigurationProperties(prefix = "arrokoth.authorization.server")
|
|
||||||
public class AuthorizationServerProperties {
|
|
||||||
|
|
||||||
private static final String DEFAULT_LOGIN_PAGE = "/login";
|
|
||||||
private static final String DEFAULT_LOGOUT_SUCCESS_URL = "/login?logout";
|
|
||||||
private static final List<String> DEFAULT_PERMIT_URLS = Arrays.asList(
|
|
||||||
"/login", "/logout", "/connect/logout",
|
|
||||||
"/assets/**", "/static/**", "/webjars/**", "/error", "/oauth2/**"
|
|
||||||
);
|
|
||||||
/**
|
|
||||||
* 授权确认页面路径,默认为 "/oauth2/consent"
|
|
||||||
*/
|
|
||||||
private String consentPage = "/oauth2/consent";
|
|
||||||
/**
|
|
||||||
* OAuth2 授权端点路径,默认为 "/oauth2/authorize"
|
|
||||||
*/
|
|
||||||
private String authorizationEndpoint = "/oauth2/authorize";
|
|
||||||
/**
|
|
||||||
* JWT Issuer 值,默认为 "https://www.arrokoth-info.com"
|
|
||||||
*/
|
|
||||||
private String issuer = "https://www.arrokoth-info.com";
|
|
||||||
/**
|
|
||||||
* 登出成功后的跳转 URL。
|
|
||||||
* 默认值为 {@link #DEFAULT_LOGOUT_SUCCESS_URL},即 "/login?logout"。
|
|
||||||
* 如果在 application.yml 中配置了 app.security.logout-success-url,则使用配置值;
|
|
||||||
* 否则使用该默认值。
|
|
||||||
*/
|
|
||||||
private String logoutSuccessUrl = DEFAULT_LOGOUT_SUCCESS_URL;
|
|
||||||
/**
|
|
||||||
* 需要放行(无需认证即可访问)的 URL 列表。
|
|
||||||
* 默认为空列表,由 {@link #getMergedPermittedUrls()} 方法结合默认值进行合并处理。
|
|
||||||
*/
|
|
||||||
private List<String> permitUrls = new ArrayList<>();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 合并默认值与自定义配置,并去重
|
|
||||||
*/
|
|
||||||
public List<String> getMergedPermittedUrls() {
|
|
||||||
return Stream.concat(DEFAULT_PERMIT_URLS.stream(), permitUrls.stream())
|
|
||||||
.distinct()
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package com.arrokoth.standalone.authorization.properties;
|
|
||||||
|
|
||||||
|
|
||||||
import cn.hutool.json.JSONUtil;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 授权服务器相关配置属性
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Data
|
|
||||||
@Component
|
|
||||||
@ConfigurationProperties(prefix = "arrokoth.security.web")
|
|
||||||
public class SecurityWebProperties {
|
|
||||||
|
|
||||||
public static final String AXIOS_LOGIN_PROCESSING_URL = "/home/login";
|
|
||||||
|
|
||||||
|
|
||||||
public static final String DEFAULT_LOGIN_PROCESSING_URL = "/web/login";
|
|
||||||
|
|
||||||
private static final String DEFAULT_LOGOUT_SUCCESS_URL = "/login?logout";
|
|
||||||
|
|
||||||
private static final String DEFAULT_LOGOUT_PAGE = "/login";
|
|
||||||
|
|
||||||
|
|
||||||
private static final List<String> DEFAULT_PERMIT_URLS = Arrays.asList(
|
|
||||||
DEFAULT_LOGIN_PROCESSING_URL,
|
|
||||||
"/login", "/logout", "/connect/logout", "/home/login",
|
|
||||||
"/assets/**", "/static/**", "/webjars/**",
|
|
||||||
"/actuator/**",
|
|
||||||
"/error", "/oauth2/**"
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
private static final List<String> SWAGGER_URLS = Arrays.asList(
|
|
||||||
"/swagger-ui/**",
|
|
||||||
"/v3/api-docs/**",
|
|
||||||
"/swagger-resources/**",
|
|
||||||
"/webjars/**",
|
|
||||||
"/doc.html",
|
|
||||||
"/swagger-ui.html"
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 登出成功后的跳转 URL。
|
|
||||||
* 默认值为 {@link #DEFAULT_LOGOUT_SUCCESS_URL},即 "/login?logout"。
|
|
||||||
* 如果在 application.yml 中配置了 app.security.logout-success-url,则使用配置值;
|
|
||||||
* 否则使用该默认值。
|
|
||||||
*/
|
|
||||||
private String logoutSuccessUrl = DEFAULT_LOGOUT_SUCCESS_URL;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private String loginPage = DEFAULT_LOGOUT_PAGE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理登录请求
|
|
||||||
*/
|
|
||||||
private String loginProcessingUrl = DEFAULT_LOGIN_PROCESSING_URL;
|
|
||||||
/**
|
|
||||||
* 需要放行(无需认证即可访问)的 URL 列表。
|
|
||||||
* 默认为空列表,由 {@link #getMergedPermittedUrls()} 方法结合默认值进行合并处理。
|
|
||||||
*/
|
|
||||||
private List<String> permitUrls = new ArrayList<>();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 合并默认值与自定义配置,并去重
|
|
||||||
*/
|
|
||||||
public List<String> getMergedPermittedUrls() {
|
|
||||||
List<String> merged = Stream.of(DEFAULT_PERMIT_URLS, SWAGGER_URLS, permitUrls)
|
|
||||||
.flatMap(List::stream)
|
|
||||||
.distinct()
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
log.info("Merged permitted urls: {}", JSONUtil.toJsonStr(merged));
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.arrokoth.standalone.authorization.service;
|
|
||||||
|
|
||||||
import com.arrokoth.standalone.authorization.domain.request.LoginRequest;
|
|
||||||
import com.arrokoth.standalone.authorization.domain.response.BasicUser;
|
|
||||||
import com.arrokoth.standalone.authorization.domain.response.Token;
|
|
||||||
|
|
||||||
public interface AuthorizationService {
|
|
||||||
|
|
||||||
|
|
||||||
Token login(LoginRequest loginRequest);
|
|
||||||
|
|
||||||
void logout(String jti);
|
|
||||||
|
|
||||||
BasicUser getBasicUser();
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,70 @@
|
|||||||
package com.arrokoth.standalone.authorization.service;
|
package com.arrokoth.standalone.authorization.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
||||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.Set;
|
import java.util.*;
|
||||||
|
|
||||||
public interface OAuth2ConsentService {
|
/**
|
||||||
|
* OAuth2 授权确认服务实现类
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class OAuth2ConsentService {
|
||||||
|
|
||||||
OAuth2ConsentService.ConsentResponse getConsentDetails(Principal principal,
|
private final RegisteredClientRepository registeredClientRepository;
|
||||||
String clientId,
|
private final OAuth2AuthorizationConsentService authorizationConsentService;
|
||||||
String scope,
|
|
||||||
String state,
|
/**
|
||||||
String principalName);
|
* 获取用户授权的详细信息,包括需要审批和已审批的 scopes
|
||||||
|
*
|
||||||
|
* @param clientId 客户端 ID
|
||||||
|
* @param scope 请求的 scope 列表(空格分隔)
|
||||||
|
* @param principalName 用户名
|
||||||
|
* @return 包含待批准和已批准 scopes 的 ConsentResult 对象
|
||||||
|
*/
|
||||||
|
public ConsentResponse getConsentDetails(Principal principal,
|
||||||
|
String clientId,
|
||||||
|
String scope,
|
||||||
|
String state,
|
||||||
|
String principalName) {
|
||||||
|
Set<String> scopesToApprove = new HashSet<>();
|
||||||
|
Set<String> previouslyApprovedScopes = new HashSet<>();
|
||||||
|
|
||||||
|
RegisteredClient registeredClient = Optional.ofNullable(
|
||||||
|
registeredClientRepository.findByClientId(clientId))
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("客户端不存在"));
|
||||||
|
|
||||||
|
OAuth2AuthorizationConsent currentAuthorizationConsent =
|
||||||
|
this.authorizationConsentService.findById(registeredClient.getId(), principalName);
|
||||||
|
|
||||||
|
Set<String> authorizedScopes = Optional.ofNullable(currentAuthorizationConsent)
|
||||||
|
.map(OAuth2AuthorizationConsent::getScopes)
|
||||||
|
.orElse(Set.of());
|
||||||
|
|
||||||
|
Arrays.stream(StringUtils.delimitedListToStringArray(scope, " "))
|
||||||
|
.filter(requestedScope -> !OidcScopes.OPENID.equals(requestedScope))
|
||||||
|
.forEach(requestedScope -> {
|
||||||
|
if (authorizedScopes.contains(requestedScope)) {
|
||||||
|
previouslyApprovedScopes.add(requestedScope);
|
||||||
|
} else {
|
||||||
|
scopesToApprove.add(requestedScope);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ConsentResponse(
|
||||||
|
Collections.unmodifiableSet(scopesToApprove),
|
||||||
|
Collections.unmodifiableSet(previouslyApprovedScopes),
|
||||||
|
registeredClient
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,10 +74,11 @@ public interface OAuth2ConsentService {
|
|||||||
* @param previouslyApprovedScopes 已经授权过的 scopes
|
* @param previouslyApprovedScopes 已经授权过的 scopes
|
||||||
* @param registeredClient 客户端注册信息
|
* @param registeredClient 客户端注册信息
|
||||||
*/
|
*/
|
||||||
record ConsentResponse(
|
public record ConsentResponse(
|
||||||
Set<String> scopesToApprove,
|
Set<String> scopesToApprove,
|
||||||
Set<String> previouslyApprovedScopes,
|
Set<String> previouslyApprovedScopes,
|
||||||
RegisteredClient registeredClient
|
RegisteredClient registeredClient
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package com.arrokoth.standalone.authorization.service.impl;
|
|
||||||
|
|
||||||
import com.arrokoth.standalone.authorization.domain.request.LoginRequest;
|
|
||||||
import com.arrokoth.standalone.authorization.domain.response.BasicUser;
|
|
||||||
import com.arrokoth.standalone.authorization.domain.response.Token;
|
|
||||||
import com.arrokoth.standalone.authorization.service.AuthorizationService;
|
|
||||||
import com.arrokoth.standalone.authorization.util.JwtUtils;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class AuthorizationServiceImpl implements AuthorizationService {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Token login(LoginRequest loginRequest) {
|
|
||||||
String admin = JwtUtils.createAccessToken("admin");
|
|
||||||
|
|
||||||
Token token = new Token();
|
|
||||||
token.setAccess_token(admin);
|
|
||||||
token.setExpires_in(JwtUtils.DEFAULT_EXPIRATION);
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void logout(String jti) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BasicUser getBasicUser() {
|
|
||||||
BasicUser basicUser = new BasicUser();
|
|
||||||
basicUser.setUsername("admin");
|
|
||||||
basicUser.setAvatar("https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif");
|
|
||||||
basicUser.setIntroduction("I am a super administrator...");
|
|
||||||
basicUser.setRoles(List.of("admin"));
|
|
||||||
return basicUser;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
package com.arrokoth.standalone.authorization.service.impl;
|
|
||||||
|
|
||||||
import com.arrokoth.standalone.authorization.service.OAuth2ConsentService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
|
||||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
|
||||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
|
||||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
|
||||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
import java.security.Principal;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OAuth2 授权确认服务实现类
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class OAuth2ConsentServiceImpl implements OAuth2ConsentService {
|
|
||||||
|
|
||||||
private final RegisteredClientRepository registeredClientRepository;
|
|
||||||
private final OAuth2AuthorizationConsentService authorizationConsentService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户授权的详细信息,包括需要审批和已审批的 scopes
|
|
||||||
*
|
|
||||||
* @param clientId 客户端 ID
|
|
||||||
* @param scope 请求的 scope 列表(空格分隔)
|
|
||||||
* @param principalName 用户名
|
|
||||||
* @return 包含待批准和已批准 scopes 的 ConsentResult 对象
|
|
||||||
*/
|
|
||||||
public ConsentResponse getConsentDetails(Principal principal,
|
|
||||||
String clientId,
|
|
||||||
String scope,
|
|
||||||
String state,
|
|
||||||
String principalName) {
|
|
||||||
Set<String> scopesToApprove = new HashSet<>();
|
|
||||||
Set<String> previouslyApprovedScopes = new HashSet<>();
|
|
||||||
|
|
||||||
RegisteredClient registeredClient = Optional.ofNullable(
|
|
||||||
registeredClientRepository.findByClientId(clientId))
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException("客户端不存在"));
|
|
||||||
|
|
||||||
OAuth2AuthorizationConsent currentAuthorizationConsent =
|
|
||||||
this.authorizationConsentService.findById(registeredClient.getId(), principalName);
|
|
||||||
|
|
||||||
Set<String> authorizedScopes = Optional.ofNullable(currentAuthorizationConsent)
|
|
||||||
.map(OAuth2AuthorizationConsent::getScopes)
|
|
||||||
.orElse(Set.of());
|
|
||||||
|
|
||||||
Arrays.stream(StringUtils.delimitedListToStringArray(scope, " "))
|
|
||||||
.filter(requestedScope -> !OidcScopes.OPENID.equals(requestedScope))
|
|
||||||
.forEach(requestedScope -> {
|
|
||||||
if (authorizedScopes.contains(requestedScope)) {
|
|
||||||
previouslyApprovedScopes.add(requestedScope);
|
|
||||||
} else {
|
|
||||||
scopesToApprove.add(requestedScope);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return new ConsentResponse(
|
|
||||||
Collections.unmodifiableSet(scopesToApprove),
|
|
||||||
Collections.unmodifiableSet(previouslyApprovedScopes),
|
|
||||||
registeredClient
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ public class OAuth2AuthorizationServiceStore {
|
|||||||
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public OAuth2AuthorizationService authorizationService() {
|
public OAuth2AuthorizationService oAuth2AuthorizationService() {
|
||||||
// 基于db的oauth2认证服务,还有一个基于内存的服务实现InMemoryOAuth2AuthorizationService
|
// 基于db的oauth2认证服务,还有一个基于内存的服务实现InMemoryOAuth2AuthorizationService
|
||||||
return new InMemoryOAuth2AuthorizationService();
|
return new InMemoryOAuth2AuthorizationService();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.arrokoth.standalone.authorization.store.redis;
|
package com.arrokoth.standalone.authorization.store.redis;
|
||||||
|
|
||||||
import com.arrokoth.standalone.authorization.util.JwtUtils;
|
import com.arrokoth.basic.util.JwtUtils;
|
||||||
import com.nimbusds.jose.JOSEException;
|
import com.nimbusds.jose.JOSEException;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
|||||||
@@ -1,183 +0,0 @@
|
|||||||
package com.arrokoth.standalone.authorization.util;
|
|
||||||
|
|
||||||
import com.nimbusds.jose.JOSEException;
|
|
||||||
import com.nimbusds.jose.JWSAlgorithm;
|
|
||||||
import com.nimbusds.jose.JWSHeader;
|
|
||||||
import com.nimbusds.jose.crypto.MACSigner;
|
|
||||||
import com.nimbusds.jose.crypto.MACVerifier;
|
|
||||||
import com.nimbusds.jwt.JWTClaimsSet;
|
|
||||||
import com.nimbusds.jwt.SignedJWT;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public class JwtUtils {
|
|
||||||
|
|
||||||
// ================== 配置常量 ==================
|
|
||||||
private static final String ISSUER = "your-issuer";
|
|
||||||
public static final Long DEFAULT_EXPIRATION = 3600000L; // 默认有效期 1小时 (ms)
|
|
||||||
private static final String SECRET_KEY = generateSecureSecret();
|
|
||||||
|
|
||||||
// ================== 公共方法 ==================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建一个新的访问 Token
|
|
||||||
*
|
|
||||||
* @param username 用户标识(如 username 或 user id)
|
|
||||||
* @return JWT Token 字符串
|
|
||||||
*/
|
|
||||||
public static String createAccessToken(String username) {
|
|
||||||
try {
|
|
||||||
return createAccessToken(username, username, DEFAULT_EXPIRATION);
|
|
||||||
} catch (JOSEException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建一个新的访问 Token,并指定过期时间
|
|
||||||
*
|
|
||||||
* @param subject 用户标识
|
|
||||||
* @param expiration 过期时间(毫秒)
|
|
||||||
* @return JWT Token 字符串
|
|
||||||
*/
|
|
||||||
public static String createAccessToken(String subject, String username, Long expiration) throws JOSEException {
|
|
||||||
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
|
|
||||||
.subject(subject)
|
|
||||||
.issuer(ISSUER)
|
|
||||||
.issueTime(new Date())
|
|
||||||
.expirationTime(new Date(System.currentTimeMillis() + expiration))
|
|
||||||
.jwtID(UUID.randomUUID().toString())
|
|
||||||
.claim("username", username) // 自定义字段存 username
|
|
||||||
.build();
|
|
||||||
|
|
||||||
JWSHeader header = new JWSHeader(JWSAlgorithm.HS256);
|
|
||||||
SignedJWT signedJWT = new SignedJWT(header, claimsSet);
|
|
||||||
|
|
||||||
MACSigner signer = new MACSigner(SECRET_KEY.getBytes());
|
|
||||||
signedJWT.sign(signer);
|
|
||||||
|
|
||||||
return signedJWT.serialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析并验证 Token
|
|
||||||
*
|
|
||||||
* @param token 要解析的 JWT 字符串
|
|
||||||
* @return JWT 的 Claims
|
|
||||||
*/
|
|
||||||
public static JWTClaimsSet parseToken(String token) throws ParseException, JOSEException {
|
|
||||||
SignedJWT signedJWT = SignedJWT.parse(token);
|
|
||||||
|
|
||||||
MACVerifier verifier = new MACVerifier(SECRET_KEY.getBytes());
|
|
||||||
if (!signedJWT.verify(verifier)) {
|
|
||||||
throw new JOSEException("Invalid signature");
|
|
||||||
}
|
|
||||||
|
|
||||||
return signedJWT.getJWTClaimsSet();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提取用户名
|
|
||||||
*
|
|
||||||
* @param token JWT Token 字符串
|
|
||||||
* @return 用户名
|
|
||||||
*/
|
|
||||||
public static String extractUsername(String token) throws ParseException, JOSEException {
|
|
||||||
return parseToken(token).getSubject();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Token 的过期时间
|
|
||||||
*
|
|
||||||
* @param token JWT Token 字符串
|
|
||||||
* @return 过期时间
|
|
||||||
*/
|
|
||||||
public static Date extractExpiration(String token) throws ParseException, JOSEException {
|
|
||||||
return parseToken(token).getExpirationTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Token 的 jti (JWT ID)
|
|
||||||
*
|
|
||||||
* @param token JWT Token 字符串
|
|
||||||
* @return jti 值
|
|
||||||
*/
|
|
||||||
public static String extractJti(String token) throws ParseException, JOSEException {
|
|
||||||
return parseToken(token).getJWTID();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断 Token 是否已过期
|
|
||||||
*
|
|
||||||
* @param token JWT Token 字符串
|
|
||||||
* @return 是否有效
|
|
||||||
*/
|
|
||||||
public static boolean isTokenExpired(String token) {
|
|
||||||
try {
|
|
||||||
return extractExpiration(token).before(new Date());
|
|
||||||
} catch (Exception e) {
|
|
||||||
return true; // 出错也视为过期
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证 Token 是否属于当前用户且未过期
|
|
||||||
*
|
|
||||||
* @param token JWT Token 字符串
|
|
||||||
* @param userDetails 用户信息
|
|
||||||
* @return 是否有效
|
|
||||||
*/
|
|
||||||
public static boolean validateToken(String token, UserDetails userDetails) {
|
|
||||||
try {
|
|
||||||
String username = extractUsername(token);
|
|
||||||
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================== 私有工具方法 ==================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成安全的密钥(Base64 编码的 256 位密钥)
|
|
||||||
*/
|
|
||||||
private static String generateSecureSecret() {
|
|
||||||
byte[] key = new byte[32]; // 256 bits
|
|
||||||
new SecureRandom().nextBytes(key);
|
|
||||||
return Base64.getEncoder().encodeToString(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 提取 Token 的辅助方法
|
|
||||||
public static String extractTokenFromHeader(HttpServletRequest request) {
|
|
||||||
String header = request.getHeader("Authorization");
|
|
||||||
if (header != null && header.startsWith("Bearer ")) {
|
|
||||||
return header.substring(7);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================== 测试入口 ==================
|
|
||||||
public static void main(String[] args) {
|
|
||||||
try {
|
|
||||||
String token = createAccessToken("user@example.com");
|
|
||||||
System.out.println("Generated Token: " + token);
|
|
||||||
|
|
||||||
JWTClaimsSet claims = parseToken(token);
|
|
||||||
System.out.println("Subject: " + claims.getSubject());
|
|
||||||
System.out.println("Issuer: " + claims.getIssuer());
|
|
||||||
System.out.println("Expiration Time: " + claims.getExpirationTime());
|
|
||||||
|
|
||||||
System.out.println("Is Valid? " + validateToken(token, null));
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,15 @@ server:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
|
aop:
|
||||||
|
proxy-target-class: true
|
||||||
|
datasource:
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
url: jdbc:mysql://127.0.0.1:3306/db_base_authorization?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||||
|
username: root
|
||||||
|
password: yyds@8848
|
||||||
|
|
||||||
application:
|
application:
|
||||||
name: authorization-server-standalone
|
name: authorization-server-standalone
|
||||||
data:
|
data:
|
||||||
@@ -28,9 +35,18 @@ arrokoth:
|
|||||||
issuer: https://www.arrokoth-info.com
|
issuer: https://www.arrokoth-info.com
|
||||||
security:
|
security:
|
||||||
web:
|
web:
|
||||||
|
swagger-ui: false
|
||||||
login-page: /login
|
login-page: /login
|
||||||
logout-success-url: /login?logout
|
logout-success-url: /login?logout
|
||||||
permit-urls:
|
permit-urls:
|
||||||
- /home/login
|
- /home/login
|
||||||
|
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
root: INFO
|
||||||
|
com.arrokoth: DEBUG
|
||||||
|
org.springdoc: INFO
|
||||||
|
org.springframework: INFO
|
||||||
|
org.springframework.security: DEBUG
|
||||||
|
org.springframework.security.oauth2: DEBUG
|
||||||
Reference in New Issue
Block a user