Spring securty<五> 认证–帐号/邮箱/手机号+密码
文章目录
本地项目的基础环境
环境 | 版本 |
---|---|
jdk | 1.8.0_201 |
maven | 3.6.0 |
Spring-boot | 2.3.3.RELEASE |
1、简介
spring security是一个提供身份验证、授权和防止常见攻击的框架,它对命令式和反应式应用程序都有一流的支持,是保护基于Spring的应用程序的事实上的标准。
详细可以参看《spring security官网》
2、认证(登录)
通过之前的两篇文章的介绍,应该也比较清楚了基本的概念了安全框架里的核心的概念了,从这篇开始,主要开始细化讲代码层面上的开发了;在权限框架中,认证这个部分,也算是最难的了,之后的几篇,也是主要讲述认证相关的。
3、认证的流程
认证的流程中,我们把上一篇《Spring securty<四> 认证的源码解析》的最后一个图拿过来,整个流程也会按照这个方向去写代码;
整个登录的过程中,会通过常用的登录方式,要详细编写实际应用中的代码,代码案例中,为了简便,数据库查询操作,使用模拟操作;
暂时整理的案例中,会通过2篇博文,演示如下两种登录方式:
1、帐号/邮箱/手机号+密码 登录;

2、手机号+短信验证码 登录
当前这篇博文,主要讲述的第一种,帐号/邮箱/手机号+密码
其他形式的登录,例如:QQ登录、微信登录、微博登录……这些都是基于OAuth2协议的,后续有时间了,详细讲解这块协议的时候,在说明;
3、构建基础代码的项目
复制项目《Spring securty<三> 认证案例代码》中的项目,修改名称为badger-spring-security-4
;
4、帐号/邮箱/手机号+密码
4.1、代码设计分析
从百度的那个登录可以看到,登录的按钮只有一个,那么入口就是一个,一个入口,要兼顾不同的ID来登录;
那么从认证的流程图里,我们的步骤如下:
1、AuthenticationFilter
业务处理拦截器,只能是一个(当然,你也可以是多个,然后向拦截器链
往下分发,这个做法,不推荐,业务逻辑整复杂了);
2、AuthenticationManager
认证管理器,一般使用默认的就可以了,默认的管理器,初始化的时候,就已经注入容器了,就是一个单例实例;
3、AuthenticationProvider
认证提供者,按照上述的要求,认证提供者可以是一个,也可以是三个,各有各的好处;
**一个认证提供者的做法:**其实,在提供者的验证方法中,执行UserDetailsService
查询用户明细的时候,使用OR
操作,sql类似如下
select * from user where username='xxx' or email='xxx@qq.com' or phone='186xxx13511'
这样,就能查出具体的用户了;优点也可以看得到,只用查询一次数据库;
**多个认证提供者的做法:**一个AuthenticationProvider对应一个查询用户的方法,上述中,我们有三个,那个就是三种;对应的sql分别如下:
select * from user where username ='xxx';
select * from user where email='xxx@qq.com';
select * from user where phone='186xxx13511';
AuthenticationManager
认证管理器认证提供者的时候,上一篇的源码分析中,也可以看到,是遍历执行的,其中一个认证成功,就是认证通过;
4.2、代码编写
在整个认证过程中,代码编写的地方,其实并不多;
拦截器
:按照实际需求来,有必要就写,没有必要就用官方的;AuthenticationManager
:不用写;AuthenticationProvider
:需要写,每个验证的规则不一样UserDetailsService
:需要写,虽然查询的结果都是找到这个用户,但是查询的条件不一样;AbstractAuthenticationToken
:token类型,按照实际需求来,有必要就写,没有必要就用官方的;AuthenticationSuccessHandler
:成功处理器,全局写一次;AuthenticationFailureHandler
:成功处理器,全局写一次;
那么综合下来,我们需要的代码:拦截器
、AuthenticationProvider
、UserDetailsService
、AbstractAuthenticationToken
;
1、拦截器的编写
登录拦截器,我们使用的帐号+密码的形式登录,那么spring security
中,有一个默认的org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
处理器,上一篇文章中,我们也看过源码分析了,拦截的POST
请求,请求参数为@RequestParam
形式的,参数字段名称为username
和password
;我们也继续使用这个拦截器;
如果有另外的需求,比如说,新加一个图片验证码,那么也有两种处理方案:
- 在
UsernamePasswordAuthenticationFilter
拦截器之前,新增了一个图片验证码的拦截器,单独校验; - 复制
UsernamePasswordAuthenticationFilter
代码,新增一个验证参数imageCode
来验证图片验证码
这里,就不演示这个了,非常好实现;
2、AuthenticationProvider
代码编写
package com.badger.spring.boot.security.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
/**
* 帐号密码登录校验器
* @author liqi
*/
@Slf4j
public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider {
@Getter
@Setter
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
public UsernamePasswordAuthenticationProvider(UserDetailsService userDetailsService) {
super();
this.userDetailsService = userDetailsService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 获取表单用户名
String username = (String) authentication.getPrincipal();
// 获取表单用户填写的密码
String password = (String) authentication.getCredentials();
UserDetails userDetails;
try {
userDetails = userDetailsService.loadUserByUsername(username);
} catch (Exception e) {
// 异常不是鉴权异常的时候,异常无法向上抛出,异常处理的controller无法返回默认异常,需要把异常处理成鉴权异常
throw new AuthenticationServiceException(e.getMessage());
}
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
log.info("当前登录人:{},当前登录密码:{}", username, password);
throw new BadCredentialsException("用户密码不正确");
}
return new UsernamePasswordAuthenticationToken(username, null, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
// 帐号密码登录使用的校验器
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
代码也是比较简单,按照上述图中的认证流程

-
按照我们的需求,表单中,用户输入的用户名,那么username取的就是用户名、输入手机号,username就是手机号,这个好理解吧?
// 获取表单用户名 String username = (String) authentication.getPrincipal(); // 获取表单用户填写的密码 String password = (String) authentication.getCredentials();
-
Authentication authenticate(Authentication authentication)
:验证密码的过程中,需要使用UserDetailsService
的实现,查询具体的用户;我构造方法中,接收的是接口类型的参数(策略模式); -
boolean supports(Class<?> authentication)
:验证具体的类型;验证过程中,我们使用的参数是username
和password
,两个参数,就不在具体去再实现一个;
那么,下面就开始编写UserDetailsService
接口的实现代码
3、代码编写UserDetailsService查询用户明细
3.1、编写数据库的实体类,只用了目前需要的字段
/**
* 用户实体
* @author liqi
*/
@Data
public class UserEntity {
/**主键*/
private Integer id;
/**用户名称*/
private String name;
/**用户帐号*/
private String username;
/**手机号*/
private String password;
/**邮箱*/
private String email;
/**手机号*/
private String phone;
}
3.2、UserDetailsService接口代码如下
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
可以看到,根据一个参数,返回一个UserDetails
对象,而UserDetails
对象,也是一个接口;
3.3、实现UserDetails
接口
/**
* 用户系统的领域模型
* @author liqi
*/
public class SystemUserDetails implements UserDetails {
private static final long serialVersionUID = -7127141675788677116L;
/**用户名.*/
@Setter
private String username;
/**密码.*/
@Setter
private String password;
/**用户角色信息*/
@Setter
private Collection<? extends GrantedAuthority> authorities;
/**当前用户信息*/
@Getter
@Setter
private UserEntity user;
public SystemUserDetails() {
super();
}
public SystemUserDetails(String username, String password, UserEntity user,
Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.authorities = authorities;
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
// 账户是否过期
return true;
}
@Override
public boolean isAccountNonLocked() {
// 帐户未锁定
return true;
}
@Override
public boolean isCredentialsNonExpired() {
// 凭据未过期
return true;
}
@Override
public boolean isEnabled() {
// 账户是否停用
return true;
}
}
接口里的方法名称,具体作用,看注释吧;
3.4、实现 帐号+密码 UserDetailsService接口
采用匿名类的方式实现,使用个list存放了一些默认用户,实际项目中,采用数据库,直接查询就可以了
package com.badger.spring.boot.security.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import com.badger.spring.boot.security.entity.SystemUserDetails;
import com.badger.spring.boot.security.entity.UserEntity;
@Configuration
public class SecurityConfig {
static final List<UserEntity> USER_LIST = new ArrayList<>();
static {
for (int i = 1; i < 6; i++) {
UserEntity userEntity = new UserEntity();
userEntity.setId(i);
userEntity.setName("测试人员" + i);
userEntity.setUsername("ceshi_" + i);
// 密码使用 PasswordEncoder 类对123456 加密之后的结果
userEntity.setPassword("$2a$10$D1q09WtH./yTfFTh35n0k.o6yZIXwxIW1/ex6/EjYTF7EiNxXyF7m");
userEntity.setEmail("100" + i + "@qq.com");
userEntity.setPhone("186xxxx351" + i);
USER_LIST.add(userEntity);
}
}
/************************帐号密码登录**********************/
@Bean
public UserDetailsService usernamePasswordUserDetails() {
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity user = null;
for (UserEntity userEntity : USER_LIST) {
if (username.equals(userEntity.getUsername()) || username.equals(userEntity.getPhone())
|| username.equals(userEntity.getEmail())) {
user = userEntity;
}
}
if (user != null) {
return new SystemUserDetails(user.getUsername(), user.getPassword(), user, null);
}
throw new UsernameNotFoundException("用户未注册,请先注册");
}
};
}
@Bean
public AuthenticationProvider usernamePasswordAuthenticationProvider() {
return new UsernamePasswordAuthenticationProvider(usernamePasswordUserDetails());
}
}
根据上面的认证流程图中,一个AuthenticationProvider
匹配一个UserDetailsService
;
4、认证流程串联代码
修改项目中的WebSecurityConfig
类,结果如下
package com.badger.spring.boot.security.config;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] EXCLUDE_URLS = { "/**/*.js", "/**/*.css", "/**/*.jpg", "/**/*.png", "/**/*.gif",
"/v2/**", "/errors", "/error", "/favicon.ico", "/swagger-ui.html/**", "/swagger-ui/**", "/webjars/**",
"/swagger-resources/**", "/auth/login" };
@Autowired
private AuthenticationSuccessHandler successHandler;
@Autowired
private AuthenticationFailureHandler failureHandler;
@Autowired
AccessDeniedHandler deniedHandler;
@Autowired
AuthenticationEntryPoint entryPoint;
@Autowired
private List<AuthenticationProvider> authenticationProviderList;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 登录处理器
for (AuthenticationProvider authenticationProvider : authenticationProviderList) {
http.authenticationProvider(authenticationProvider);
}
// 全局异常配置
http.exceptionHandling().accessDeniedHandler(deniedHandler).authenticationEntryPoint(entryPoint);
http.authorizeRequests().antMatchers(EXCLUDE_URLS).permitAll();
// 表单操作
FormLoginConfigurer<HttpSecurity> formLogin = http.formLogin();
// 表单请求成功处理器、失败处理器;与loginPage冲突,配置后,loginPage不生效
formLogin.successHandler(successHandler).failureHandler(failureHandler);
// 表单提交的post请求地址,用户参数名称
formLogin.loginProcessingUrl("/auth/login");
http.csrf().disable();
}
}
跟之前的代码比较起来:
1、去掉了在内存中,加密的默认的用户;
2、把所有的注入到spring容器中的认证提供者,拿出来,放入到AuthenticationManager
管理起来;
@Autowired
private List<AuthenticationProvider> authenticationProviderList;
// 登录处理器
for (AuthenticationProvider authenticationProvider : authenticationProviderList) {
http.authenticationProvider(authenticationProvider);
}
5、测试演示
项目启动后,执行得到结果
curl -X POST "http://localhost:8080/auth/login?username=ceshi_1&password=123456" -H "accept: */*"
curl -X POST "http://localhost:8080/auth/login?username=1001@qq.com&password=123456" -H "accept: */*"
curl -X POST "http://localhost:8080/auth/login?username=186xxxx3511&password=123456" -H "accept: */*"
{
"code": 200,
"message": "ceshi_1"
}
{
"code": 200,
"message": "1001@qq.com"
}
{
"code": 200,
"message": "186xxxx3511"
}
详细的代码,可以查看《码云》