update
This commit is contained in:
33
pom.xml
33
pom.xml
@@ -37,6 +37,18 @@
|
||||
</properties>
|
||||
<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>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
@@ -56,6 +68,18 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -70,7 +94,7 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
<version>2.11.1</version> <!-- 根据需要选择最新版本 -->
|
||||
<version>2.12.1</version> <!-- 根据需要选择最新版本 -->
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -84,13 +108,6 @@
|
||||
<scope>test</scope>
|
||||
</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 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.session</groupId>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.arrokoth.standalone.authorization;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
|
||||
@MapperScan("com.arrokoth.basic.mapper")
|
||||
@ComponentScan(basePackages = "com.arrokoth")
|
||||
@SpringBootApplication
|
||||
public class StandaloneServerApplication {
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.arrokoth.standalone.authorization.config;
|
||||
|
||||
import com.arrokoth.standalone.authorization.properties.AuthorizationServerProperties;
|
||||
import com.arrokoth.standalone.authorization.properties.SecurityWebProperties;
|
||||
import com.arrokoth.basic.properties.AuthorizationServerProperties;
|
||||
import com.arrokoth.basic.properties.SecurityWebProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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.properties.AuthorizationServerProperties;
|
||||
import com.arrokoth.standalone.authorization.properties.SecurityWebProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -25,34 +27,12 @@ import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@AutoConfigureBefore(BasicAutoConfiguration.class)
|
||||
@AllArgsConstructor
|
||||
@EnableConfigurationProperties(SecurityWebProperties.class)
|
||||
public class SecurityWebAutoConfigurer {
|
||||
|
||||
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 过滤器
|
||||
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
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.util.JwtUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
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;
|
||||
|
||||
@@ -23,7 +15,7 @@ import java.security.Principal;
|
||||
public class AuthorizationController {
|
||||
|
||||
|
||||
private final AuthorizationService authorizationService;
|
||||
|
||||
private final OAuth2ConsentService oAuth2ConsentService;
|
||||
|
||||
|
||||
@@ -32,30 +24,8 @@ public class AuthorizationController {
|
||||
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")
|
||||
public String consent(Principal principal, Model model,
|
||||
@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;
|
||||
|
||||
import com.arrokoth.basic.util.JwtUtils;
|
||||
import com.arrokoth.standalone.authorization.store.redis.RedisTokenService;
|
||||
import com.arrokoth.standalone.authorization.util.JwtUtils;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -51,9 +51,9 @@ public class JwtRequestFilter extends OncePerRequestFilter {
|
||||
|
||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||
|
||||
if (JwtUtils.validateToken(jwt, userDetails)) {
|
||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
||||
userDetails, null, userDetails.getAuthorities());
|
||||
if (JwtUtils.validateToken(jwt, userDetails.getUsername())) {
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||
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;
|
||||
|
||||
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.Set;
|
||||
import java.util.*;
|
||||
|
||||
public interface OAuth2ConsentService {
|
||||
/**
|
||||
* OAuth2 授权确认服务实现类
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OAuth2ConsentService {
|
||||
|
||||
OAuth2ConsentService.ConsentResponse getConsentDetails(Principal principal,
|
||||
String clientId,
|
||||
String scope,
|
||||
String state,
|
||||
String principalName);
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -21,10 +74,11 @@ public interface OAuth2ConsentService {
|
||||
* @param previouslyApprovedScopes 已经授权过的 scopes
|
||||
* @param registeredClient 客户端注册信息
|
||||
*/
|
||||
record ConsentResponse(
|
||||
public record ConsentResponse(
|
||||
Set<String> scopesToApprove,
|
||||
Set<String> previouslyApprovedScopes,
|
||||
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
|
||||
public OAuth2AuthorizationService authorizationService() {
|
||||
public OAuth2AuthorizationService oAuth2AuthorizationService() {
|
||||
// 基于db的oauth2认证服务,还有一个基于内存的服务实现InMemoryOAuth2AuthorizationService
|
||||
return new InMemoryOAuth2AuthorizationService();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 lombok.extern.slf4j.Slf4j;
|
||||
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:
|
||||
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:
|
||||
name: authorization-server-standalone
|
||||
data:
|
||||
@@ -28,9 +35,18 @@ arrokoth:
|
||||
issuer: https://www.arrokoth-info.com
|
||||
security:
|
||||
web:
|
||||
swagger-ui: false
|
||||
login-page: /login
|
||||
logout-success-url: /login?logout
|
||||
permit-urls:
|
||||
- /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