自定义授权模式(手机号+验证码)
oauth2提供的四种授权模式分别是授权码模式、密码模式、客户端凭证模式和隐式授权模式。Spring Security的实现主要是通过四个不同的TokenGranter来实现的,通过源码的中的抽象类AbstractTokenGranter查看继承它的子类。
通过子类的实现可以清晰地看到四种不同的授权模式实现类。接下来我们自定义授权模式(手机号+验证码)。
自定义Token
通过继承AbstractAuthenticationToken抽象类来实现我们需要记录的信息,包括手机号、密码、验证码和储存验证码的key。
public class MobilePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// mobile phone
private final Object principal;
// password
private Object credentials;
// verify code
private final String code;
private final String key;
// before authentication , authorities is null
public MobilePasswordAuthenticationToken(String mobilePhone, String password, String code, String key) {
super(null);
this.principal = mobilePhone;
this.credentials = password;
this.code = code;
this.key = key;
super.setAuthenticated(false);
}
// after authentication , get the authorities
public MobilePasswordAuthenticationToken(Object principal, Object credentials, String code, Collection<? extends GrantedAuthority> authorities, String key) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
this.code = code;
this.key = key;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
public String getCode() {
return code;
}
public String getKey() {
return key;
}
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");
} else {
super.setAuthenticated(false);
}
}
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
自定义TokenGranter
通过自定义的TokenGranter对请求头的信息进行封装,生成我们自定义的MobilePasswordAuthenticationToken,交给实际处理认证的Provider进行认证。
public class MobilePassWordGranter extends AbstractTokenGranter {
// 自定义授权模式的标识符,用来判断请求头使用哪种授权模式
private static final String GRANT_TYPE = "mobile_code";
private final AuthenticationManager authenticationManager;
public MobilePassWordGranter(AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService,
OAuth2RequestFactory requestFactory,
AuthenticationManager authenticationManager
) {
super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
this.authenticationManager = authenticationManager;
}
/**
* 封装MobilePasswordAuthenticationToken,进行认证
* @param client ClientDetails
* @param tokenRequest TokenRequest
* @return OAuth2Authentication
*/
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
// 从请求头中取出携带的参数,参数名可以自定义
Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
String username = parameters.get("username");
String password = parameters.get("password");
String code = parameters.get("code");
String key = parameters.get("key");
log.info("username=={}, password=={}, code=={}, key=={}", username, password, code, key);
// Protect from downstream leaks of password这里可以将请求头中的密码去掉,保护请求信息
parameters.remove("password");
// 生成我们自定义的Token,用于下一步的认证
Authentication userAuth = new MobilePasswordAuthenticationToken(username, password, code, key);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
userAuth = authenticationManager.authenticate(userAuth);
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}
自定义认证处理器AuthenticationProvider
通过继承AuthenticationProvider来自定义我们的authenticate方法,实现认证逻辑。
public class MobilePasswordAuthenticationProvider implements AuthenticationProvider {
private final PasswordEncoder passwordEncoder;
private final SmsCodeUserDetailService smsCodeUserDetailService;
private final RedisCache redisCache;
private final RSAUtils rsaUtils;
public MobilePasswordAuthenticationProvider(SmsCodeUserDetailService smsCodeUserDetailService, PasswordEncoder passwordEncoder, RedisCache redisCache, RSAUtils rsaUtils) {
this.smsCodeUserDetailService = smsCodeUserDetailService;
this.redisCache = redisCache;
this.rsaUtils = rsaUtils;
this.passwordEncoder = passwordEncoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
MobilePasswordAuthenticationToken authenticationToken = (MobilePasswordAuthenticationToken) authentication;
// 取出我们封装的参数信息
String mobile = authenticationToken.getPrincipal().toString();
String password = authenticationToken.getCredentials().toString();
String code = authenticationToken.getCode();
String key = authenticationToken.getKey();
// 参数校验
if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password))
throw new ServerOauth2Exception("账号密码不能为空!!!");
if (StringUtils.isEmpty(code) || StringUtils.isEmpty(key))
throw new ServerOauth2Exception("验证码不能为空!!!");
String codeCache = redisCache.getObject(key);
if (StringUtils.isEmpty(codeCache))
throw new ServerOauth2Exception("验证码已过期!!!");
if (!StringUtils.equals(code, codeCache))
throw new ServerOauth2Exception("验证码错误!!!");
// 从数据库中加载对应手机号的用户
LoginUser loginUser = smsCodeUserDetailService.loadUserByPhone(mobile);
if (ObjectUtil.isNull(loginUser))
throw new ServerOauth2Exception("账号不存在!!!");
// 这里是使用非对称加密来传输前端密码,可忽略
// try {
// // 私钥解密公钥加密的密码
// password = rsaUtils.decryptByPrivateKey(password);
// } catch (Exception e){
// throw new ServerOauth2Exception(e.getMessage());
// }
// 通过passwordEncoder来匹配数据库中加密的用户密码和请求中传输的密码是否匹配
if (!passwordEncoder.matches(password, loginUser.getPassword()))
throw new ServerOauth2Exception("密码不正确!!!");
// 将完整的用户信息封装到自定义Token中,包括用户权限信息
MobilePasswordAuthenticationToken mobilePasswordAuthenticationToken = new MobilePasswordAuthenticationToken(loginUser, password, null, loginUser.getAuthorities(), "");
mobilePasswordAuthenticationToken.setDetails(authentication.getDetails());
return mobilePasswordAuthenticationToken;
}
@Override
public boolean supports(Class<?> aClass) {
return MobilePasswordAuthenticationToken.class.isAssignableFrom(aClass);
}
}
从数据库中加载用户信息
这里就是一个普通的service接口和实现类,无需继承其他类,通过依赖注入使用即可。
public class SmsCodeUserDetailServiceImpl implements SmsCodeUserDetailService {
private SysAuthMapper sysAuthMapper;
@Autowired
public void setSysAuthMapper(SysAuthMapper sysAuthMapper) {
this.sysAuthMapper = sysAuthMapper;
}
public LoginUser loadUserByPhone(String phone) throws UsernameNotFoundException {
SysUser sysUserByPhone = sysAuthMapper.findSysUserByPhone(phone);
if (Objects.isNull(sysUserByPhone))
throw new ServerOauth2Exception("用户不存在!!!");
LoginUser loginUser = new LoginUser()
.setUserId(sysUserByPhone.getUserId())
.setPhone(sysUserByPhone.getPhoneNumber())
.setPassword(sysUserByPhone.getPassword());
List<SysPermission> permissions = sysUserByPhone.getSysRole().getPermissions();
List<SimpleGrantedAuthority> authorities = permissions.stream()
.map(sysPermission -> new SimpleGrantedAuthority(sysPermission.getPermissionName())).collect(Collectors.toList());
loginUser.setAuthorities(authorities);
return loginUser;
}
}