Spring Authorization Server入门 (十五) 分离授权确认与设备码校验页面

前言

       在之前的文章(实现授权码模式使用前后端分离的登录页面)中实现了前后端分离的登录页面,但这篇文章中只分离了登录页面,鉴于部分读者好奇授权确认页面分离的实现,就实现一下授权确认页面的分离,同时设备码流程的授权确认页面与授权码流程的授权确认页面是同一个,这里也需要兼容一下,还有就是设备码流程中有一个校验设备码的页面,这里也需要分离出来。

前文中有提到,在前后端分离的模式下,在页面发起的请求需要响应json不能重定向了,所以需要修改相关接口调用成功后响应json。话不多说,直接上代码。

编码

需要修改的内容

  1. 重定向至授权确认页面时直接携带相关参数重定向至前端项目中
  2. 提供接口查询登录用户在发起授权的客户端中相关scope信息
  3. 重定向至设备码校验页面时携带当前sessionId(nonceId)重定向至前端项目中
  4. 编写授权确认失败处理类,在调用确认授权接口失败时响应json
  5. 编写授权成功处理类,在调用授权确认接口成功时响应json
  6. 编写校验设备码成功响应类,在校验设备码成功后响应json
  7. 修改重定向至登录页面处理,兼容在请求校验设备码时登录信息过期处理
  8. 将以上内容添加至认证服务配置中
  9. 前端项目中编写授权确认、设备码校验、设备码校验成功页面

重定向至授权确认页面时直接携带相关参数重定向至前端项目中

在AuthorizationController中编写/oauth2/consent/redirect接口,借助认证服务跳转至前端的,跳转时携带sessionId保持登录状态

@SneakyThrows
@ResponseBody
@GetMapping(value = "/oauth2/consent/redirect")
public Result<String> consentRedirect(HttpSession session,HttpServletRequest request,HttpServletResponse response,@RequestParam(OAuth2ParameterNames.SCOPE) String scope,@RequestParam(OAuth2ParameterNames.STATE) String state,@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,@RequestHeader(name = NONCE_HEADER_NAME, required = false) String nonceId,@RequestParam(name = OAuth2ParameterNames.USER_CODE, required = false) String userCode) {// 携带当前请求参数与nonceId重定向至前端页面UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(CONSENT_PAGE_URI).queryParam(OAuth2ParameterNames.SCOPE, UriUtils.encode(scope, StandardCharsets.UTF_8)).queryParam(OAuth2ParameterNames.STATE, UriUtils.encode(state, StandardCharsets.UTF_8)).queryParam(OAuth2ParameterNames.CLIENT_ID, clientId).queryParam(OAuth2ParameterNames.USER_CODE, userCode).queryParam(NONCE_HEADER_NAME, ObjectUtils.isEmpty(nonceId) ? session.getId() : nonceId);String uriString = uriBuilder.build(Boolean.TRUE).toUriString();if (ObjectUtils.isEmpty(userCode) || !UrlUtils.isAbsoluteUrl(DEVICE_ACTIVATE_URI)) {// 不是设备码模式或者设备码验证页面不是前后端分离的,无需返回json,直接重定向redirectStrategy.sendRedirect(request, response, uriString);return null;}// 兼容设备码,需响应JSON,由前端进行跳转return Result.success(uriString);
}

提供接口查询登录用户在发起授权的客户端中相关scope信息

在AuthorizationController中编写/oauth2/consent/parameters接口

@ResponseBody
@GetMapping(value = "/oauth2/consent/parameters")
public Result<Map<String, Object>> consentParameters(Principal principal,@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,@RequestParam(OAuth2ParameterNames.SCOPE) String scope,@RequestParam(OAuth2ParameterNames.STATE) String state,@RequestParam(name = OAuth2ParameterNames.USER_CODE, required = false) String userCode) {// 获取consent页面所需的参数Map<String, Object> consentParameters = getConsentParameters(scope, state, clientId, userCode, principal);return Result.success(consentParameters);
}

修改/oauth2/consent接口

@GetMapping(value = "/oauth2/consent")
public String consent(Principal principal, Model model,@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,@RequestParam(OAuth2ParameterNames.SCOPE) String scope,@RequestParam(OAuth2ParameterNames.STATE) String state,@RequestParam(name = OAuth2ParameterNames.USER_CODE, required = false) String userCode) {// 获取consent页面所需的参数Map<String, Object> consentParameters = getConsentParameters(scope, state, clientId, userCode, principal);// 转至model中,让框架渲染页面consentParameters.forEach(model::addAttribute);return "consent";
}

编写公共方法getConsentParameters

/*** 根据授权确认相关参数获取授权确认与未确认的scope相关参数** @param scope     scope权限* @param state     state* @param clientId  客户端id* @param userCode  设备码授权流程中的用户码* @param principal 当前认证信息* @return 页面所需数据*/
private Map<String, Object> getConsentParameters(String scope,String state,String clientId,String userCode,Principal principal) {// Remove scopes that were already approvedSet<String> scopesToApprove = new HashSet<>();Set<String> previouslyApprovedScopes = new HashSet<>();RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);if (registeredClient == null) {throw new RuntimeException("客户端不存在");}OAuth2AuthorizationConsent currentAuthorizationConsent =this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());Set<String> authorizedScopes;if (currentAuthorizationConsent != null) {authorizedScopes = currentAuthorizationConsent.getScopes();} else {authorizedScopes = Collections.emptySet();}for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {if (OidcScopes.OPENID.equals(requestedScope)) {continue;}if (authorizedScopes.contains(requestedScope)) {previouslyApprovedScopes.add(requestedScope);} else {scopesToApprove.add(requestedScope);}}Map<String, Object> parameters = new HashMap<>(7);parameters.put("clientId", registeredClient.getClientId());parameters.put("clientName", registeredClient.getClientName());parameters.put("state", state);parameters.put("scopes", withDescription(scopesToApprove));parameters.put("previouslyApprovedScopes", withDescription(previouslyApprovedScopes));parameters.put("principalName", principal.getName());parameters.put("userCode", userCode);if (StringUtils.hasText(userCode)) {parameters.put("requestURI", "/oauth2/device_verification");} else {parameters.put("requestURI", "/oauth2/authorize");}return parameters;
}

