security表单身份认证思路解析,即用户名密码登录认证
先理一下现有的思路:
首先需要向 security 的过滤链中添加一个过滤器,能够捕捉我们的登录请求,然后通过请求中的手机号码等信息封装一个用户信息 ,并交给用户信息处理者进行处理匹配,这个用户匹配到对应的处理者之后,通过处理者的业务调用,拿到对应的数据源,然后进行密码匹配,匹配成功就返回已认证的用户信息,逐级返回到过滤器中,过滤器中将用户信息存到安全上下文Holder中,方便后续请求使用,并跳过后面过滤链。
- 首先先创建用户信息配置类 PhonePasswordAuthenticationToken ,并继承 UsernamePasswordAuthenticationToken
2.自定义的 UserDetailsService - 然后创建 PhoneAuthenticationProvider 实现 AuthenticationProvider 其中的二个方法
4.在SecurityConfig添加身份认证
5.SysLoginService业务层调用不同的接口
创建用户信息配置类 PhonePasswordAuthenticationToken
//继承 UsernamePasswordAuthenticationToken ,不继承 AbstractAuthenticationToken 的原因是后续会进行类型匹配,现在不需要那么高深的配置,所以先继承 UsernamePasswordAuthenticationToken
public class PhonePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
private static final long serialVersionUID = 6339981734663827267L;
//注意,Authentication 对象中,会需要密码信息(Credentials)、身份信息(Principal)、权限信息(Authorities)、细节信息(Details),但不一定都需要
public PhonePasswordAuthenticationToken(Object principal, Object credentials) {
//调用父类的构造函数,创建一个未认证的 Token
super(principal, credentials);
}
public PhonePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
//调用父类的构造函数,创建一个已认证的 Token
super(principal, credentials, authorities);
}
}
实现 UsernamePasswordAuthenticationToken 之后,就只需要创建两个默认的构造器,也就是调用 UsernamePasswordAuthenticationToken 的构造方法创建 Token 对象。
修改自定义的 UserDetailsService实现类
创建不同的数据查询入口
第一种用户验证
@Service
@Primary
public class UserDetailsServiceImpl implements UserDetailsService
{
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private ISysUserService userService;
@Autowired
private SysPasswordService passwordService;
@Autowired
private SysPermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
SysUser user = userService.selectUserByUserName(username);
......
}
public UserDetails createLoginUser(SysUser user)
{
......
}
}
第二种用户验证
先用一个接口继承
public interface MyUserDetailsService extends UserDetailsService {
}
再用一个类来实现
```java
public class MyUserDetailsServiceImpl implements MyUserDetailsService {
...
//通过 phone 获取用户信息,前提手机号码能定位唯一用户
public UserDetails loadUserByPhone(String phone) throws UsernameNotFoundException {
//从持久层获取数据
User user = userMapper.getUserInfoByPhone(phone);
if(user == null){
//这是 security 自带的异常,会在过滤连中捕捉到
throw new UsernameNotFoundException("用户不存在!");
}
//现在 user 对象里面的 roles 是 string 类型,并且用逗号隔开的,我们需要将 roles 设置到 authorities 类型中。
//我们需要把他修改为 security 可识别的权限类型 ,GrantedAuthority 接口是 security 保存权限的类型,SimpleGrantedAuthority 是它的实现类,也是security 最常使用的。
//AuthorityUtils.commaSeparatedStringToAuthorityList( String )是 security 提供的用于将逗号隔开的权限字符串切割成权限列表。
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
return user;
}
}
然后创建 PhoneAuthenticationProvider
@Slf4j
@Component
public class PhoneAuthenticationProvider implements AuthenticationProvider {
@Autowired
private MyUserDetailsService userDetailsService;
/** 密码加密规则 */
@Autowired
private PasswordEncoder bCryptPasswordEncoder;
/**
* 进行验证码认证
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
long time = System.currentTimeMillis();
log.info("用户名/密码 开始登录验证 time:{}", time);
String username = authentication.getName();
String password = authentication.getCredentials().toString();
// 1、去调用自己实现的UserDetailsService,返回UserDetails
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 2、密码进行检查,这里调用了PasswordEncoder,检查 UserDetails 是否可用。
if (Objects.isNull(userDetails) || !bCryptPasswordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("账号或密码错误");
}
// 3、返回经过认证的Authentication
PhonePasswordAuthenticationToken result = new PhonePasswordAuthenticationToken(userDetails, null, Collections.emptyList());
result.setDetails(authentication.getDetails());
log.info("用户名/密码 登录验证完成 time:{}, existTime:{}", time, (System.currentTimeMillis() - time));
return result;
}
@Override
public boolean supports(Class<?> authentication) {
boolean res = PhonePasswordAuthenticationToken.class.isAssignableFrom(authentication);
log.info("用户名/密码 是否进行登录验证 res:{}", res);
return res;
}
}
在SecurityConfig 加下
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
/** 用户 认证器 */
@Autowired
private PhoneAuthenticationProvider phoneAuthenticationProvider;
......
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
auth.authenticationProvider(phoneAuthenticationProvider);
}
}
在服层区调用不同的登录SysLoginService
if(type==1)
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
else
authentication = authenticationManager.authenticate(new PhonePasswordAuthenticationToken(username, password));