当前位置:   article > 正文

【Spring Security】实现多种认证方式_springsecurity多种认证详解

springsecurity多种认证详解

一、引言

实际系统通常需要实现多种认证方式,比如用户名密码、手机验证码、邮箱等等。Spring Security可以通过自定义认证器AuthenticationProvider 来实现不同的认证方式。接下来介绍一下SpringSecurity具体如何来实现多种认证方式。

二、具体步骤

这里我们以用户名密码、手机验证码两种方式来进行演示,其他一些登录方式类似。

2.1 自定义认证器AuthenticationProvider

首先针对每一种登录方式,我们可以定义其对应的认证器AuthenticationProvider,以及对应的认证信息Authentication实际场景中这两个一般是配套使用。认证器AuthenticationProvider有一个认证方法authenticate(),我们需要实现该认证方法,认证成功之后返回认证信息Authentication。

2.1.1 手机验证码

针对手机验证码方式,我们可以定义以下两个类
MobilecodeAuthenticationProvider.class

  1. import com.kamier.security.web.service.MyUser;
  2. import org.springframework.security.authentication.AuthenticationProvider;
  3. import org.springframework.security.authentication.BadCredentialsException;
  4. import org.springframework.security.core.Authentication;
  5. import org.springframework.security.core.AuthenticationException;
  6. import org.springframework.security.core.userdetails.UserDetailsService;
  7. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  8. import java.util.HashMap;
  9. import java.util.Map;
  10. public class MobilecodeAuthenticationProvider implements AuthenticationProvider {
  11. private UserDetailsService userDetailsService;
  12. @Override
  13. public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  14. MobilecodeAuthenticationToken mobilecodeAuthenticationToken = (MobilecodeAuthenticationToken) authentication;
  15. String phone = mobilecodeAuthenticationToken.getPhone();
  16. String mobileCode = mobilecodeAuthenticationToken.getMobileCode();
  17. System.out.println("登陆手机号:" + phone);
  18. System.out.println("手机验证码:" + mobileCode);
  19. // 模拟从redis中读取手机号对应的验证码及其用户名
  20. Map dataFromRedis = new HashMap();
  21. dataFromRedis.put("code", "6789");
  22. dataFromRedis.put("username", "admin");
  23. // 判断验证码是否一致
  24. if (!mobileCode.equals(dataFromRedis.get("code"))) {
  25. throw new BadCredentialsException("验证码错误");
  26. }
  27. // 如果验证码一致,从数据库中读取该手机号对应的用户信息
  28. MyUser loadedUser = (MyUser) userDetailsService.loadUserByUsername(dataFromRedis.get("username"));
  29. if (loadedUser == null) {
  30. throw new UsernameNotFoundException("用户不存在");
  31. } else {
  32. MobilecodeAuthenticationToken result = new MobilecodeAuthenticationToken(loadedUser, null, loadedUser.getAuthorities());
  33. return result;
  34. }
  35. }
  36. @Override
  37. public boolean supports(Class> aClass) {
  38. return MobilecodeAuthenticationToken.class.isAssignableFrom(aClass);
  39. }
  40. public void setUserDetailsService(UserDetailsService userDetailsService) {
  41. this.userDetailsService = userDetailsService;
  42. }
  43. }

注意这里的supports方法,是实现多种认证方式的关键,认证管理器AuthenticationManager会通过这个supports方法来判定当前需要使用哪一种认证方式

MobilecodeAuthenticationToken.class

  1. import org.springframework.security.authentication.AbstractAuthenticationToken;
  2. import org.springframework.security.core.GrantedAuthority;
  3. import java.util.Collection;
  4. /**
  5. * 手机验证码认证信息,在UsernamePasswordAuthenticationToken的基础上添加属性 手机号、验证码
  6. */
  7. public class MobilecodeAuthenticationToken extends AbstractAuthenticationToken {
  8. private static final long serialVersionUID = 530L;
  9. private Object principal;
  10. private Object credentials;
  11. private String phone;
  12. private String mobileCode;
  13. public MobilecodeAuthenticationToken(String phone, String mobileCode) {
  14. super(null);
  15. this.phone = phone;
  16. this.mobileCode = mobileCode;
  17. this.setAuthenticated(false);
  18. }
  19. public MobilecodeAuthenticationToken(Object principal, Object credentials, Collection extends GrantedAuthority> authorities) {
  20. super(authorities);
  21. this.principal = principal;
  22. this.credentials = credentials;
  23. super.setAuthenticated(true);
  24. }
  25. public Object getCredentials() {
  26. return this.credentials;
  27. }
  28. public Object getPrincipal() {
  29. return this.principal;
  30. }
  31. public String getPhone() {
  32. return phone;
  33. }
  34. public String getMobileCode() {
  35. return mobileCode;
  36. }
  37. public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
  38. if (isAuthenticated) {
  39. throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
  40. } else {
  41. super.setAuthenticated(false);
  42. }
  43. }
  44. public void eraseCredentials() {
  45. super.eraseCredentials();
  46. this.credentials = null;
  47. }
  48. }
