现在开发个应用登录比以前麻烦的多。产品经理说用户名密码登录、短信登录都得弄上,如果搞个小程序连小程序登录也得安排上,差不多就是我全都要。

多种登录途径达到一个效果确实不太容易,今天胖哥在Spring Security中实现了这三种登录你全都要的效果,爽的飞起,还不点个赞先。

大致原理

虽然不需要知道原理,但是还是要满足一下需要知道原理的同学。不过这里不会太深入,只会说核心的部分。更多的相关知识可以去看胖哥的Spring Security干货教程。

登录的几大组件

在Spring Security中我们需要实现登录认证就需要实现AbstractAuthenticationProcessingFilter;还需要一个处理具体登录逻辑的AuthenticationProvider;而每个AuthenticationProvider又对应一种Authentication。执行流程如下:

登录的基本流程

原理呢大概就是这样子的,接下来的工作就是按照上面封装每种登录的逻辑了。

ChannelUserDetailsService

在整个Spring Security体系中只允许有一个UserDetailsService注入Spring IoC,所以我扩展了这个接口:

public interface ChannelUserDetailsService extends UserDetailsService {/*** 验证码登录** @param phone the phone* @return the user details*/UserDetails loadByPhone(String phone);/*** openid登录** @param openId the open id* @return the user details*/UserDetails loadByOpenId(String openId);}

这样三种登录都能使用一个UserDetailsService了,当然如果你的登录渠道更多你可以增加更多的实现。

验证码登录

关于验证码登录以前有专门的文章来讲解登录流程和实现细节这里就不再赘述了,有兴趣可以去看相关的文章。这里提一句验证码登录的URI为/login/captcha,这是一个比较关键的细节后面有关于它的更多运用。开发中我们需要实现上面的loadByPhone,另外还需要实现验证码的校验服务逻辑:

public interface CaptchaService {/*** Send captcha code string.** @param phone the phone* @return the boolean*/boolean sendCaptchaCode(String phone);/*** 根据手机号去缓存中获取验证码同{@code captcha}进行对比,对比成功从缓存中主动清除验证码** @param phone   手机号* @param captcha 前端传递的验证码* @return the boolean*/boolean verifyCaptchaCode(String phone,String captcha);
}

微信小程序登录

微信小程序登录这里需要重点说一下.首先前端会传递一个clientIdjsCode, 我们比较陌生的是clientId的目的是为了标识小程序的配置appidsecret,这样我们可以同时适配多个小程序。这里我设计了一个获取小程序客户端的函数式接口:

@FunctionalInterface
public interface MiniAppClientService {/*** Get mini app client.** @return {@link MiniAppClient}* @see MiniAppClient#getAppId()* @see MiniAppClient#getSecret()*/MiniAppClient get(String clientId);
}

然后就可以请求微信服务器的登录接口code2session了,拿到openid后注册或者登录(实现loadByOpenId),同时还要缓存sessionKey用来加解密使用:

/*** 缓存sessionKey,这里只实现put ,get可以根据cachekey规则去实现获取。** @author felord.cn* @since 1.0.8.RELEASE*/
public interface MiniAppSessionKeyCache {/*** Put.** @param cacheKey   the cache key* @param sessionKey the session key*/void put(String cacheKey,String sessionKey);}

对应的AuthenticationProvider实现

