上篇文章已经大致把SpringSecurity的设计思路捋出来了,这次就开始着手改造一下。

总体思路

大体的设计思路和之前比较一致,只是在配置方面做了调整,重新梳理如下:

  1. 构建一个特定的Token类,例如PasswordAuthenticationToken,这个类需要继承AbstractAuthenticationToken,在需要做认证的地方把他new出来;
  2. 构建认证处理器类PasswordAuthenticationProvider类,实现AuthenticationProvider接口,并重写其中的authenticate()supports()方法;
  3. 构造PasswordAuthenticationFilter类继承自AbstractAuthenticationProcessingFilter并重写其中的attemptAuthentication()方法,同时重写AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher,AuthenticationManager authenticationManager)方法用于注入AuthenticationManager
  4. 构造PasswordAuthenticationConfigurer类,继承自AbstractHttpConfigurer并重写configure方法,他可是提供了一个非常宝贵的HttpSecurity入参。
  5. 构造相应的成功及异常处理器。

结果

思路清楚了就直接上代码吧,关键位置都做了注释,一遍看不懂就多看几遍。先看下改造后的整体结构。

先来PasswordAuthenticationToken

/*** 基于用户名(手机号)、密码、验证码登录的认证实体*/
public class PasswordAuthenticationToken extends AbstractAuthenticationToken {private final String loginId;private final String captchaId;private final String captchaValue;private final LoginUserPojo principal;private final String credentials;/*** 登录验证** @param loginId      用户名或手机号* @param credentials  MD5+SM3密码* @param captchaId    图形验证码id* @param captchaValue 输入的图形验证码值*/public PasswordAuthenticationToken(String loginId, String credentials, String captchaId, String captchaValue) {super(null);this.loginId = loginId;this.credentials = credentials;this.captchaId = captchaId;this.captchaValue = captchaValue;this.principal = null;this.setAuthenticated(false);}/*** 授权信息** @param principal   LoginUserPojo* @param credentials token* @param authorities 角色清单*/public PasswordAuthenticationToken(LoginUserPojo principal, String credentials, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;this.credentials = credentials;this.loginId = null;this.captchaId = null;this.captchaValue = null;this.setAuthenticated(true);}public String getLoginId() {return loginId;}public String getCaptchaId() {return captchaId;}public String getCaptchaValue() {return captchaValue;}@Overridepublic LoginUserPojo getPrincipal() {return principal;}@Overridepublic String getCredentials() {return credentials;}
}

PasswordAuthenticationProvider

