一、引言

理论零碎通常须要实现多种认证形式,比方用户名明码、手机验证码、邮箱等等。Spring Security能够通过自定义认证器AuthenticationProvider 来实现不同的认证形式。接下来介绍一下SpringSecurity具体如何来实现多种认证形式。

二、具体步骤

这里咱们以用户名明码、手机验证码两种形式来进行演示,其余一些登录形式相似。

2.1 自定义认证器AuthenticationProvider

首先针对每一种登录形式,咱们能够定义其对应的认证器AuthenticationProvider,以及对应的认证信息Authentication理论场景中这两个个别是配套应用。认证器AuthenticationProvider有一个认证办法authenticate(),咱们须要实现该认证办法,认证胜利之后返回认证信息Authentication。

2.1.1 手机验证码

针对手机验证码形式,咱们能够定义以下两个类
MobilecodeAuthenticationProvider.class

import com.kamier.security.web.service.MyUser;import org.springframework.security.authentication.AuthenticationProvider;import org.springframework.security.authentication.BadCredentialsException;import org.springframework.security.core.Authentication;import org.springframework.security.core.AuthenticationException;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import java.util.HashMap;import java.util.Map;public class MobilecodeAuthenticationProvider implements AuthenticationProvider {    private UserDetailsService userDetailsService;    @Override    public Authentication authenticate(Authentication authentication) throws AuthenticationException {        MobilecodeAuthenticationToken mobilecodeAuthenticationToken = (MobilecodeAuthenticationToken) authentication;        String phone = mobilecodeAuthenticationToken.getPhone();        String mobileCode = mobilecodeAuthenticationToken.getMobileCode();        System.out.println("登陆手机号:" + phone);        System.out.println("手机验证码:" + mobileCode);        // 模仿从redis中读取手机号对应的验证码及其用户名        Map<String, String> dataFromRedis = new HashMap<>();        dataFromRedis.put("code", "6789");        dataFromRedis.put("username", "admin");        // 判断验证码是否统一        if (!mobileCode.equals(dataFromRedis.get("code"))) {            throw new BadCredentialsException("验证码谬误");        }        // 如果验证码统一,从数据库中读取该手机号对应的用户信息        MyUser loadedUser = (MyUser) userDetailsService.loadUserByUsername(dataFromRedis.get("username"));        if (loadedUser == null) {            throw new UsernameNotFoundException("用户不存在");        } else {            MobilecodeAuthenticationToken result = new MobilecodeAuthenticationToken(loadedUser, null, loadedUser.getAuthorities());            return result;        }    }    @Override    public boolean supports(Class<?> aClass) {        return MobilecodeAuthenticationToken.class.isAssignableFrom(aClass);    }    public void setUserDetailsService(UserDetailsService userDetailsService) {        this.userDetailsService = userDetailsService;    }}

留神这里的supports办法,是实现多种认证形式的要害,认证管理器AuthenticationManager会通过这个supports办法来断定以后须要应用哪一种认证形式

MobilecodeAuthenticationToken.class

import org.springframework.security.authentication.AbstractAuthenticationToken;import org.springframework.security.core.GrantedAuthority;import java.util.Collection;/** * 手机验证码认证信息,在UsernamePasswordAuthenticationToken的根底上增加属性 手机号、验证码 */public class MobilecodeAuthenticationToken extends AbstractAuthenticationToken {    private static final long serialVersionUID = 530L;    private Object principal;    private Object credentials;    private String phone;    private String mobileCode;    public MobilecodeAuthenticationToken(String phone, String mobileCode) {        super(null);        this.phone = phone;        this.mobileCode = mobileCode;        this.setAuthenticated(false);    }    public MobilecodeAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {        super(authorities);        this.principal = principal;        this.credentials = credentials;        super.setAuthenticated(true);    }    public Object getCredentials() {        return this.credentials;    }    public Object getPrincipal() {        return this.principal;    }    public String getPhone() {        return phone;    }    public String getMobileCode() {        return mobileCode;    }    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");        } else {            super.setAuthenticated(false);        }    }    public void eraseCredentials() {        super.eraseCredentials();        this.credentials = null;    }}

2.1.2 用户名明码

针对用户名明码形式,咱们能够间接应用自带的DaoAuthenticationProvider以及对应的UsernamePasswordAuthenticationToken。

2.2 实现UserDetailService

UserDetailService服务用以返回以后登录用户的用户信息,能够每一种认证形式实现对应的UserDetailService,也能够应用同一个。这里咱们应用同一个UserDetailService服务,代码如下:

MyUserDetailsService.class

import com.google.common.collect.Lists;import org.springframework.security.core.AuthenticationException;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.stereotype.Service;@Servicepublic class MyUserDetailsService implements UserDetailsService {    @Override    public UserDetails loadUserByUsername(String username) throws AuthenticationException {        MyUser myUser;        // 这里模仿从数据库中获取用户信息        if (username.equals("admin")) {            myUser = new MyUser("admin", new BCryptPasswordEncoder().encode("123456"), Lists.newArrayList("p1", "p2"));            myUser.setAge(25);            myUser.setSex(1);            myUser.setAddress("xxxx小区");            return myUser;        } else {            throw new UsernameNotFoundException("用户不存在");        }    }}

MyUser.class

import com.google.common.collect.Lists;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.userdetails.User;import java.util.List;import java.util.Optional;import java.util.stream.Collectors;public class MyUser extends User {    private int sex;    private int age;    private String address;    public MyUser(String username, String password, List<String> authorities) {        super(username, password, Optional.ofNullable(authorities).orElse(Lists.newArrayList()).stream()                .map(str -> (GrantedAuthority) () -> str)                .collect(Collectors.toList()));    }    public int getSex() {        return sex;    }    public void setSex(int sex) {        this.sex = sex;    }    public int getAge() {        return age;    }    public void setAge(int age) {        this.age = age;    }    public String getAddress() {        return address;    }    public void setAddress(String address) {        this.address = address;    }}

2.3 对立解决认证异样

定义一个认证异样处理器,对立解决认证异样AuthenticationException,如下

@Componentpublic class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {    @Override    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {        R result = R.error("用户未登录或已过期");        response.setContentType("text/json;charset=utf-8");        response.getWriter().write(new Gson().toJson(result));    }}

2.4 配置器WebSecurityConfigurer

在配置器中咱们去实例化一个认证管理器AuthenticationManager,这个认证管理器中蕴含了两个认证器,别离是MobilecodeAuthenticationProvider(手机验证码)、DaoAuthenticationProvider(用户名明码)。

重写config办法进行security的配置:

  1. 登录相干接口的放行,其余接口须要认证
  2. 配置认证异样处理器

MySecurityConfigurer.class

@Configurationpublic class MySecurityConfigurer extends WebSecurityConfigurerAdapter {    @Autowired    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;    @Autowired    private UserDetailsService myUserDetailsService;    @Autowired    private TokenAuthenticationFilter tokenAuthenticationFilter;    @Bean    public PasswordEncoder passwordEncoder() {        return new BCryptPasswordEncoder();    }    @Bean    public MobilecodeAuthenticationProvider mobilecodeAuthenticationProvider() {        MobilecodeAuthenticationProvider mobilecodeAuthenticationProvider = new MobilecodeAuthenticationProvider();        mobilecodeAuthenticationProvider.setUserDetailsService(myUserDetailsService);        return mobilecodeAuthenticationProvider;    }    @Bean    public DaoAuthenticationProvider daoAuthenticationProvider() {        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());        daoAuthenticationProvider.setUserDetailsService(myUserDetailsService);        return daoAuthenticationProvider;    }    /**     * 定义认证管理器AuthenticationManager     * @return     */    @Bean    public AuthenticationManager authenticationManager() {        List<AuthenticationProvider> authenticationProviders = new ArrayList<>();        authenticationProviders.add(mobilecodeAuthenticationProvider());        authenticationProviders.add(daoAuthenticationProvider());        ProviderManager authenticationManager = new ProviderManager(authenticationProviders);//        authenticationManager.setEraseCredentialsAfterAuthentication(false);        return authenticationManager;    }    @Override    public void configure(HttpSecurity http) throws Exception {        http                // 敞开csrf                .csrf().disable()                // 解决认证异样                .exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint)                .and()                // 权限配置,登录相干的申请放行,其余须要认证                .authorizeRequests()                .antMatchers("/login/*").permitAll()                .anyRequest().authenticated()                .and()                // 增加token认证过滤器                .addFilterAfter(tokenAuthenticationFilter, LogoutFilter.class)                // 不应用session会话治理                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);    }}

到这里实现多种认证形式根本就完结了。

但在理论我的项目中,认证胜利后通常会返回一个token令牌(如jwt等),后续咱们将token放到申请头中进行申请,后端校验该token,校验胜利后再拜访相应的接口,所以这里在下面的配置中加了一个token认证过滤器TokenAuthenticationFilter

TokenAuthenticationFilter的代码如下:

@Component@WebFilterpublic class TokenAuthenticationFilter extends OncePerRequestFilter {    @Override    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {        String token = httpServletRequest.getHeader("token");        // 如果没有token,跳过该过滤器        if (!StringUtils.isEmpty(token)) {            // 模仿redis中的数据            Map<String, MyUser> map = new HashMap<>();            map.put("test_token1", new MyUser("admin", new BCryptPasswordEncoder().encode("123456"), Lists.newArrayList("p1", "p2")));            map.put("test_token2", new MyUser("root", new BCryptPasswordEncoder().encode("123456"), Lists.newArrayList("p1")));            // 这里模仿从redis获取token对应的用户信息            MyUser myUser = map.get(token);            if (myUser != null) {                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(myUser, null, myUser.getAuthorities());                SecurityContextHolder.getContext().setAuthentication(authRequest);            } else {                throw new PreAuthenticatedCredentialsNotFoundException("token不存在");            }        }        filterChain.doFilter(httpServletRequest, httpServletResponse);    }}

三、测试验证

编写一个简略的Controller来验证多种登录形式,代码如下:

@RestController@RequestMapping("/login")public class LoginController {    @Autowired    private AuthenticationManager authenticationManager;    /**     * 用户名明码登录     * @param username     * @param password     * @return     */    @GetMapping("/usernamePwd")    public R usernamePwd(String username, String password) {        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);        Authentication authenticate = null;        try {            authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);        } catch (Exception e) {            e.printStackTrace();            return R.error("登陆失败");        }        String token = UUID.randomUUID().toString().replace("-", "");        return R.ok(token, "登陆胜利");    }    /**     * 手机验证码登录     * @param phone     * @param mobileCode     * @return     */    @GetMapping("/mobileCode")    public R mobileCode(String phone, String mobileCode) {        MobilecodeAuthenticationToken mobilecodeAuthenticationToken = new MobilecodeAuthenticationToken(phone, mobileCode);        Authentication authenticate = null;        try {            authenticate = authenticationManager.authenticate(mobilecodeAuthenticationToken);        } catch (Exception e) {            e.printStackTrace();            return R.error("验证码谬误");        }        String token = UUID.randomUUID().toString().replace("-", "");        return R.ok(token, "登陆胜利");    }}
  • 用户名明码
    拜访/login/usernamePwd接口进行登录,账号密码为admin/123456,能够看到拜访胜利,如下图
  • 手机验证码
    拜访/login/mobileCode接口进行登录,如下图
  • 带token拜访
    在申请头带上token拜访接口,如下图
  • 不带token拜访

到这里Spring Security实现多种认证形式就完结了,如有谬误,感激斧正。