一、引言
理论零碎通常须要实现多种认证形式,比方用户名明码、手机验证码、邮箱等等。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的配置:
- 登录相干接口的放行,其余接口须要认证
- 配置认证异样处理器
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实现多种认证形式就完结了,如有谬误,感激斧正。