网站首页 > 文章精选 正文
介绍
Spring Security OAuth2 默认实现的四种授权模式在实际的应用场景中往往满足不了预期。 需要扩展如下需求:
- 手机号+短信验证码登陆
- 微信授权登录
本次主要通过继承Spring Security OAuth2 抽象类和接口,来实现对oauth2/token接口的手机号+短信的认证授权。
代码
https://gitee.com/LearningTech/springoatuh2
开发环境
- JDK 17
- Spring Boot 3
核心概念和流程
- SecurityFilterChain: 表示Spring Security的过滤器链。实现安全配置和认证扩展配置
- RegisteredClientRepository: 表示自定义的授权客户端信息,需要进行配置。这个客户端信息是oauth2/token中需要进行认证的信息。
- AbstractAuthenticationToken: 表示用户认证信息。 需要对其进行扩展
- AuthenticationProvider: 验证登录信息,实现token的生成。需要对其进行扩展
- AuthenticationConverter: 实现对AbstractAuthenticationToken自定义扩展类的转换。
主要流程就是,实现上述AbstractAuthenticationToken、AuthenticationProvider、AuthenticationConverter三个抽象类和接口的扩展。并通过实现AuthenticationSuccessHandler扩展类,用来返回token给http response中。
AuthorizationServerConfig.java
Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
// 自定义授权模式转换器
.accessTokenRequestConverter(new MobilePhoneAuthenticationConverter())
.accessTokenRequestConverter(new UsernamePasswordGrantAuthenticationConverter())
// 自定义授权响应
.accessTokenResponseHandler(new CustomizerAuthenticationSuccessHandler())
.errorResponseHandler(new CustomizerAuthenticationFailureHandler())
)
.oidc(Customizer.withDefaults());
http.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
OAuth2Error error = new OAuth2Error(
"unauthorized",
authException.getMessage(),
"https://tools.ietf.org/html/rfc6750#section-3.1"
);
new ObjectMapper().writeValue(response.getOutputStream(), error);
})
);
// 添加自定义的认证提供者
http.authenticationProvider(mobilePhoneAuthenticationProvider);
return http.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("mobile-client")
.clientSecret(passwordEncoder.encode("secret"))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(new AuthorizationGrantType("mobile_phone")) // 自定义授权类型
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/mobile-client")
.scope("message.read")
.scope("message.write")
.tokenSettings(TokenSettings.builder()
.accessTokenFormat(OAuth2TokenFormat.REFERENCE) // 设置访问令牌格式为 REFERENCE
.build())
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
SecurityConfig.java
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/send-sms", "/oauth2/token").permitAll()
.anyRequest().authenticated()
)
.csrf((csrf) -> csrf.ignoringRequestMatchers("/send-sms", "/oauth2/token"));
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
MobilePhoneAuthenticationConverter.java
public class MobilePhoneAuthenticationConverter implements AuthenticationConverter {
@Override
public Authentication convert(HttpServletRequest request) {
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!"mobile_phone".equals(grantType)) {
return null;
}
String phoneNumber = request.getParameter("phone_number");
String smsCode = request.getParameter("sms_code");
String clientId = request.getParameter(OAuth2ParameterNames.CLIENT_ID);
if (phoneNumber == null || smsCode == null || clientId == null) {
throw new OAuth2AuthenticationException(new OAuth2Error("invalid_request"));
}
return new MobilePhoneAuthenticationToken(phoneNumber, smsCode, clientId);
}
}
MobilePhoneAuthenticationProvider.java
@Component
public class MobilePhoneAuthenticationProvider implements AuthenticationProvider {
@Autowired
private OAuth2AuthorizationService authorizationService;
@Autowired
private OAuth2TokenGenerator<OAuth2Token> tokenGenerator;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
MobilePhoneAuthenticationToken mobilePhoneAuthentication = (MobilePhoneAuthenticationToken) authentication;
// 验证手机号和验证码的逻辑...
String phoneNumber = (String) mobilePhoneAuthentication.getPrincipal();
String smsCode = (String) mobilePhoneAuthentication.getCredentials();
// 这里应该添加实际的验证逻辑
if (!"123456".equals(smsCode)) { // 示例验证,实际应该查询数据库或缓存
throw new BadCredentialsException("Invalid SMS code");
}
OAuth2ClientAuthenticationToken clientPrincipal =
getAuthenticatedClientElseThrowInvalidClient();
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(mobilePhoneAuthentication)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizedScopes(registeredClient.getScopes())
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
.authorizationGrantType(new AuthorizationGrantType("mobile_phone"))
.build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedAccessToken instanceof OAuth2AccessToken)) {
throw new OAuth2AuthenticationException(new OAuth2Error("server_error", "The token generator failed to generate the access token.", null));
}
OAuth2AccessToken accessToken = (OAuth2AccessToken) generatedAccessToken;
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {
tokenContext = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(mobilePhoneAuthentication)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizedScopes(registeredClient.getScopes())
.tokenType(OAuth2TokenType.REFRESH_TOKEN)
.authorizationGrantType(new AuthorizationGrantType("mobile_phone"))
.build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
throw new OAuth2AuthenticationException(new OAuth2Error("server_error", "The token generator failed to generate the refresh token.", null));
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
}
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(phoneNumber)
.authorizationGrantType(new AuthorizationGrantType("mobile_phone"))
.token(accessToken)
.refreshToken(refreshToken)
.build();
this.authorizationService.save(authorization);
return new OAuth2AccessTokenAuthenticationToken(
registeredClient, clientPrincipal, accessToken, refreshToken, Collections.emptyMap());
}
@Override
public boolean supports(Class<?> authentication) {
return MobilePhoneAuthenticationToken.class.isAssignableFrom(authentication);
}
private OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient() {
// 这里需要实现获取当前认证的客户端逻辑
// 例如,从 SecurityContextHolder 中获取
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof OAuth2ClientAuthenticationToken) {
return (OAuth2ClientAuthenticationToken) authentication;
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
MobilePhoneAuthenticationToken.java
public class MobilePhoneAuthenticationToken extends AbstractAuthenticationToken {
private final String phoneNumber;
private final String smsCode;
private final String clientId;
public MobilePhoneAuthenticationToken(String phoneNumber, String smsCode, String clientId) {
super(null);
this.phoneNumber = phoneNumber;
this.smsCode = smsCode;
this.clientId = clientId;
setAuthenticated(false);
}
public MobilePhoneAuthenticationToken(String phoneNumber, String smsCode, String clientId, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.phoneNumber = phoneNumber;
this.smsCode = smsCode;
this.clientId = clientId;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.smsCode;
}
@Override
public Object getPrincipal() {
return this.phoneNumber;
}
public String getClientId() {
return this.clientId;
}
}
测试验证
猜你喜欢
- 2024-12-30 简单的使用SpringBoot整合SpringSecurity
- 2024-12-30 Spring Security 整合OAuth2 springsecurity整合oauth2+jwt+vue
- 2024-12-30 DeepSeek-Coder-V2震撼发布,尝鲜体验
- 2024-12-30 一个数组一行代码,Spring Security就接管了Swagger认证授权
- 2024-12-30 简单漂亮的(图床工具)开源图片上传工具——PicGo
- 2024-12-30 Spring Boot(十一):Spring Security 实现权限控制
- 2024-12-30 绝了!万字搞定 Spring Security,写得太好了
- 2024-12-30 SpringBoot集成Spring Security springboot集成springsecurity
- 2024-12-30 SpringSecurity密码加密方式简介 spring 密码加密
- 2024-12-30 Spring cloud Alibaba 从入门到放弃
- 最近发表
- 标签列表
-
- newcoder (56)
- 字符串的长度是指 (45)
- drawcontours()参数说明 (60)
- unsignedshortint (59)
- postman并发请求 (47)
- python列表删除 (50)
- 左程云什么水平 (56)
- 计算机网络的拓扑结构是指() (45)
- 稳压管的稳压区是工作在什么区 (45)
- 编程题 (64)
- postgresql默认端口 (66)
- 数据库的概念模型独立于 (48)
- 产生系统死锁的原因可能是由于 (51)
- 数据库中只存放视图的 (62)
- 在vi中退出不保存的命令是 (53)
- 哪个命令可以将普通用户转换成超级用户 (49)
- noscript标签的作用 (48)
- 联合利华网申 (49)
- swagger和postman (46)
- 结构化程序设计主要强调 (53)
- 172.1 (57)
- apipostwebsocket (47)
- 唯品会后台 (61)
- 简历助手 (56)
- offshow (61)