a引入jar
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
默认系统提供三张表
这里主要绑定 client和scope的关系,自定义的时候一般不用
CREATE TABLE oauth2_authorization_consent (
registered_client_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
authorities varchar(1000) NOT NULL,
PRIMARY KEY (registered_client_id, principal_name)
);
用户每次授权记录表 需要使用
/*
IMPORTANT:
If using PostgreSQL, update ALL columns defined with 'blob' to 'text',
as PostgreSQL does not support the 'blob' data type.
*/
CREATE TABLE oauth2_authorization (
id varchar(100) NOT NULL,
registered_client_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
authorization_grant_type varchar(100) NOT NULL,
authorized_scopes varchar(1000) DEFAULT NULL,
attributes blob DEFAULT NULL,
state varchar(500) DEFAULT NULL,
authorization_code_value blob DEFAULT NULL,
authorization_code_issued_at timestamp DEFAULT NULL,
authorization_code_expires_at timestamp DEFAULT NULL,
authorization_code_metadata blob DEFAULT NULL,
access_token_value blob DEFAULT NULL,
access_token_issued_at timestamp DEFAULT NULL,
access_token_expires_at timestamp DEFAULT NULL,
access_token_metadata blob DEFAULT NULL,
access_token_type varchar(100) DEFAULT NULL,
access_token_scopes varchar(1000) DEFAULT NULL,
oidc_id_token_value blob DEFAULT NULL,
oidc_id_token_issued_at timestamp DEFAULT NULL,
oidc_id_token_expires_at timestamp DEFAULT NULL,
oidc_id_token_metadata blob DEFAULT NULL,
refresh_token_value blob DEFAULT NULL,
refresh_token_issued_at timestamp DEFAULT NULL,
refresh_token_expires_at timestamp DEFAULT NULL,
refresh_token_metadata blob DEFAULT NULL,
user_code_value blob DEFAULT NULL,
user_code_issued_at timestamp DEFAULT NULL,
user_code_expires_at timestamp DEFAULT NULL,
user_code_metadata blob DEFAULT NULL,
device_code_value blob DEFAULT NULL,
device_code_issued_at timestamp DEFAULT NULL,
device_code_expires_at timestamp DEFAULT NULL,
device_code_metadata blob DEFAULT NULL,
PRIMARY KEY (id)
);
client 数据表 可以使用 也可以自定义
CREATE TABLE "public"."oauth2_registered_client" (
"id" varchar(100) COLLATE "pg_catalog"."default" NOT NULL,
"client_id" varchar(100) COLLATE "pg_catalog"."default" NOT NULL,
"client_id_issued_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"client_secret" varchar(200) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
"client_secret_expires_at" timestamp(6),
"client_name" varchar(200) COLLATE "pg_catalog"."default" NOT NULL,
"client_authentication_methods" varchar(1000) COLLATE "pg_catalog"."default" NOT NULL,
"authorization_grant_types" varchar(1000) COLLATE "pg_catalog"."default" NOT NULL,
"redirect_uris" varchar(1000) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
"post_logout_redirect_uris" varchar(1000) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
"scopes" varchar(1000) COLLATE "pg_catalog"."default" NOT NULL,
"client_settings" varchar(2000) COLLATE "pg_catalog"."default" NOT NULL,
"token_settings" varchar(2000) COLLATE "pg_catalog"."default" NOT NULL
)
自定义上面三张表的实现bean 主要是操作上面三张表
/**
* client 管理 增删改查
*
* @return
*/
@Bean
public RegisteredClientRepository registeredClientRepository() {
return new PandaRegisteredClientRepository(clientService);
}
/**
* 授权表
* 用户每次授权都会保存一次
*
* @param jdbcTemplate
* @param registeredClientRepository
* @return
*/
@Bean
public OAuth2AuthorizationService oAuth2AuthorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
/**
* 此表是 client 和 scope 的关系表 如果不操作默认表一定要重写
*
* @param mappingService
* @param registeredClientRepository
* @return
*/
@Bean
public OAuth2AuthorizationConsentService oAuth2AuthorizationConsentService(ClientScopeMappingService mappingService, RegisteredClientRepository registeredClientRepository) {
return new PandaOAuth2AuthorizationConsentService(mappingService, registeredClientRepository);
}
自定义 client PandaRegisteredClientRepository
public class PandaRegisteredClientRepository implements RegisteredClientRepository {
private final ClientService clientService;
public PandaRegisteredClientRepository(ClientService clientService) {
this.clientService = clientService;
}
@Override
public void save(RegisteredClient registeredClient) {
if (findById(registeredClient.getId()) != null) {
clientService.update(registeredClient);
} else {
clientService.save(registeredClient);
}
}
@Override
public RegisteredClient findById(String id) {
return clientService.getClient(id);
}
@Override
public RegisteredClient findByClientId(String clientId) {
return clientService.getClientByClientId(clientId);
}
}
增删改查就不写了写主要的,数据读取出来组装 RegisteredClient
private RegisteredClient.Builder builder(Client client) {
List<ClientScope> clientScopes = client.getScopes();
List<String> scopes = clientScopes.stream().map(ClientScope::getName).toList();
boolean requireAuthorizationConsent = false;
if (!client.getClientSettings().isEmpty()) {
requireAuthorizationConsent = Boolean.parseBoolean((String) client.getClientSettings().getOrDefault(Oauth2Const.REQUIRE_AUTHORIZATION_CONSENT, "false"));
}
RegisteredClient.Builder builder = RegisteredClient.withId(client.getId())
.clientId(client.getClientId())
.clientName(client.getName())
.clientSecret(client.getClientSecret())
.clientAuthenticationMethods(s -> {
s.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
s.add(ClientAuthenticationMethod.CLIENT_SECRET_POST);
})
.authorizationGrantTypes(type -> {
type.add(AuthorizationGrantType.AUTHORIZATION_CODE);
type.add(AuthorizationGrantType.REFRESH_TOKEN);
type.add(AuthorizationGrantType.CLIENT_CREDENTIALS);
})
.scopes(s -> s.addAll(scopes))
.clientSettings(
ClientSettings.builder()
//是否需要授权统同意
.requireAuthorizationConsent(requireAuthorizationConsent)
//仅支持 PCKE
.requireProofKey(false)
.build()
)
//设置token配置
.tokenSettings(
TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(120))
.reuseRefreshTokens(true)
.refreshTokenTimeToLive(Duration.ofMinutes(120))
.authorizationCodeTimeToLive(Duration.ofMinutes(120))
.build()
);
client.getRedirectUris().forEach(uri -> {
if (uri.startsWith("http")) {
builder.redirectUri(uri);
} else {
builder.redirectUri(client.getClientUrl() + uri);
}
});
client.getLogoutUrl().forEach(uri -> {
if (uri.startsWith("http")) {
builder.postLogoutRedirectUri(uri);
} else {
builder.postLogoutRedirectUri(client.getClientUrl() + uri);
}
});
log.info("client: {}", builder.build());
return builder;
}
PandaOAuth2AuthorizationConsentService 操作授权信息 对应表 oauth2_authorization_consent 我使用的自己的表
public class PandaOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
/**
* client-scope 关系表
*/
private ClientScopeMappingService clientScopeMappingService;
/**
* client 管理
*/
private RegisteredClientRepository registeredClientRepository;
public PandaOAuth2AuthorizationConsentService(ClientScopeMappingService clientScopeMappingService, RegisteredClientRepository registeredClientRepository) {
this.clientScopeMappingService = clientScopeMappingService;
this.registeredClientRepository = registeredClientRepository;
}
@Override
public void save(OAuth2AuthorizationConsent consent) {
log.info("authorizationConsent: {} {}", consent.getRegisteredClientId(), consent.getScopes());
}
@Override
public void remove(OAuth2AuthorizationConsent consent) {
log.info("authorizationConsent: {} {}", consent.getRegisteredClientId(), consent.getScopes());
}
/**
* 如果这里的scope 为空 会无法通过授权
* @param registeredClientId the identifier for the {@link RegisteredClient}
* @param principalName the name of the {@link Principal}
* @return
*/
@Override
public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
log.info("authorizationConsent: {} {}", registeredClientId, principalName);
var client = registeredClientRepository.findById(registeredClientId);
OAuth2AuthorizationConsent.Builder builder = OAuth2AuthorizationConsent.withId(registeredClientId, principalName);
if (client != null) {
client.getScopes().forEach(builder::scope);
}
return builder.build();
}
}
读取用户信息返回到 jwt的 OidcUserInfoService
public class OidcUserInfoService {
private final UserEntityService userEntityService;
public OidcUserInfoService(UserEntityService userEntityService) {
this.userEntityService = userEntityService;
}
public boolean checkGrantScope(String scope, Collection<? extends GrantedAuthority> authorities) {
log.info("authorities: {} {} {}", authorities, new SimpleGrantedAuthority("SCOPE_" + scope), authorities.contains(new SimpleGrantedAuthority("SCOPE_" + scope)));
return authorities.contains(new SimpleGrantedAuthority("SCOPE_" + scope));
}
public OidcUserInfo loadUser(Authentication principal) {
log.info("principal: {} {}", principal, principal.getClass());
// 获取授权信息
Collection<? extends GrantedAuthority> authorities = principal.getAuthorities();
// 不能为空
var user = userEntityService.getByName(principal.getName());
if (user.isPresent()) {
var u = user.get();
// 获取角色
List<ScopeClaimDto> roles = new ArrayList<>();
if (checkGrantScope(ClientConst.Scope.ROLE, authorities)) {
u.getRoles().forEach(role -> {
roles.add(new ScopeClaimDto(StringUtils.isNoneBlank(role.getOriginId()) ? role.getOriginId() : role.getId(), role.getName(), role.getValue(), role.getAttributes()));
});
}
// 获取组
List<ScopeClaimDto> groups = new ArrayList<>();
if (checkGrantScope(ClientConst.Scope.GROUP, authorities)) {
u.getGroups().forEach(group -> {
groups.add(new ScopeClaimDto(StringUtils.isNoneBlank(group.getOriginId()) ? group.getOriginId() : group.getId(), group.getName(), group.getDescription(), group.getAttributes()));
});
}
OidcUserInfo.Builder builder = OidcUserInfo.builder()
.subject(principal.getName())
.name(u.getFirstName() + " " + u.getLastName())
.nickname(u.getNickname())
.picture(u.getAvtar())
.givenName(u.getFirstName())
.familyName(u.getLastName())
.gender(UserConst.Gender.VALUES.getOrDefault(String.valueOf(u.getGender()), ""))
.locale(Locale.ROOT.toString())
.birthdate(u.getBirthdate() != null ? u.getBirthdate().format(DateTimeFormatter.ISO_DATE) : "")
.email(checkGrantScope(OidcScopes.EMAIL, authorities) ? u.getEmail() : "")
.emailVerified(checkGrantScope(OidcScopes.EMAIL, authorities) ? u.getEmailVerified() : null)
.phoneNumber(checkGrantScope(OidcScopes.PHONE, authorities) ? u.getPhoneNumber() : "")
.phoneNumberVerified(checkGrantScope(OidcScopes.PHONE, authorities) ? u.getPhoneNumberVerified() : null)
.updatedAt(u.getUtcModified().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
.claim("openid", u.getId())
.claim("originId", u.getOriginId())
.claim("roles", roles)
.claim("groups", groups)
.claim("attributes", u.getAttributes());
return builder.build();
}
return new OidcUserInfo(Map.of());
}
}
PandaOAuth2AuthorizationCodeRequestAuthenticationConverter 解决自定义界面 scope接收的问题
public class PandaOAuth2AuthorizationCodeRequestAuthenticationConverter implements AuthenticationConverter {
private static final String DEFAULT_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken(
"anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
private static final RequestMatcher OIDC_REQUEST_MATCHER = createOidcRequestMatcher();
@Override
public Authentication convert(HttpServletRequest request) {
if (!"GET".equals(request.getMethod()) && !OIDC_REQUEST_MATCHER.matches(request)) {
return null;
}
MultiValueMap<String, String> parameters =
"GET".equals(request.getMethod()) ?
PandaOAuth2EndpointUtils.getQueryParameters(request) :
PandaOAuth2EndpointUtils.getFormParameters(request);
// response_type (REQUIRED)
String responseType = parameters.getFirst(OAuth2ParameterNames.RESPONSE_TYPE);
if (!StringUtils.hasText(responseType) ||
parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE);
} else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) {
throwError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE);
}
String authorizationUri = request.getRequestURL().toString();
// client_id (REQUIRED)
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
if (!StringUtils.hasText(clientId) ||
parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
}
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
if (principal == null) {
principal = ANONYMOUS_AUTHENTICATION;
}
// redirect_uri (OPTIONAL)
String redirectUri = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
if (StringUtils.hasText(redirectUri) &&
parameters.get(OAuth2ParameterNames.REDIRECT_URI).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI);
}
// 重写 解决自定义授权页面 scope 传参问题 可以接收参数行 1 2 3或者 1,2,3
// scope (OPTIONAL)
Set<String> scopes = null;
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope) &&
parameters.get(OAuth2ParameterNames.SCOPE).isEmpty()) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
}
if (StringUtils.hasText(scope)) {
if (parameters.get(OAuth2ParameterNames.SCOPE).size() > 1) {
scopes = new HashSet<>(parameters.get(OAuth2ParameterNames.SCOPE));
} else {
scopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
}
// state (RECOMMENDED)
String state = parameters.getFirst(OAuth2ParameterNames.STATE);
if (StringUtils.hasText(state) &&
parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
}
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
String codeChallenge = parameters.getFirst(PkceParameterNames.CODE_CHALLENGE);
if (StringUtils.hasText(codeChallenge) &&
parameters.get(PkceParameterNames.CODE_CHALLENGE).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI);
}
// code_challenge_method (OPTIONAL for public clients) - RFC 7636 (PKCE)
String codeChallengeMethod = parameters.getFirst(PkceParameterNames.CODE_CHALLENGE_METHOD);
if (StringUtils.hasText(codeChallengeMethod) &&
parameters.get(PkceParameterNames.CODE_CHALLENGE_METHOD).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI);
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.RESPONSE_TYPE) &&
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
!key.equals(OAuth2ParameterNames.REDIRECT_URI) &&
!key.equals(OAuth2ParameterNames.SCOPE) &&
!key.equals(OAuth2ParameterNames.STATE)) {
additionalParameters.put(key, (value.size() == 1) ? value.getFirst() : value.toArray(new String[0]));
}
});
return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, clientId, principal,
redirectUri, state, scopes, additionalParameters);
}
private static RequestMatcher createOidcRequestMatcher() {
RequestMatcher postMethodMatcher = request -> "POST".equals(request.getMethod());
RequestMatcher responseTypeParameterMatcher = request ->
request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE) != null;
RequestMatcher openidScopeMatcher = request -> {
String scope = request.getParameter(OAuth2ParameterNames.SCOPE);
return StringUtils.hasText(scope) && scope.contains(OidcScopes.OPENID);
};
return new AndRequestMatcher(
postMethodMatcher, responseTypeParameterMatcher, openidScopeMatcher);
}
private static void throwError(String errorCode, String parameterName) {
throwError(errorCode, parameterName, DEFAULT_ERROR_URI);
}
private static void throwError(String errorCode, String parameterName, String errorUri) {
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
}
}
准备工作已结束
oauth2 独立配置文件 最好不要和 security 放在一起
证书部分一定要生成固定证书
@Configuration
public class Auth2Config {
@Resource
private SniperConfigProperties configProperties;
@Resource
private RegisteredClientRepository registeredClientRepository;
@Resource
private OAuth2AuthorizationService oAuth2AuthorizationService;
@Resource
private OAuth2AuthorizationConsentService oAuth2AuthorizationConsentService;
/**
* 执行顺序很重要 Order 一定小于权限控制
* 设置 order 提高他的加载顺序 放在类上无效
*
* @param http
* @return
* @throws Exception
*/
@Bean
@Order(1)
public SecurityFilterChain oauth2Server(HttpSecurity http,
OidcUserInfoService oidcUserInfoService) throws Exception {
// 默认
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper = (context) -> {
OidcUserInfoAuthenticationToken authentication = context.getAuthentication();
JwtAuthenticationToken principal = (JwtAuthenticationToken) authentication.getPrincipal();
log.info("principal: {}", principal);
return oidcUserInfoService.loadUser(principal);
};
// OAuth2AuthorizationCodeRequestAuthenticationConverter
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
// Enable OpenID Connect 1.0
.oidc(oidc -> {
oidc.userInfoEndpoint(u -> {
u.userInfoMapper(userInfoMapper);
});
})
// 授权阶段
.authorizationEndpoint(e -> e
// /oauth2/consent 自定义路径不经过参数处理器
.consentPage("/oauth2/consent")
// 重写解决 scope 传参问题 传多个就要使用这个 默认 a b c可使用默认
.authorizationRequestConverters(c -> {
c.add(new PandaOAuth2AuthorizationCodeRequestAuthenticationConverter());
})
// 验证 client 信息完整性
.errorResponseHandler(new Oauth2ErrorHandler())
)
// 处理 client 参数不和数据库一直检查
.clientAuthentication(client -> client
.errorResponseHandler(new Oauth2ErrorHandler())
)
// 授权之后
// 验证 client 错误
.tokenEndpoint(end -> end
.errorResponseHandler(new Oauth2ErrorHandler())
)
;
http
// Redirect to the login page when not authenticated from the
// authorization endpoint
.exceptionHandling(handling -> handling
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint(ClientConst.OAUTH2_ERROR_PAGE),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
.accessDeniedHandler((request, response, accessDeniedException) -> log.error("accessDeniedHandler: {}", accessDeniedException.getMessage()))
)
// Accept access tokens for User Info and/or Client Registration
.oauth2ResourceServer(server -> server
.jwt(Customizer.withDefaults())
.authenticationEntryPoint(new PandaAuthenticationEntryPoint(ClientConst.OAUTH2_LOGIN))
.accessDeniedHandler((request, response, accessDeniedException) -> log.error("jwt-accessDeniedHandler: {}", accessDeniedException.getMessage()))
)
;
return http.build();
}
private static KeyPair generateRsaKey() {
KeyPair keyPair;
KeyPairGenerator keyPairGenerator;
try {
keyPairGenerator = KeyPairGenerator.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
return keyPair;
}
public static PublicKey getPublicKey(String key) throws Exception {
byte[] keyBytes = Base64.decode(key);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}
public static PrivateKey getPrivateKey(String key) throws Exception {
byte[] keyBytes = Base64.decode(key);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
/**
* 默认发放令牌
*
* @return
*/
@Bean
public JWKSource<SecurityContext> jwkSource() throws Exception {
//KeyPair keyPair = generateRsaKey();
//RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
//RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAPublicKey publicKey = (RSAPublicKey) getPublicKey(configProperties.getJwtPublicKey());
RSAPrivateKey privateKey = (RSAPrivateKey) getPrivateKey(configProperties.getJwtPrivateKey());
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(configProperties.getJwtKeyId())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
/**
* 解码已签名的令牌
*
* @param jwkSource
* @return
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration
.jwtDecoder(jwkSource)
;
}
/**
* 配置 AuthorizationServerSettings 的实例
*
* @return
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
.authorizationEndpoint("/oauth2/authorize")
.oidcUserInfoEndpoint("/userinfo")
.build();
}
}
自定义错误 Oauth2ErrorHandler 默认的错误返回不好看在这里 可以自定义返回内容
@Slf4j
public class Oauth2ErrorHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
log.warn("errorResponseHandler: {}", exception.getClass(), exception);
if (exception instanceof OAuth2AuthenticationException e) {
// client 获取token失败,client 信息不匹配
log.warn("e: {}", e.getError());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().print(JSON.toJSONString(e.getError()));
response.getWriter().flush();
}
}
}
自定义授权页面
@Slf4j
@Controller
@RequestMapping("oauth2")
public class Oauth2Controller extends BaseRootController {
@Resource
private RegisteredClientRepository registeredClientRepository;
@Resource
private ClientService clientService;
@Resource
private RedisTemplate<String, String> redisTemplate;
@Resource
private IdentityProviderService identityProviderService;
private final RequestCache requestCache = new HttpSessionRequestCache();
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@GetMapping("login")
public String index(Model model, HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession();
Object ssle = session.getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
if (ssle instanceof AuthenticationException e) {
model.addAttribute("SPRING_SECURITY_LAST_EXCEPTION", e.getMessage());
}
boolean loginPhone;
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest == null) {
return redirect("/");
}
String[] clientId = savedRequest.getParameterValues(OAuth2ParameterNames.CLIENT_ID);
if (clientId == null) {
return redirect("/");
}
var client = clientService.getByClientId(clientId[0]);
if (client.isEmpty()) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT, "OAuth 2.0 Parameter: " + OAuth2ParameterNames.CLIENT_ID, "");
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
}
loginPhone = client.get().getAllowPhoneLogin();
//获取第三方认证
List<IdentityProvider> providers = identityProviderService.listIdentity(client.get().getRealmId());
List<String> loginUrls = new ArrayList<>();
providers.forEach(p -> {
switch (p.getProviderId()) {
case IdentityProviderConst.Provider.WX, IdentityProviderConst.Provider.WX_OPEN -> {
WxIdentityProviderService wxIdentityProviderService = new WxIdentityProviderService();
wxIdentityProviderService.setConfig(p.getAttributes());
wxIdentityProviderService.setRedisTemplate(redisTemplate);
loginUrls.add(wxIdentityProviderService.getEndpoint(""));
}
}
});
model.addAttribute("loginUrls", loginUrls);
model.addAttribute("loginPhone", loginPhone);
return "home/oauth2/login";
}
/**
* @param model
* @param principal
* @param clientId
* @param scope
* @param state 用户自定义参数
* @return
*/
@GetMapping("consent")
public String authorize(Model model, Principal principal,
HttpServletRequest request, HttpServletResponse response,
@RequestParam(value = OAuth2ParameterNames.RESPONSE_TYPE, required = false, defaultValue = "code") String responseType,
@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
@RequestParam(OAuth2ParameterNames.SCOPE) String scope,
@RequestParam(OAuth2ParameterNames.REDIRECT_URI) String redirect_uri,
@RequestParam(value = OAuth2ParameterNames.STATE, required = false, defaultValue = "") String state) throws PageNotFoundException {
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (registeredClient == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT, "OAuth 2.0 Parameter: " + OAuth2ParameterNames.CLIENT_ID, "");
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
}
if (!registeredClient.getRedirectUris().contains(redirect_uri)) {
redirect_uri = registeredClient.getRedirectUris().iterator().next();
}
// 自定义无需用户授权处理
boolean requireAuthorizationConsent = registeredClient.getClientSettings().isRequireAuthorizationConsent();
if (!requireAuthorizationConsent) {
try {
String url = UriComponentsBuilder.fromUriString("/oauth2/authorize")
.queryParam(OAuth2ParameterNames.RESPONSE_TYPE, responseType)
.queryParam(OAuth2ParameterNames.CLIENT_ID, clientId)
.queryParam(OAuth2ParameterNames.SCOPE, scope)
.queryParam(OAuth2ParameterNames.REDIRECT_URI, redirect_uri)
.queryParam(OAuth2ParameterNames.STATE, state)
.toUriString();
redirectStrategy.sendRedirect(request, response, url);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 主要检查 scope是否写的规范检查而已
var client = clientService.getByClientId(clientId);
if (client.isEmpty()) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT, "OAuth 2.0 Parameter: " + OAuth2ParameterNames.CLIENT_ID, "");
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
}
// openid 不需要同意 openid 必传
if (!scope.contains(OidcScopes.OPENID)) {
scope = OidcScopes.OPENID + " " + scope;
}
Map<String, ClientScope> clientScopes2 = client.get()
.getScopes()
.stream()
.filter(s -> s.getDisplay() != null && s.getDisplay() == 1)
.collect(Collectors.toMap(ClientScope::getName, Function.identity(), (c1, c2) -> c1, LinkedHashMap::new));
List<ClientScope> scopesToApprove = new ArrayList<>();
if (registeredClient.getScopes() != null) {
Set<String> scopes = registeredClient.getScopes();
for (String s : StringUtils.split(scope, " ")) {
if (scopes.contains(s)) {
scopesToApprove.add(clientScopes2.getOrDefault(s, new ClientScope()));
}
}
}
scopesToApprove.sort(Comparator.comparingInt(ClientScope::getSort));
model.addAttribute("requireAuthorizationConsent", requireAuthorizationConsent);
model.addAttribute("clientId", clientId);
model.addAttribute("clientName", registeredClient.getClientName());
model.addAttribute("responseType", responseType);
model.addAttribute("state", state);
model.addAttribute("scopes", scopesToApprove);
model.addAttribute("scopesChecked", JSON.toJSONString(StringUtils.split(scope, " ")));
model.addAttribute("principalName", principal.getName());
model.addAttribute("redirectUri", redirect_uri);
return "home/oauth2/authorize";
}
}
登录页面参数和 普通 security 一样
授权html
<div class="container">
<div class="card" id="app">
<div class="card-header">
<div class="py-5">
<h1 class="text-center">Consent required</h1>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="text-center">
<p>
<span class="font-weight-bold text-primary">${clientName}</span>
wants to
access your
account
<span class="font-weight-bold">${principalName}</span>
</p>
</div>
</div>
<div class="row pb-3">
<div class="text-center">
<p>The following permissions are requested by the above app.<br/>Please
review
these and consent if you approve.
</p>
</div>
</div>
<div class="row">
<div>
<form name="consent_form" id="consent_form" method="post" action="/oauth2/authorize"
>
<input type="hidden" name="client_id" value="${clientId}">
<input type="hidden" name="state" value="${state}">
<input type="hidden" name="response_type" value="${responseType}">
<input type="hidden" name="redirect_uri" value="${redirectUri}">
<input type="hidden" name="scope" v-model="scope">
<#-- 下面和上面隐藏字段都可以提交,如果使用下面提交的话需要使用自定义转换器用上面的话可以不用添加自定义转换器 后端只允许提交一个-->
<#list scopes as s>
<div class="form-check" style="width: 30%; margin: auto">
<input class="form-check-input" type="checkbox" name="scope_bak" v-model="scope_consent"
value="${s.name}" id="${s.id}" disabled>
<label class="form-check-label" for="${s.id}">${s.displayText}</label>
</div>
</#list>
<div class="form-group pt-3 text-center">
<button class="btn btn-primary btn-lg" type="submit" id="submit-consent">Submit Consent
</button>
</div>
<div class="form-group text-center">
<button class="btn btn-link regular" type="button" @click="cancelConsent"
id="cancel-consent">
Cancel
</button>
</div>
</form>
</div>
</div>
<div class="row pt-4">
<div class="col text-center"><p><small>Your consent to provide access is required.<br/>If you do not
approve,
click Cancel, in which case no information will be shared with the app.</small></p>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="/static/js/jquery-3.7.1.min.js"></script>
<script type="text/javascript" src="/static/plugins/bootstrap-5.3.2/js/bootstrap.bundle.min.js"></script>
<script type="text/javascript" charset="utf8" src="/static/js/vue_3.3.4.global.prod.min.js"></script>
<script>
const Counter = {
data() {
return {
scope: "",
scope_consent: ${scopesChecked?no_esc}
= }
},
watch: {
scope_content(n, o) {
this.scope = this.scope_consent.join(" ")
}
},
mounted() {
this.scope = this.scope_consent.join(" ")
},
methods: {
cancelConsent() {
this.scope_consent = []
this.scope = ""
document.consent_form.reset();
document.consent_form.submit();
//当用户取消的时候应该定向去某个页面,当重置表单提交的时候因为缺少不要参数报404错误,所以这里取消的时候去一个固定页面后者禁止取消
}
}
}
const app = Vue.createApp(Counter).mount('#app')
</script>
关于 security 控制部分 这里面还掺杂 session 控制等
@Bean
SecurityFilterChain filterChainOauth2(HttpSecurity http, SessionRegistry sessionRegistry) throws Exception {
log.info("securityFilter-oauth2");
http.apply(smsCodeAuthenticationSecurityConfig);
var builder = new MvcRequestMatcher.Builder(handlerMappingIntrospector);
return http.securityMatcher("/oauth2/**")
.authorizeHttpRequests(ahr -> ahr
.requestMatchers(
new AntPathRequestMatcher(ClientConst.OAUTH2_LOGIN)
)
.permitAll()
.anyRequest().authenticated()
)
.formLogin(login -> login
.loginPage(ClientConst.OAUTH2_LOGIN)
.permitAll()
.successHandler(new Oauth2SuccessHandler(ClientConst.OAUTH2_LOGIN, logLoginService, informationService))
.failureHandler(new Oauth2FailureHandler(ClientConst.OAUTH2_LOGIN, userEntityService, logLoginService))
)
.exceptionHandling(h -> h
.accessDeniedHandler(new HttpAccessDeniedHandler(ClientConst.OAUTH2_ERROR_PAGE))
.authenticationEntryPoint(new PandaAuthenticationEntryPoint(ClientConst.OAUTH2_ERROR_PAGE))
)
//SessionManagementFilter
.sessionManagement(sm -> sm
.sessionFixation().newSession()
.addSessionAuthenticationStrategy(new PandaSessionControlAuthenticationStrategy(sessionRegistry, informationService, MAXIMUM_SESSIONS))
.maximumSessions(MAXIMUM_SESSIONS)
//是否保留已经登录的用户;为true,新用户无法登录;为 false,旧用户被踢出
.maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry)
.expiredSessionStrategy(new HttpExpiredSessionStrategy(sessionRegistry))
)
//.addFilterAfter(new HttpSessionAuthFilter(sessionRegistry, configProperties.getSessionTimeout()), UsernamePasswordAuthenticationFilter.class)
.csrf(AbstractHttpConfigurer::disable)
.headers(h -> h.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.userDetailsService(pandaUserDetailsService)
.build();
}