【Spring Security】 Oauth2 短信验证码授权模式获取令牌案例
【Spring Security】 Oauth2 短信验证码授权模式获取令牌案例
Metadata
title: 【Spring Security】 Oauth2 短信验证码授权模式获取令牌案例
date: 2023-02-05 10:18
tags:
- 行动阶段/完成
- 主题场景/组件
- 笔记空间/KnowladgeSpace/ProgramSpace/ModuleSpace
- 细化主题/Module/SpringSecurity
categories:
- SpringSecurity
keywords:
- SpringSecurity
description: 【Spring Security】 Oauth2 短信验证码授权模式获取令牌案例
【Spring Security】 Oauth2 短信验证码授权模式获取令牌案例
在 Oauth2 平台中,自家平台使用使用密码模式获取令牌,然后携带令牌去访问资源服务器。
对于首页登录来说,Security Oauth2 已经提供了密码模式,但是肯定也需要其他模式,比如手机短信、QQ、微信等社交平台进行登录。
密码模式分析
在之前 Oauth2 端点源码解析中,我们分析了端点令牌发放的源码,了解到是有令牌颁发器来发放的。
密码模式使用了 ResourceOwnerPasswordTokenGranter,主要进行了以下几步操作:
- 获取请求中的用户名 密码
- 常见预认证对象 UsernamePasswordAuthenticationToken
- 调用认证管理器,调用 ProviderManager 进行认证
- 认证成功,返回 OAuth2Authentication 认证对象
总结: 我们自定义一个短信登录的令牌颁发器就行了,直接可以在其中进行认证,也可以交给认证管理器,调用认证程序进行认证。
手机短信登录功能实现案例
1. 添加获取短信验证码接口
@RestController
@RequestMapping("/sms")
@Slf4j
public class SmsEndpoint {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 发送验证码接口
*
* @param phone
* @return
*/
@GetMapping("/send/code")
public Map<String,String> msmCode(String phone) {
// 1. 获取到手机号
log.info(phone + "请求获取验证码");
// 2. 模拟调用短信平台获取验证码,以手机号为KEY,验证码为值,存入Redis,过期时间一分钟
String code = RandomUtil.randomNumbers(6);
redisTemplate.opsForValue().setIfAbsent(phone, code, 60*10, TimeUnit.SECONDS);
String saveCode = redisTemplate.opsForValue().get(phone);// 缓存中的code
Long expire = redisTemplate.opsForValue().getOperations().getExpire(phone, TimeUnit.SECONDS); // 查询过期时间
// 3. 验证码应该通过短信发给用户,这里直接返回吧
Map<String,String> result=new HashMap<>();
result.put("code",saveCode);
result.put("过期时间",expire+"秒");
return result;
}
}
2. 创建短信登录认证令牌
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 1L;
private final Object principal;
private Object credentials;
public SmsAuthenticationToken(Object principal, Object credentials) {
super((Collection) null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
public SmsAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
3. 创建短信短信登录 Provider
public class SmsAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsServiceImpl;
private RedisTemplate<String, String> redisTemplate;
public SmsAuthenticationProvider(UserDetailsService userDetailsServiceImpl, RedisTemplate<String, String> redisTemplate) {
this.userDetailsServiceImpl = userDetailsServiceImpl;
this.redisTemplate = redisTemplate;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
Object principal = authentication.getPrincipal();// 获取凭证也就是用户的手机号
String phone = "";
if (principal instanceof UserDetails) {
phone = ((UserDetails)principal).getUsername();
} else if (principal instanceof AuthenticatedPrincipal) {
phone = ((AuthenticatedPrincipal)principal).getName();
} else if (principal instanceof Principal) {
phone = ((Principal)principal).getName();
} else {
phone = principal == null ? "" : principal.toString();
}
String inputCode = (String) authentication.getCredentials(); // 获取输入的验证码
// 1. 检验Redis手机号的验证码
String redisCode = redisTemplate.opsForValue().get(phone);
if (StrUtil.isEmpty(redisCode)) {
throw new BadCredentialsException("验证码已经过期或尚未发送,请重新发送验证码");
}
if (!inputCode.equals(redisCode)) {
throw new BadCredentialsException("输入的验证码不正确,请重新输入");
}
// 2. 根据手机号查询用户信息, 这里演示,直接查了user的信息
UserDetails userDetails = userDetailsServiceImpl.loadUserByUsername("user");
if (userDetails == null) {
throw new InternalAuthenticationServiceException("phone用户不存在,请注册");
}
// 3. 重新创建已认证对象,
SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(principal,inputCode, userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> aClass) {
return SmsAuthenticationToken.class.isAssignableFrom(aClass);
}
}
4. 创建短信验证码登录令牌颁发器
public class SmsCodeGranter extends AbstractTokenGranter {
// 修改授权模式为sms_code
private static final String GRANT_TYPE = "sms_code";
private final AuthenticationManager authenticationManager;
public SmsCodeGranter(AuthenticationManager authenticationManager,
AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
}
protected SmsCodeGranter(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<String, String>(tokenRequest.getRequestParameters());
String phone = parameters.get("phone");
String smsCode = parameters.get("smsCode");
// Protect from downstream leaks of password
parameters.remove("smsCode");
Authentication userAuth = new SmsAuthenticationToken(phone, smsCode);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
userAuth = authenticationManager.authenticate(userAuth);
if (userAuth == null) {
throw new InternalAuthenticationServiceException("phone用户不存在,请注册");
}
} 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);
}
// 3. 重新创建Oau已认证对象,
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}
5. 修改授权服务器配置
修改 WebSecurityConfigure,主要是在认证管理器中添加 SmsAuthenticationProvider。
@Configuration
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class MyWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
public void configure(WebSecurity web) throws Exception {
// 将 check_token 暴露出去,否则资源服务器访问时报错
web.ignoring().antMatchers("/oauth/check_token","/sms/send/code");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
// 在内存中创建用户并为密码加密
.withUser("user").password(passwordEncoder().encode("123456")).roles("USER");
}
// 密码解析器
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 配置认证管理器
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 将Provider添加到认证管理器中
*
* @return
* @throws Exception
*/
@Override
protected AuthenticationManager authenticationManager() throws Exception {
ProviderManager authenticationManager = new ProviderManager(Arrays.asList(new SmsAuthenticationProvider(myUserDetailsService,redisTemplate), daoAuthenticationProvider()));
authenticationManager.setEraseCredentialsAfterAuthentication(false);
return authenticationManager;
}
@Bean
DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setUserDetailsService(myUserDetailsService);
daoAuthenticationProvider.setHideUserNotFoundExceptions(false); // 设置显示找不到用户异常
return daoAuthenticationProvider;
}
}
AuthorizationServerConfigure 配置类中,主要是将颁发器添加到端点配置中。
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
// AuthorizationServer配置
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
// tokenkey这个endpoint当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个 endpoint完全公开
.tokenKeyAccess("permitAll()")
// checkToken这个endpoint完全公开
.checkTokenAccess("permitAll()")
// 允许表单认证
.allowFormAuthenticationForClients();
}
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenStore jwtTokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
MyWebResponseExceptionTranslator myWebResponseExceptionTranslator;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 配置客户端
clients
// 使用内存设置
.inMemory()
// client_id
.withClient("client")
// client_secret
.secret(passwordEncoder.encode("secret"))
// 授权类型: 授权码、刷新令牌、密码、客户端、简化模式、短信验证码
.authorizedGrantTypes("authorization_code", "refresh_token", "password", "client_credentials", "implicit", "sms_code")
// 授权范围,也可根据这个范围标识,进行鉴权
.scopes("app")
// 授权码模式 授权页面是否自动授权
//.autoApprove(false)
// 拥有的权限
.authorities("add:user")
// 允许访问的资源服务 ID
//.resourceIds("oauth2-resource-server001-demo")
// 注册回调地址
.redirectUris("http://localhost:20000/code", "http://localhost:9001/resource001/login");
}
// 端点配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 配置端点允许的请求方式
endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
// 配置认证管理器
endpoints.authenticationManager(authenticationManager);
// 自定义异常翻译器,用于处理OAuth2Exception
endpoints.exceptionTranslator(myWebResponseExceptionTranslator);
// 重新组装令牌颁发者,加入自定义授权模式
endpoints.tokenGranter(getTokenGranter(endpoints));
/* // 添加JWT令牌
// JWT令牌转换器
endpoints.accessTokenConverter(jwtAccessTokenConverter);
// JWT 存储令牌
endpoints.tokenStore(jwtTokenStore);*/
}
private TokenGranter getTokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
// 默认tokenGranter集合
List<TokenGranter> granters = new ArrayList<>(Collections.singletonList(endpoints.getTokenGranter()));
// 增加短信验证码模式
granters.add(new SmsCodeGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
// 组合tokenGranter集合
return new CompositeTokenGranter(granters);
}
}
测试
1. 获取验证码
2. 使用验证码授权模式登陆
3. 使用用户名密码模式
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 蝶梦庄生!
评论