如何用Angular登录替代Spring Security OAuth2服务器的默认登录?
解耦Spring Security默认登录,用Angular实现自定义登录流程
一、Spring Security OAuth2授权服务器配置调整
1. 关闭默认登录页面,开放核心端点
通过配置SecurityFilterChain,关闭Spring Security自带的表单登录,同时放行OAuth2授权、令牌端点,并配置跨域允许Angular客户端访问:
@Bean public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http .cors(cors -> cors.configurationSource(corsConfigurationSource())) .formLogin(form -> form.disable()) // 禁用默认登录页 .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())); return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of("http://localhost:4200")); // 替换为你的Angular域名 config.setAllowedMethods(List.of("GET", "POST", "OPTIONS")); config.setAllowedHeaders(List.of("*")); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; }
2. 自定义认证端点(处理用户名密码验证)
创建REST接口接收Angular的登录请求,完成用户名密码校验后生成授权码:
@RestController @RequestMapping("/api/auth") public class AuthController { private final AuthenticationManager authManager; private final OAuth2AuthorizationService authzService; private final RegisteredClientRepository clientRepo; public AuthController(AuthenticationManager authManager, OAuth2AuthorizationService authzService, RegisteredClientRepository clientRepo) { this.authManager = authManager; this.authzService = authzService; this.clientRepo = clientRepo; } @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest request) { // 1. 校验用户名密码 UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( request.getUsername(), request.getPassword()); Authentication auth = authManager.authenticate(authToken); SecurityContextHolder.getContext().setAuthentication(auth); // 2. 获取客户端配置 RegisteredClient client = clientRepo.findByClientId(request.getClientId()); if (client == null) { return ResponseEntity.badRequest().body("无效客户端ID"); } // 3. 构建授权请求并生成授权码 OAuth2AuthorizationRequest authzRequest = OAuth2AuthorizationRequest.authorizationCode() .clientId(client.getClientId()) .redirectUri(request.getRedirectUri()) .scope(client.getScopes()) .state(request.getState()) .build(); OAuth2Authorization authz = OAuth2Authorization.withRegisteredClient(client) .principalName(auth.getName()) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .attribute(OAuth2AuthorizationRequest.class.getName(), authzRequest) .build(); authzService.save(authz); // 4. 返回授权码 return ResponseEntity.ok(new AuthResponse(authzRequest.getAuthorizationCode().getValue())); } // 请求/响应实体类 public static class LoginRequest { private String username; private String password; private String clientId; private String redirectUri; private String state; // Getter/Setter } public static class AuthResponse { private String code; public AuthResponse(String code) { this.code = code; } // Getter } }
同时配置AuthenticationManager供认证使用:
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); }
二、Angular客户端实现流程
1. 构建登录表单
创建基础登录组件,包含用户名、密码输入框:
<form (ngSubmit)="handleLogin()"> <input type="text" [(ngModel)]="username" name="username" placeholder="用户名" required> <input type="password" [(ngModel)]="password" name="password" placeholder="密码" required> <button type="submit">登录</button> </form>
2. 提交登录请求并换取令牌
在组件TS代码中,先发送请求获取授权码,再用授权码换取access_token:
import { HttpClient } from '@angular/common/http'; import { Component } from '@angular/core'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent { username: string = ''; password: string = ''; private clientId = 'your-client-id'; // 与后端配置一致 private redirectUri = 'http://localhost:4200/callback'; // 与后端配置一致 private state = this.generateRandomState(); constructor(private http: HttpClient) {} // 生成随机state防CSRF generateRandomState(): string { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); } handleLogin() { const loginPayload = { username: this.username, password: this.password, clientId: this.clientId, redirectUri: this.redirectUri, state: this.state }; // 第一步:获取授权码 this.http.post<any>('http://localhost:8080/api/auth/login', loginPayload) .subscribe(res => { // 第二步:用授权码换令牌 this.exchangeCodeForToken(res.code); }); } exchangeCodeForToken(code: string) { const tokenForm = new FormData(); tokenForm.append('grant_type', 'authorization_code'); tokenForm.append('code', code); tokenForm.append('redirect_uri', this.redirectUri); tokenForm.append('client_id', this.clientId); // 若客户端配置了密钥,需添加:tokenForm.append('client_secret', 'your-client-secret'); this.http.post<any>('http://localhost:8080/oauth2/token', tokenForm) .subscribe(tokenRes => { // 存储令牌到本地(建议用HttpOnly Cookie更安全) localStorage.setItem('access_token', tokenRes.access_token); localStorage.setItem('refresh_token', tokenRes.refresh_token); // 跳转至应用首页 window.location.href = '/home'; }); } }
三、关键注意事项
- 客户端配置一致性:Angular中使用的
clientId、redirectUri必须与后端RegisteredClient中的配置完全匹配。 - 安全防护:务必验证
state参数防止CSRF攻击;令牌存储优先选择HttpOnly Cookie,避免XSS风险。 - 后续请求处理:Angular可通过HTTP拦截器,在所有请求头中自动添加
Authorization: Bearer {access_token},实现接口权限验证。
内容的提问来源于stack exchange,提问作者Carlos C.M
相关产品推荐
相关产品推荐