package cn.felord.security.autoconfigure.miniapp;import cn.felord.security.autoconfigure.ChannelUserDetailsService;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;import java.net.URI;
import java.util.Collection;
import java.util.Objects;/***  Miniapp authentication provider.** @author felord.cn* @since 1.0.8.RELEASE*/
public class MiniAppAuthenticationProvider implements AuthenticationProvider, MessageSourceAware {private static final String ENDPOINT = "https://api.weixin.qq.com/sns/jscode2session";private final GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();private final MiniAppClientService miniAppClientService;private final ChannelUserDetailsService channelUserDetailsService;private final MiniAppSessionKeyCache miniAppSessionKeyCache;private final RestOperations restOperations;private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();/*** Instantiates a new Captcha authentication provider.* appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code** @param miniAppClientService      the mini app client supplier* @param channelUserDetailsService the channel user details service* @param miniAppSessionKeyCache    the mini app session key cache*/public MiniAppAuthenticationProvider(MiniAppClientService miniAppClientService, ChannelUserDetailsService channelUserDetailsService, MiniAppSessionKeyCache miniAppSessionKeyCache) {this.miniAppClientService = miniAppClientService;this.channelUserDetailsService = channelUserDetailsService;this.miniAppSessionKeyCache = miniAppSessionKeyCache;this.restOperations = new RestTemplate();}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {Assert.isInstanceOf(MiniAppAuthenticationToken.class, authentication,() -> messages.getMessage("MiniAppAuthenticationProvider.onlySupports","Only MiniAppAuthenticationToken is supported"));MiniAppAuthenticationToken unAuthenticationToken = (MiniAppAuthenticationToken) authentication;String clientId = unAuthenticationToken.getName();String jsCode = (String) unAuthenticationToken.getCredentials();ObjectNode response = this.getResponse(miniAppClientService.get(clientId), jsCode);String openId = response.get("openid").asText();String sessionKey = response.get("session_key").asText();UserDetails userDetails = channelUserDetailsService.loadByOpenId(openId);String username = userDetails.getUsername();miniAppSessionKeyCache.put(username,sessionKey);return createSuccessAuthentication(authentication, userDetails);}@Overridepublic boolean supports(Class<?> authentication) {return MiniAppAuthenticationToken.class.isAssignableFrom(authentication);}@Overridepublic void setMessageSource(MessageSource messageSource) {this.messages = new MessageSourceAccessor(messageSource);}/*** 认证成功将非授信凭据转为授信凭据.* 封装用户信息 角色信息。** @param authentication the authentication* @param user           the user* @return the authentication*/protected Authentication createSuccessAuthentication(Authentication authentication, UserDetails user) {Collection<? extends GrantedAuthority> authorities = authoritiesMapper.mapAuthorities(user.getAuthorities());MiniAppAuthenticationToken authenticationToken = new MiniAppAuthenticationToken(user, null, authorities);authenticationToken.setDetails(authentication.getDetails());return authenticationToken;}/*** 请求微信服务器登录接口 code2session* @param miniAppClient miniAppClient* @param jsCode jsCode* @return ObjectNode*/private ObjectNode getResponse(MiniAppClient miniAppClient, String jsCode) {MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();queryParams.add("appid", miniAppClient.getAppId());queryParams.add("secret", miniAppClient.getSecret());queryParams.add("js_code", jsCode);queryParams.add("grant_type", "authorization_code");URI uri = UriComponentsBuilder.fromHttpUrl(ENDPOINT).queryParams(queryParams).build().toUri();ResponseEntity<ObjectNode> response = restOperations.exchange(RequestEntity.get(uri).build(), ObjectNode.class);ObjectNode body = response.getBody();if (Objects.isNull(body)) {throw new BadCredentialsException("miniapp response is null");}// openid session_key unionid errcode errmsgfinal int defaultVal = -2;if (body.get("errcode").asInt(defaultVal) != 0) {throw new BadCredentialsException(body.get("errmsg").asText("unknown error"));}return body;}
}

AbstractAuthenticationProcessingFilter实现参考文末源码,没有什么特色。

登录渠道聚合

最终验证码登录为:

POST /login/captcha?phone=182****0032&captcha=596001 HTTP/1.1
Host: localhost:8085

小程序登录为:

POST /login/miniapp?clientId=wx12342&code=asdfasdfasdfasdfsd HTTP/1.1
Host: localhost:8085

但是我们要配置两套过滤器,要能配置一个聚合过滤器就完美了,我观察了一下它们的URI,如果能解析出验证码登录为captcha、小程序为miniapp就能根据对应的标识路由到对应的过滤器处理了。事实上是可以的:

RequestMatcher matcher =  new AntPathRequestMatcher("/login/{channal}", "POST");String channel = LOGIN_REQUEST_MATCHER.matcher(request).getVariables().get("channel");

为此我增强了AbstractAuthenticationProcessingFilter,让它能够获取渠道:

public abstract class AbstractChannelAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {protected AbstractChannelAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher) {super(requiresAuthenticationRequestMatcher);}/*** 用来获取登录渠道标识** @return the string*/protected abstract String channel();
}

验证码和小程序的过滤器只需要实现这个接口即可,小程序的就这样实现:

