折(mo)腾(yu)了好几天,终于把双重身份认证实现了。(账号密码jwt+短信验证码)

看了很多视频,照葫芦画瓢敲了两三次,遇到各种各样的bug,比如循环依赖(通过@PostConstructor+setter解决)、框架报错等,翻了上百次csdn才逐渐摸清。总算对spring-security有了一个大概的认识,写一点学习心得,希望能帮到初学者,同时以备自己未来复习。


spring-security的核心无非就在于一个AuthenticationManager。起辅助作用的有自上而下有Filter类、Provider类、Token类。项目结构中,Filter类在Controller类之上,我放在aop包下。Provider类实现核心登录功能,我放在core包下。Token类则是pojo,我放在domain包下,供参考。

我们目前只用上AuthenticationManager的默认实现ProviderManager类。它的核心在于其有一个List<AuthenticationProvider> providers,当外部调用它进行身份认证时,他会尝试调用这里的每个provider进行认证,并将结果返回。在配置类中可以修改这个provider列表,传入自定义的provider。


jwt认证实现的逻辑大致为:用户通过所有人(准确的说是匿名用户)都可以访问的登录api提交账号密码,Service层将其封装为token对象并调用AuthenticationManager进行身份认证,若成功则将用户信息写redis缓存,并返回一个token(这里特指token字符串)给前端。之后,用户访问其他需要身份认证的api时,只需请求头里携带该token,即可被Filter顺利放行。

要设置身份认证,首先要把所有没登录的用户拦截,而保留登录api给未登录用户。这就是配置类public class WebSecurityConfig extends WebSecurityConfigurerAdapter。打上@EnableGlobalMethodSecurity(prePostEnabled = true)注解后即可在各个api前,使用@PreAuthorize注解控制访问权限,而不必编写大量链式调用配置代码。这个注解常用参数有”permitAll()”(所有)、”isAnonymous()”(匿名,即未认证),”isAuthenticated()”(已认证)、"hasAuthority('xxx')"(特定身份登录)等。

此外,配置类中还需重写@Overrider protected void configure(@NotNull HttpSecurity http)方法进行配置。具体来说,前后端分离开发需关闭csrf与session功能:http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);并将过滤器添加至框架自己的过滤器链中:http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);一般还需要编写@Bean public PasswordEncoder passwordEncoder(),重写@Bean @Override public AuthenticationManager authenticationManagerBean()方法(返回父类方法)才能正常运行。

WebSecurityConfig.java

