[权限管理系统]spring boot +spring security短信认证+redis整合

  现在主流的登录方式主要有 3 种:账号密码登录、短信验证码登录和第三方授权登录,前面一节Spring security(三)---认证过程已分析了spring security账号密码方式登陆,现在我们来分析一下spring security短信方式认证登陆。

  Spring security 短信方式、IP验证等类似模式登录方式验证,可以根据账号密码方式登录步骤仿写出来,其主要以以下步骤进行展开:

  1. 自定义Filter:

  2. 自定义Authentication

  3. 自定义AuthenticationProvider

  4. 自定义UserDetailsService

  5. SecurityConfig配置

1. 自定义filter:

  自定义filter可以根据UsernamePasswordAuthenticationFilter过滤器进行仿写,其实质即实现AbstractAuthenticationProcessingFilter抽象类,主要流程分为:

  1. 构建构造器,并在构造器中进行配置请求路径以及请求方式的过滤

  2. 自定义attemptAuthentication()认证步骤

  3. 在2步骤中认证过程中需要AuthenticationProvider进行最终的认证,在认证filter都需要将AuthenticationProvider设置进filter中,而管理AuthenticationProvider的是AuthenticationManager,因此我们创建过滤器filter的时候需要设置AuthenticationManager,这步具体详情在5.1 SecurityConfig配置步骤

在第2步中attemptAuthentication()认证方法主要进行以下步骤:   

  1).post请求认证;   

  2).request请求获取手机号码和验证码;   

  3).用自定义的Authentication对象封装手机号码和验证码;   

  4).使用AuthenticationManager.authenticate()方法进行验证。

自定义filter实现代码:

public class SmsAuthenticationfilter extends AbstractAuthenticationProcessingFilter {
private boolean postOnly = true;

public SmsAuthenticationfilter() {
super(new AntPathRequestMatcher(SecurityConstants.APP_MOBILE_LOGIN_URL, "POST"));
}
@Override
public 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());
}
Assert.hasText(SecurityConstants.MOBILE_NUMBER_PARAMETER, "mobile parameter must not be empty or null"); String mobile = request.getParameter(SecurityConstants.MOBILE_NUMBER_PARAMETER);
String smsCode = request.ge+tParameter(SecurityConstants.MOBILE_VERIFY_CODE_PARAMETER);
if (mobile == null) {
mobile="";
}
if(smsCode == null){
smsCode="";
}
mobile = mobile.trim();
smsCode = smsCode.trim();
SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile,smsCode);

// Allow subclasses to set the "details" property
setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest);
}protected void setDetails(HttpServletRequest request,
SmsAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}

public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}

}

2. Authentication:

  在filter以及后面的认证都需要使用到自定义的Authentication对象,自定义Authentication对象可以根据UsernamePasswordAuthenticationToken进行仿写,实现AbstractAuthenticationToken抽象类。

自定义SmsAuthenticationToken:

public class SmsAuthenticationToken extends AbstractAuthenticationToken {

private final Object principal;
private Object credentials;

public SmsAuthenticationToken(Object principal,Object credentials ) {
super(null);
this.principal = principal;
this.credentials=credentials;
setAuthenticated(false);
}

public SmsAuthenticationToken(Object principal, Object credentials,Collection<? extends GrantedAuthority> authorities) {
super(null);
this.principal = principal;
this.credentials=credentials;
setAuthenticated(true);
}

@Override
public Object getCredentials() {
return this.credentials=credentials;
}

@Override
public Object getPrincipal() {
return this.principal;
}

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");
}

super.setAuthenticated(false);
}

@Override
public void eraseCredentials() {
super.eraseCredentials();

}
}

3.AuthenticationProvider

  AuthenticationProvider最终认证策略入口,短信方式验证需自定义AuthenticationProvider。可以根据AbstractUserDetailsAuthenticationProvider进行仿写,实现AuthenticationProvider以及MessageSourceAware接口。认证逻辑可以定义实现。

自定义AuthenticationProvider:

public class SmsAuthenticationProvide implements AuthenticationProvider, MessageSourceAware {
private UserDetailsService userDetailsService;
private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

@Override
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}