/*** 基于用户名(手机号)、密码、验证码的认证处理器*/
public class PasswordAuthenticationProvider implements AuthenticationProvider {private static final String IMG_CAPTCHA_REDIS_PREFIX = "SK:CAPTCHA:IMG:";private final UserDetailServiceImpl userDetailService;private final RedisCacheUtil redisCacheUtil;public PasswordAuthenticationProvider(UserDetailServiceImpl userDetailService, RedisCacheUtil redisCacheUtil) {this.userDetailService = userDetailService;this.redisCacheUtil = redisCacheUtil;}/*** 验证主逻辑*/@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {PasswordAuthenticationToken authToken = (PasswordAuthenticationToken) authentication;// 验证码校验if (!checkImgCaptcha(authToken.getCaptchaId(), authToken.getCaptchaValue())) {throw new BadCaptchaException("验证码有误或已过期,请重新输入");}// 密码校验LoginUserPojo userDetails = (LoginUserPojo) userDetailService.loadUserByUsername(authToken.getLoginId());if (!new BCryptPasswordEncoder().matches(authToken.getCredentials(), userDetails.getPassword())) {throw new BadCredentialsException("用户名或密码错误,请重新输入");}// 用户状态校验if (!userDetails.isEnabled() || !userDetails.isAccountNonLocked() || !userDetails.isAccountNonExpired()) {throw new LockedException("用户已禁用,请联系管理员启用");}return new PasswordAuthenticationToken(userDetails, authToken.getCredentials(), userDetails.getAuthorities());}/*** 当类型为PasswordAuthenticationToken的认证实体进入时才走此Provider*/@Overridepublic boolean supports(Class<?> authentication) {return PasswordAuthenticationToken.class.isAssignableFrom(authentication);}/*** 校验验证码正确与否,验证完成后删除当前码值** @param id    验证码对应的id* @param value 用户输入的验证码结果* @return true or false*/private boolean checkImgCaptcha(String id, String value) {if (StringUtils.isBlank(id) || StringUtils.isBlank(value)) {return false;}CaptchaCodePojo captchaCode = redisCacheUtil.getObject(IMG_CAPTCHA_REDIS_PREFIX + id);redisCacheUtil.deleteObject(IMG_CAPTCHA_REDIS_PREFIX + id);return !Objects.isNull(captchaCode) && value.equals(captchaCode.getResult());}
}

PasswordAuthenticationConfigurer

/*** 基于用户名(手机号)、密码、验证码的登录拦截器配置类*/
public class PasswordAuthenticationConfigurer extends AbstractHttpConfigurer<PasswordAuthenticationConfigurer, HttpSecurity> {@Overridepublic void configure(HttpSecurity builder) {// 拦截 POST /login 请求RequestMatcher matcher = new AntPathRequestMatcher("/login", "POST", true);UserDetailServiceImpl userDetailService = builder.getSharedObject(ApplicationContext.class).getBean(UserDetailServiceImpl.class);RedisCacheUtil redisCacheUtil = builder.getSharedObject(ApplicationContext.class).getBean(RedisCacheUtil.class);AuthenticationManager localAuthManager = builder.getSharedObject(AuthenticationManager.class);PasswordAuthenticationFilter filter = new PasswordAuthenticationFilter(matcher, localAuthManager);filter.setAuthenticationSuccessHandler(new LoginSuccessHandler(userDetailService));filter.setAuthenticationFailureHandler(new LoginFailHandler());// 务必注意这里与配置类中声明的先后顺序builder.authenticationProvider(new PasswordAuthenticationProvider(userDetailService, redisCacheUtil)).addFilterBefore(filter, AuthenticationTokenFilter.class);}
}

PasswordAuthenticationFilter

/*** 用户名密码登录拦截器*/
public class PasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {public PasswordAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher, AuthenticationManager authenticationManager) {super(requiresAuthenticationRequestMatcher, authenticationManager);}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {JSONObject params = HttpRequestUtil.getBodyJson(request);Authentication authentication = new PasswordAuthenticationToken(params.getString("loginKey"), params.getString("password"), params.getString("id"), params.getString("value"));return this.getAuthenticationManager().authenticate(authentication);}
}

Token认证拦截器AuthenticationTokenFilter

/*** Token认证拦截器*/
public class AuthenticationTokenFilter extends OncePerRequestFilter {private final RedisCacheUtil redisCacheUtil;public AuthenticationTokenFilter(RedisCacheUtil redisCacheUtil) {this.redisCacheUtil = redisCacheUtil;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String token = JwtTokenUtil.getToken(request);if (Objects.nonNull(token) && JwtTokenUtil.checkToken(token) && redisCacheUtil.hasKey(TokenConstant.TOKEN_REDIS_PREFIX + token)) {// 从redis中获取数据LoginUserPojo userPojo = redisCacheUtil.getObject(TokenConstant.TOKEN_REDIS_PREFIX + token);// 写入上下文PasswordAuthenticationToken authenticationToken = new PasswordAuthenticationToken(userPojo, token, userPojo.getAuthorities());authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authenticationToken);// 刷新ttlredisCacheUtil.setExpire(TokenConstant.TOKEN_REDIS_PREFIX + token, TokenConstant.TOKEN_EXPIRE_TIME, TokenConstant.TOKEN_EXPIRE_TIME_UNIT);}filterChain.doFilter(request, response);}
}

LoginFailHandler

/*** 密码认证失败处理器*/
public class LoginFailHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {JSONObject res = new JSONObject();res.put("success", false);res.put("msg", e.getMessage());response.setStatus(HttpStatus.SC_FORBIDDEN);response.setContentType(ContentType.APPLICATION_JSON.getMimeType());response.setCharacterEncoding(StandardCharsets.UTF_8.name());response.getWriter().print(res.toJSONString());}
}

LoginSuccessHandler

/*** 登录成功后处理器*/
public class LoginSuccessHandler implements AuthenticationSuccessHandler {private final UserDetailServiceImpl userDetailsService;public LoginSuccessHandler(UserDetailServiceImpl userDetailsService) {this.userDetailsService = userDetailsService;}@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {LoginUserPojo loginUserPojo = (LoginUserPojo) authentication.getPrincipal();// 更新登陆时间userDetailsService.updateLoginTime(loginUserPojo.getUserId());// 构建token并缓存String token = userDetailsService.buildToken(loginUserPojo);JSONObject res = new JSONObject();res.put("success", true);res.put("msg", "OK");res.put("data", token);response.setStatus(HttpStatus.SC_OK);response.setContentType(ContentType.APPLICATION_JSON.getMimeType());response.setCharacterEncoding(StandardCharsets.UTF_8.name());response.getWriter().write(res.toString());}
}

UserDetailServiceImpl

/*** SpringSecurity登录处理类*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {@Autowiredprivate SysUserMapper sysUserMapper;@Autowiredprivate RedisCacheUtil redisCacheUtil;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {Map<String, Object> userData = sysUserMapper.getByUserNameOrUserPhone(username);if (MapUtils.isEmpty(userData)) {throw new UsernameNotFoundException("用户名或密码错误,请重新输入");}// 封装基础数据LoginUserPojo userPojo = new LoginUserPojo();userPojo.setUserId(MapUtils.getString(userData, "user_id"));userPojo.setUserName(MapUtils.getString(userData, "user_name"));userPojo.setUserPhone(MapUtils.getString(userData, "user_phone"));userPojo.setNickName(MapUtils.getString(userData, "nick_name"));userPojo.setPassword(MapUtils.getString(userData, "password"));userPojo.setUserStatus(MapUtils.getIntValue(userData, "user_status"));userPojo.setLastLogin(MapUtils.getString(userData, "last_login"));userPojo.setLastUpdatePassword(MapUtils.getString(userData, "last_update_password"));userPojo.setCreateUser(MapUtils.getString(userData, "create_user"));userPojo.setCreateTime(MapUtils.getString(userData, "create_time"));userPojo.setUpdateUser(MapUtils.getString(userData, "update_user"));userPojo.setUpdateTime(MapUtils.getString(userData, "update_time"));// 封装角色信息if (StringUtils.isNotBlank(MapUtils.getString(userData, "role_code"))) {List<UserGrantedAuthority> grantedAuthorityList = new ArrayList<>();String[] roleCodes = MapUtils.getString(userData, "role_code").split(",");String[] roleNames = MapUtils.getString(userData, "role_name").split(",");String[] roleTypes = MapUtils.getString(userData, "role_type").split(",");for (int i = 0; i < roleCodes.length; i++) {grantedAuthorityList.add(new UserGrantedAuthority(roleCodes[i], roleNames[i], Integer.valueOf(roleTypes[i])));}userPojo.setAuthorities(grantedAuthorityList);}return userPojo;}/*** 更新用户登录时间** @param userId 用户ID*/public void updateLoginTime(String userId) {SysUserEntity userEntity = new SysUserEntity();userEntity.setUserId(userId);userEntity.setLastLogin(DateTimeUtil.getCurrentDate("yyyy-MM-dd HH:mm:ss"));sysUserMapper.updateByUserId(userEntity);}/*** 根据用户信息构造token并写入redis** @param loginUserPojo LoginUserPojo* @return token*/public String buildToken(LoginUserPojo loginUserPojo) {JSONObject user = new JSONObject();user.put("userId", loginUserPojo.getUserId());user.put("userName", loginUserPojo.getUserName());user.put("roleCode", loginUserPojo.getAuthorities().stream().map(UserGrantedAuthority::getRoleCode).collect(Collectors.joining(",")));// 生成tokenString token = JwtTokenUtil.createJwtToken(user);redisCacheUtil.setObject(TokenConstant.TOKEN_REDIS_PREFIX + token, loginUserPojo, TokenConstant.TOKEN_EXPIRE_TIME, TokenConstant.TOKEN_EXPIRE_TIME_UNIT);return token;}
}

还有最后的Security配置类SpringSecurityConfig

/*** SpringSecurity配置类*/
@EnableWebSecurity
@PropertySource("classpath:authfilter.properties")
public class SpringSecurityConfig {@Value("${exclude_urls}")private String excludeUrls;@Autowiredprivate AuthenticationLogoutHandler logoutHandler;@Beanpublic SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {return httpSecurity.csrf().disable().formLogin().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeHttpRequests().antMatchers(StringUtils.split(excludeUrls, ",")).permitAll().anyRequest().authenticated().and().exceptionHandling().authenticationEntryPoint(new AuthenticationFailHandler()).and().logout().logoutUrl("/logout").logoutSuccessHandler(logoutHandler).and()// 务必注意这里的先后顺序,否则会报NULL异常.apply(new TokenAuthenticationConfigurer()).and().apply(new PasswordAuthenticationConfigurer()).and().apply(new OAuthAuthenticationConfigurer()).and().build();}
}

经过测试完美执行,自定义Provider可以仅在Local作用域中生效,不干扰全局AuthenticationManager

另外我想尽可能避免将所有类都标记为@Component,所以部分业务依赖项我是通过上下文ApplicationContext获取的,而这个实例正好也在httpSecuritysharedObject中。


后记

当我完成所有修改后已经过了三天多了,这段时间Security简直就是我的梦魇。

但我也和大多数人一样,看之前骂spring一个过滤器的事设计的这么复杂干什么,看完之后才发现给我上了一课:什么叫设计模式,什么叫开闭原则。跟各位写框架的大佬比,我还是太年轻了。

不过倒也发现了一些问题,比如在他推荐使用SecurityFilterChain的时候,javadoc里面却仍然是传统方式的例子。满心欢喜的以为能水一个issue呢结果已经被人修复好了。。。。。

附用到的一些比较有价值的资料(还是推荐spring官方文档,要一字一句的读才能体会到设计者的思路):
一文搞懂SpringSecurity+JWT前后端分离
避坑指南(三):Spring Security Oauth2框架如何初始化AuthenticationManager
Spring Security 实战干货:你不了解的Spring Security架构
Spring Security 实战干货:WebSecurityConfigurerAdapter即将被移除
SpringSecurity之ProviderManager是如何产生的,如何与UsernamePasswordAuthenticationFilter光联在一起
Spring Security小教程 Vol 3. 身份验证的入口-AbstractAuthenticationProcessingFilter
Spring Security without the WebSecurityConfigurerAdapter
ProviderManager

SpringSecurity自定义多Provider时提示No AuthenticationProvider found for问题的解决方案与原理(四)相关推荐