/*** Spring-Security配置* @author Icy* @version 1.0.0* @since 1.0.0*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Autowiredprivate SmsAuthenticationProvider smsAuthenticationProvider;@Autowiredprivate UserDetailsService userDetailsService;@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Beanpublic AuthenticationProvider authenticationProvider() {DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();authenticationProvider.setUserDetailsService(userDetailsService);authenticationProvider.setPasswordEncoder(passwordEncoder());return authenticationProvider;}@Overrideprotected void configure(@NotNull HttpSecurity http) throws Exception {// 关闭csrf与session功能http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);// 将token校验过滤器与短信验证码过滤器添加到过滤器链中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}@Overrideprotected void configure(@NotNull AuthenticationManagerBuilder auth) {auth.authenticationProvider(authenticationProvider());auth.authenticationProvider(smsAuthenticationProvider);}
}

传统账号认证可不用自行编写provider,实现UserDetailsService供框架查询用户密码和权限信息即可。具体来说重写@Override public UserDetails loadUserByUsername(String s)方法,调用DAO层查找用户,并使用装饰器模式将普通UserPO包装为User实体实现UserDetails。需要做的一些工作:重写@Override public Collection<? extends GrantedAuthority> getAuthorities()方法将List<String>权限列表映射为框架提供的权限SimpleGrantedAuthority类:authorities = roleList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());并且设置@JSONField(serialize = false) private List<SimpleGrantedAuthority> authorities不进行JSON序列化,防止redis存取出现异常。此外,重写@Override public String getUsername()方法按需返回,比如手机号密码登录就返回原UserPO的phone。

AuthServiceImpl.java

    @Overridepublic UserDetails loadUserByUsername(@Validated @NotEmpty String s) {// 查询用户信息UserPO userPO = userRepository.selectByPhone(s);if (userPO == null) {throw new DbException(ServiceCode.USER_NOT_FOUND);}List<String> roleList = userRepository.listRoleById(userPO.getUserId());// 把数据封装成UserDetails返回return new User(userPO, roleList);}

UserPO.java

/*** t_user表* @author Icy* @version 1.0.0* @since 1.0.0*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_user")
public class UserPO implements Serializable {private static final long serialVersionUID = 3042112031982669233L;@TableId(type = IdType.AUTO)private Long userId;@TableFieldprivate Date createTime;@TableFieldprivate Date updateTime;@TableField("user_name")private String name;@TableField("user_phone")private String phone;@TableFieldprivate Long schoolId;@TableField("user_password")private String password;@TableField("user_qq")private String QQ;@TableField("user_email")private String email;
}

User.java

/*** 身份认证业务用户实体类* @author Icy* @version 1.0.0* @since 1.0.0*/
@Data
public class User implements UserDetails {private static final long serialVersionUID = -6634318190888926617L;private UserPO user;private List<String> roleList;// 只需序列化roleList即可,否则存入redis出现异常@JSONField(serialize = false)private List<SimpleGrantedAuthority> authorities;public User(UserPO user, List<String> roleList) {this.user = user;this.roleList = roleList;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {if (authorities != null) {return authorities;}// 将roleList中String类型权限信息封装成SimpleGrantedAuthority对象authorities = roleList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());return authorities;}@Overridepublic String getPassword() {return user.getPassword();}// 使用Spring-Security框架的“用户名”实际上是手机号@Overridepublic String getUsername() {return user.getPhone();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

用户登录后得到的token如何起作用?我们需要重写一个public class JwtAuthenticationTokenFilter extends OncePerRequestFilter并将其在配置类中加入过滤器链即可。重写protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, @NotNull FilterChain filterChain)方法,从请求头获取token,无token则进行拦截(即return返回):调用filterChain通知其他拦截器工作,最后返回。有token则使用JWTUtil进行解析(我使用了hutool提供的工具类),读redis缓存用户数据(若无则报错返回)(特别需要注意:Filter层位于Controller层之上,不可抛出异常,不会被Controller层中全局Advice捕获,需封装原生方法写入response)。成功读取后,创建Token并存入spring-security框架SecurityContextHolder即可。

JwtAuthenticationFilter.java