@Override
public Authentication authenticate(Authentication authentication) {
Assert.isInstanceOf(SmsAuthenticationToken.class, authentication,
messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
//将验证信息保存在SecurityContext以供UserDetailsService进行验证
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(authenticationToken);
String mobile = (String) authenticationToken.getPrincipal();
if (mobile == null) {
throw new InternalAuthenticationServiceException("can't obtain user info ");
}
mobile = mobile.trim();
//进行验证以及获取用户信息
UserDetails user = userDetailsService.loadUserByUsername(mobile);
if (user == null) {
throw new InternalAuthenticationServiceException("can't obtain user info ");
}
SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(user, user.getAuthorities());
return smsAuthenticationToken;
}

@Override
public boolean supports(Class<?> authentication) {
return (SmsAuthenticationToken.class.isAssignableFrom(authentication));
}

public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}

public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
}

4. UserDetailsService

  在AuthenticationProvider最终认证策略入口,认证方式实现逻辑是在UserDetailsService。可以根据自己项目自定义认证逻辑。

自定义UserDetailsService:

public class SmsUserDetailsService implements UserDetailsService {
@Autowired
private RedisUtil redisUtil;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//从SecurityContext获取认证所需的信息(手机号码、验证码)
SecurityContext context = SecurityContextHolder.getContext();
SmsAuthenticationToken authentication = (SmsAuthenticationToken) context.getAuthentication();
if(!additionalAuthenticationChecks(username,authentication)){
return null;
}
//获取用户手机号码对应用户的信息,包括权限等
return new User("admin", "123456", Arrays.asList(new SimpleGrantedAuthority("admin")));
}

public boolean additionalAuthenticationChecks(String mobile, SmsAuthenticationToken smsAuthenticationToken) {
//获取redis中手机键值对应的value验证码
String smsCode = redisUtil.get(mobile).toString();
//获取用户提交的验证码
String credentials = (String) smsAuthenticationToken.getCredentials();
if(StringUtils.isEmpty(credentials)){
return false;
}
if (credentials.equalsIgnoreCase(smsCode)) {
return true;
}
return false;
}
}

5.SecurityConfig

5.1 自定义Sms短信验证组件配置SecurityConfig

  将自定义组件配置SecurityConfig中,可以根据AbstractAuthenticationFilterConfigurer(子类FormLoginConfigurer)进行仿写SmsAuthenticationSecurityConfig,主要进行以下配置:

  1. 将默认AuthenticationManager(也可以定义的)设置到自定义的filter过滤器中

  2. 将自定义的UserDetailsService设置到自定义的AuthenticationProvide中以供使用

  3. 将过滤器添加到过滤链路中,实施过滤操作。(一般以加在UsernamePasswordAuthenticationFilter前)

配置SmsAuthenticationSecurityConfig:

 @Component
public class SmsAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private UserDetailsService userDetailsService;

@Override
public void configure(HttpSecurity http) throws Exception {
//创建并配置好自定义SmsAuthenticationfilter,
SmsAuthenticationfilter smsAuthenticationfilter = new SmsAuthenticationfilter();
smsAuthenticationfilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsAuthenticationfilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler());
smsAuthenticationfilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler());
//创建并配置好自定义SmsAuthenticationProvide
SmsAuthenticationProvide smsAuthenticationProvide=new SmsAuthenticationProvide();
smsAuthenticationProvide.setUserDetailsService(userDetailsService);
http.authenticationProvider(smsAuthenticationProvide);
//将过滤器添加到过滤链路中
http.addFilterAfter(smsAuthenticationfilter, UsernamePasswordAuthenticationFilter.class);
}

@Bean
public CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler() {
return new CustomAuthenticationSuccessHandler();
} @Bean
public CustomAuthenticationFailureHandler customAuthenticationFailureHandler() {
return new CustomAuthenticationFailureHandler();
}
}

5.2 SecurityConfig主配置

  SecurityConfig主配置可以参照第二节Spring Security(二)--WebSecurityConfigurer配置以及filter顺序进行配置。