/*** The type Mini app authentication filter.** @author felord.cn* @since 1.0.8.RELEASE*/
public class MiniAppAuthenticationFilter extends AbstractChannelAuthenticationProcessingFilter {/*** The constant CHANNEL_ID.*/private static final String CHANNEL_ID = "miniapp";private static final String SPRING_SECURITY_FORM_MINI_CLIENT_KEY = "clientId";/*** The constant SPRING_SECURITY_FORM_PHONE_KEY.*/private static final String SPRING_SECURITY_FORM_JS_CODE_KEY = "jsCode";/*** Instantiates a new Captcha authentication filter.*/public MiniAppAuthenticationFilter() {super(new AntPathRequestMatcher("/login/" + CHANNEL_ID, "POST"));}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {if (!request.getMethod().equals(HttpMethod.POST.name())) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}String clientId = obtainClientId(request);String jsCode = obtainJsCode(request);MiniAppAuthenticationToken authRequest = new MiniAppAuthenticationToken(clientId,jsCode);// Allow subclasses to set the "details" propertysetDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}@Overridepublic String channel() {return CHANNEL_ID;}protected String obtainClientId(HttpServletRequest request) {String clientId = request.getParameter(SPRING_SECURITY_FORM_MINI_CLIENT_KEY);if (!StringUtils.hasText(clientId)) {throw new IllegalArgumentException("clientId is required");}return clientId.trim();}/*** Obtain JS CODE.** @param request the request* @return the string*/protected String obtainJsCode(HttpServletRequest request) {String jsCode = request.getParameter(SPRING_SECURITY_FORM_JS_CODE_KEY);if (!StringUtils.hasText(jsCode)) {throw new IllegalArgumentException("js_code is required");}return jsCode.trim();}/*** Sets details.** @param request     the request* @param authRequest the auth request*/protected void setDetails(HttpServletRequest request,MiniAppAuthenticationToken authRequest) {authRequest.setDetails(authenticationDetailsSource.buildDetails(request));}
}

这样我们的聚合过滤器就产生了:

public class ChannelAuthenticationFilter extends AbstractAuthenticationProcessingFilter {private static final String CHANNEL_URI_VARIABLE_NAME = "channel";private static final RequestMatcher LOGIN_REQUEST_MATCHER = new AntPathRequestMatcher("/login/{" + CHANNEL_URI_VARIABLE_NAME + "}", "POST");private final List<? extends AbstractChannelAuthenticationProcessingFilter> channelFilters;public ChannelAuthenticationFilter(List<? extends AbstractChannelAuthenticationProcessingFilter> channelFilters) {super(LOGIN_REQUEST_MATCHER);this.channelFilters = CollectionUtils.isEmpty(channelFilters) ? Collections.emptyList() : channelFilters;}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {String channel = LOGIN_REQUEST_MATCHER.matcher(request).getVariables().get(CHANNEL_URI_VARIABLE_NAME);for (AbstractChannelAuthenticationProcessingFilter channelFilter : channelFilters) {String rawChannel = channelFilter.channel();if (Objects.equals(channel, rawChannel)) {return channelFilter.attemptAuthentication(request, response);}}throw new ProviderNotFoundException("No Suitable Provider");}}

然后注入Spring IoC:

