如何跳过Spring Authorization Server的/login重定向,用自定义Angular登录页实现OAuth2登录
实现自定义Angular登录页对接Spring授权服务器的方案
完全可以实现你描述的流程——用自定义Angular登录页替代授权服务器的默认登录页,由前端专属的Spring OAuth客户端后端接管整个认证流程。以下是具体的实现步骤和配置:
1. 调整Spring OAuth客户端的认证异常处理
首先修改Spring Security配置,让未认证的请求返回401 Unauthorized而非重定向到授权服务器的登录页:
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .anyRequest().authenticated() ) .oauth2Client(oauth2 -> oauth2 .clientRegistrationRepository(clientRegistrationRepository()) .authorizedClientService(authorizedClientService()) ) .exceptionHandling(exceptions -> exceptions .authenticationEntryPoint((request, response, authException) -> { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.getWriter().write("{\"error\":\"Unauthorized\",\"message\":\"请先登录\"}"); }) ) // SPA场景下可禁用CSRF,根据实际业务调整 .csrf(csrf -> csrf.disable()); return http.build(); }
2. 后端实现登录接口,接管授权码流程
后端提供登录接口接收Angular提交的用户名密码,完成授权码流程的全步骤:
核心流程:
- 用用户凭证调用授权服务器登录接口,建立会话
- 复用该会话请求授权服务器的授权端点,获取授权码
- 用授权码调用令牌端点,换取
access_token和refresh_token - 将令牌存储在后端会话中,供后续资源请求使用
示例登录接口代码:
@RestController @RequestMapping("/api") public class AuthController { private final RestTemplate restTemplate; private final ClientRegistration clientRegistration; private final AuthorizedClientService authorizedClientService; public AuthController(RestTemplateBuilder restTemplateBuilder, ClientRegistrationRepository clientRegistrationRepository, AuthorizedClientService authorizedClientService) { this.restTemplate = restTemplateBuilder.build(); this.clientRegistration = clientRegistrationRepository.findByRegistrationId("angular-backend"); this.authorizedClientService = authorizedClientService; } @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest, HttpSession session) { // 1. 调用授权服务器登录接口,获取会话Cookie HttpHeaders loginHeaders = new HttpHeaders(); loginHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap<String, String> loginParams = new LinkedMultiValueMap<>(); loginParams.add("username", loginRequest.getUsername()); loginParams.add("password", loginRequest.getPassword()); loginParams.add("submit", "Login"); ResponseEntity<String> loginResponse = restTemplate.postForEntity( "http://auth-server:9000/login", new HttpEntity<>(loginParams, loginHeaders), String.class ); String sessionCookie = loginResponse.getHeaders().getFirst(HttpHeaders.SET_COOKIE); // 2. 请求授权服务器获取授权码 HttpHeaders authHeaders = new HttpHeaders(); authHeaders.add(HttpHeaders.COOKIE, sessionCookie); String authUrl = UriComponentsBuilder.fromUriString("http://auth-server:9000/oauth2/authorize") .queryParam("response_type", "code") .queryParam("client_id", clientRegistration.getClientId()) .queryParam("redirect_uri", clientRegistration.getRedirectUri()) .queryParam("scope", String.join(" ", clientRegistration.getScopes())) .build().toUriString(); ClientHttpResponse authResponse = restTemplate.execute( authUrl, HttpMethod.GET, request -> request.getHeaders().addAll(authHeaders), response -> response ); String redirectUrl = authResponse.getHeaders().getFirst(HttpHeaders.LOCATION); String authorizationCode = UriComponentsBuilder.fromUriString(redirectUrl) .build().getQueryParams().getFirst("code"); // 3. 用授权码换取令牌 OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("angular-backend") .principal(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), null)) .attribute(OAuth2ParameterNames.CODE, authorizationCode) .build(); OAuth2AuthorizedClient authorizedClient = authorizedClientService.authorize(authorizeRequest); session.setAttribute("authorizedClient", authorizedClient); return ResponseEntity.ok("登录成功"); } static class LoginRequest { private String username; private String password; // 生成getter/setter } }
3. 授权服务器配置调整
确保授权服务器允许后端客户端跳过用户确认,并配置正确的回调地址:
@Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("angular-backend-client") .clientSecret("{noop}client-secret") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .redirectUri("http://localhost:8080/login/oauth2/code/angular-backend") .scope("read") .scope("write") .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build()) // 跳过用户确认 .build(); return new InMemoryRegisteredClientRepository(client); }
4. Angular前端处理逻辑
4.1 配置HTTP拦截器,处理401跳转
拦截所有请求,收到401时跳转到自定义登录页:
import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { Router } from '@angular/router'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private router: Router) {} intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { return next.handle(request).pipe( catchError((error: HttpErrorResponse) => { if (error.status === 401) { sessionStorage.setItem('redirectUrl', this.router.url); this.router.navigate(['/login']); } return throwError(() => error); }) ); } }
4.2 登录组件实现
提交表单到后端登录接口,登录成功后跳转到之前的目标页面:
import { Component } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Router } from '@angular/router'; @Component({ selector: 'app-login', templateUrl: './login.component.html' }) export class LoginComponent { username: string = ''; password: string = ''; constructor(private http: HttpClient, private router: Router) {} onSubmit() { this.http.post('/api/login', { username: this.username, password: this.password }) .subscribe({ next: () => { const redirectUrl = sessionStorage.getItem('redirectUrl') || '/dashboard'; this.router.navigate([redirectUrl]); sessionStorage.removeItem('redirectUrl'); }, error: () => alert('用户名或密码错误') }); } }
关键注意事项
- 安全层面:必须用HTTPS传输所有认证相关请求,避免密码和令牌泄露;后端存储令牌时建议使用HttpOnly Cookie,防止XSS攻击。
- 会话管理:后端调用授权服务器接口时,要正确传递会话Cookie,确保授权流程的连续性。
- 令牌刷新:实现令牌自动刷新逻辑,避免用户频繁登录;可借助Spring的
OAuth2AuthorizedClientService自动处理令牌刷新。
内容的提问来源于stack exchange,提问作者bindCake
相关产品推荐
相关产品推荐