2.1.2 用户名密码

针对用户名密码方式,我们可以直接使用自带的DaoAuthenticationProvider以及对应的UsernamePasswordAuthenticationToken。

2.2 实现UserDetailService

UserDetailService服务用以返回当前登录用户的用户信息,可以每一种认证方式实现对应的UserDetailService,也可以使用同一个。这里我们使用同一个UserDetailService服务,代码如下:

MyUserDetailsService.class

  1. import com.google.common.collect.Lists;
  2. import org.springframework.security.core.AuthenticationException;
  3. import org.springframework.security.core.userdetails.UserDetails;
  4. import org.springframework.security.core.userdetails.UserDetailsService;
  5. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  6. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  7. import org.springframework.stereotype.Service;
  8. @Service
  9. public class MyUserDetailsService implements UserDetailsService {
  10. @Override
  11. public UserDetails loadUserByUsername(String username) throws AuthenticationException {
  12. MyUser myUser;
  13. // 这里模拟从数据库中获取用户信息
  14. if (username.equals("admin")) {
  15. myUser = new MyUser("admin", new BCryptPasswordEncoder().encode("123456"), Lists.newArrayList("p1", "p2"));
  16. myUser.setAge(25);
  17. myUser.setSex(1);
  18. myUser.setAddress("xxxx小区");
  19. return myUser;
  20. } else {
  21. throw new UsernameNotFoundException("用户不存在");
  22. }
  23. }
  24. }

MyUser.class

  1. import com.google.common.collect.Lists;
  2. import org.springframework.security.core.GrantedAuthority;
  3. import org.springframework.security.core.userdetails.User;
  4. import java.util.List;
  5. import java.util.Optional;
  6. import java.util.stream.Collectors;
  7. public class MyUser extends User {
  8. private int sex;
  9. private int age;
  10. private String address;
  11. public MyUser(String username, String password, List authorities) {
  12. super(username, password, Optional.ofNullable(authorities).orElse(Lists.newArrayList()).stream()
  13. .map(str -> (GrantedAuthority) () -> str)
  14. .collect(Collectors.toList()));
  15. }
  16. public int getSex() {
  17. return sex;
  18. }
  19. public void setSex(int sex) {
  20. this.sex = sex;
  21. }
  22. public int getAge() {
  23. return age;
  24. }
  25. public void setAge(int age) {
  26. this.age = age;
  27. }
  28. public String getAddress() {
  29. return address;
  30. }
  31. public void setAddress(String address) {
  32. this.address = address;
  33. }
  34. }

2.3 统一处理认证异常

定义一个认证异常处理器,统一处理认证异常AuthenticationException,如下

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

2.4 配置器WebSecurityConfigurer

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

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

  1. 登录相关接口的放行,其他接口需要认证
  2. 配置认证异常处理器

MySecurityConfigurer.class

  1. @Configuration
  2. public class MySecurityConfigurer extends WebSecurityConfigurerAdapter {
  3. @Autowired
  4. private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
  5. @Autowired
  6. private UserDetailsService myUserDetailsService;
  7. @Autowired
  8. private TokenAuthenticationFilter tokenAuthenticationFilter;
  9. @Bean
  10. public PasswordEncoder passwordEncoder() {
  11. return new BCryptPasswordEncoder();
  12. }
  13. @Bean
  14. public MobilecodeAuthenticationProvider mobilecodeAuthenticationProvider() {
  15. MobilecodeAuthenticationProvider mobilecodeAuthenticationProvider = new MobilecodeAuthenticationProvider();
  16. mobilecodeAuthenticationProvider.setUserDetailsService(myUserDetailsService);
  17. return mobilecodeAuthenticationProvider;
  18. }
  19. @Bean
  20. public DaoAuthenticationProvider daoAuthenticationProvider() {
  21. DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
  22. daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
  23. daoAuthenticationProvider.setUserDetailsService(myUserDetailsService);
  24. return daoAuthenticationProvider;
  25. }
  26. /**
  27. * 定义认证管理器AuthenticationManager
  28. * @return
  29. */
  30. @Bean
  31. public AuthenticationManager authenticationManager() {
  32. List authenticationProviders = new ArrayList();
  33. authenticationProviders.add(mobilecodeAuthenticationProvider());
  34. authenticationProviders.add(daoAuthenticationProvider());
  35. ProviderManager authenticationManager = new ProviderManager(authenticationProviders);
  36. // authenticationManager.setEraseCredentialsAfterAuthentication(false);
  37. return authenticationManager;
  38. }
  39. @Override
  40. public void configure(HttpSecurity http) throws Exception {
  41. http
  42. // 关闭csrf
  43. .csrf().disable()
  44. // 处理认证异常
  45. .exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint)
  46. .and()
  47. // 权限配置,登录相关的请求放行,其余需要认证
  48. .authorizeRequests()
  49. .antMatchers("/login/*").permitAll()
  50. .anyRequest().authenticated()
  51. .and()
  52. // 添加token认证过滤器
  53. .addFilterAfter(tokenAuthenticationFilter, LogoutFilter.class)
  54. // 不使用session会话管理
  55. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
  56. }
  57. }

