本文扩展了spring security 的登录方式,增长手机验证码登录、二维码登录。 主要实现方式为使用自定义filter、 AuthenticationProvider、 AbstractAuthenticationToken 根据不一样登录方式分别处理

srping security 登录流程

关于二维码登录

二维码扫码登录前提是已在微信端登录,流程以下:github

  • 用户点击二维码登录,调用后台接口生成二维码(带参数key), 返回二维码连接、key到页面
  • 页面显示二维码,提示扫码,并经过此key创建websocket
  • 用户扫码,获取参数key,点击登录调用后台并传递key
  • 后台根据微信端用户登录状态拿到userdetail, 并在缓存(redis)中维护 key: userDetail 关联关系
  • 后台根据websocket: key通知对于前台页面登录
  • 页面用此key登录
    最后一步用户经过key登录就是本文的二维码扫码登录部分,实际过程当中注意二维码超时,redis超时等处理

自定义LoginFilter

@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {if (postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}// 登录类型:user:用户密码登录;phone:手机验证码登录;qr:二维码扫码登录String type = obtainParameter(request, "type");String mobile = obtainParameter(request, "mobile");MyAuthenticationToken authRequest;String principal;String credentials;// 手机验证码登录if("phone".equals(type)){principal = obtainParameter(request, "phone");credentials = obtainParameter(request, "verifyCode");}// 二维码扫码登录else if("qr".equals(type)){principal = obtainParameter(request, "qrCode");credentials = null;}// 帐号密码登录else {principal = obtainParameter(request, "username");credentials = obtainParameter(request, "password");if(type == null)type = "user";}if (principal == null) {principal = "";}if (credentials == null) {credentials = "";}principal = principal.trim();authRequest = new MyAuthenticationToken(principal, credentials, type, mobile);// Allow subclasses to set the "details" propertysetDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}private void setDetails(HttpServletRequest request,AbstractAuthenticationToken authRequest) {authRequest.setDetails(authenticationDetailsSource.buildDetails(request));}private String obtainParameter(HttpServletRequest request, String parameter) {return request.getParameter(parameter);}

自定义 AbstractAuthenticationToken

继承 AbstractAuthenticationToken,添加属性 type,用于后续判断。

public class MyAuthenticationToken extends AbstractAuthenticationToken {private static final long serialVersionUID = 110L;private final Object principal;private Object credentials;private String type;private String mobile;/*** This constructor can be safely used by any code that wishes to create a* <code>UsernamePasswordAuthenticationToken</code>, as the {@link* #isAuthenticated()} will return <code>false</code>.**/public MyAuthenticationToken(Object principal, Object credentials,String type, String mobile) {super(null);this.principal = principal;this.credentials = credentials;this.type = type;this.mobile = mobile;this.setAuthenticated(false);}/*** This constructor should only be used by <code>AuthenticationManager</code> or <code>AuthenticationProvider</code>* implementations that are satisfied with producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)* token token.** @param principal* @param credentials* @param authorities*/public MyAuthenticationToken(Object principal, Object credentials,String type, String mobile, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;this.credentials = credentials;this.type = type;this.mobile = mobile;super.setAuthenticated(true);}@Overridepublic Object getCredentials() {return this.credentials;}@Overridepublic Object getPrincipal() {return this.principal;}public String getType() {return this.type;}public String getMobile() {return this.mobile;}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;}
}

自定义 AuthenticationProvider

实现 AuthenticationProvider

