在构建现代Web应用时,认证与授权是绕不开的核心功能。Spring Security OAuth2作为Java生态中最成熟的解决方案之一,提供了开箱即用的安全能力。但在实际项目中,我们往往需要深度定制登录流程和认证规则。本文将基于Spring Security OAuth2授权服务器0.4.2版本,手把手带你实现以下关键功能:
首先确保你的Spring Boot项目版本在2.7.x以上(对应Spring Security 5.7+)。在pom.xml中添加关键依赖:
xml复制<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.4.2</version>
</dependency>
<!-- 其他必要依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
提示:spring-security-oauth2-authorization-server是Spring官方新推出的授权服务器实现,相比老旧的spring-security-oauth2-autoconfigure,它完全适配Spring Security 5.x+架构,且持续维护更新。
我们的定制化方案将涉及以下核心组件:
现代Spring Security采用多过滤器链设计,我们需要分别配置授权服务器和资源服务器的安全规则:
java复制@Configuration
@EnableWebSecurity
@Slf4j
public class SecurityConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) // 高优先级
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// 应用OAuth2默认安全配置
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// 自定义表单登录配置
http.formLogin(form -> form
.loginPage("/auth/login") // 登录页URL
.loginProcessingUrl("/server-login") // 处理端点
.permitAll()
);
// 替换默认认证过滤器
http.addFilterAt(customUsernamePasswordAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
@Order(1) // 低优先级
public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/userInfo").authenticated()
.anyRequest().permitAll()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults())
);
return http.build();
}
}
关键点说明:
@Order注解确保过滤器链的正确执行顺序addFilterAt用自定义过滤器替换默认实现- 资源服务器配置需禁用CSRF以适配JWT
我们需要扩展UsernamePasswordAuthenticationFilter以支持额外参数:
java复制public class CustomUsernamePasswordAuthenticationFilter
extends UsernamePasswordAuthenticationFilter {
private static final String CODE_PARAM = "code";
private static final String VERIFY_TOKEN_PARAM = "verifyToken";
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
String username = obtainUsername(request);
String password = obtainPassword(request);
String code = request.getParameter(CODE_PARAM);
String verifyToken = request.getParameter(VERIFY_TOKEN_PARAM);
// 创建自定义Token
CustomAuthenticationToken authRequest =
new CustomAuthenticationToken(
username, password,
Collections.emptyList(),
code, verifyToken);
// 设置请求详情
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
创建CustomAuthenticationToken继承标准实现:
java复制@Data
@EqualsAndHashCode(callSuper = true)
public class CustomAuthenticationToken
extends UsernamePasswordAuthenticationToken {
private final String code;
private final String verifyToken;
public CustomAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities,
String code, String verifyToken) {
super(principal, credentials, authorities);
this.code = code;
this.verifyToken = verifyToken;
}
}
实现AuthenticationProvider接口处理认证逻辑:
java复制@Component
@Slf4j
public class CustomAuthenticationProvider
implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
if (!supports(authentication.getClass())) {
return null;
}
CustomAuthenticationToken authRequest =
(CustomAuthenticationToken) authentication;
// 获取认证要素
String username = authRequest.getName();
String password = authRequest.getCredentials().toString();
String code = authRequest.getCode();
String verifyToken = authRequest.getVerifyToken();
log.info("认证请求 - 用户名: {}, 验证码: {}, Token: {}",
username, code, verifyToken);
// TODO: 实现你的业务校验逻辑
validateCodeAndToken(code, verifyToken);
UserDetails user = loadUser(username, password);
// 返回认证成功的Token
return new CustomAuthenticationToken(
user, password,
user.getAuthorities(),
code, verifyToken);
}
@Override
public boolean supports(Class<?> authentication) {
return CustomAuthenticationToken.class
.isAssignableFrom(authentication);
}
private void validateCodeAndToken(String code, String token) {
// 实现你的验证码校验逻辑
}
private UserDetails loadUser(String username, String password) {
// 实现你的用户加载逻辑
}
}
java复制@Configuration
public class AuthorizationServerConfig {
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("20210903")
.clientSecret("{noop}cas123456")
.clientAuthenticationMethod(
ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8082/client/callbackCode")
.scope("openid")
.scope("message.read")
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(false)
.build())
.build();
return new InMemoryRegisteredClientRepository(client);
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAKey rsaKey = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
.privateKey(keyPair.getPrivate())
.keyID(UUID.randomUUID().toString())
.build();
return new ImmutableJWKSet<>(new JWKSet(rsaKey));
}
private static KeyPair generateRsaKey() {
try {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
return generator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}
html复制<!DOCTYPE html>
<html>
<head>
<title>系统登录</title>
<style>
/* 样式优化略 */
</style>
</head>
<body>
<div class="login-container">
<h2>系统登录</h2>
<div class="error-message" th:if="${param.error}">
认证失败:用户名或密码错误
</div>
<form action="/server-login" method="post">
<div class="form-group">
<label>用户名</label>
<input type="text" name="username" required>
</div>
<div class="form-group">
<label>密码</label>
<input type="password" name="password" required>
</div>
<div class="form-group">
<label>验证码</label>
<input type="text" name="code" required>
<img src="/captcha" onclick="this.src='/captcha?t='+Date.now()">
</div>
<input type="hidden" name="verifyToken" th:value="${session.verifyToken}">
<button type="submit">登录</button>
</form>
</div>
</body>
</html>
java复制@RestController
public class CaptchaController {
@GetMapping("/captcha")
public void generateCaptcha(HttpServletResponse response,
HttpSession session) throws IOException {
// 生成随机验证码
String code = RandomUtil.randomString(4);
// 存入session
session.setAttribute("captcha", code);
// 生成图片并输出
response.setContentType("image/png");
CaptchaUtil.createLineCaptcha(100, 40)
.write(response.getOutputStream());
}
}
java复制private void validateCode(String inputCode, HttpServletRequest request) {
String sessionCode = (String) request.getSession()
.getAttribute("captcha");
if (!inputCode.equalsIgnoreCase(sessionCode)) {
throw new BadCredentialsException("验证码错误");
}
}
现象:自定义登录页无法跳转,返回404错误
原因:资源服务器过滤器链拦截了登录页请求
解决:确保登录页路径在资源服务器配置中放行:
java复制@Bean
@Order(1)
public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/auth/**").permitAll()
// 其他配置...
);
// ...
}
现象:提交登录表单时出现403错误
解决方案:根据场景选择以下任一种:
java复制http.csrf(csrf -> csrf.disable());
html复制<input type="hidden"
name="${_csrf.parameterName}"
th:value="${_csrf.token}">
现象:自定义字段无法传递到AuthenticationProvider
排查步骤:
在微服务架构下,建议将会话信息存储到Redis:
yaml复制# application.yml
spring:
session:
store-type: redis
redis:
host: localhost
port: 6379
使用Spring Security的速率限制功能:
java复制http.formLogin(form -> form
.loginProcessingUrl("/server-login")
.failureHandler(new SimpleUrlAuthenticationFailureHandler() {
private final RateLimiter limiter = RateLimiter.create(5); // 5次/秒
@Override
public void onAuthenticationFailure(...) {
if (!limiter.tryAcquire()) {
response.sendError(429, "尝试次数过多");
return;
}
super.onAuthenticationFailure(request, response, exception);
}
})
);
扩展认证令牌支持OTP:
java复制public class CustomAuthenticationToken ... {
private String otpCode;
// ...
}
// 在认证提供器中校验
if (!otpService.validate(user.getUsername(), authRequest.getOtpCode())) {
throw new BadCredentialsException("动态验证码错误");
}
通过以上完整实现,我们构建了一个具备企业级安全特性的认证系统,既保持了Spring Security OAuth2的强大功能,又完美适配了业务定制化需求。实际部署时,建议结合具体业务场景补充审计日志、异常监控等生产级功能。