到这里实现多种认证方式基本就结束了。

但在实际项目中,认证成功后通常会返回一个token令牌(如jwt等),后续我们将token放到请求头中进行请求,后端校验该token,校验成功后再访问相应的接口,所以这里在上面的配置中加了一个token认证过滤器TokenAuthenticationFilter

TokenAuthenticationFilter的代码如下:

  1. @Component
  2. @WebFilter
  3. public class TokenAuthenticationFilter extends OncePerRequestFilter {
  4. @Override
  5. protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
  6. String token = httpServletRequest.getHeader("token");
  7. // 如果没有token,跳过该过滤器
  8. if (!StringUtils.isEmpty(token)) {
  9. // 模拟redis中的数据
  10. Map map = new HashMap();
  11. map.put("test_token1", new MyUser("admin", new BCryptPasswordEncoder().encode("123456"), Lists.newArrayList("p1", "p2")));
  12. map.put("test_token2", new MyUser("root", new BCryptPasswordEncoder().encode("123456"), Lists.newArrayList("p1")));
  13. // 这里模拟从redis获取token对应的用户信息
  14. MyUser myUser = map.get(token);
  15. if (myUser != null) {
  16. UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(myUser, null, myUser.getAuthorities());
  17. SecurityContextHolder.getContext().setAuthentication(authRequest);
  18. } else {
  19. throw new PreAuthenticatedCredentialsNotFoundException("token不存在");
  20. }
  21. }
  22. filterChain.doFilter(httpServletRequest, httpServletResponse);
  23. }
  24. }

三、测试验证

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

  1. @RestController
  2. @RequestMapping("/login")
  3. public class LoginController {
  4. @Autowired
  5. private AuthenticationManager authenticationManager;
  6. /**
  7. * 用户名密码登录
  8. * @param username
  9. * @param password
  10. * @return
  11. */
  12. @GetMapping("/usernamePwd")
  13. public R usernamePwd(String username, String password) {
  14. UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
  15. Authentication authenticate = null;
  16. try {
  17. authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. return R.error("登陆失败");
  21. }
  22. String token = UUID.randomUUID().toString().replace("-", "");
  23. return R.ok(token, "登陆成功");
  24. }
  25. /**
  26. * 手机验证码登录
  27. * @param phone
  28. * @param mobileCode
  29. * @return
  30. */
  31. @GetMapping("/mobileCode")
  32. public R mobileCode(String phone, String mobileCode) {
  33. MobilecodeAuthenticationToken mobilecodeAuthenticationToken = new MobilecodeAuthenticationToken(phone, mobileCode);
  34. Authentication authenticate = null;
  35. try {
  36. authenticate = authenticationManager.authenticate(mobilecodeAuthenticationToken);
  37. } catch (Exception e) {
  38. e.printStackTrace();
  39. return R.error("验证码错误");
  40. }
  41. String token = UUID.randomUUID().toString().replace("-", "");
  42. return R.ok(token, "登陆成功");
  43. }
  44. }
  • 用户名密码
    访问/login/usernamePwd接口进行登录,账号密码为admin/123456,可以看到访问成功,如下图

  • 手机验证码
    访问/login/mobileCode接口进行登录,如下图

  • 带token访问
    在请求头带上token访问接口,如下图

  • 不带token访问

到这里Spring Security实现多种认证方式就结束了,如有错误,感谢指正。

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号