代码与 AbstractUserDetailsAuthenticationProvider 基本一致,只需修改 authenticate 方法 及 createSuccessAuthentication 方法中的 UsernamePasswordAuthenticationToken 为咱们的 token, 改成:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {// 此处修改断言自定义的 MyAuthenticationTokenAssert.isInstanceOf(MyAuthenticationToken.class, authentication, this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.onlySupports", "Only MyAuthenticationToken is supported"));// ...}protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {MyAuthenticationToken result = new MyAuthenticationToken(principal, authentication.getCredentials(),((MyAuthenticationToken) authentication).getType(),((MyAuthenticationToken) authentication).getMobile(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));result.setDetails(authentication.getDetails());return result;}

继承provider

继承咱们自定义的AuthenticationProvider,编写验证方法additionalAuthenticationChecks及 retrieveUser缓存

/*** 自定义验证* @param userDetails* @param authentication* @throws AuthenticationException*/protected void additionalAuthenticationChecks(UserDetails userDetails, MyAuthenticationToken authentication) throws AuthenticationException {Object salt = null;if(this.saltSource != null) {salt = this.saltSource.getSalt(userDetails);}if(authentication.getCredentials() == null) {this.logger.debug("Authentication failed: no credentials provided");throw new BadCredentialsException(this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));} else {String presentedPassword = authentication.getCredentials().toString();// 验证开始if("phone".equals(authentication.getType())){// 手机验证码验证,调用公共服务查询后台验证码缓存: key 为authentication.getPrincipal()的value, 并判断其与验证码是否匹配,此处写死为 1000if(!"1000".equals(presentedPassword)){this.logger.debug("Authentication failed: verifyCode does not match stored value");throw new BadCredentialsException(this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.badCredentials", "Bad verifyCode"));}}else if(MyLoginAuthenticationFilter.SPRING_SECURITY_RESTFUL_TYPE_QR.equals(authentication.getType())){// 二维码只须要根据 qrCode 查询到用户便可,因此此处无需验证}else {// 用户名密码验证if(!this.passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) {this.logger.debug("Authentication failed: password does not match stored value");throw new BadCredentialsException(this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));}}}}protected final UserDetails retrieveUser(String username, MyAuthenticationToken authentication) throws AuthenticationException {UserDetails loadedUser;try {// 调用loadUserByUsername时加入type前缀loadedUser = this.getUserDetailsService().loadUserByUsername(authentication.getType() + ":" + username);} catch (UsernameNotFoundException var6) {if(authentication.getCredentials() != null) {String presentedPassword = authentication.getCredentials().toString();this.passwordEncoder.isPasswordValid(this.userNotFoundEncodedPassword, presentedPassword, (Object)null);}throw var6;} catch (Exception var7) {throw new InternalAuthenticationServiceException(var7.getMessage(), var7);}if(loadedUser == null) {throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");} else {return loadedUser;}}

自定义 UserDetailsService

查询用户时根据类型采用不一样方式查询: 帐号密码根据用户名查询用户; 验证码根据 phone查询用户, 二维码可调用公共服务微信

@Overridepublic UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException {BaseUser baseUser;String[] parameter = var1.split(":");// 手机验证码调用FeignClient根据电话号码查询用户if("phone".equals(parameter[0])){ResponseData<BaseUser> baseUserResponseData = baseUserService.getUserByPhone(parameter[1]);if(baseUserResponseData.getData() == null || !ResponseCode.SUCCESS.getCode().equals(baseUserResponseData.getCode())){logger.error("找不到该用户,手机号码:" + parameter[1]);throw new UsernameNotFoundException("找不到该用户,手机号码:" + parameter[1]);}baseUser = baseUserResponseData.getData();} else if("qr".equals(parameter[0])){// 扫码登录根据key从redis查询用户baseUser = null;} else {// 帐号密码登录调用FeignClient根据用户名查询用户ResponseData<BaseUser> baseUserResponseData = baseUserService.getUserByUserName(parameter[1]);if(baseUserResponseData.getData() == null || !ResponseCode.SUCCESS.getCode().equals(baseUserResponseData.getCode())){logger.error("找不到该用户,用户名:" + parameter[1]);throw new UsernameNotFoundException("找不到该用户,用户名:" + parameter[1]);}baseUser = baseUserResponseData.getData();}// 调用FeignClient查询角色ResponseData<List<BaseRole>> baseRoleListResponseData = baseRoleService.getRoleByUserId(baseUser.getId());List<BaseRole> roles;if(baseRoleListResponseData.getData() == null ||  !ResponseCode.SUCCESS.getCode().equals(baseRoleListResponseData.getCode())){logger.error("查询角色失败!");roles = new ArrayList<>();}else {roles = baseRoleListResponseData.getData();}//调用FeignClient查询菜单ResponseData<List<BaseModuleResources>> baseModuleResourceListResponseData = baseModuleResourceService.getMenusByUserId(baseUser.getId());// 获取用户权限列表List<GrantedAuthority> authorities = convertToAuthorities(baseUser, roles);// 存储菜单到redisif( ResponseCode.SUCCESS.getCode().equals(baseModuleResourceListResponseData.getCode()) && baseModuleResourceListResponseData.getData() != null){resourcesTemplate.delete(baseUser.getId() + "-menu");baseModuleResourceListResponseData.getData().forEach(e -> {resourcesTemplate.opsForList().leftPush(baseUser.getId() + "-menu", e);});}// 返回带有用户权限信息的Userorg.springframework.security.core.userdetails.User user =  new org.springframework.security.core.userdetails.User(baseUser.getUserName(),baseUser.getPassword(), isActive(baseUser.getActive()), true, true, true, authorities);return new BaseUserDetail(baseUser, user);}

配置WebSecurityConfigurerAdapter

将咱们自定义的类配置到spring security 登录流程

@Configuration
@Order(ManagementServerProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {// 自动注入UserDetailsService@Autowiredprivate BaseUserDetailService baseUserDetailService;@Overridepublic void configure(HttpSecurity http) throws Exception {http    // 自定义过滤器.addFilterAt(getMyLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)// 配置登录页/login并容许访问.formLogin().loginPage("/login").permitAll()// 登出页.and().logout().logoutUrl("/logout").logoutSuccessUrl("/backReferer")// 其他全部请求所有须要鉴权认证.and().authorizeRequests().anyRequest().authenticated()// 因为使用的是JWT,咱们这里不须要csrf.and().csrf().disable();}/*** 用户验证* @param auth*/@Overridepublic void configure(AuthenticationManagerBuilder auth) {auth.authenticationProvider(myAuthenticationProvider());}/*** 自定义密码验证* @return*/@Beanpublic MyAuthenticationProvider myAuthenticationProvider(){MyAuthenticationProvider provider = new MyAuthenticationProvider();// 设置userDetailsServiceprovider.setUserDetailsService(baseUserDetailService);// 禁止隐藏用户未找到异常provider.setHideUserNotFoundExceptions(false);// 使用BCrypt进行密码的hashprovider.setPasswordEncoder(new BCryptPasswordEncoder(6));return provider;}/*** 自定义登录过滤器* @return*/@Beanpublic MyLoginAuthenticationFilter getMyLoginAuthenticationFilter() {MyLoginAuthenticationFilter filter = new MyLoginAuthenticationFilter();try {filter.setAuthenticationManager(this.authenticationManagerBean());} catch (Exception e) {e.printStackTrace();}filter.setAuthenticationSuccessHandler(new MyLoginAuthSuccessHandler());filter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/login?error"));return filter;}
}

Spring Cloud OAuth2 扩展登录方式:帐户密码登录、 手机验证码登录、 二维码扫码登录相关推荐

  1. spring cloud oauth2系列篇(二)深入authorization_code授权码模式完整实现

    项目的源码地址:https://github.com/daxian-zhu/online_edu 项目是最新的,文章可能不是最新的 这里先简单的介绍项目,不然有的地方可能我表达不清楚容易造成误解 on ...

  2. java调用授权接口oauth2_微信授权就是这个原理,Spring Cloud OAuth2 授权码模式

    上一篇文章Spring Cloud OAuth2 实现单点登录介绍了使用 password 模式进行身份认证和单点登录.本篇介绍 Spring Cloud OAuth2 的另外一种授权模式-授权码模式 ...

  3. 面试官:能说一说微信授权的原理吗?(Spring Cloud OAuth2 授权码模式)

    我是风筝,公众号「古时的风筝」,一个简单的程序员鼓励师. 文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在里面. 上一篇文章Spring Cloud OA ...

  4. Spring Cloud OAuth2 认证服务器

    1.Spring Cloud OAuth2介绍 Spring Cloud OAuth2 是 Spring Cloud 体系对OAuth2协议的实现,可以用来做多个微服务的统一认证(验证身份合法性)授权 ...

  5. Spring Cloud OAuth2 JWT 微服务认证服务器得构建

    文章目录 Spring Cloud OAuth2 JWT 微服务认证服务器得构建 前言 认证服务得搭建 `AuthorizationServer` `WebSecurityConfig` `Autho ...

  6. spring boot高性能实现二维码扫码登录(中)——Redis版

    前言 本打算用CountDownLatch来实现,但有个问题我没有考虑,就是当用户APP没有扫二维码的时候,线程会阻塞5分钟,这反而造成性能的下降.好吧,现在回归传统方式:前端ajax每隔1秒或2秒发 ...

  7. spring boot高性能实现二维码扫码登录(上)——单服务器版

    前言 目前网页的主流登录方式是通过手机扫码二维码登录.我看了网上很多关于扫码登录博客后,发现基本思路大致是:打开网页,生成uuid,然后长连接请求后端并等待登录认证相应结果,而后端每个几百毫秒会循环查 ...

  8. 《深入理解 Spring Cloud 与微服务构建》第十七章 使用 Spring Cloud OAuth2 保护微服务系统

    <深入理解 Spring Cloud 与微服务构建>第十七章 使用 Spring Cloud OAuth2 保护微服务系统 文章目录 <深入理解 Spring Cloud 与微服务构 ...

  9. Spring Cloud OAuth2中访问/oauth/token报Unsupported grant type: password问题的解决

    Spring Cloud OAuth2中访问/oauth/token报Unsupported grant type: password问题的解决 问题分析 问题解决 问题分析 在新建的Spring C ...

最新文章

  1. 程序员该如何抉择公司?
  2. 冲刺第一天 12.29 SAT
  3. (一)U盘安装ubuntu18.04.1
  4. ERROR in static/js/vendor.js from UglifyJs UUnexpected token: name (Dom7)
  5. 在Blazor中构建数据库应用程序——第4部分——UI控件
  6. Python中的len函数
  7. C++ 为什么要引入异常处理机制
  8. UVA515 King
  9. 6.7. exists, not exists
  10. Effective C++ -----条款05:了解C++默默编写并调用哪些函数
  11. 西门子S7-1200控制伺服/步进电机方法与接线(全)
  12. querydsl动态 sql_Spring-data-jpa扩展查询 QueryDSL 实践
  13. linux uwf开放80端口,SELinux - osc_a3uwfsx7的个人空间 - OSCHINA - 中文开源技术交流社区...
  14. PS更换证件照背景颜色
  15. Stata:调节中介效应检验
  16. ArcGIS的.prj文件生成proj4格式的字符串
  17. 目标检测里,视频与图像有何区别?
  18. python中row是什么意思_Python中的2D列表(row total/grand total)
  19. 计算当前四边形是否为凸四边形
  20. Android 屏幕dp、dpi、px、ppi、density的区别

热门文章

  1. 试试支持 DTLS 的 FreeCoAP
  2. 使用概念模型 和心智模型的_为什么要使用模型?
  3. LoRaWAN版本历史及协议格式说明
  4. 使用winscp连接vbox时出现:验证日志(具体情况参见会话日志): 使用用户名 “vagrant“。 验证失败【No supported authentication methods 。。。。】
  5. 阿里云Ubuntu 镜像配置方法
  6. 常见GPU卡精度支持一览表
  7. 安创安全OA使用的技术
  8. hdu5304 Eastest Magical Day Seep Group#39;s Summer 状压dp+生成树
  9. 转: ES6异步编程:Thunk函数的含义与用法
  10. 从自助到共享,移动物联卡共享洗衣机这阵风口能吹多久?