SecurityConfig主配置:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SmsAuthenticationSecurityConfig smsAuthenticationSecurityConfig;
@Autowired
private CustomAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private CustomAuthenticationFailureHandler authenticationFailureHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable().and()
.formLogin()
.loginPage(SecurityConstants.APP_FORM_LOGIN_PAGE)
//配置form登陆的自定义URL
.loginProcessingUrl(SecurityConstants.APP_FORM_LOGIN_URL)
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.and()
//配置smsAuthenticationSecurityConfig
.apply(smsAuthenticationSecurityConfig)
.and()
//运行通过URL
.authorizeRequests()
.antMatchers(SecurityConstants.APP_MOBILE_VERIFY_CODE_URL,
SecurityConstants.APP_USER_REGISTER_URL)
.permitAll()
.and()
.csrf().disable();
}
@Bean
public ObjectMapper objectMapper(){
return new ObjectMapper();
}
}

6.其他

6.1 redis

RedisUtil工具类:

@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}

/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}

redisConfig配置类:

@Configuration
public class RedisConfig {
@Autowired
private RedisProperties properties;
@Bean
@SuppressWarnings("all")
@ConditionalOnClass(RedisConnectionFactory.class)
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
@Qualifier("redisConnectionFactory")
public RedisConnectionFactory redisConnectionFactory(){
RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
redisConfig.setHostName(properties.getHost());
redisConfig.setPort(properties.getPort());
redisConfig.setPassword(RedisPassword.of(properties.getPassword()));
redisConfig.setDatabase(properties.getDatabase());
//redis连接池数据设置
JedisClientConfiguration.JedisClientConfigurationBuilder builder = JedisClientConfiguration.builder();
if (this.properties.getTimeout() != null) {
Duration timeout = this.properties.getTimeout();
builder.readTimeout(timeout).connectTimeout(timeout);
}
RedisProperties.Pool pool = this.properties.getJedis().getPool();
if (pool != null) {
builder.usePooling().poolConfig(this.jedisPoolConfig(pool));
}
JedisClientConfiguration jedisClientConfiguration = builder.build();
//根据两个配置类生成JedisConnectionFactory
return new JedisConnectionFactory(redisConfig,jedisClientConfiguration);

}
private JedisPoolConfig jedisPoolConfig(RedisProperties.Pool pool) {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(pool.getMaxActive());
config.setMaxIdle(pool.getMaxIdle());
config.setMinIdle(pool.getMinIdle());
if (pool.getMaxWait() != null) {
config.setMaxWaitMillis(pool.getMaxWait().toMillis());
}
return config;
}
}

7.总结

  可以根据短信验证登陆模式去实现类似的验证方式,可以结合本节的例子进行跟项目结合起来,减少开发时间。后续还有第三方登陆方式分析以案例。最后错误请评论指出!

往期文章:

各位看官还可以吗?喜欢的话,动动手指点个赞

[权限管理系统(四)]-spring boot +spring security短信认证+redis整合的更多相关文章

  1. 新书上线:《Spring Boot+Spring Cloud+Vue+Element项目实战:手把手教你开发权限管理系统》,欢迎大家买回去垫椅子垫桌脚

    新书上线 大家好,笔者的新书<Spring Boot+Spring Cloud+Vue+Element项目实战:手把手教你开发权限管理系统>已上线,此书内容充实.材质优良,乃家中必备垫桌脚 ...

  2. 基于Spring Boot+Spring Security+JWT+Vue前后端分离的开源项目

    一.前言 最近整合Spring Boot+Spring Security+JWT+Vue 完成了一套前后端分离的基础项目,这里把它开源出来分享给有需要的小伙伴们 功能很简单,单点登录,前后端动态权限配 ...

  3. 255.Spring Boot+Spring Security:使用md5加密

    说明 (1)JDK版本:1.8 (2)Spring Boot 2.0.6 (3)Spring Security 5.0.9 (4)Spring Data JPA 2.0.11.RELEASE (5)h ...

  4. 256.Spring Boot+Spring Security: MD5是加密算法吗?

    说明 (1)JDK版本:1.8 (2)Spring Boot 2.0.6 (3)Spring Security 5.0.9 (4)Spring Data JPA 2.0.11.RELEASE (5)h ...

  5. Spring Boot+Spring Security:获取用户信息和session并发控制

    说明 (1)JDK版本:1.8(2)Spring Boot 2.0.6(3)Spring Security 5.0.9(4)Spring Data JPA 2.0.11.RELEASE(5)hiber ...

  6. 快速搭建基于Spring Boot + Spring Security 环境

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 1.Spring Security 权限管理框架介绍 简介: Spring Security 提供了基于 ...

  7. Spring Boot+Spring Security+JWT 实现 RESTful Api 认证(二)

    Spring Boot+Spring Security+JWT 实现 RESTful Api 认证(二) 摘要 上一篇https://javaymw.com/post/59我们已经实现了基本的登录和t ...

  8. Spring boot +Spring Security + Thymeleaf 认证失败返回错误信息

    [Please make sure to select the branch corresponding to the version of Thymeleaf you are using] Stat ...

  9. Spring Boot+Spring Security+JWT 实现 RESTful Api 认证(一)

    标题 Spring Boot+Spring Security+JWT 实现 RESTful Api 认证(一) 技术 Spring Boot 2.Spring Security 5.JWT 运行环境 ...