@Configuration(proxyBeanMethods = false)
public class ChannelAuthenticationConfiguration {/*** 短信验证码登录过滤器** @param channelUserDetailsService the channel user details service* @param captchaService            the captcha service* @param jwtTokenGenerator         the jwt token generator* @return the captcha authentication provider*/@Bean@ConditionalOnBean({ChannelUserDetailsService.class, CaptchaService.class, JwtTokenGenerator.class})CaptchaAuthenticationFilter captchaAuthenticationFilter(ChannelUserDetailsService channelUserDetailsService,CaptchaService captchaService,JwtTokenGenerator jwtTokenGenerator) {CaptchaAuthenticationProvider captchaAuthenticationProvider = new CaptchaAuthenticationProvider(channelUserDetailsService, captchaService);CaptchaAuthenticationFilter captchaAuthenticationFilter = new CaptchaAuthenticationFilter();ProviderManager providerManager = new ProviderManager(Collections.singletonList(captchaAuthenticationProvider));captchaAuthenticationFilter.setAuthenticationManager(providerManager);captchaAuthenticationFilter.setAuthenticationSuccessHandler(new LoginAuthenticationSuccessHandler(jwtTokenGenerator));SimpleAuthenticationEntryPoint authenticationEntryPoint = new SimpleAuthenticationEntryPoint();captchaAuthenticationFilter.setAuthenticationFailureHandler(new AuthenticationEntryPointFailureHandler(authenticationEntryPoint));return captchaAuthenticationFilter;}/*** 小程序登录过滤器** @param miniAppClientService      the mini app client service* @param channelUserDetailsService the channel user details service* @param jwtTokenGenerator         the jwt token generator* @return the mini app authentication filter*/@Bean@ConditionalOnBean({ChannelUserDetailsService.class, MiniAppClientService.class,MiniAppSessionKeyCache.class, JwtTokenGenerator.class})MiniAppAuthenticationFilter miniAppAuthenticationFilter(MiniAppClientService miniAppClientService,ChannelUserDetailsService channelUserDetailsService,MiniAppSessionKeyCache miniAppSessionKeyCache,JwtTokenGenerator jwtTokenGenerator) {MiniAppAuthenticationFilter miniAppAuthenticationFilter = new MiniAppAuthenticationFilter();MiniAppAuthenticationProvider miniAppAuthenticationProvider = new MiniAppAuthenticationProvider(miniAppClientService, channelUserDetailsService,miniAppSessionKeyCache);ProviderManager providerManager = new ProviderManager(Collections.singletonList(miniAppAuthenticationProvider));miniAppAuthenticationFilter.setAuthenticationManager(providerManager);miniAppAuthenticationFilter.setAuthenticationSuccessHandler(new LoginAuthenticationSuccessHandler(jwtTokenGenerator));SimpleAuthenticationEntryPoint authenticationEntryPoint = new SimpleAuthenticationEntryPoint();miniAppAuthenticationFilter.setAuthenticationFailureHandler(new AuthenticationEntryPointFailureHandler(authenticationEntryPoint));return miniAppAuthenticationFilter;}/*** Channel authentication filter channel authentication filter.** @param channelFilters the channel filters* @return the channel authentication filter*/@Beanpublic ChannelAuthenticationFilter channelAuthenticationFilter(List<? extends AbstractChannelAuthenticationProcessingFilter> channelFilters) {return new ChannelAuthenticationFilter(channelFilters);}
}

看上去好像还有优化空间。

我们只需要把它配置到HttpSecurity就可以了实现三种登录了。