  1. Oracle EBS FA创建资产时提示错误不能获得摊派到日期信息解决方案

    Oracle EBS FA 创建资产时提示错误 不能获得摊派到日期信息解决方案 英文错误: Error: Unable to get prorate date information Cause: Y ...

  2. 将折旧表分配至公司代码时提示公司代码分录不完全解决方案

    [将折旧表分配至公司代码时提示公司代码分录不完全解决方案] 给公司代码维护折旧表,保存时候提示:"XXXX的公司代码分录不完全-参见长文本". 这个问题的原因其实是非应税税码未分配 ...

  3. 此程序将从您的计算机删除adobe,电脑中安装Adobe软件时提示该程序已安装的最佳解决方案...

    很多用户都喜欢在电脑中安装这款Adobe软件来使用,不过有时候会碰到一些故障,就有用户在电脑中安装Adobe软件的时候,却提示该程序已安装或者无法安装的现象,这是怎么回事呢,经过分析一般是之前安装过没 ...

  4. centos挂载硬盘时提示mount:unknown filesystem type 'LVM2_member'的解决方案

    我写博文主要是为了记录自己学习中遇到的问题 centos挂载硬盘时提示mount:unknown filesystem type 'LVM2_member'的解决方案 我的是因为重新挂载了LV后面再挂 ...