/*** JWT令牌过滤器* @apiNote 注意:该层不可抛异常,需调用WriteJSON方法写回* @author Icy* @version 1.0.0* @since 1.0.0*/
@Component
@AllArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {private final RedisUtil redisUtil;private final WriterUtil writerUtil;@Overrideprotected void doFilterInternal(@NotNull HttpServletRequest request,@NotNull HttpServletResponse response,@NotNull FilterChain filterChain) throws ServletException, IOException {String token = request.getHeader("token");if (StrUtil.isEmpty(token)) {// 无token,拦截filterChain.doFilter(request, response);return;}try {// 解析tokenString uid = (String)JWTUtil.parseToken(token).getPayload("uid");// alibaba的JsonObject,只能用Object接收,否则报强转异常Object json = redisUtil.get("login:" + uid);// 读redisif (json == null) {// 用户未登录。不能抛异常,Filter层在Advice之上。writerUtil.WriteJSON(response, new ResponseVO(ApiCode.AUTH_FAILURE));return;}User user = JSON.parseObject(json.toString(), User.class);UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authenticationToken);} catch (Exception e) {// token不合法。不能抛异常,Filter层在Advice之上。writerUtil.WriteJSON(response, new ResponseVO(ApiCode.AUTH_ILLEGAL_TOKEN));return;}// 处理完,放行filterChain.doFilter(request, response);}
}

至于登出,从SecurityContextHolder获取Token对象得到userid等信息,清除redis中对jwt的缓存即可。

AuthServiceImpl.java

    @Overridepublic void logout() {// 获取用户idUsernamePasswordAuthenticationToken authentication =(UsernamePasswordAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();User user = (User)authentication.getPrincipal();// 如果无值会被Filter拦截,无需ifn判断Long uid = user.getUser().getUserId();redisUtil.delete("login:" + uid);}

若需多种登录认证,首先需要提供传统账号密码登录的provider,在配置类中通过@Bean public AuthenticationProvider authenticationProvider()方法实现,具体来说,一般使用框架自带的DaoAuthenticationProvider,使用setter设置UserDetailsService(供框架查询用户密码和权限信息)、PasswordEncoder(一般为BCrypt)即可返回。还需重写@Override protected void configure(@NotNull AuthenticationManagerBuilder auth)方法,设置使用的一系列provider。

看我干嘛,完整配置类代码上面给了

下面以短信验证码举例。短信验证码发送使用腾讯云申请个人密钥,调用sdk发送即可,同时将验证码写入redis缓存。具体参考第三方文档,这里不再赘述。首先需要重写一个自定义Provider:public class SmsAuthenticationProvider implements AuthenticationProvider,具体重写其中的@Override public Authentication authenticate(Authentication authentication)方法,读token对象中信息与redis缓存信息是否吻合,否则抛出异常返回。成功后,创建新的认证对象,并交由service写入redis,返回token。

SmsAuthenticationProvider.java

/*** 短信验证码登录认证逻辑* @author Icy* @version 1.0.0* @since 1.0.0*/
@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {@Autowiredprivate GrantedAuthoritiesMapper authoritiesMapper;@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate AuthService authService;@Beanpublic GrantedAuthoritiesMapper grantedAuthoritiesMapper() {return new NullAuthoritiesMapper();}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {if (!(authentication instanceof SmsVerifyCodeAuthenticationToken)) {throw new ApiException();}SmsVerifyCodeAuthenticationToken smsVerifyCodeAuthenticationToken =(SmsVerifyCodeAuthenticationToken) authentication;String phone = smsVerifyCodeAuthenticationToken.getName();String code = smsVerifyCodeAuthenticationToken.getVerifyCode();UserDetails user = userDetailsService.loadUserByUsername(phone);if (user == null) {throw new ServiceException(ServiceCode.USER_NOT_FOUND);}// 验证码校验,失败抛异常返回,成功创建新token(认证后)authService.codeVerify(phone, code);return createSuccessAuthentication(authentication, user);}// 当前Provider仅支持校验短信验证码token@Overridepublic boolean supports(Class<?> authentication) {return SmsVerifyCodeAuthenticationToken.class.isAssignableFrom(authentication);}/*** 认证成功,将非认证token转为认证token* @param authentication 身份认证* @param user 用户信息* @return 认证token*/protected Authentication createSuccessAuthentication(@NotNull Authentication authentication,@NotNull UserDetails user) {Collection<? extends GrantedAuthority> authorities = authoritiesMapper.mapAuthorities(user.getAuthorities());SmsVerifyCodeAuthenticationToken authenticationToken =new SmsVerifyCodeAuthenticationToken(user, null, authorities);authenticationToken.setDetails(authentication.getDetails());return authenticationToken;}
}

还需重写自己的Token类:public class SmsVerifyCodeAuthenticationToken extends AbstractAuthenticationToken,基本就是对着框架的UsernamePasswordAuthenticationToken疯狂照抄(一模一样),把credentials字段改为存储验证码,外部调用getCredentials返回验证码即可同账号密码验证。

SmsVerifyCodeAuthenticationToken.java

完全就照着UsernamePasswordAuthenticationToken扒的,我都不好意思贴

/*** 短信验证码token* @author Icy* @version 1.0.0* @since 1.0.0*/
public class SmsVerifyCodeAuthenticationToken extends AbstractAuthenticationToken {private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;private final Object principal;private String verifyCode;/*** 未登录认证* @param principal 认证信息*/public SmsVerifyCodeAuthenticationToken(Object principal, String verifyCode) {super(null);this.principal = principal;this.verifyCode = verifyCode;setAuthenticated(false);}public SmsVerifyCodeAuthenticationToken(Object principal, String verifyCode, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;this.verifyCode = verifyCode;super.setAuthenticated(true);}@Overridepublic Object getCredentials() {return verifyCode;}@Overridepublic Object getPrincipal() {return principal;}public String getVerifyCode() {return verifyCode;}@Overridepublic void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");super.setAuthenticated(false);}@Overridepublic void eraseCredentials() {super.eraseCredentials();verifyCode = null;}
}

Spring Boot + Spring-Security实现前后端分离双重身份认证初学者指南(手机号密码JWT + 短信验证码)相关推荐

  1. 记一次Spring boot 和Vue的前后端分离的入门培训

    记一次Spring boot 和Vue的前后端分离的入门培训 由于公司之前是写C#的,现在要转 Java分布式 + vue,所以进行一次前后端的简单培训. 前端工具和环境: Node.js V10.1 ...

  2. 基于Spring boot + Mybatis +Netty 实现前后端分离的聊天App,部署到阿里云线上服务器...

    前后端分离Spring boot 项目部署 了解前后端分离项目 配置云服务器 java maven tomcat nginx mysql 部署后端项目 部署前端项目 部署Java环境 1.下载JDK软 ...

  3. 鸿鹄工程项目管理系统 Spring Cloud+Spring Boot+Mybatis+Vue+ElementUI+前后端分离构建工程项目管理系统

    鸿鹄工程项目管理系统 Spring Cloud+Spring Boot+Mybatis+Vue+ElementUI+前后端分离构建工程项目管理系统 1. 项目背景 一.随着公司的快速发展,企业人员和经 ...

  4. Springboot + Spring Security 实现前后端分离登录认证及权限控制

    Spring Security简介 Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展 ...

  5. Java Spring boot element ui activiti前后端分离,流程审批,权限管理框架

    基于react ant design pro typescript 技术框架已经重磅推出 预览地址 系统介绍 是什么? 使用springboot,activiti,mybatis,vue elemen ...

  6. Spring Boot + Vue.js 实现前后端分离(附源码)

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者: 梁小生0101 链接:juejin.im/post/5c6 ...

  7. Spring Boot + Vue + Shiro 实现前后端分离、权限控制

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 来源:http://sina.lt/gauR 本文总结自实习中对项 ...

  8. Docker中Spring boot+VueJS+MongoDB的前后端分离哲学摔跤

    一图胜千言 目标 想将VueJs,Spring boot,MongoDB全部都放到Docker中运行,并且做到VueJs和Spring boot在不同都Docker容器中. Docker带来的变化 开 ...

  9. Spring Boot + Vue + Shiro 实现前后端分离、权限控制 (附源码)

    点击上方[全栈开发者社区]→右上角[...]→[设为星标⭐] 原项目采用Springboot+freemarker模版,开发过程中觉得前端逻辑写的实在恶心,后端Controller层还必须返回Free ...

最新文章

  1. Linux别名的创建删除【alias】和【unalias】
  2. python操作系统-PYTHON-操作系统基础
  3. UNITY_MATRIX_IT_MV[Matrix]
  4. Amazon Go亮相:消费者无需结账即可完成店面购物
  5. ClewareControl 2.4 发布,传感器控制程序
  6. 几大搜索引擎的网站登录入口
  7. 3.1 scrapy框架 -- 安装与基本使用
  8. 第2章 程序的灵魂-算法
  9. win10 远程桌面卡顿_win10系统使用远程桌面卡顿的解决教程
  10. ylbtech-DBD-WeShop(微店)
  11. gocv-go语言调用opencv入门
  12. CompareTo和compare的区别
  13. linux 启动 参数,Linux启动参数
  14. POJ 1265 Area(Pick定理)
  15. 安卓小人html制作,告白小人在线制作
  16. QT学习——Qt工具介绍,Qt助手使用,第一个Qt程序,Qt字符串和字符编码
  17. Visual Studio Code 多开
  18. 对话李开复:怎样做最好的自己
  19. 富士胶片出展世界5G大会展示8K镜头等尖端产品
  20. Windows下安装配置PHP

热门文章

  1. PHP事务数据库写法,PHP 操作 MySQL 执行数据库事务
  2. 基于51单片机的火灾预警系统设计
  3. 字节输入输出流,字符输入输出流
  4. 作为一名IT工程师该如何提前预防职业病
  5. IDEA查看源码时,CRTL+点击,方法或者变量不能提示出来,以便进入
  6. 转载:信号与系统的简介
  7. 2.SPSS入门基础——数据录入数据管理编程规则图表可视化
  8. 关于css--的封装、继承、多态
  9. java sign函数_C / C ++中是否有标准的符号函数(signum,sgn)?
  10. java将域名解析为IP地址,获取网卡的配置信息