随机推荐

  1. nyoj 366 D的小L (全排列)

    D的小L 时间限制:4000 ms  |  内存限制:65535 KB 难度:2   描述       一天TC的匡匡找ACM的小L玩三国杀,但是这会小L忙着哩,不想和匡匡玩但又怕匡匡生气,这时小L给 ...

  2. nyoj 7 街区最短路径问题 (曼哈顿距离(出租车几何) or 暴力)

    街区最短路径问题 时间限制:3000 ms  |  内存限制:65535 KB 难度:4   描述 一个街区有很多住户,街区的街道只能为东西.南北两种方向. 住户只可以沿着街道行走. 各个街道之间的间 ...

  3. VLAN实验(1)Access接口

    1.选择两台S3700的交换机,5台PC机,并按照下图链接好并填写IP,完成此拓扑图 2.由于现在我们还没有划分VLAN,这5台PC,还在同一个VLAN中,现在我们启动所有的设备,这是所有的主机应该是 ...

  4. python+appium搭建的测试环境

    : 1,安装jdk JDK下载好jdk直接点下一步就可以了,然后开始配置变量classpath, path, Java_home:再运行cmd,并输入Java和javac看输出判断环境变量是否配好了. ...

  5. (三十六)golang--文件的基本操作

    文件程序中是以流的形式操作的. 流:数据在数据源(文件)和程序(内存)之间经历的路径: 输入流:数据从数据源到程序的路径: 输出流:数据从程序到数据源的路径: 常用的文件操作和函数: 1.常用的文件操 ...

  6. 对js中局部变量、全局变量和闭包的理解

    对js中局部变量.全局变量和闭包的理解 局部变量 对于局部变量,js给出的定义是这样的:在 JavaScript函数内部声明的变量(使用 var)是局部变量,所以只能在函数内部访问它.(该变量的作用域 ...

  7. Flex调用本地文件分析

    最近在用Flex做一个相册的功能,因为图片数据很多,所以想调用本地文件的方式做. 但是B/S的缘故,很多安全上的限制给我造成了不小的麻烦,把我这个小菜鸟弄的晕头转向. 第一,刚开始,查了很多资料发现都 ...

  8. Stream系列(五)Min Max Average方法使用

    最小值,最大值,平均值 EmployeeTestCase.java package com.example.demo; import lombok.Data; import lombok.ToStri ...

  9. jenkins + maven + nexus + [ svn 或 GitLab 或 GitHub ]

    目录 介绍 DevOps平台四大模块 针对DevOps开源项目 Jenkins 介绍 Maven 介绍 maven的核心概念介绍 SVN介绍 Nexus介绍 Maven私服的 个特性: 流程图 环境搭 ...

  10. HTML标签的for属性

    HTML 标签的 for 属性 for 属性规定 label 与哪个表单元素绑定. 隐式和显式的联系 label通常以下面两种方式中的一种来和表单控件相联系: 将表单控件作为标记标签的内容,这样的就是 ...