  5. scp时提示【Read-only file system】的解决方案

    scp时提示Read-only file system这是由于没有权限读写目标目录,解决方案如下: # 在远程终端以root权限执行如下命令: mount -o remount rw / 参考链接: ...

  6. 笔记本重置找不到恢复环境_重置Windows10系统时提示“找不到恢复环境”的解决方案...

    如果我们在使用win10系统过程中,遇到了一些无法解决的问题的话,就可以开启系统自带的重置功能来处理.不过,部分用户在使用Win10重置功能时,却碰到了"找不到恢复环境"的情况,这 ...

  7. 十八、启动jmeter时提示findstr不是内部命令的解决方案

    1.jmeter环境配置后,点击jmeter.bat,提示以下错误 2.先cmd检查,输入命令 findstr /? 说明C:\Windows\System32\findstr.exe未生效,因安装极 ...

  8. WordPress更新时提示无法连接到FTP服务器的解决方案

    这几天在搭建主站的时候,更新wordpress时无法连接到FTP原因服务器 解决方法如下: 在WordPress目录下找到wp-config.php文件并编辑,在最后一行加上: define('FS_ ...

  9. 安装OpenCV时提示缺少boostdesc_bgm.i文件的问题解决方案

    安装OpenCV时,会遇到下面的错误 /home/zhang/slam/opencv-3.4.5/opencv_contrib/modules/xfeatures2d/src/boostdesc.cp ...

  10. 3. SpringSecurity 自定义手机号登录

    距离上一次更新,不知不觉已经过去了半个月了,人真的是不能放松,一放松就肆意妄为了.希望这个月内可以把 SpringSecurity 系列更新完毕吧,加油!. OK,言归正传上一章我们利用 Spring ...

最新文章

  1. java jtextfield设置不可见_java – JPanel设置为不可见,除默认值之外的组合框选择将其设置为可见,但组件丢失...
  2. Mysql索引基本概念及案例总结(含索引的使用注意事项)
  3. vb 获取汉字拼音的首字母
  4. 730阵列卡支持多大硬盘_730元/瓶的光瓶李渡酒销售过亿后,李渡还有哪些大招?...
  5. 4,GIL全局解释器锁,event事件,信号量
  6. orgmode导出html,含有python代码块的ORG-MODE导出为HTML时出错
  7. C语言-07其它相关
  8. ReactNative字体大小不随系统字体大小变化而变化
  9. 电影天堂电影链接爬取
  10. Css3中的响应式布局的应用
  11. Java集合Map(四)
  12. 使用Jorm简单的增删查改数据库
  13. 安卓手机怎么彻底清理手机内存_手机内存难清理?试试直接删掉这3个文件夹...
  14. 2022新华三十大技术趋势白皮书
  15. 财务 - 注册会计师
  16. 【安卓】设置界面为横屏的设置方法
  17. IDL多进程批处理遥感数据
  18. Latex更改参考文献格式
  19. MySQLSyntaxException:FUNCTION xxxxxxxx.JSON_EXTRACT does not exist
  20. 尚硅谷谷粒学院学习笔记9--前台用户登录,注册,整合jwt,微信登录

热门文章

  1. 基于阿里云的一般性系统安全措施介绍
  2. 华为qq邮箱服务器密码忘了,华为手机qq邮箱无法登录电子邮件鉴权失败的解决办法...
  3. srs信道估计_SRS 上行信道质量测量
  4. 我们无法更新系统保留的分区_什么是系统保留分区,您可以删除它吗?(Windows10 科普)2020...
  5. android mediaplayer单曲循环播放,android mediaplayer永远在ICS上循环播放
  6. 计算机中¥符号按哪个键,人民币键盘符号怎么打 电脑怎么打人民币符号
  7. 【以太坊】代币创建过程
  8. 网站服务器域名费用入什么科目,企业域名服务费记什么科目
  9. 关于MATLAB的saveas函数错误
  10. OpenCV4学习笔记(17)——常用导数算子