Spring securty<五> 认证--帐号/邮箱/手机号+密码

Spring securty<五> 认证–帐号/邮箱/手机号+密码


本地项目的基础环境

环境版本
jdk1.8.0_201
maven3.6.0
Spring-boot2.3.3.RELEASE

1、简介

spring security是一个提供身份验证、授权和防止常见攻击的框架,它对命令式和反应式应用程序都有一流的支持,是保护基于Spring的应用程序的事实上的标准。

详细可以参看《spring security官网》

2、认证(登录)

通过之前的两篇文章的介绍,应该也比较清楚了基本的概念了安全框架里的核心的概念了,从这篇开始,主要开始细化讲代码层面上的开发了;在权限框架中,认证这个部分,也算是最难的了,之后的几篇,也是主要讲述认证相关的。

《Spring securty<一> 简介入门案例》

《Spring securty<二> 配置项详解》

《Spring securty<三> 认证案例代码》

《Spring securty<四> 认证的源码解析》

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:成功处理器,全局写一次;

那么综合下来,我们需要的代码:拦截器AuthenticationProviderUserDetailsServiceAbstractAuthenticationToken;

1、拦截器的编写

登录拦截器,我们使用的帐号+密码的形式登录,那么spring security中,有一个默认的org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter处理器,上一篇文章中,我们也看过源码分析了,拦截的POST请求,请求参数为@RequestParam形式的,参数字段名称为usernamepassword;我们也继续使用这个拦截器;

如果有另外的需求,比如说,新加一个图片验证码,那么也有两种处理方案:

  • 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):验证具体的类型;验证过程中,我们使用的参数是usernamepassword,两个参数,就不在具体去再实现一个;

那么,下面就开始编写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"
}

详细的代码,可以查看《码云》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

葵花下的獾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值