漳州企业网站建设,erp管理系统介绍,wordpress 国人主题,惠州网站设计方案前言
《Spring Security学习#xff08;五#xff09;——账号密码的存取》一文已经能满足一般应用的情况。但实际商业应用也会存在如下的情况#xff1a;用户提交的账号密码#xff0c;能在本地的保存的账号密码匹配上#xff0c;或者能在远端服务认证中匹配上#xff…前言
《Spring Security学习五——账号密码的存取》一文已经能满足一般应用的情况。但实际商业应用也会存在如下的情况用户提交的账号密码能在本地的保存的账号密码匹配上或者能在远端服务认证中匹配上就算认证通过。这种通常类似于单点登录或者认证中心之类的。本文不去探索这种架构只是单纯讨论如果存在两种认证规则下如何处理。
多种认证方式使用Spring Security时有好几种处理方式。根据不同的情况架构师考虑不同的解决方案。本文先从比较简单的方案说起。
假设能匹配数据库保存的账号密码或者能匹配内存中保存的账号密码两者之一就算认证通过。附加条件
这两种认证方式是通过同一方式提交用户密码这两种认证方式是一条链式的处理方式。比如先判断第一种方式是否通过认证不通过则判断第二种。
上述情况我们考虑使用多个provider去处理。 架构
前面的文章讲过ProviderManager是AuthenticationManager的实现。其架构图如下 在《Spring Security学习五——账号密码的存取》 中我们说过DaoAuthenticationProvider撬动UserDetailsService和PasswordEncoder的“杠杆”。DaoAuthenticationProvider则是AuthenticationProvider的默认实现。Spring Security通过AuthenticationProvider调用获取UserDetails然后进行匹配最后返回UsernamePasswordAuthenticationTokenAuthentication接口的具体。
对于Spring Security的当存在多个AuthenticationProvider的实现时用户提交的账号密码满足其中任意一个AuthenticationProvider返回正确的Authentication即可。 自定义AuthenticationProvider
我们做个简单的自定义AuthenticationProvider。自定义MyProvider类
Data
public class MyProvider extends AbstractUserDetailsAuthenticationProvider{private UserDetailsServiceImpl userDetailsServiceImpl;private PasswordEncoder passwordEncoder;private static final String USER_NOT_FOUND_PASSWORD userNotFoundPassword;private volatile String userNotFoundEncodedPassword;Overrideprotected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {prepareTimingAttackProtection();try {UserDetails loadedUser userDetailsServiceImpl.loadUserByMemory(username);if (loadedUser null) {throw new InternalAuthenticationServiceException(UserDetailsService returned null, which is an interface contract violation);}return loadedUser;}catch (UsernameNotFoundException ex) {mitigateAgainstTimingAttack(authentication);throw ex;}catch (InternalAuthenticationServiceException ex) {throw ex;}catch (Exception ex) {throw new InternalAuthenticationServiceException(ex.getMessage(), ex);}}Overrideprotected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {if (authentication.getCredentials() null) {this.logger.debug(Failed to authenticate since no credentials provided);throw new BadCredentialsException(this.messages.getMessage(AbstractUserDetailsAuthenticationProvider.badCredentials, Bad credentials));}String presentedPassword authentication.getCredentials().toString();if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {this.logger.debug(Failed to authenticate since password does not match stored value);throw new BadCredentialsException(this.messages.getMessage(AbstractUserDetailsAuthenticationProvider.badCredentials, Bad credentials));}}private void prepareTimingAttackProtection() {if (this.userNotFoundEncodedPassword null) {this.userNotFoundEncodedPassword this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);}}private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {if (authentication.getCredentials() ! null) {String presentedPassword authentication.getCredentials().toString();this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);}}
}
自定义Provider应该实现AuthenticationProvider接口并实现authenticate方法。这里我们参考DaoAuthenticationProvider来写重写retrieveUser方法调用我们自定义的userDetailsServiceImpl的loadUserByMemory方法从内存获取。additionalAuthenticationChecks方法的作用是对提交的密码和系统中保存的密码进行匹配这里直接参考DaoAuthenticationProvider类的写法。prepareTimingAttackProtection和mitigateAgainstTimingAttack都是用来防御计时攻击的非认证必要。
在《Spring Security学习二——使用数据库保存密码》中我们定义了SysUserServiceImpl这里进行一些修改
Component
public class UserDetailsServiceImpl implements UserDetailsService{Autowiredprivate SysUserService userService;private static MapString, SysUserEntity userMap;static {userMap new HashMapString, SysUserEntity();SysUserEntity sysUser new SysUserEntity();sysUser.setUsername(test);sysUser.setPassword(test###);userMap.put(test, sysUser);}Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {QueryWrapperSysUserEntity queryWrapper new QueryWrapperSysUserEntity();queryWrapper.eq(username, username);queryWrapper.last(limit 1);SysUserEntity user userService.getOne(queryWrapper);if(user null) {throw new UsernameNotFoundException(username not found);}return (new LoginUser(user));}public UserDetails loadUserByMemory(String username) throws UsernameNotFoundException {if(StrUtil.isNotEmpty(username)) {SysUserEntity user userMap.get(username);if(user null) {throw new UsernameNotFoundException(username not found);}return (new LoginUser(user));}return null;}
}
首先我们增加一个loadUserByUsername方法用于读取map中保存的测试账号。另外之前当我们查询找不到对象的时候抛出了RuntimeException按照Spring Security的设计思想来说是不对的应该抛出UsernameNotFoundException让上层接住这个异常。抛出RuntimeException会直接终止后面provider的匹配不符合Spring Security的设计理念。
自定义密码加密器还是使用之前文章的版本MyPasswordEncoder为原密码后加3个#
public class MyPasswordEncoder implements PasswordEncoder {Overridepublic String encode(CharSequence rawPassword) {return rawPassword ###;}Overridepublic boolean matches(CharSequence rawPassword, String encodedPassword) {if (encodedPassword.equals(rawPassword ###)) {return true;}return false;}
}
另外由于自定义加密方式所以数据库密码也要改一下 最后修改一下WebSecurityConfig的配置把自定义的MyProvider加进去
EnableWebSecurity
public class WebSecurityConfig{Beanpublic MyPasswordEncoder PasswordEncoder() {return new MyPasswordEncoder();}Beanpublic SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(authorize - authorize.anyRequest().authenticated()).formLogin(Customizer.withDefaults());MyProvider myProvider new MyProvider();myProvider.setPasswordEncoder(PasswordEncoder());UserDetailsServiceImpl UserDetailsServiceImpl SpringUtils.getBean(UserDetailsServiceImpl.class);myProvider.setUserDetailsServiceImpl(UserDetailsServiceImpl);http.authenticationProvider(myProvider);DaoAuthenticationProvider daoAuthenticationProvider new DaoAuthenticationProvider();daoAuthenticationProvider.setPasswordEncoder(PasswordEncoder());daoAuthenticationProvider.setUserDetailsService(UserDetailsServiceImpl);http.authenticationProvider(daoAuthenticationProvider);return http.build();}
}
16行新建MyProvider17行设置自定义的加密器18-19行设置UserDetailsServiceImpl。20行加到HttpSecurity中。22行设置DaoAuthenticationProvider到HttpSecurity中。
之后我们同样是访问/hello路径然后在默认登录页分别尝试使用jake/123test/test两个账号登陆。会发现通过两种认证方式都可以登陆成功。 小结
本文使用自定义Provider的方式让用户匹配多种认证方式。这个只是最简单的方式。从系列文章的本文章开始就涉及很多需要了解Spring Security架构设计的情况读者最好还是阅读源码去理解。否则生搬硬套很难达到实际开发想要达到的目的。本文主要涉及ProviderManager、DaoAuthenticationProvider、AbstractUserDetailsAuthenticationProvider的源码。读者最好阅读一下以了解本文实现的思路。
其实还有个小疑问WebSecurityConfig似乎我们去掉DaoAuthenticationProvider的配置一样能达到同样的效果那我们的配置是不是有点多余了其实这涉及另一个概念留到下一篇文章再讲。