Spring Security OAuth2自定义授权模式
Spring Security OAuth2自定义授权模式
在实际开发过程中Spring Security OAuth2
默认提供的五种授权模式不够用的情况下,就需要我们自己来定义授权模式。
例如国内比较常见的应用场景:短信验证码授权登录。
验证码登录
其实实现这个需求有多种实现方式:
第一种:写一个controller
接收登录,逻辑通过则在controller
中组装token
。
第二种:以Spring Security
认证的思维,编写一个短信验证码的过滤器,配置在过滤器链上,加一个登录成功处理器,在登录成功后组装token
。
这两种写法都可以实现短信验证码登录的这个需求,但是都不太规范。
规范写法:按照Spring Security OAuth2
框架的的设计思想来完成自定义授权模式。
Spring Security OAuth2
的授权思想和Spring Security
有点区别:授权认证的时机不同。
Spring Security OAuth2
是在请求进入controller
后得到请求参数,然后使用Spring Security OAuth2
提供的一些内置组件完成token
的生成,而Spring Security
则是依赖过滤器链来实现的,Spring Security
是在请求未到达controller
时,被匹配的过滤器拦截,然后进行认证,生成token
。
实现思路
密码模式的授权流程:
重要知识点:
- 每种授权类型都对应一个实现类
TokenGranter
- 所有
TokenGranter
实现类都通过CompositeTokenGranter
中的tokenGranters
集合存储 - 通过判断
grantType
参数来定位具体使用哪个TokenGranter
实现类来处理 - 每种授权方式都对应一个
AuthenticationProvider
TokenGranter
类会new
一个AuthenticationToken
实现类传递给ProviderManager
所以自定义一个授权类型,必须构建自己的TokenGranter
、AuthenticationProvider
、AuthenticationToken
。
验证码模式
1、实现UserDetailsService
后面主要填充的逻辑就是根据phone
查询用户信息。
@Service
public class SmsUserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}
2、自定义AuthenticationToken
SmsVerificationCodeAuthenticationToken
。
public class SmsVerificationCodeAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
public SmsVerificationCodeAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}
public SmsVerificationCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
// 设置为已认证
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException( "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
3、自定义TokenGranter
SmsVerificationCodeTokenGranter
。
@Slf4j
@Slf4j
public class SmsVerificationCodeTokenGranter extends AbstractTokenGranter {
/**
* 授权类型,和password是一样的作用
*/
private static final String grantType = "sms_code";
private final AuthenticationManager authenticationManager;
public SmsVerificationCodeTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, grantType);
}
protected SmsVerificationCodeTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
super(tokenServices, clientDetailsService, requestFactory, grantType);
this.authenticationManager = authenticationManager;
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
// 得到请求参数
Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
// 手机号
String phone = parameters.get("phone");
// 验证码
String verificationCode = parameters.get("verification_code");
log.info("[登录请求 phone<{}> verificationCode<{}>]", phone, verificationCode);
Authentication userAuth = new SmsVerificationCodeAuthenticationToken(parameters);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
// 调用authenticationManager进行认证,会根据SmsVerificationCodeAuthenticationToken找到对应的Provider
userAuth = authenticationManager.authenticate(userAuth);
} catch (AccountStatusException ase) {
//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
throw new InvalidGrantException(ase.getMessage());
} catch (BadCredentialsException e) {
// If the username/password are wrong the spec says we should send 400/invalid grant
throw new InvalidGrantException(e.getMessage());
}
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate user,phone number is: " + phone);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}
4、自定义AuthenticationProvider
这个类就是真正的处理类,经过TokenGranter
后,会找到对应的AuthenticationProvider
。
SmsVerificationCodeAuthenticationProvider
。
@Slf4j
public class SmsVerificationCodeAuthenticationProvider implements AuthenticationProvider {
/**
* 得到UserDetailsService对象用来获取用户信息
*/
private UserDetailsService userDetailsService;
public SmsVerificationCodeAuthenticationProvider(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
/**
* 过请求参数查询数据库用户信息,进行匹配
*
* @param authentication the authentication request object.
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsVerificationCodeAuthenticationToken authenticationToken = (SmsVerificationCodeAuthenticationToken) authentication;
Map<String, String> parameters = (Map<String, String>) authenticationToken.getPrincipal();
log.info("[parameters <{}>]", parameters);
// 手机号
String phone = parameters.get("phone");
// 验证码
String verificationCode = parameters.get("verification_code");
log.info("[登录请求 phone<{}> verificationCode<{}>]", phone, verificationCode);
// 1.根据phone查询用户信息是否存在
// 2.判断验证码 是否有效
UserDetails user = new User("rcbb", "rcbb", true, true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList("user_add,user_update,ROLE_ADMIN"));
SmsVerificationCodeAuthenticationToken smsVerificationCodeAuthenticationToken = new SmsVerificationCodeAuthenticationToken(user, user.getAuthorities());
smsVerificationCodeAuthenticationToken.setDetails(authenticationToken.getDetails());
return smsVerificationCodeAuthenticationToken;
}
@Override
public boolean supports(Class<?> authentication) {
// 判断authentication是否是SmsCodeAuthenticationToken类型
return SmsVerificationCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
5、将自定义的Provider
注入容器
SmsVerificationCodeAuthenticationConfig
。
@Component
public class SmsVerificationCodeAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity builder) throws Exception {
SmsVerificationCodeAuthenticationProvider provider = new SmsVerificationCodeAuthenticationProvider(userDetailsService);
builder.authenticationProvider(provider);
}
}
在WebSecurityConfig
中,注入自定义的授权配置类。
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 短信安全验证配置
@Autowired
private SmsVerificationCodeAuthenticationConfig smsVerificationCodeAuthenticationConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.apply(smsVerificationCodeAuthenticationConfig);
}
...
}
6、加到CompositeTokenGranter
集合中
clients
中的authorizedGrantTypes
允许的授权类型添加sms_code
。
将自定义的授权类型加到集合CompositeTokenGranter
中。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 基于内存便于测试,使用in-memory存储
clients.inMemory()
// client_id
.withClient("rcbb")
// 未加密
//.secret("secret")
// 加密
.secret(passwordEncoder.encode("rcbb"))
// 资源列表
//.resourceIds("res1")
// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token","sms_code")
// 允许的授权范围
.scopes("all", "ROLE_ADMIN", "ROLE_USER")
// false跳转到授权页面
//.autoApprove(false)
//加上验证回调地址
.redirectUris("http://rcbb.cc");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
List<TokenGranter> tokenGranters = new ArrayList<>(Collections.singleton(endpoints.getTokenGranter()));
tokenGranters.add(new SmsVerificationCodeTokenGranter(authenticationManager, tokenService(), clientDetailsService, new DefaultOAuth2RequestFactory(clientDetailsService)));
// 配置认证管理器
endpoints.authenticationManager(authenticationManager)
.tokenServices(tokenService())
.tokenGranter(new CompositeTokenGranter(tokenGranters));
}
}
7、测试
使用自定义grant_type=sms_code
。
该请求curl
:
curl --location --request POST 'http://127.0.0.1:3000/oauth/token' \
--header 'Authorization: Basic cmNiYjpyY2Ji' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=sms_code' \
--data-urlencode 'phone=13838384388' \
--data-urlencode 'verification_code=123456'
上面例子仅为梳理整个流程,还缺少业务逻辑上数据的校验。
- 4
- 0
-
分享