重定向至设备码校验页面时携带当前sessionId(nonceId)重定向至前端项目中

在AuthorizationController中编写/activate/redirect接口,由认证服务重定向,携带sessionId以保持登录状态

@GetMapping("/activate/redirect")
public String activateRedirect(HttpSession session,@RequestParam(value = "user_code", required = false) String userCode) {UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(DEVICE_ACTIVATE_URI).queryParam("userCode", userCode).queryParam(NONCE_HEADER_NAME, session.getId());return "redirect:" + uriBuilder.build(Boolean.TRUE).toUriString();
}

编写授权确认失败处理类,在调用确认授权接口失败时响应json

package com.example.authorization.handler;import com.example.model.Result;
import com.example.util.JsonUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.util.UrlUtils;import java.io.IOException;
import java.nio.charset.StandardCharsets;import static com.example.constant.SecurityConstants.CONSENT_PAGE_URI;/*** 授权确认失败处理** @author vains*/
public class ConsentAuthenticationFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {// 获取当前认证信息Authentication authentication = SecurityContextHolder.getContext().getAuthentication();// 获取具体的异常OAuth2AuthenticationException authenticationException = (OAuth2AuthenticationException) exception;OAuth2Error error = authenticationException.getError();// 异常信息String message;if (authentication == null) {message = "登录已失效";} else {// 第二次点击“拒绝”会因为之前取消时删除授权申请记录而找不到对应的数据,导致抛出 [invalid_request] OAuth 2.0 Parameter: statemessage = error.toString();}// 授权确认页面提交的请求,因为授权申请与授权确认提交公用一个过滤器,这里判断一下if (request.getMethod().equals(HttpMethod.POST.name()) && UrlUtils.isAbsoluteUrl(CONSENT_PAGE_URI)) {// 写回json异常Result<Object> result = Result.error(HttpStatus.BAD_REQUEST.value(), message);response.setCharacterEncoding(StandardCharsets.UTF_8.name());response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.getWriter().write(JsonUtils.objectCovertToJson(result));response.getWriter().flush();} else {// 在地址栏输入授权申请地址或设备码流程的验证地址错误(user_code错误)response.sendError(HttpStatus.BAD_REQUEST.value(), error.toString());}}}

编写授权成功处理类,在调用授权确认接口成功时响应json

package com.example.authorization.handler;import com.example.model.Result;
import com.example.util.JsonUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;import java.io.IOException;
import java.nio.charset.StandardCharsets;import static com.example.constant.SecurityConstants.CONSENT_PAGE_URI;
import static org.springframework.security.oauth2.core.OAuth2ErrorCodes.INVALID_REQUEST;/*** 授权确认前后端分离适配响应处理** @author vains*/
public class ConsentAuthorizationResponseHandler implements AuthenticationSuccessHandler {private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {// 获取将要重定向的回调地址String redirectUri = this.getAuthorizationResponseUri(authentication);if (request.getMethod().equals(HttpMethod.POST.name()) && UrlUtils.isAbsoluteUrl(CONSENT_PAGE_URI)) {// 如果是post请求并且CONSENT_PAGE_URI是完整的地址,则响应jsonResult<String> success = Result.success(redirectUri);response.setCharacterEncoding(StandardCharsets.UTF_8.name());response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.getWriter().write(JsonUtils.objectCovertToJson(success));response.getWriter().flush();return;}// 否则重定向至回调地址this.redirectStrategy.sendRedirect(request, response, redirectUri);}/*** 获取重定向的回调地址** @param authentication 认证信息* @return 地址*/private String getAuthorizationResponseUri(Authentication authentication) {OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;if (ObjectUtils.isEmpty(authorizationCodeRequestAuthentication.getRedirectUri())) {String authorizeUriError = "Redirect uri is not null";throw new OAuth2AuthorizationCodeRequestAuthenticationException(new OAuth2Error(INVALID_REQUEST, authorizeUriError, (null)), authorizationCodeRequestAuthentication);}if (authorizationCodeRequestAuthentication.getAuthorizationCode() == null) {String authorizeError = "AuthorizationCode is not null";throw new OAuth2AuthorizationCodeRequestAuthenticationException(new OAuth2Error(INVALID_REQUEST, authorizeError, (null)), authorizationCodeRequestAuthentication);}UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(authorizationCodeRequestAuthentication.getRedirectUri()).queryParam(OAuth2ParameterNames.CODE, authorizationCodeRequestAuthentication.getAuthorizationCode().getTokenValue());if (StringUtils.hasText(authorizationCodeRequestAuthentication.getState())) {uriBuilder.queryParam(OAuth2ParameterNames.STATE,UriUtils.encode(authorizationCodeRequestAuthentication.getState(), StandardCharsets.UTF_8));}// build(true) -> Components are explicitly encodedreturn uriBuilder.build(true).toUriString();}}

编写校验设备码成功响应类,在校验设备码成功后响应json

package com.example.authorization.handler;import com.example.model.Result;
import com.example.util.JsonUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;import java.io.IOException;
import java.nio.charset.StandardCharsets;import static com.example.constant.SecurityConstants.DEVICE_ACTIVATED_URI;/*** 校验设备码成功响应类** @author vains*/
public class DeviceAuthorizationResponseHandler implements AuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {// 写回json数据Result<Object> result = Result.success(DEVICE_ACTIVATED_URI);response.setCharacterEncoding(StandardCharsets.UTF_8.name());response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.getWriter().write(JsonUtils.objectCovertToJson(result));response.getWriter().flush();}
}

修改重定向至登录页面处理,兼容在请求校验设备码时登录信息过期处理

package com.example.authorization.handler;import com.example.constant.SecurityConstants;
import com.example.model.Result;
import com.example.util.JsonUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.ObjectUtils;import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;import static com.example.constant.SecurityConstants.DEVICE_ACTIVATE_URI;/*** 重定向至登录处理** @author vains*/
@Slf4j
public class LoginTargetAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();/*** @param loginFormUrl URL where the login page can be found. Should either be*                     relative to the web-app context path (include a leading {@code /}) or an absolute*                     URL.*/public LoginTargetAuthenticationEntryPoint(String loginFormUrl) {super(loginFormUrl);}@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {String deviceVerificationUri = "/oauth2/device_verification";// 兼容设备码前后端分离if (request.getRequestURI().equals(deviceVerificationUri)&& request.getMethod().equals(HttpMethod.POST.name())&& UrlUtils.isAbsoluteUrl(DEVICE_ACTIVATE_URI)) {// 如果是请求验证设备激活码(user_code)时未登录并且设备码验证页面是前后端分离的那种则写回jsonResult<String> success = Result.error(HttpStatus.UNAUTHORIZED.value(), ("登录已失效,请重新打开设备提供的验证地址"));response.setCharacterEncoding(StandardCharsets.UTF_8.name());response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.getWriter().write(JsonUtils.objectCovertToJson(success));response.getWriter().flush();return;}// 获取登录表单的地址String loginForm = determineUrlToUseForThisRequest(request, response, authException);if (!UrlUtils.isAbsoluteUrl(loginForm)) {// 不是绝对路径调用父类方法处理super.commence(request, response, authException);return;}StringBuffer requestUrl = request.getRequestURL();if (!ObjectUtils.isEmpty(request.getQueryString())) {requestUrl.append("?").append(request.getQueryString());}// 2023-07-11添加逻辑:重定向地址添加nonce参数,该参数的值为sessionId// 绝对路径在重定向前添加target参数String targetParameter = URLEncoder.encode(requestUrl.toString(), StandardCharsets.UTF_8);String targetUrl = loginForm + "?target=" + targetParameter + "&" + SecurityConstants.NONCE_HEADER_NAME + "=" + request.getSession(Boolean.FALSE).getId();log.debug("重定向至前后端分离的登录页面:{}", targetUrl);this.redirectStrategy.sendRedirect(request, response, targetUrl);}
}

将以上内容添加至认证服务配置中

AuthorizationConfig完整配置如下

package com.example.config;import com.example.authorization.device.DeviceClientAuthenticationConverter;
import com.example.authorization.device.DeviceClientAuthenticationProvider;
import com.example.authorization.federation.FederatedIdentityIdTokenCustomizer;
import com.example.authorization.handler.*;
import com.example.authorization.sms.SmsCaptchaGrantAuthenticationConverter;
import com.example.authorization.sms.SmsCaptchaGrantAuthenticationProvider;
import com.example.authorization.wechat.WechatAuthorizationRequestConsumer;
import com.example.authorization.wechat.WechatCodeGrantRequestEntityConverter;
import com.example.authorization.wechat.WechatMapAccessTokenResponseConverter;
import com.example.constant.RedisConstants;
import com.example.constant.SecurityConstants;
import com.example.support.RedisOperator;
import com.example.support.RedisSecurityContextRepository;
import com.example.util.SecurityUtils;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import org.springframework.util.ObjectUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;import static com.example.constant.SecurityConstants.CONSENT_PAGE_URI;
import static com.example.constant.SecurityConstants.DEVICE_ACTIVATE_URI;/*** 认证配置* {@link EnableMethodSecurity} 开启全局方法认证,启用JSR250注解支持,启用注解 {@link Secured} 支持,* 在Spring Security 6.0版本中将@Configuration注解从@EnableWebSecurity, @EnableMethodSecurity, @EnableGlobalMethodSecurity* 和 @EnableGlobalAuthentication 中移除,使用这些注解需手动添加 @Configuration 注解* {@link EnableWebSecurity} 注解有两个作用:* 1. 加载了WebSecurityConfiguration配置类, 配置安全认证策略。* 2. 加载了AuthenticationConfiguration, 配置了认证信息。** @author vains*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class AuthorizationConfig {private final RedisOperator<String> redisOperator;/*** 登录地址,前后端分离就填写完整的url路径,不分离填写相对路径*/private final String LOGIN_URL = "http://127.0.0.1:5173/login";private static final String CUSTOM_CONSENT_REDIRECT_URI = "/oauth2/consent/redirect";private static final String CUSTOM_DEVICE_REDIRECT_URI = "/activate/redirect";private final RedisSecurityContextRepository redisSecurityContextRepository;/*** 配置端点的过滤器链** @param http spring security核心配置类* @return 过滤器链* @throws Exception 抛出*/@Beanpublic SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,RegisteredClientRepository registeredClientRepository,AuthorizationServerSettings authorizationServerSettings) throws Exception {// 配置默认的设置,忽略认证端点的csrf校验OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);// 添加跨域过滤器http.addFilter(corsFilter());// 禁用 csrf 与 corshttp.csrf(AbstractHttpConfigurer::disable);http.cors(AbstractHttpConfigurer::disable);// 新建设备码converter和providerDeviceClientAuthenticationConverter deviceClientAuthenticationConverter =new DeviceClientAuthenticationConverter(authorizationServerSettings.getDeviceAuthorizationEndpoint());DeviceClientAuthenticationProvider deviceClientAuthenticationProvider =new DeviceClientAuthenticationProvider(registeredClientRepository);// 使用redis存储、读取登录的认证信息http.securityContext(context -> context.securityContextRepository(redisSecurityContextRepository));http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)// 开启OpenID Connect 1.0协议相关端点.oidc(Customizer.withDefaults())// 设置自定义用户确认授权页.authorizationEndpoint(authorizationEndpoint -> {// 校验授权确认页面是否为完整路径;是否是前后端分离的页面boolean absoluteUrl = UrlUtils.isAbsoluteUrl(CONSENT_PAGE_URI);// 如果是分离页面则重定向,否则转发请求authorizationEndpoint.consentPage(absoluteUrl ? CUSTOM_CONSENT_REDIRECT_URI : CONSENT_PAGE_URI);if (absoluteUrl) {// 适配前后端分离的授权确认页面,成功/失败响应jsonauthorizationEndpoint.errorResponseHandler(new ConsentAuthenticationFailureHandler());authorizationEndpoint.authorizationResponseHandler(new ConsentAuthorizationResponseHandler());}})// 设置设备码用户验证url(自定义用户验证页).deviceAuthorizationEndpoint(deviceAuthorizationEndpoint ->deviceAuthorizationEndpoint.verificationUri(UrlUtils.isAbsoluteUrl(DEVICE_ACTIVATE_URI) ? CUSTOM_DEVICE_REDIRECT_URI : DEVICE_ACTIVATE_URI))// 设置验证设备码用户确认页面.deviceVerificationEndpoint(deviceVerificationEndpoint -> {// 校验授权确认页面是否为完整路径;是否是前后端分离的页面boolean absoluteUrl = UrlUtils.isAbsoluteUrl(CONSENT_PAGE_URI);// 如果是分离页面则重定向,否则转发请求deviceVerificationEndpoint.consentPage(absoluteUrl ? CUSTOM_CONSENT_REDIRECT_URI : CONSENT_PAGE_URI);if (absoluteUrl) {// 适配前后端分离的授权确认页面,失败响应jsondeviceVerificationEndpoint.errorResponseHandler(new ConsentAuthenticationFailureHandler());}// 如果授权码验证页面或者授权确认页面是前后端分离的if (UrlUtils.isAbsoluteUrl(DEVICE_ACTIVATE_URI) || absoluteUrl) {// 添加响应json处理deviceVerificationEndpoint.deviceVerificationResponseHandler(new DeviceAuthorizationResponseHandler());}}).clientAuthentication(clientAuthentication ->// 客户端认证添加设备码的converter和providerclientAuthentication.authenticationConverter(deviceClientAuthenticationConverter).authenticationProvider(deviceClientAuthenticationProvider));http// 当未登录时访问认证端点时重定向至login页面.exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(new LoginTargetAuthenticationEntryPoint(LOGIN_URL),new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))// 处理使用access token访问用户信息端点和客户端注册端点.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()));// 自定义短信认证登录转换器SmsCaptchaGrantAuthenticationConverter converter = new SmsCaptchaGrantAuthenticationConverter();// 自定义短信认证登录认证提供SmsCaptchaGrantAuthenticationProvider provider = new SmsCaptchaGrantAuthenticationProvider();http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)// 让认证服务器元数据中有自定义的认证方式.authorizationServerMetadataEndpoint(metadata -> metadata.authorizationServerMetadataCustomizer(customizer -> customizer.grantType(SecurityConstants.GRANT_TYPE_SMS_CODE)))// 添加自定义grant_type——短信认证登录.tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(converter).authenticationProvider(provider));DefaultSecurityFilterChain build = http.build();// 从框架中获取provider中所需的beanOAuth2TokenGenerator<?> tokenGenerator = http.getSharedObject(OAuth2TokenGenerator.class);AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);OAuth2AuthorizationService authorizationService = http.getSharedObject(OAuth2AuthorizationService.class);// 以上三个bean在build()方法之后调用是因为调用build方法时框架会尝试获取这些类,// 如果获取不到则初始化一个实例放入SharedObject中,所以要在build方法调用之后获取// 在通过set方法设置进provider中,但是如果在build方法之后调用authenticationProvider(provider)// 框架会提示unsupported_grant_type,因为已经初始化完了,在添加就不会生效了provider.setTokenGenerator(tokenGenerator);provider.setAuthorizationService(authorizationService);provider.setAuthenticationManager(authenticationManager);return build;}/*** 配置认证相关的过滤器链(资源服务,客户端配置)** @param http spring security核心配置类* @return 过滤器链* @throws Exception 抛出*/@Beanpublic SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository) throws Exception {// 添加跨域过滤器http.addFilter(corsFilter());// 禁用 csrf 与 corshttp.csrf(AbstractHttpConfigurer::disable);http.cors(AbstractHttpConfigurer::disable);http.authorizeHttpRequests((authorize) -> authorize// 放行静态资源.requestMatchers("/assets/**", "/webjars/**", "/login", "/getCaptcha", "/getSmsCaptcha", "/error", "/oauth2/consent/parameters").permitAll().anyRequest().authenticated())// 指定登录页面.formLogin(formLogin -> {formLogin.loginPage("/login");if (UrlUtils.isAbsoluteUrl(LOGIN_URL)) {// 绝对路径代表是前后端分离,登录成功和失败改为写回json,不重定向了formLogin.successHandler(new LoginSuccessHandler());formLogin.failureHandler(new LoginFailureHandler());}});// 添加BearerTokenAuthenticationFilter,将认证服务当做一个资源服务,解析请求头中的tokenhttp.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()).accessDeniedHandler(SecurityUtils::exceptionHandler).authenticationEntryPoint(SecurityUtils::exceptionHandler));// 兼容前后端分离与不分离配置http// 当未登录时访问认证端点时重定向至login页面.exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(new LoginTargetAuthenticationEntryPoint(LOGIN_URL),new MediaTypeRequestMatcher(MediaType.TEXT_HTML)));// 联合身份认证http.oauth2Login(oauth2Login -> oauth2Login.loginPage(LOGIN_URL).authorizationEndpoint(authorization -> authorization.authorizationRequestResolver(this.authorizationRequestResolver(clientRegistrationRepository))).tokenEndpoint(token -> token.accessTokenResponseClient(this.accessTokenResponseClient())));// 使用redis存储、读取登录的认证信息http.securityContext(context -> context.securityContextRepository(redisSecurityContextRepository));return http.build();}/*** AuthorizationRequest 自定义配置** @param clientRegistrationRepository yml配置中客户端信息存储类* @return OAuth2AuthorizationRequestResolver*/private OAuth2AuthorizationRequestResolver authorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);// 兼容微信登录授权申请authorizationRequestResolver.setAuthorizationRequestCustomizer(new WechatAuthorizationRequestConsumer());return authorizationRequestResolver;}/*** 适配微信登录适配,添加自定义请求token入参处理** @return OAuth2AccessTokenResponseClient accessToken响应信息处理*/private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();tokenResponseClient.setRequestEntityConverter(new WechatCodeGrantRequestEntityConverter());// 自定义 RestTemplate,适配微信登录获取tokenOAuth2AccessTokenResponseHttpMessageConverter messageConverter = new OAuth2AccessTokenResponseHttpMessageConverter();List<MediaType> mediaTypes = new ArrayList<>(messageConverter.getSupportedMediaTypes());// 微信获取token时响应的类型为“text/plain”,这里特殊处理一下mediaTypes.add(MediaType.TEXT_PLAIN);messageConverter.setAccessTokenResponseConverter(new WechatMapAccessTokenResponseConverter());messageConverter.setSupportedMediaTypes(mediaTypes);// 初始化RestTemplateRestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(),messageConverter));restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());tokenResponseClient.setRestOperations(restTemplate);return tokenResponseClient;}/*** 跨域过滤器配置** @return CorsFilter*/@Beanpublic CorsFilter corsFilter() {// 初始化cors配置对象CorsConfiguration configuration = new CorsConfiguration();// 设置允许跨域的域名,如果允许携带cookie的话,路径就不能写*号, *表示所有的域名都可以跨域访问configuration.addAllowedOrigin("http://127.0.0.1:5173");configuration.addAllowedOrigin("http://192.168.1.102:5173");// 设置跨域访问可以携带cookieconfiguration.setAllowCredentials(true);// 允许所有的请求方法 ==> GET POST PUT Deleteconfiguration.addAllowedMethod("*");// 允许携带任何头信息configuration.addAllowedHeader("*");// 初始化cors配置源对象UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource();// 给配置源对象设置过滤的参数// 参数一: 过滤的路径 == > 所有的路径都要求校验是否跨域// 参数二: 配置类configurationSource.registerCorsConfiguration("/**", configuration);// 返回配置好的过滤器return new CorsFilter(configurationSource);}/*** 自定义jwt,将权限信息放至jwt中** @return OAuth2TokenCustomizer的实例*/@Beanpublic OAuth2TokenCustomizer<JwtEncodingContext> oAuth2TokenCustomizer() {return new FederatedIdentityIdTokenCustomizer();}/*** 自定义jwt解析器,设置解析出来的权限信息的前缀与在jwt中的key** @return jwt解析器 JwtAuthenticationConverter*/@Beanpublic JwtAuthenticationConverter jwtAuthenticationConverter() {JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();// 设置解析权限信息的前缀,设置为空是去掉前缀grantedAuthoritiesConverter.setAuthorityPrefix("");// 设置权限信息在jwt claims中的keygrantedAuthoritiesConverter.setAuthoritiesClaimName(SecurityConstants.AUTHORITIES_KEY);JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);return jwtAuthenticationConverter;}/*** 将AuthenticationManager注入ioc中,其它需要使用地方可以直接从ioc中获取** @param authenticationConfiguration 导出认证配置* @return AuthenticationManager 认证管理器*/@Bean@SneakyThrowspublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) {return authenticationConfiguration.getAuthenticationManager();}/*** 配置密码解析器,使用BCrypt的方式对密码进行加密和验证** @return BCryptPasswordEncoder*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 配置客户端Repository** @param jdbcTemplate    db 数据源信息* @param passwordEncoder 密码解析器* @return 基于数据库的repository*/@Beanpublic RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())// 客户端id.clientId("messaging-client")// 客户端秘钥,使用密码解析器加密.clientSecret(passwordEncoder.encode("123456"))// 客户端认证方式,基于请求头的认证.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)// 配置资源服务器使用该客户端获取授权时支持的方式.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)// 客户端添加自定义认证.authorizationGrantType(new AuthorizationGrantType(SecurityConstants.GRANT_TYPE_SMS_CODE))// 授权码模式回调地址,oauth2.1已改为精准匹配,不能只设置域名,并且屏蔽了localhost,本机使用127.0.0.1访问.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc").redirectUri("https://www.baidu.com")// 该客户端的授权范围,OPENID与PROFILE是IdToken的scope,获取授权时请求OPENID的scope时认证服务会返回IdToken.scope(OidcScopes.OPENID).scope(OidcScopes.PROFILE)// 自定scope.scope("message.read").scope("message.write")// 客户端设置,设置用户需要确认授权.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()).build();// 基于db存储客户端,还有一个基于内存的实现 InMemoryRegisteredClientRepositoryJdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);// 初始化客户端RegisteredClient repositoryByClientId = registeredClientRepository.findByClientId(registeredClient.getClientId());if (repositoryByClientId == null) {registeredClientRepository.save(registeredClient);}// 设备码授权客户端RegisteredClient deviceClient = RegisteredClient.withId(UUID.randomUUID().toString()).clientId("device-message-client")// 公共客户端.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)// 设备码授权.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)// 自定scope.scope("message.read").scope("message.write").build();RegisteredClient byClientId = registeredClientRepository.findByClientId(deviceClient.getClientId());if (byClientId == null) {registeredClientRepository.save(deviceClient);}// PKCE客户端RegisteredClient pkceClient = RegisteredClient.withId(UUID.randomUUID().toString()).clientId("pkce-message-client")// 公共客户端.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)// 授权码模式,因为是扩展授权码流程,所以流程还是授权码的流程,改变的只是参数.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)// 授权码模式回调地址,oauth2.1已改为精准匹配,不能只设置域名,并且屏蔽了localhost,本机使用127.0.0.1访问.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc").clientSettings(ClientSettings.builder().requireProofKey(Boolean.TRUE).build())// 自定scope.scope("message.read").scope("message.write").build();RegisteredClient findPkceClient = registeredClientRepository.findByClientId(pkceClient.getClientId());if (findPkceClient == null) {registeredClientRepository.save(pkceClient);}return registeredClientRepository;}/*** 配置基于db的oauth2的授权管理服务** @param jdbcTemplate               db数据源信息* @param registeredClientRepository 上边注入的客户端repository* @return JdbcOAuth2AuthorizationService*/@Beanpublic OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {// 基于db的oauth2认证服务,还有一个基于内存的服务实现InMemoryOAuth2AuthorizationServicereturn new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);}/*** 配置基于db的授权确认管理服务** @param jdbcTemplate               db数据源信息* @param registeredClientRepository 客户端repository* @return JdbcOAuth2AuthorizationConsentService*/@Beanpublic OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {// 基于db的授权确认管理服务,还有一个基于内存的服务实现InMemoryOAuth2AuthorizationConsentServicereturn new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);}/*** 配置jwk源,使用非对称加密,公开用于检索匹配指定选择器的JWK的方法** @return JWKSource*/@Bean@SneakyThrowspublic JWKSource<SecurityContext> jwkSource() {// 先从redis获取String jwkSetCache = redisOperator.get(RedisConstants.AUTHORIZATION_JWS_PREFIX_KEY);if (ObjectUtils.isEmpty(jwkSetCache)) {KeyPair keyPair = generateRsaKey();RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();RSAKey rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();// 生成jwsJWKSet jwkSet = new JWKSet(rsaKey);// 转为json字符串String jwkSetString = jwkSet.toString(Boolean.FALSE);// 存入redisredisOperator.set(RedisConstants.AUTHORIZATION_JWS_PREFIX_KEY, jwkSetString);return new ImmutableJWKSet<>(jwkSet);}// 解析存储的jwsJWKSet jwkSet = JWKSet.parse(jwkSetCache);return new ImmutableJWKSet<>(jwkSet);}/*** 生成rsa密钥对,提供给jwk** @return 密钥对*/private static KeyPair generateRsaKey() {KeyPair keyPair;try {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");keyPairGenerator.initialize(2048);keyPair = keyPairGenerator.generateKeyPair();} catch (Exception ex) {throw new IllegalStateException(ex);}return keyPair;}/*** 配置jwt解析器** @param jwkSource jwk源* @return JwtDecoder*/@Beanpublic JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);}/*** 添加认证服务器配置,设置jwt签发者、默认端点请求地址等** @return AuthorizationServerSettings*/@Beanpublic AuthorizationServerSettings authorizationServerSettings() {return AuthorizationServerSettings.builder()/*设置token签发地址(http(s)://{ip}:{port}/context-path, http(s)://domain.com/context-path)如果需要通过ip访问这里就是ip,如果是有域名映射就填域名,通过什么方式访问该服务这里就填什么*/.issuer("http://192.168.1.102:8080").build();}}

前端项目中编写授权确认、设备码校验、设备码校验成功页面

编写授权确认页面Consent.vue

<script setup lang="ts">
import { type Ref, ref } from 'vue'
import axios from 'axios'
import { createDiscreteApi } from 'naive-ui'const { message } = createDiscreteApi(['message'])// 获取授权确认信息响应
const consentResult: Ref<any> = ref()
// 所有的scope
const scopes = ref()
// 已授权的scope
const approvedScopes = ref()axios({method: 'GET',url: `http://192.168.1.102:8080/oauth2/consent/parameters${window.location.search}`
}).then((r) => {let result = r.dataif (result.success) {consentResult.value = result.datascopes.value = [...result.data.previouslyApprovedScopes, ...result.data.scopes]approvedScopes.value = result.data.previouslyApprovedScopes.map((e: any) => e.scope)} else {message.warning(result.message)}}).catch((e) => message.error(e.message))/*** 提交授权确认** @param cancel true为取消*/
const submitApprove = (cancel: boolean) => {const data = new FormData()if (!cancel) {// 如果不是取消添加scopeif (approvedScopes.value !== null &&typeof approvedScopes.value !== 'undefined' &&approvedScopes.value.length > 0) {approvedScopes.value.forEach((e: any) => data.append('scope', e))}}data.append('state', consentResult.value.state)data.append('client_id', consentResult.value.clientId)data.append('user_code', consentResult.value.userCode)axios({method: 'POST',// @ts-ignoredata: new URLSearchParams(data),headers: {nonceId: getQueryString('nonceId'),'Content-Type': 'application/x-www-form-urlencoded'},url: `http://192.168.1.102:8080${consentResult.value.requestURI}`}).then((r) => {let result = r.dataif (result.success) {window.location.href = result.data} else {if (result.message && result.message.indexOf('access_denied') > -1) {// 可以跳转至一个单独的页面提醒.message.warning('您未选择scope或拒绝了本次授权申请.')} else {message.warning(result.message)}}}).catch((e) => message.error(e.message))
}/*** 获取地址栏参数* @param name 地址栏参数的key*/
function getQueryString(name: string) {var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i')var r = window.location.search.substr(1).match(reg)if (r != null) {return unescape(r[2])}return null
}
</script><template><header><img alt="Vue logo" class="logo" src="../../assets/logo.svg" width="125" height="125" /><div class="wrapper"><HelloWorld msg="OAuth 授权请求" /></div></header><main><n-card v-if="consentResult && consentResult.userCode">您已经提供了代码<b>{{ consentResult.userCode }}</b>,请验证此代码是否与设备上显示的代码匹配。</n-card><br /><n-card :title="`${consentResult.clientName} 客户端`" v-if="consentResult"><template #header-extra>账号:<b>{{ consentResult.principalName }}</b></template>此第三方应用请求获得以下权限</n-card><n-scrollbar style="max-height: 230px"><n-checkbox-group v-model:value="approvedScopes"><n-list><n-list-item v-for="scope in scopes"><template #prefix><n-checkbox :value="scope.scope"> </n-checkbox></template><n-thing :title="scope.scope" :description="scope.description" /></n-list-item></n-list></n-checkbox-group></n-scrollbar><br /><n-button type="info" @click="submitApprove(false)" strong>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</n-button>&nbsp;&nbsp;&nbsp;&nbsp;<n-button type="warning" @click="submitApprove(true)">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</n-button></main>
</template><style scoped>
header {line-height: 1.5;
}.logo {display: block;margin: 0 auto 2rem;
}@media (min-width: 1024px) {header {display: flex;place-items: center;padding-right: calc(var(--section-gap) / 2);}.logo {margin: 0 2rem 0 0;}header .wrapper {display: flex;place-items: flex-start;flex-wrap: wrap;}
}b,
h3,
::v-deep(.n-card-header__main) {font-weight: bold !important;
}
</style>

编写设备码验证页面Activate.vue

<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
import { createDiscreteApi } from 'naive-ui'const { message } = createDiscreteApi(['message'])const userCode = ref({userCode: getQueryString('userCode')
})/*** 提交授权确认** @param cancel true为取消*/
const submit = () => {const data = {user_code: userCode.value.userCode}axios({method: 'POST',data,headers: {nonceId: getQueryString('nonceId'),'Content-Type': 'application/x-www-form-urlencoded'},url: `http://192.168.1.102:8080/oauth2/device_verification`}).then((r) => {let result = r.dataif (result.success) {window.location.href = result.data} else {message.warning(result.message)}}).catch((e) => message.error(e.message))
}if (userCode.value.userCode) {submit()
}/*** 获取地址栏参数* @param name 地址栏参数的key*/
function getQueryString(name: string) {var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i')var r = window.location.search.substr(1).match(reg)if (r != null) {return unescape(r[2])}return null
}
</script><template><header><img alt="Vue logo" class="logo" src="../../assets/devices.png" width="125" height="125" /><div class="wrapper"><HelloWorld msg="设备激活" /></div></header><main><n-card> 输入激活码对设备进行授权。 </n-card><br /><n-card><n-form-item-row label="Activation Code"><n-inputv-model:value="userCode.userCode"placeholder="User Code"maxlength="9"show-countclearable/></n-form-item-row><n-button type="info" @click="submit" block strong> 登录 </n-button></n-card></main>
</template><style scoped>
header {line-height: 1.5;
}.logo {display: block;margin: 0 auto 2rem;
}@media (min-width: 1024px) {header {display: flex;place-items: center;padding-right: calc(var(--section-gap) / 2);}.logo {margin: 0 2rem 0 0;}header .wrapper {display: flex;place-items: flex-start;flex-wrap: wrap;}
}b,
h3,
::v-deep(.n-card-header__main) {font-weight: bold !important;
}
</style>

编写设备码验证成功页面

<script lang="ts" setup></script>
<template><header><img alt="Vue logo" class="logo" src="../../assets/devices.png" width="125" height="125" /><div class="wrapper"><HelloWorld msg="设备激活" /></div></header><main><div style="font-size: 30px">您已成功激活您的设备。<br />请返回到您的设备继续。</div></main>
</template>
<style scoped>
header {line-height: 1.5;
}.logo {display: block;margin: 0 auto 2rem;
}@media (min-width: 1024px) {header {display: flex;place-items: center;padding-right: calc(var(--section-gap) / 2);}.logo {margin: 0 2rem 0 0;}header .wrapper {display: flex;place-items: flex-start;flex-wrap: wrap;}
}b,
h3,
::v-deep(.n-card-header__main) {font-weight: bold !important;
}
</style>

vue-router路由配置index.ts

import { createRouter, createWebHistory } from 'vue-router'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: '/login',name: 'login',component: () => import('../views/login/Login.vue')},{path: '/consent',name: 'consent',// route level code-splitting// this generates a separate chunk (About.[hash].js) for this route// which is lazy-loaded when the route is visited.component: () => import('../views/consent/Consent.vue')},{path: '/activate',name: 'activate',// route level code-splitting// this generates a separate chunk (About.[hash].js) for this route// which is lazy-loaded when the route is visited.component: () => import('../views/device/Activate.vue')},{path: '/activated',name: 'activated',// route level code-splitting// this generates a separate chunk (About.[hash].js) for this route// which is lazy-loaded when the route is visited.component: () => import('../views/device/Activated.vue')}]
})export default router

附一下常量类SecurityConstants

package com.example.constant;/*** security 常量类** @author vains*/
public class SecurityConstants {/*** 授权确认页面地址*/public static final String DEVICE_ACTIVATED_URI = "http://127.0.0.1:5173/activated";/*** 授权确认页面地址*/public static final String DEVICE_ACTIVATE_URI = "http://127.0.0.1:5173/activate";/*** 授权确认页面地址*/public static final String CONSENT_PAGE_URI = "http://127.0.0.1:5173/consent";/*** 微信登录相关参数——openid:用户唯一id*/public static final String WECHAT_PARAMETER_OPENID = "openid";/*** 微信登录相关参数——forcePopup:强制此次授权需要用户弹窗确认*/public static final String WECHAT_PARAMETER_FORCE_POPUP = "forcePopup";/*** 微信登录相关参数——secret:微信的应用秘钥*/public static final String WECHAT_PARAMETER_SECRET = "secret";/*** 微信登录相关参数——appid:微信的应用id*/public static final String WECHAT_PARAMETER_APPID = "appid";/*** 三方登录类型——微信*/public static final String THIRD_LOGIN_WECHAT = "wechat";/*** 三方登录类型——Gitee*/public static final String THIRD_LOGIN_GITEE = "gitee";/*** 三方登录类型——Github*/public static final String THIRD_LOGIN_GITHUB = "github";/*** 随机字符串请求头名字*/public static final String NONCE_HEADER_NAME = "nonceId";/*** 登录方式入参名*/public static final String LOGIN_TYPE_NAME = "loginType";/*** 验证码id入参名*/public static final String CAPTCHA_ID_NAME = "captchaId";/*** 验证码值入参名*/public static final String CAPTCHA_CODE_NAME = "code";/*** 登录方式——短信验证码*/public static final String SMS_LOGIN_TYPE = "smsCaptcha";/*** 登录方式——账号密码登录*/public static final String PASSWORD_LOGIN_TYPE = "passwordLogin";/*** 权限在token中的key*/public static final String AUTHORITIES_KEY = "authorities";/*** 自定义 grant type —— 短信验证码*/public static final String GRANT_TYPE_SMS_CODE = "urn:ietf:params:oauth:grant-type:sms_code";/*** 自定义 grant type —— 短信验证码 —— 手机号的key*/public static final String OAUTH_PARAMETER_NAME_PHONE = "phone";/*** 自定义 grant type —— 短信验证码 —— 短信验证码的key*/public static final String OAUTH_PARAMETER_NAME_SMS_CAPTCHA = "sms_captcha";}

到此为止编码就结束了

最后

因为理论部分在之前的文章中已经讲过了,这次就没写理论了,直接贴了一大堆的代码,本次代码写的比较仓促,测试的也不是很全面,如果发现有什么问题可以在评论区留言。


代码仓库地址

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/26537.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JVM系统优化实践(24):ZGC(一)

您好&#xff0c;这里是「码农镖局」CSDN博客&#xff0c;欢迎您来&#xff0c;欢迎您再来&#xff5e; 截止到目前&#xff0c;算上ZGC&#xff0c;Java一共有九种类型的GC&#xff0c;它们分别是&#xff1a; 1、Serial GC 串行/作用于新生代/复制算法/响应速度优先/适用于单…

C++教程 - How to C++系列专栏第0篇

关于专栏 这个专栏是优质的C教程专栏 本专栏一致使用操作系统&#xff1a;macOS Ventura&#xff0c;代码编辑器&#xff1a;CLion&#xff0c;C编译器&#xff1a;Clang 感谢一路相伴的朋友们&#xff0c;感谢你们的支持 ^ _ ^ 博主反馈非常及时&#xff0c;如果你在阅读…

NativePHP:使用PHP构建跨平台桌面应用的新框架

NativePHP是一个用于使用PHP构建桌面应用的框架。它允许PHP开发人员使用熟悉的工具和技术创建跨平台的原生应用。NativePHP具有一系列易于使用的类&#xff0c;一套用于构建和打包应用程序的工具以及一个静态跨平台PHP运行时。 官网地址&#xff1a;https://nativephp.com PH…

真的不想知道录音转文字怎么弄才简单吗

哇哦&#xff01;听说你想知道如何将录音转成文字&#xff1f;这简直是一个超酷的技能&#xff0c;让我来为你揭开这个神奇的面纱吧&#xff01;想象一下&#xff0c;当你有一堆录音文件需要处理时&#xff0c;你不再需要费尽心思地一遍遍倾听、抄写。现在&#xff0c;你只需要…

LumenSceneData 初始化 [1]

前置信息&#xff1a; 灯光从World到Scene的流程。 UE4 Lights UWorld to FScene [1]_spawnactor failed because of collision at the spaw_sh15285118586的博客-CSDN博客 mesh从world到Scene流程&#xff0c;与灯光类似 void UStaticMeshComponent::CreateRenderState_Con…

Kubectl 详解

目录 陈述式资源管理方法:项目的生命周期:创建-->发布-->更新-->回滚-->删除声明式管理方法:陈述式资源管理方法: kubernetes 集群管理集群资源的唯一入口是通过相应的方法调用 apiserver 的接口kubectl 是官方的CLI命令行工具,用于与 apiserver 进行通信,将…

基于YOLOv7的密集场景行人检测识别分析系统

密集场景下YOLO系列模型的精度如何&#xff1f;本文的主要目的就是想要基于密集场景基于YOLOv7模型开发构建人流计数系统&#xff0c;简单看下效果图&#xff1a; 这里实验部分使用到的数据集为VSCrowd数据集。 实例数据如下所示&#xff1a; 下载到本地解压缩后如下所示&…

webpack 静态模块打包工具

webpack 为什么? 把静态模块内容&#xff0c;压缩&#xff0c;整合&#xff0c;转译等(前端工程化) 把less/sass转成css代码把ES6 降级成ES5支持多种模块文件类型&#xff0c;多种模块标准语法 vite 为什么不直接学习vite 而学习webpack 因为很多项目还是基于webpack来进…

js 判断对象为数组.html

<!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8" /> <title>数组判断</title> </head> <body> <script> function isArray(obj) { /* 判断对象 obj 是否是数组。*/ return typeof o…

vue加载大量数据优化

在Vue中加载大量数据并形成列表时&#xff0c;可以通过以下方法来优化性能&#xff1a; 分页加载&#xff1a;不要一次性加载所有的数据&#xff0c;而是分批加载数据&#xff0c;每次只加载当前页需要显示的数据量。可以使用第三方库如vue-infinite-loading来实现无限滚动加载…

找免费商用的图片素材就上这6个网站。

分享6个免费商用的高清图片素材库&#xff0c;你想要找到这里都能找到&#xff0c;赶紧收藏起来吧~ 菜鸟图库 https://www.sucai999.com/pic.html?vNTYwNDUx 网站主要是为新手设计师提供免费素材的&#xff0c;素材的质量都很高&#xff0c;类别也很多&#xff0c;像平面、UI…

Git Submodule 更新子库失败 fatal: Unable to fetch in submodule path

编辑本地目录 .git/config 文件 在 [submodule “Assets/CommonModule”] 项下 加入 fetch refs/heads/:refs/remotes/origin/

常规VUE项目优化实践,跟着做就对了!

总结&#xff1a; 主要优化方式&#xff1a; imagemin优化打包大小&#xff08;96M->50M&#xff09;&#xff0c;但是以打包速度为代价&#xff0c;通过在构建过程中压缩图片来实现&#xff0c;可根据需求开启。字体压缩&#xff1a;目前项目内引用为思源字体&#xff0c…

认识所有权

专栏简介&#xff1a;本专栏作为Rust语言的入门级的文章&#xff0c;目的是为了分享关于Rust语言的编程技巧和知识。对于Rust语言&#xff0c;虽然历史没有C、和python历史悠远&#xff0c;但是它的优点可以说是非常的多&#xff0c;既继承了C运行速度&#xff0c;还拥有了Java…

C++——文件操作

一、文本文件 C中输入输出是通过流对象进行操作&#xff0c;对于文件来说写文件就是将内容从程序输出到文件&#xff0c;需要用到写文件流ofstream&#xff1b;而读文件就是将内容从文件输入到程序&#xff0c;需要用到读文件流ifstream&#xff1b;这两个文件流类都包含在头文…

f1tenth的多点导航+路径规划+pursuit追踪

文章目录 一、发布起点,终点二、 记录规划轨迹点三、pursuit追踪一、发布起点,终点 pub_amcl: #!/usr/bin/env python3import rospy from geometry_msgs.msg import PoseWithCovarianceStampeddef publish_amcl_pose():# 初始化节点rospy.init_node(amcl_pose_publisher)# …

oracle的管道函数

Oracle管道函数(Pipelined Table Function)oracle管道函数 1、管道函数即是可以返回行集合&#xff08;可以使嵌套表nested table 或数组 varray&#xff09;的函数&#xff0c;我们可以像查询物理表一样查询它或者将其赋值给集合变量。 2、管道函数为并行执行&#xff0c;在…

P1257 平面上的最接近点对

题目 思路 详见加强加强版 代码 #include<bits/stdc.h> using namespace std; #define int long long const int maxn4e510; pair<int,int> a[maxn]; int n; double d1e16; pair<int,int> vl[maxn],vr[maxn]; void read() { cin>>n;for(int i1;i<…

Android性能优化—数据结构优化

优化数据结构是提高Android应用性能的重要一环。在Android开发中&#xff0c;ArrayList、LinkedList和HashMap等常用的数据结构的正确使用对APP性能的提升有着重大的影响。 一、ArrayList ArrayList内部使用的是数组&#xff0c;默认大小10&#xff0c;当数组长度不足时&…