SpringSecurity实战(八)-通用第三方登陆-自定义认证配置实现
文章目录
- 目标
- 通用第三方登陆设计
- 思路
- 自定义登陆流程
- 代码实现
- 核心依赖
- 创建第三方授权应用
- 调用第三方平台部分
- login.html
- controller 层
- service 层
- 核心配置
- 通用第三方登陆配置
- 本系统的授权认证部分
- 认证流程一:已经绑定现有用户流程
- OtherSysOauth2LoginFilter
- OtherSysOauth2LoginAuthenticationToken
- OtherSysOauth2LoginProvider
- OtherSysOauth2LoginUserDetailsServiceImpl
- 认证成功事件或者失败事件
- 认证流程二:没有绑定现有用户流程
- 绑定用户页面
- UserBindFilter
- BindAuthenticationToken
- BindLoginProvider
- UserDetailsServiceImpl
- 核心配置
- 综上所述
目标
- 了解JustAuth对接SpringSecurity通用第三方登陆
参考:JustAuth
SpringSecurity实战(五)-认证流程源码分析
查看github账户授予权限的应用
markdown画流程图,流程图语法
上篇已经介绍了第三方登陆相关概念和一些流行框架。
通用第三方登陆设计
需求:参考Gitee 网站的第三方登陆实现流程,将其他第三方平台接入系统,并融入SpringSecurity的管理中。
选用框架:基于Justauth框架的实现,对接Spring Security。(如果不使用Spring Security,只需要使用justauth框架即可。)
登陆流程:
第三方登陆分两种情况:
1,(已绑定用户)第三方平台授权,直接登陆
2,(未绑定用户)第三方平台授权,用户绑定后,登陆
思路
关于第三方登陆实现:
直接参考 JustAuth 提供的说明文档,简单明了。JustAuth
关于对接Spring Security的思路:
在前几篇介绍中,我们使用了Spring Security 进行系统的认证和授权,提供了基于用户名和密码表单登陆的方式,并且使用了Spring Session 和 Redisson 框架来管理会话信息。那么第三方登录将作为本系统用户登陆方式的一种扩展,我们期望不要由于第三方登录的出现,修改之前的认证授权流程和会话管理。所以自定义一套认证流程是一个不错的选择。
有一个大家比较关心的问题:
第三方用户信息怎么与现有用户系统整合?
最简单的就是设计一张第三方用户信息表,将第三方用户信息绑定到现有用户信息上。在登陆过程后,将第三方用户信息替换为现有用户信息实现登陆。
-- 第三方系统用户表
CREATE TABLE `other_sys_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',`scope` varchar(64) DEFAULT NULL COMMENT '第三方系统',`uuid` varchar(64) DEFAULT NULL COMMENT '第三方系统唯一账户',`user_id` bigint(20) DEFAULT NULL COMMENT '关联系统用户表的id',`username` varchar(64) DEFAULT NULL COMMENT '登陆用户名',`create_time` datetime DEFAULT NULL COMMENT '绑定时间(创建时间)',`update_time` datetime DEFAULT NULL COMMENT '更新时间',PRIMARY KEY (`id`),KEY `idx_user_id` (`user_id`) USING BTREE COMMENT '用户id索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
一个现有用户可能会匹配多个第三方系统的用户信息(1:n)。
JustAuth也给出一种登陆方案: JustAuth与用户系统整合流程图 可以参考学习
怎么设计一个通用的第三方登陆实现呢?
无论是要对接哪个平台,只需简单的配置,就可以实现第三方系统登陆。
每个平台厂商对OAuth2的实现不同,api接口等会不一样。JustAuth框架提供了大多数平台的对接接口,在一定程度上屏蔽了登陆的平台差异。对于我们实现的难点,在于怎么知道用户选择了那个第三方平台授权登陆。想知道这个很easy,用户使用哪个平台登陆,就将对应平台的syscode(比如 qq,github等等),当做参数传入。
自定义登陆流程
根据上述的登陆流程,会细分出两个认证流程:
已经绑定现有用户的情况下(说明之前已经使用第三方登陆,登陆本系统)。
点击第三方登陆,直接登陆本系统
没有绑定现有用户的情况下(说明第一次使用第三方登陆,登陆本系统)。
点击第三方登陆,判断没有绑定用户,跳转绑定用户界面,绑定后登陆
平行四边形中就是需要创建的类。
其实OtherSysOauth2LoginAuthenticationToken 以及后面的认证(Provider)用户详情服务可以不用重新创建,直接使用UsernamePasswordAuthenticationToken那一套就可以了。但是为了演示整个认证流程,我还是重写了Token以及认证(Provider)和用户详情服务,万一用上了呢。
代码实现
项目环境:spring boot 2.2.7 Jpa java8 mysql5.7 JustAuth1.15.8
代码参考:https://github.com/gengzi/gengzi_spring_security/tree/master/gengzi_spring_security
核心依赖
<!-- JustAuth 对接第三方登陆框架 --><dependency><groupId>me.zhyd.oauth</groupId><artifactId>JustAuth</artifactId><version>1.15.8</version></dependency>
JustAuth 官方文档参考: https://justauth.wiki/#/
数据库文件:sys_manager.sql
创建第三方授权应用
不介绍了,直接参考把,写的非常清楚。
https://justauth.wiki/# 集成第三方部分。
主要是配置一下回调地址(redirectUri ),例如:http://localhost:8081/api/v1/oauth/callback/{sysCode}
注意最后一个参数sysCode ,就是对应平台的code,这也是实现通用第三方登陆的关键。
比如Github网站OAuth2应用,配置:http://localhost:8081/api/v1/oauth/callback/github
比如Gitee网站OAuth2应用,配置:http://localhost:8081/api/v1/oauth/callback/gitee
调用第三方平台部分
页面:
login.html
代码参考:login.html
<h2>其他方式登录</h2>
<button class="but" onClick="other_login('github');" type="button" style="margin-top:10px">github授权登陆</button>
<button class="but" onClick="other_login('gitee');" type="button" style="margin-top:10px" >gitee授权登陆</button><script>// 第三方登陆function other_login(sys) {window.location.href = "/api/v1/oauth/login?oauthSysCode="+sys;}
</script>
controller 层
代码参考:Oauth2LoginController
@Api(value = "第三方登陆", tags = {"第三方登陆"})
@Controller
@RequestMapping("/api/v1/oauth")
public class Oauth2LoginController {@Autowiredprivate AuthRequestService authRequestService;@ApiOperation(value = "登陆接口", notes = "登陆接口")@ApiImplicitParams({@ApiImplicitParam(name = "oauthSysCode", value = "第三方系统syscode", required = true)})@GetMapping("/login")public String oauthLogin(@RequestParam("oauthSysCode") String oauthSysCode) {// 根据code ,获取对应第三方系统的 AuthRequestAuthRequest authRequest = authRequestService.getAuthRequest(oauthSysCode);// 重定向认证地址return "redirect:" + authRequest.authorize(AuthStateUtils.createState());}}
service 层
这里就是通用第三方登陆,根据syscode,构造对应平台请求。
代码参考:AuthRequestServiceImpl.java
@Service
public class AuthRequestServiceImpl implements AuthRequestService {// 读取第三方登陆配置的实体类@Autowiredprivate AuthRequestConfigEntity authRequestConfigEntity;@SneakyThrows@Overridepublic AuthRequest getAuthRequest(String sys) {AuthRequest authRequest = null;List<AuthRequestConfigEntity.AuthRequestInfo> othersys = authRequestConfigEntity.getOthersys();// 如果配置多个相同名称的 第三方系统,仅获取第一个配置信息Optional<AuthRequestConfigEntity.AuthRequestInfo> info = othersys.stream().filter(authRequestInfo -> authRequestInfo.getName().equalsIgnoreCase(sys)).findFirst();AuthRequestConfigEntity.AuthRequestInfo authRequestInfo = info.orElseThrow(() -> new RrException("不存在" + sys + "该系统的第三方登陆配置,请在yml文件中加入该系统的配置", RspCodeEnum.ERROR.getCode()));// 构造 AuthConfigAuthConfig config = AuthConfig.builder().clientId(authRequestInfo.getClientId()).clientSecret(authRequestInfo.getClientSecret()).redirectUri(authRequestInfo.getRedirectUri()).build();// 反射,使用有参的构造方法,创建对象Class aClass = Oauth2LoginConstant.sysMappingClazz.get(sys);Constructor constructor = aClass.getConstructor(AuthConfig.class);Object obj = constructor.newInstance(config);if (obj instanceof AuthRequest) {authRequest = (AuthRequest) obj;}return authRequest;}
}
再引入一个常量类, syscode 匹配 对应平台的请求类,就是通过Map 来实现的。还有一种更加简单彻底的做法,全部有配置文件控制,可以发挥下思路,也是同样的道理。
/**
* <h1>第三方登陆的全局静态变量</h1>
*
* @author gengzi
* @date 2020年11月28日18:15:35
*/
public class Oauth2LoginConstant {// TODO 懒得写了,有需要增加的第三方系统,增加属性配置就可以了public static final String SYS_GITHUB = "github";public static final String SYS_GITEE = "gitee";// 映射的classpublic static HashMap<String, Class> sysMappingClazz = new HashMap<>();// 允许第三方登陆的系统public static final String SYS_SOURCE[] = {Oauth2LoginConstant.SYS_GITHUB, Oauth2LoginConstant.SYS_GITEE};static {sysMappingClazz.putIfAbsent(SYS_GITHUB, AuthGithubRequest.class);sysMappingClazz.putIfAbsent(SYS_GITEE, AuthGiteeRequest.class);}}
核心配置
application.yml
如果你还需要增加其他第三方登陆,继续追加配置即可。
oauth2:othersys:- name: githubclient_id: # 客户端身份标识符(应用id),一般在申请完Oauth应用后,由第三方平台颁发,唯一client_secret: #配置 客户端密钥,一般在申请完Oauth应用后,由第三方平台颁发redirectUri: http://localhost:8081/api/v1/oauth/callback/github # 回调地址- name: giteeclient_id: # 客户端身份标识符(应用id),一般在申请完Oauth应用后,由第三方平台颁发,唯一client_secret: #配置 客户端密钥,一般在申请完Oauth应用后,由第三方平台颁发redirectUri: http://localhost:8081/api/v1/oauth/callback/gitee # 回调地址
可以参数: 配置转对象
配置对象:将配置转换为对象实体,方便我们使用。
代码参考:AuthRequestConfigEntity.java
@Configuration
@ConfigurationProperties(prefix = "oauth2")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AuthRequestConfigEntity<T> implements Serializable {// 所有的第三方登陆的客户端信息private List<AuthRequestInfo> othersys = new ArrayList<>();/*** 具体信息*/@Data@AllArgsConstructor@NoArgsConstructorpublic static class AuthRequestInfo {// 第三方系统名称private String name;// 客户端idprivate String clientId;// 客户端密钥private String clientSecret;// 回调地址private String redirectUri;}}
上述步骤,仅仅到了第三方授权页面。(图示第二个)
通用第三方登陆配置
对于如果需要增加其他第三方平台登陆支持:
只需要增加 application.yml 中的oauth2的配置和常量类 Oauth2LoginConstant 中的,系统code 与认证请求类的匹配。
本系统的授权认证部分
当用户点击“同意授权”,第三方平台就开始回调之前配置的本系统的回调地址,也就要进入认证登陆流程了。
认证流程一:已经绑定现有用户流程
OtherSysOauth2LoginFilter
代码参考: OtherSysOauth2LoginFilter.java
只粘核心代码,这里上述中的 AuthRequestServiceImpl 用来返回对应平台的,认证请求类。
public class OtherSysOauth2LoginFilter extends AbstractAuthenticationProcessingFilter {// 拦截路径,触发该filter 的执行private static final String REDIRECTURI = "/api/v1/oauth/callback/**";/*** 初始化拦截路径*/public OtherSysOauth2LoginFilter() {super(new AntPathRequestMatcher(REDIRECTURI));}// 预验证方法 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {OtherSysOauth2LoginAuthenticationToken token = null;// 解析地址,获得第三方平台syscodeString path = request.getServletPath();String[] sysArr = path.split("/api/v1/oauth/callback/");String sys = sysArr[sysArr.length - 1];// 判断当前系统是否支持该平台登陆boolean contains = Arrays.asList(Oauth2LoginConstant.SYS_SOURCE).contains(sys);if (!contains) {throw new AuthenticationServiceException("暂不支持此系统登录(This system login is not currently supported)");}// 构造AuthRequestAuthRequest authRequest = this.getAuthRequest(sys);if (authRequest == null) {throw new AuthenticationServiceException("暂不支持此系统登录(This system login is not currently supported)");}// 去第三方平台获取用户信息AuthResponse<AuthUser> authResponse = authRequest.login(this.getCallback(request));if (authResponse.ok()) {// 获取第三方登陆信息成功AuthUser data = authResponse.getData();// 用户id 一般是唯一的。建议通过uuid + source的方式唯一确定一个用户,这样可以解决用户身份归属的问题。String id = data.getUuid();// 根据当前系统和uuid,查询数据库,获取绑定本系统的用户信息OtherSysUser otherSysUser = this.getOtherUsersService().getOtherSysUserByUUIDAndScope(sys, id);if (otherSysUser == null) {// 未绑定过,第一次使用这个第三方平台登陆String uuid = UUID.randomUUID().toString();// 缓存一下第三方平台的用户信息,方便后续使用redisUtil.set(Oauth2LoginRedisKeysConstant.OTHER_SYS_USER_INFO + uuid, data, 300);// 跳转到绑定页面response.sendRedirect("/oauthlogin.html?token=" + uuid + "&scope=" + sys);return null;} else {// 存在绑定用户信息,说明不是第一次使用这个第三方平台登陆,执行认证流程ReturnData returnData = this.getUsersService().loadUserByUsername(otherSysUser.getUsername());// 这里直接使用了 user 的全部信息,设置到了 principal 主体中,方便使用token = new OtherSysOauth2LoginAuthenticationToken(returnData.getInfo());}}// 设置额外参数this.setDetails(request, token);// 去认证return this.getAuthenticationManager().authenticate(token);} }
OtherSysOauth2LoginAuthenticationToken
这里要注意还没有进入认证时, setAuthenticated(false); 要设置为false。
认证完毕后,如果认证成功,再设置为 true。super.setAuthenticated(true); // must use super, as we override
代码参考:OtherSysOauth2LoginAuthenticationToken.java
public class OtherSysOauth2LoginAuthenticationToken extends AbstractAuthenticationToken {private final Object principal;private Object credentials;public OtherSysOauth2LoginAuthenticationToken(Object principal, Object credentials) {super(null);this.principal = principal;this.credentials = credentials;setAuthenticated(false);}public OtherSysOauth2LoginAuthenticationToken(Object authUser) {super(null);this.principal = authUser;setAuthenticated(false);}/*** 使用提供的权限数组创建令牌。** @param authorities 权限集合* @param principal 用户名* @param credentials 密码*/public OtherSysOauth2LoginAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object principal, Object credentials) {super(authorities);this.principal = principal;this.credentials = credentials;super.setAuthenticated(true); // must use super, as we override}// 密码@Overridepublic Object getCredentials() {return this.credentials;}// 用户名@Overridepublic Object getPrincipal() {return this.principal;}}
OtherSysOauth2LoginProvider
代码参考:OtherSysOauth2LoginProvider.java
/**
* <h1>第三方登陆的提供者</h1>
* <p>
* 参考: {@link AbstractUserDetailsAuthenticationProvider} 实现
* 一些方法直接默认使用。
* 可以将AbstractUserDetailsAuthenticationProvider 整个类拷贝后,仅修改一些参数和方法
* <p>
* 作用:
* 判断token类型是否一致,不是OtherSysOauth2LoginAuthenticationToken ,不执行认证流程。这样将表单登陆,绑定用户登陆,还是第三方直接登录(已经绑定)
* 认证过程区分开。
* <p>
* 校验基本的信息后,就赋予认证成功的标识。
* <p>
* <p>
* 注意: 该Provider 并没有提供对密码的校验。因为第三方登陆,不会输入本系统用户名和密码。只要数据库表能找到该用户,默认该用户已经认证成功了。
* 如果需要真正执行密码校验的操作,请参阅{@link DaoAuthenticationProvider} 的 additionalAuthenticationChecks 方法实现。
*
* @author gengzi
* @date 2020年12月5日12:41:07
*/
@Slf4j
public class OtherSysOauth2LoginProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();private UserCache userCache = new NullUserCache();private boolean forcePrincipalAsString = false;protected boolean hideUserNotFoundExceptions = true;private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();// 用户详情服务,用于查询用户详情信息private OtherSysOauth2LoginUserDetailsServiceImpl userDetailsService;@Overridepublic void afterPropertiesSet() throws Exception {}@Overridepublic void setMessageSource(MessageSource messageSource) {this.messages = new MessageSourceAccessor(messageSource);}/*** 认证方法** @param authentication {@link OtherSysOauth2LoginAuthenticationToken} token* @return* @throws AuthenticationException*/@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {// 判断认证方式是否属于 OtherSysOauth2LoginAuthenticationTokenAssert.isInstanceOf(OtherSysOauth2LoginAuthenticationToken.class, authentication, "仅支持OtherSysOauth2LoginAuthenticationToken类型的认证");boolean cacheWasUsed = true;// 根据信息获取 UserDetails 的信息UserDetail principal = (UserDetail) authentication.getPrincipal();String id = principal.getUsername();UserDetails user = this.userCache.getUserFromCache(String.valueOf(id));if (user == null) {cacheWasUsed = false;user = retrieveUser(String.valueOf(id),(OtherSysOauth2LoginAuthenticationToken) authentication);}try {preAuthenticationChecks.check(user);} catch (AuthenticationException exception) {throw exception;}postAuthenticationChecks.check(user);if (!cacheWasUsed) {this.userCache.putUserInCache(user);}Object principalToReturn = user;if (forcePrincipalAsString) {principalToReturn = user.getUsername();}return createSuccessAuthentication(principalToReturn, authentication, user);}/*** 创建成功认证** @param principal 主体* @param authentication token* @param user 用户详情* @return*/protected Authentication createSuccessAuthentication(Object principal,Authentication authentication, UserDetails user) {// Ensure we return the original credentials the user supplied,// so subsequent attempts are successful even with encoded passwords.// Also ensure we return the original getDetails(), so that future// authentication events after cache expiry contain the detailsOtherSysOauth2LoginAuthenticationToken result = new OtherSysOauth2LoginAuthenticationToken(authoritiesMapper.mapAuthorities(user.getAuthorities()),principal, authentication.getCredentials());result.setDetails(authentication.getDetails());return result;}/*** 检索用户详情* <p>* 从数据库中查询** @param username 用户名* @param authentication* @return* @throws AuthenticationException*/protected UserDetails retrieveUser(String username, OtherSysOauth2LoginAuthenticationToken authentication) throws AuthenticationException {try {UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);if (loadedUser == null) {throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");}return loadedUser;} catch (InternalAuthenticationServiceException ex) {throw ex;} catch (Exception ex) {throw new InternalAuthenticationServiceException(ex.getMessage(), ex);}}public void setUserDetailsService(OtherSysOauth2LoginUserDetailsServiceImpl userDetailsService) {this.userDetailsService = userDetailsService;}protected OtherSysOauth2LoginUserDetailsServiceImpl getUserDetailsService() {return userDetailsService;}@Overridepublic boolean supports(Class<?> authentication) {return (OtherSysOauth2LoginAuthenticationToken.class.isAssignableFrom(authentication));}/*** 默认的预身份验证检查*/private class DefaultPreAuthenticationChecks implements UserDetailsChecker {@Overridepublic void check(UserDetails user) {if (!user.isAccountNonLocked()) {log.debug("User account is locked");throw new LockedException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked","User account is locked"));}if (!user.isEnabled()) {log.debug("User account is disabled");throw new DisabledException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled","User is disabled"));}if (!user.isAccountNonExpired()) {log.debug("User account is expired");throw new AccountExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired","User account has expired"));}}}private class DefaultPostAuthenticationChecks implements UserDetailsChecker {@Overridepublic void check(UserDetails user) {if (!user.isCredentialsNonExpired()) {log.debug("User account credentials have expired");throw new CredentialsExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired","User credentials have expired"));}}}
}
OtherSysOauth2LoginUserDetailsServiceImpl
代码参考:OtherSysOauth2LoginUserDetailsServiceImpl.java
其实跟之前实现的 UserDetailsService 没啥差别。
/**
* <h1>用户详细服务impl</h1>
* <p>
* 用于返回根据用户名返回用户详细信息,以便于供 security 使用
*
* @author gengzi
* @date 2020年11月3日15:24:43
*/
@Service("otherSysOauth2LoginUserDetailsServiceImpl")
public class OtherSysOauth2LoginUserDetailsServiceImpl {@Autowiredprivate UsersService usersService;public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {ReturnData result = usersService.loadUserByUsername(username);if (RspCodeEnum.NOTOKEN.getCode() == result.getStatus()) {throw new RrException(RspCodeEnum.ACCOUNT_NOT_EXIST.getDesc());}UserDetail userDetail = (UserDetail) result.getInfo();if (userDetail == null) {throw new RrException(RspCodeEnum.ACCOUNT_NOT_EXIST.getDesc());}//账号不可用if (userDetail.getStatus() == UserStatusEnum.DISABLE.getValue()) {userDetail.setEnabled(false);}return userDetail;}
}
认证成功事件或者失败事件
当认证完成后,就会执行默认或者自定义的认证成功事件或者失败事件。
可以根据业务需求修改,这里都返回的是 json 的数据
失败:
/**
* <h1>用户认证失败处理器</h1>
* 响应失败的json 信息
*
*
* @author gengzi
* @date 2020年11月24日13:23:40
*/
@Component
public class UserAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {@SneakyThrows@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {response.setContentType("application/json;charset=UTF-8");ReturnData ret = ReturnData.newInstance();ret.setFailure(e.getMessage());response.getWriter().write(JSON.toJSONString(ret));}
}
成功:
/**
* <h1>用户认证成功处理器</h1>
* 从session中获取,响应脱敏的用户信息
*
* @author gengzi
* @date 2020年11月24日13:23:40
*/
@Component
@Slf4j
public class UserAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {@SneakyThrows@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {response.setContentType("application/json;charset=UTF-8");ReturnData ret = ReturnData.newInstance();ret.setSuccess();ret.setInfo(authentication);response.getWriter().write(JSON.toJSONString(ret));}
}
认证流程二:没有绑定现有用户流程
依然要执行 OtherSysOauth2LoginFilter ,只不过判断用户未绑定后,跳转到了绑定页面
绑定用户页面
跳转绑定用户页面
绑定页面地址:"/oauthlogin.html?token=" + uuid + “&scope=” + sys
例如:http://localhost:8081/oauthlogin.html?token=3db4aa22-0aea-4604-9c6e-56fe7ffc86ce&scope=github
代码参考:oauthlogin.html
<h1>绑定系统账号</h1><form method="post" action="/otherlogin"><input type="hidden" name="scope" id="scope" value="github"><input type="hidden" name="token" id="token" value=""><input type="text" required="required" placeholder="用户名" name="username"></input><input type="password" required="required" placeholder="密码" name="password"></input><button class="but" type="submit">绑定并登陆账户</button></form><script>$(function () {// 获取路径中的参数function getUrlParms(name) {var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");var r = window.location.search.substr(1).match(reg);if (r != null)return unescape(r[2]);return null;}var token = getUrlParms("token");var scope = getUrlParms("scope");$("#token").val(token);$("#scope").val(scope);});</script>
UserBindFilter
代码参考:UserBindFilter.java
/**
* <h1>用户绑定过滤器</h1>
* <p>
* 触发条件: 当第三方登陆用户与本系统用户绑定时,执行该过滤器
* <p>* 匹配路径:/otherlogin Post 请求
* <p>
* 根据绑定用户入参,将第三方用户信息入库,与本系统用户关联
* <p>
* 参见:UsernamePasswordAuthenticationFilter 实现
* <p>* 使用示例:
** UserBindFilter userBindFilter = new UserBindFilter();* userBindFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));* userBindFilter.setRedisUtil(redisUtil);* userBindFilter.setOtherSysUserDao(otherSysUserDao);* userBindFilter.setSysUsersDao(sysUsersDao);* // 再加入到 spring security 的过滤器链中* httpSecurity.addFilterBefore(userBindFilter, UsernamePasswordAuthenticationFilter.class);
*
* @author gengzi
* @date 2020年11月24日10:45:17
*/
public class UserBindFilter extendsAbstractAuthenticationProcessingFilter {// ~ Static fields/initializers// =====================================================================================public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";public static final String SPRING_SECURITY_FORM_TOKEN_KEY = "token";public static final String SPRING_SECURITY_FORM_SCOPE_KEY = "scope";private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;private String tokenParameter = SPRING_SECURITY_FORM_TOKEN_KEY;private String scopeParameter = SPRING_SECURITY_FORM_SCOPE_KEY;private boolean postOnly = true;private RedisUtil redisUtil;private OtherUsersService otherUsersService;private OtherSysUserDao otherSysUserDao;private SysUsersDao sysUsersDao;public SysUsersDao getSysUsersDao() {return sysUsersDao;}public void setSysUsersDao(SysUsersDao sysUsersDao) {this.sysUsersDao = sysUsersDao;}public OtherSysUserDao getOtherSysUserDao() {return otherSysUserDao;}public void setOtherSysUserDao(OtherSysUserDao otherSysUserDao) {this.otherSysUserDao = otherSysUserDao;}public OtherUsersService getOtherUsersService() {return otherUsersService;}public void setOtherUsersService(OtherUsersService otherUsersService) {this.otherUsersService = otherUsersService;}public RedisUtil getRedisUtil() {return redisUtil;}public void setRedisUtil(RedisUtil redisUtil) {this.redisUtil = redisUtil;}// ~ Constructors// ===================================================================================================public UserBindFilter() {super(new AntPathRequestMatcher("/otherlogin", "POST"));}// ~ Methods// ========================================================================================================@Overridepublic Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {// 校验请求方式if (postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}// 获取请求的入参String username = obtainUsername(request);String password = obtainPassword(request);String token = obtainToken(request);String sys = obtainScope(request);// 参数校验部分if (username == null) {username = "";}if (password == null) {password = "";}if (token == null) {throw new AuthenticationServiceException("token 参数缺失(The token parameter is missing)");}// 从redis 缓存中获取第三方用户信息AuthUser authUser = (AuthUser) this.getRedisUtil().get(Oauth2LoginRedisKeysConstant.OTHER_SYS_USER_INFO + token);if (authUser == null) {throw new AuthenticationServiceException("绑定超时,请重新登陆绑定(Binding timed out, please log in again to bind)");}SysUsers sysUser = this.getSysUsersDao().findByUsername(username);if (sysUser == null) {throw new AuthenticationServiceException("输入用户名不存在(Enter username does not exist)");}// 将用户信息构造为 OtherSysUser 对象String uuid = authUser.getUuid();OtherSysUser otherSysUser = new OtherSysUser();otherSysUser.setScope(sys);otherSysUser.setUuid(uuid);otherSysUser.setCreateTime(new Date());otherSysUser.setUserId(sysUser.getId());otherSysUser.setUsername(sysUser.getUsername());username = username.trim();// 构造tokenBindAuthenticationToken bindAuthenticationToken = new BindAuthenticationToken(username, password,otherSysUser);// 设置额外数据setDetails(request, bindAuthenticationToken);// 移除缓存的用户数据,以防用户下次继续使用this.getRedisUtil().del(Oauth2LoginRedisKeysConstant.OTHER_SYS_USER_INFO + token);// 执行认证return this.getAuthenticationManager().authenticate(bindAuthenticationToken);}// 获取密码@Nullableprotected String obtainPassword(HttpServletRequest request) {return request.getParameter(passwordParameter);}// 获取用户名@Nullableprotected String obtainUsername(HttpServletRequest request) {return request.getParameter(usernameParameter);}// 获取token@Nullableprotected String obtainToken(HttpServletRequest request) {return request.getParameter(tokenParameter);}// 获取scope@Nullableprotected String obtainScope(HttpServletRequest request) {return request.getParameter(scopeParameter);}protected void setDetails(HttpServletRequest request,BindAuthenticationToken authRequest) {authRequest.setDetails(authenticationDetailsSource.buildDetails(request));}public void setUsernameParameter(String usernameParameter) {Assert.hasText(usernameParameter, "Username parameter must not be empty or null");this.usernameParameter = usernameParameter;}public void setPasswordParameter(String passwordParameter) {Assert.hasText(passwordParameter, "Password parameter must not be empty or null");this.passwordParameter = passwordParameter;}public void setPostOnly(boolean postOnly) {this.postOnly = postOnly;}public final String getUsernameParameter() {return usernameParameter;}public final String getPasswordParameter() {return passwordParameter;}
}
BindAuthenticationToken
代码参考:BindAuthenticationToken.java
/**
* <h1>绑定登陆认证令牌</h1>
* <p>
* 参考: UsernamePasswordAuthenticationToken 实现
* <p>
* 将登录信息构造一个成认证令牌,传递数据
*
* @author gengzi
* @date 2020年12月5日10:49:24
*/
public class BindAuthenticationToken extends AbstractAuthenticationToken {private final Object principal;private Object credentials;// 绑定信息private Object bindInfo;public Object getBindInfo() {return bindInfo;}public void setBindInfo(Object bindInfo) {this.bindInfo = bindInfo;}public BindAuthenticationToken(Object principal, Object credentials) {super(null);this.principal = principal;this.credentials = credentials;setAuthenticated(false);}public BindAuthenticationToken(Object principal, Object credentials, Object bindInfo) {super(null);this.principal = principal;this.credentials = credentials;this.bindInfo = bindInfo;setAuthenticated(false);}public BindAuthenticationToken(Object authUser) {super(null);this.principal = authUser;setAuthenticated(false);}/*** 使用提供的权限数组创建令牌。** @param authorities 权限集合* @param principal 用户名* @param credentials 密码*/public BindAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object principal, Object credentials) {super(authorities);this.principal = principal;this.credentials = credentials;super.setAuthenticated(true); // must use super, as we override}// 密码@Overridepublic Object getCredentials() {return this.credentials;}// 用户名 或者 主体@Overridepublic Object getPrincipal() {return this.principal;}}
BindLoginProvider
代码参考BindLoginProvider.java
关注注释部分的代码即可。其他代码都是 AbstractUserDetailsAuthenticationProvider 的
/**
* <h1>绑定登陆的提供者</h1>
* <p>
* 参考:AbstractUserDetailsAuthenticationProvider 实现
* <p>
* 作用:
* 1,校验用户信息,用户名和密码是否正确
* 2,用户信息正确,再将第三方用户信息存入数据库
*
* @author gengzi
* @date 2020年12月5日10:55:59
*/
@Slf4j
public class BindLoginProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {protected final Log logger = LogFactory.getLog(getClass());// ~ Instance fields// ================================================================================================protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();private UserCache userCache = new NullUserCache();private boolean forcePrincipalAsString = false;protected boolean hideUserNotFoundExceptions = true;private UserDetailsChecker preAuthenticationChecks = new BindLoginProvider.DefaultPreAuthenticationChecks();private UserDetailsChecker postAuthenticationChecks = new BindLoginProvider.DefaultPostAuthenticationChecks();private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();// 用户详情服务private UserDetailsServiceImpl userDetailsService;// 保存第三方用户信息的daoprivate OtherSysUserDao otherSysUserDao;// 密码encoderprivate PasswordEncoder passwordEncoder;public PasswordEncoder getPasswordEncoder() {return passwordEncoder;}public void setPasswordEncoder(PasswordEncoder passwordEncoder) {this.passwordEncoder = passwordEncoder;}public OtherSysUserDao getOtherSysUserDao() {return otherSysUserDao;}public void setOtherSysUserDao(OtherSysUserDao otherSysUserDao) {this.otherSysUserDao = otherSysUserDao;}public UserDetailsServiceImpl getUserDetailsService() {return userDetailsService;}public void setUserDetailsService(UserDetailsServiceImpl userDetailsService) {this.userDetailsService = userDetailsService;}// ~ Methods// ========================================================================================================// protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
// BindAuthenticationToken authentication)
// throws AuthenticationException;@Overridepublic final void afterPropertiesSet() throws Exception {Assert.notNull(this.userCache, "A user cache must be set");Assert.notNull(this.messages, "A message source must be set");doAfterPropertiesSet();}@Overridepublic Authentication authenticate(Authentication authentication)throws AuthenticationException {Assert.isInstanceOf(BindAuthenticationToken.class, authentication,() -> messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports","Only BindAuthenticationToken is supported"));// Determine usernameString username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED": authentication.getName();boolean cacheWasUsed = true;UserDetails user = this.userCache.getUserFromCache(username);if (user == null) {cacheWasUsed = false;try {user = retrieveUser(username,(BindAuthenticationToken) authentication);} catch (UsernameNotFoundException notFound) {logger.debug("User '" + username + "' not found");if (hideUserNotFoundExceptions) {throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));} else {throw notFound;}}Assert.notNull(user,"retrieveUser returned null - a violation of the interface contract");}try {preAuthenticationChecks.check(user);additionalAuthenticationChecks(user,(BindAuthenticationToken) authentication);} catch (AuthenticationException exception) {if (cacheWasUsed) {cacheWasUsed = false;user = retrieveUser(username,(BindAuthenticationToken) authentication);preAuthenticationChecks.check(user);additionalAuthenticationChecks(user,(BindAuthenticationToken) authentication);} else {throw exception;}}postAuthenticationChecks.check(user);if (!cacheWasUsed) {this.userCache.putUserInCache(user);}Object principalToReturn = user;if (forcePrincipalAsString) {principalToReturn = user.getUsername();}// 增加保存第三方用户信息BindAuthenticationToken bind = (BindAuthenticationToken) authentication;OtherSysUser bindInfo = (OtherSysUser) bind.getBindInfo();this.getOtherSysUserDao().save(bindInfo);return createSuccessAuthentication(principalToReturn, authentication, user);}protected Authentication createSuccessAuthentication(Object principal,Authentication authentication, UserDetails user) {BindAuthenticationToken result = new BindAuthenticationToken(principal, authentication.getCredentials(),authoritiesMapper.mapAuthorities(user.getAuthorities()));result.setDetails(authentication.getDetails());return result;}protected void doAfterPropertiesSet() throws Exception {}public UserCache getUserCache() {return userCache;}public boolean isForcePrincipalAsString() {return forcePrincipalAsString;}public boolean isHideUserNotFoundExceptions() {return hideUserNotFoundExceptions;}protected UserDetails retrieveUser(String username, BindAuthenticationToken authentication) throws AuthenticationException {try {UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);if (loadedUser == null) {throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");}return loadedUser;} catch (InternalAuthenticationServiceException ex) {throw ex;} catch (Exception ex) {throw new InternalAuthenticationServiceException(ex.getMessage(), ex);}}public void setForcePrincipalAsString(boolean forcePrincipalAsString) {this.forcePrincipalAsString = forcePrincipalAsString;}public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;}@Overridepublic void setMessageSource(MessageSource messageSource) {this.messages = new MessageSourceAccessor(messageSource);}public void setUserCache(UserCache userCache) {this.userCache = userCache;}@Overridepublic boolean supports(Class<?> authentication) {return (BindAuthenticationToken.class.isAssignableFrom(authentication));}protected UserDetailsChecker getPreAuthenticationChecks() {return preAuthenticationChecks;}public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) {this.preAuthenticationChecks = preAuthenticationChecks;}protected UserDetailsChecker getPostAuthenticationChecks() {return postAuthenticationChecks;}public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {this.postAuthenticationChecks = postAuthenticationChecks;}public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {this.authoritiesMapper = authoritiesMapper;}private class DefaultPreAuthenticationChecks implements UserDetailsChecker {@Overridepublic void check(UserDetails user) {if (!user.isAccountNonLocked()) {logger.debug("User account is locked");throw new LockedException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked","User account is locked"));}if (!user.isEnabled()) {logger.debug("User account is disabled");throw new DisabledException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled","User is disabled"));}if (!user.isAccountNonExpired()) {logger.debug("User account is expired");throw new AccountExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired","User account has expired"));}}}private class DefaultPostAuthenticationChecks implements UserDetailsChecker {@Overridepublic void check(UserDetails user) {if (!user.isCredentialsNonExpired()) {logger.debug("User account credentials have expired");throw new CredentialsExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired","User credentials have expired"));}}}/*** 密码验证部分* <p>* 根据密码的加密策略,对比用户输入密码和系统中存储的密码是否一致* 如果不一致,将抛出 BadCredentialsException** @param userDetails* @param authentication* @throws AuthenticationException*/protected void additionalAuthenticationChecks(UserDetails userDetails,BindAuthenticationToken authentication)throws AuthenticationException {if (authentication.getCredentials() == null) {logger.debug("Authentication failed: no credentials provided");throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}String presentedPassword = authentication.getCredentials().toString();if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {logger.debug("Authentication failed: password does not match stored value");throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}}
}
UserDetailsServiceImpl
这也是最开始实现对接 数据库的用户详情服务。
/**
* <h1>用户详细服务impl</h1>
* <p>
* 用于返回根据用户名返回用户详细信息,以便于供 security 使用
*
* @author gengzi
* @date 2020年11月3日15:24:43
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UsersService usersService;@Autowiredprivate PasswordEncoder passwordEncoder;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {ReturnData result = usersService.loadUserByUsername(username);if (RspCodeEnum.NOTOKEN.getCode() == result.getStatus()) {throw new RrException(RspCodeEnum.ACCOUNT_NOT_EXIST.getDesc());}UserDetail userDetail = (UserDetail) result.getInfo();if (userDetail == null) {throw new RrException(RspCodeEnum.ACCOUNT_NOT_EXIST.getDesc());}//账号不可用if (userDetail.getStatus() == UserStatusEnum.DISABLE.getValue()) {userDetail.setEnabled(false);}return userDetail;}
}
核心配置
上述的代码写好后,都要加入Spring Security 的核心配置中。具体代码如下:
主要是把需要的类,都set进去。
代码参考:OtherSysOauth2LoginAuthenticationSecurityConfig
/**
* <h1>第三方登陆的认证配置</h1>
* <p>
* 主要配置:
* 1,加载第三方登陆的认证过滤器 ,主要提供路径拦截和基础判断
* 设置认证管理器
* 设置用户服务类
* <p>
* 2,加载登陆提供者
* 设置用户详细服务实现
* 设置登陆成功事件
* 设置登陆失败事件
* <p>
* 将提供提供者和过滤器加入 HttpSecurity 中,并在 UsernamePasswordAuthenticationFilter 前执行逻辑判断
* <p>
* 用户过滤器配置
*
* @author gengzi
* @date 2020年11月24日10:52:39
*/
@Configuration
public class OtherSysOauth2LoginAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {@Autowiredprivate OtherSysOauth2LoginUserDetailsServiceImpl extendUserDetailsService;// ------------ 用户认证失败处理程序 -------------@Autowiredprivate UserAuthenticationFailureHandler userAuthenticationFailureHandler;// ------------ 用户认证成功处理程序 -------------@Autowiredprivate UserAuthenticationSuccessHandler userAuthenticationSuccessHandler;@Autowiredprivate UsersService usersService;@Autowiredprivate RedisUtil redisUtil;@Autowiredprivate OtherSysUserDao otherSysUserDao;@Autowiredprivate SysUsersDao sysUsersDao;@Autowiredprivate OtherUsersService otherUsersService;@Autowiredprivate AuthRequestService authRequestService;@Autowiredprivate UserDetailsServiceImpl userDetailsService;@Autowiredprivate PasswordEncoder passwordEncoder;@Overridepublic void configure(HttpSecurity builder) throws Exception {// 第三方登陆过滤器OtherSysOauth2LoginFilter filter = new OtherSysOauth2LoginFilter();filter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));filter.setUsersService(usersService);filter.setRedisUtil(redisUtil);filter.setOtherUsersService(otherUsersService);filter.setAuthenticationSuccessHandler(userAuthenticationSuccessHandler);filter.setAuthenticationFailureHandler(userAuthenticationFailureHandler);filter.setAuthRequestService(authRequestService);// 认证OtherSysOauth2LoginProvider provider = new OtherSysOauth2LoginProvider();provider.setUserDetailsService(extendUserDetailsService);builder.authenticationProvider(provider).addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);// 绑定用户FilterUserBindFilter userBindFilter = new UserBindFilter();userBindFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));userBindFilter.setRedisUtil(redisUtil);userBindFilter.setOtherSysUserDao(otherSysUserDao);userBindFilter.setSysUsersDao(sysUsersDao);userBindFilter.setAuthenticationFailureHandler(userAuthenticationFailureHandler);userBindFilter.setAuthenticationSuccessHandler(userAuthenticationSuccessHandler);// 认证BindLoginProvider bindLoginProvider = new BindLoginProvider();bindLoginProvider.setOtherSysUserDao(otherSysUserDao);bindLoginProvider.setUserDetailsService(userDetailsService);bindLoginProvider.setPasswordEncoder(passwordEncoder);builder.authenticationProvider(bindLoginProvider).addFilterBefore(userBindFilter, UsernamePasswordAuthenticationFilter.class);}
}
并加入SpringSecurity 核心配置:
代码参考:WebSecurityConfig.java
只粘核心配置部分, 将OtherSysOauth2LoginAuthenticationSecurityConfig 加入SpringSecurity的过滤器链中
@Configuration
// 启用web 认证
@EnableWebSecurity
// 启用全局的方法安全
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {// ------------ 第三方登陆的认证安全配置 -------------@Autowiredprivate OtherSysOauth2LoginAuthenticationSecurityConfig otherSysOauth2LoginAuthenticationSecurityConfig;protected void configure(HttpSecurity http) throws Exception {// 自定义表单认证方式// ----------------- 关键配置 --------------------------http.apply(otherSysOauth2LoginAuthenticationSecurityConfig).and().// ----------------- 关键配置 --------------------------addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class).authorizeRequests()// 放行swagger-ui相关的路径.antMatchers(IgnoringUrlConstant.IGNORING_URLS).permitAll().antMatchers(IgnoringUrlConstant.IGNORING_STATIC_URLS).permitAll().antMatchers(IgnoringUrlConstant.OAUTH2_URLS).permitAll().antMatchers("/getLoginCode").permitAll().antMatchers("/codeBuildNew/**").permitAll() // 都可以访问.anyRequest().authenticated().and().formLogin().loginPage("/login.html").loginProcessingUrl("/login").permitAll().and().csrf().disable()// csrf 防止跨站脚本攻击.formLogin().successHandler(userAuthenticationSuccessHandler).failureHandler(userAuthenticationFailureHandler).and().sessionManagement((sessionManagement) -> sessionManagement.maximumSessions(100).sessionRegistry(sessionRegistry()));}
}
综上所述
第三方登陆的实现依靠于JustAuth框架还是很简单的,对接SpringSecurity需要了解认证流程执行的类。上述的实现,只是作为第三方登陆演示的一种,需要优化的地方有很多,大家对于不同的需求,再修改适配即可,理解流程才是关键。
上述代码虽然能实现功能,但是编写的类还是太多,有没有更加简化的配置呢?当然有,SpringSecurity OAuth2 框架提供了第三方登陆更加优雅的解决方式,也是一种不错的选择。
听说点赞关注的人,身体健康,一夜暴富,升职加薪迎娶白富美!!!
点我领取每日福利
微信公众号:耿子blog
GitHub地址:gengzi
SpringSecurity实战(八)-通用第三方登陆-自定义认证配置实现相关推荐
- SpringSecurity实战:基于mysql自定义SpringSecurity权限认证规则
上文<Spring Security 源码分析:Spring Security 授权过程>已经详细分析了Spring Security 授权过程,接下来通过上文的授权过程我们可以自定义授权 ...
- springSecurity的学习笔记--使用spring-Security完成表单登陆,手机验证码登陆,第三方登陆
环境搭建好后,之后的练习进入了一个十分痛苦的阶段!! 但是与此同时,收获也是比较可观的. 老师通过详细的视频讲解,完成了表单登陆,包括账号密码和验证码登陆,手机验证码登陆,第三方登陆. 每一个部分都进 ...
- SpringSecurity使用自定义认证页面
SpringSecurity使用自定义认证页面 在SpringSecurity主配置文件中指定认证页面配置信息 修改认证页面的请求地址 再次启动项目后就可以看到自定义的酷炫认证页面了!
- grpc、https、oauth2等认证专栏实战17:grpc-go自定义认证之base64验证介绍
已发表的技术专栏(订阅即可观看所有专栏) 0 grpc-go.protobuf.multus-cni 技术专栏 总入口 1 grpc-go 源码剖析与实战 文章目录 2 Protobuf介绍与 ...
- python资格认证_Python怎么实现在后端的自定义认证并且实现多条件登陆
JWT扩展的登录视图,在收到用户名与密码时,也是调用Django的认证系统Auth模型中提供的**authenticate()**来检查用户名与密码是否正确. 我们可以通过修改Django认证系统的认 ...
- SpringSecurity自定义认证成功处理器
自定义认证成功处理器 代码实现 1.实现AuthenticationSuccessHandler接口 第一步:实现 AuthenticationSuccessHandler @Component(&q ...
- 利用FaceBook实现第三方登陆(自定义登陆按钮,非官方按钮)并获取用户数据
最近公司要写集成登陆SDK,具体集成那些我就不说了,其中就包含需要使用facebook登陆自己的app,于是我苦心研究facebook,写完后发现各种问题,对于问题我当然去查阅官方文档看怎么解决,结果 ...
- 第六篇 :微信公众平台开发实战Java版之如何自定义微信公众号菜单
我们来了解一下 自定义菜单创建接口: http请求方式:POST(请使用https协议) https://api.weixin.qq.com/cgi-bin/menu/create?access_to ...
- Spring Security 实战干货:玩转自定义登录
点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 1. 前言 前面的关于 Spring Security ...
- 十年阿里巴巴资深架构师整理分享的SpringSecurity实战文档
前言 SpringSecurity是一个强大且高度可定制的安全框架,致力于为Java应用提供身份认证和授权. Spring Security 的前身是 Acegi Security,在被收纳为Spri ...
最新文章
- AI:2020年6月23日北京智源大会演讲分享之智能信息检索与挖掘专题论坛——09:55-10:40刘欢教授《Challenges in Combating Disinformation》
- 阿里云天池平台官方出品!从0到1层层拆解天池大赛赛题 | 文末送书
- 算法设计原则验证实验报告_算法设计与分析实验报告 统计数字问题
- 自己动手之使用反射和泛型,动态读取XML创建类实例并赋值
- 动态内存分配到底为谁分配内存空间【浅谈动态内存的一个实例】
- 目前市场上有没有年化收益在7%以上,而且保本保息的理财?
- 五桌面工具来创建优秀的Windows环境
- linux密码带星号,Linux下实现输入密码以星号显示
- [Web 前端] SuperAgent中文使用文档
- Django 系列博客(十一)
- 基于RV1126平台imx291分析 --- media部件注册 mipi csi
- STM32学习记录0003——STM32芯片解读
- Roberts算子,matlab代码实现
- 岩土工程英语词汇A-R
- 应届生面试这样准备,最能展现自己优势!
- vue 在线阅读PDF
- STM32CUBEMX 配置12脚3641BS以及串口显示RTC时间
- md5sum命令的使用
- 基于ILP的最优PMU放置优化研究(Matlab代码实现)
- C++面向对象程序设计陈维兴版第四章所有例题
热门文章
- phpstudy的php fpm,浅谈PHP-FPM参数
- Java面向对象知识点总结
- 哈利波特英文单词统计频率
- SVN报错The working copy needs to be upgraded
- Netfilter学习之NAT类型动态配置(二)NAT类型介绍及MASQUERADE用户层的实现
- 【STM32】关于DMA控制器的介绍和使用
- PMP-7. 项目经理及其影响力
- 进入bios看了,vt 已经开了,为什么打开模拟器还显示未开启?
- 我的世界 Unity3D MineCraft 用Unity3D制作类似MineCraft我的世界的游戏 正经梳理一下开发01
- UE4中文汉字字体制作