ChannelAuthenticationFilter channelAuthenticationFilter = getChannelAuthenticationFilterBeanOrNull(applicationContext);
if (Objects.nonNull(channelAuthenticationFilter)) {httpSecurity.addFilterBefore(channelAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}

总结

今天用Spring Security实现了比较实用的多端登录,其中的很多知识点都是以往积累的,而且是借鉴了Spring框架源码的思路。完整代码已经开源,请关注:码农小胖哥 回复channellogin获取生产级别源码。

往期推荐

重装IDEA再也不愁了,一招搞定同步个人配置!

用办公电脑存不雅视频,结果被告了...

小米宣布发放价值15.3亿元的股票!人均39万,最小授予者仅24岁

为什么catch了异常,但事务还是回滚了?

骚操作!阿里云直接买www.huaweicloud.com的关键词来抢生意?

各种登录源码来了!基础登录、验证码登录、小程序登录...全都要!相关推荐

  1. 【快递下单小程序源码】可运营+快递下单微信小程序

    [快递下单小程序源码]可运营+快递下单微信小程序 演示小程序:快递大师兄 需要准备的材料 需要准备一台2H4G的服务器,并安装宝塔. 一个已经备案的域名. 下载完整安装包文件 一.首先登录宝塔创建网站 ...

  2. 小程序源码:后台版本趣味测试微信小程序源码下载支持自定义问题等等

    这是一款有后台版本的趣味测试小程序 支持用户自定义添加和删除问题 支持流量主后台设置 支持用户个人中心等等 该程序是微擎框架的,所以后台需要有微擎 PS:该小程序的登录接口并未替换更新还是采用的旧版登 ...

  3. thinkphp开发的活动报名小程序源码带后台管理完整的报名小程序源码

    介绍: 活动报名小程序源码,基于thinkphp开发的报名小程序源码,带有后台管理,用户发布活动信息.报名可以后台管理,简单测试了一下,基本都还是可以的. 不过需要注意的是,用户注册部分是发送手机短信 ...

  4. 小程序源码:端午送粽子祝福微信小程序源码下载支持打赏模式带背景音乐

    端午节的时候很火的一款小程序 给亲朋好友送祝福也是一款引流非常不错的一款小程序 支持打赏小程序模式实现盈利模式 当然了,有能力的小伙伴也可以魔改一下做成别的小程序也是可以的 该小程序无需服务器和域名, ...

  5. 七夕节微信表白墙小程序源码/基于Laravel的表白墙微信小程序源码

    七夕节微信表白墙小程序源码/基于Laravel的表白墙微信小程序源码 ☑️ 编号:ym499 ☑️ 品牌:无 ☑️ 语言:小程序 ☑️ 大小:11.2MB ☑️ 类型:微信表白墙小程序 ☑️ 支持:小 ...

  6. 小程序源码:网课查题微信小程序源码下载,题库资源丰富自动采集,支持语音拍照识别

    这是一款网课查题微信小程序源码 题库资源丰富自动采集, 支持语音拍照识别 该款采用接口方式,所以题库自动全网采集 而且该款小程序无需服务器和域名即可搭建 大家解压源码然后使用微信开发者工具打开源码 然 ...

  7. 小程序源码:最新wordpress黑金壁纸微信小程序 二开修复版源码下载支持流量主收益

    这是一款wordpress系统框架的壁纸小程序源码 相信很多人以前也有用过这类的壁纸小程序源码吧 现在给大家发的这一款是二开修复版的 和以前的安装方式差不多,支持流量主收益模式 介绍: WordPre ...

  8. [附源码]计算机毕业设计Python+uniapp基于微信小程序某企业考勤系统01yr8(程序+lw+远程部署)

    [附源码]计算机毕业设计Python+uniapp基于微信小程序某企业考勤系统01yr8(程序+lw+远程部署) 该项目含有源码.文档.程序.数据库.配套开发软件.软件安装教程 项目运行环境配置: P ...

  9. 小程序源码:最新wordpress黑金壁纸微信小程序 二开修复版-多玩法安装简单

    这是一款wordpress系统框架的壁纸小程序源码 相信很多人以前也有用过这类的壁纸小程序源码吧 现在给大家发的这一款是二开修复版的 和以前的安装方式差不多,支持流量主收益模式 介绍: WordPre ...

  10. 小程序源码:AI智能配音助手微信小程序源码支持多种声音场景选择

    大家好今天给大家带来一款配音小程序 这款小程序支持多种不同声音和场景的选择更人性化 比如说支持各地区的方言,英文,童声呀等等 另外也支持男声女声的选择,反正就是模板那些非常的多 当然啦音量,语调,语速 ...

最新文章

  1. tabBar的图标不被系统渲染
  2. 面试问红黑树,我脸都绿了。。
  3. 牛客小白月赛12 F 华华开始学信息学 (分块+树状数组)
  4. Ubuntu14.04server 安装 fping
  5. LeetCode(530)——二叉搜索树的最小绝对差(JavaScript)
  6. 电商之争:亚马逊与阿里一较高下
  7. 【算法导论】双调欧几里得旅行商问题
  8. 项目经理杂谈(上)____________加速步伐!
  9. python定时启动代码_python每天定时运行某程序代码
  10. 程序员面试宝典(第三版).pdf
  11. 高斯计传感器使用的霍尔传感器原理
  12. DSP入门必看(上)
  13. Windows安装SVN
  14. 【模电】0007 有源滤波器2(二阶有源滤低通波器)
  15. itextpdf生成列表基本用法
  16. 清华大学计算机系毕业论文 android,清华大学计算机科学与技术系
  17. selenium操作360极速浏览器的方法
  18. wordpress显示文章浏览量并且动态更新浏览量
  19. latex 背景颜色设置
  20. 网易区块链打造可信数字身份认证应用新场景,赋能科技峰会

热门文章

  1. 【认证课程】NP 理论复习之ospf(一)
  2. 配置spring-mvc + simple-spring-memcached
  3. mybatis+postgresql+insert返回主键,action,service侧
  4. python 同步 事件 event 简介
  5. python3 module 'pexpect' has no attribute 'spawn' 解决方法
  6. linux 内核 THIS_MODULE宏定义详解
  7. 在VC下执行DOS命令(VC执行EXE)
  8. c语言printf()输出格式控制
  9. Linux基本命令之vi
  10. fast system call 快速系统调用