一、引言
理论零碎通常须要实现多种认证形式,比方用户名明码、手机验证码、邮箱等等。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;
@Service
public 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,如下
@Component
public 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
@Configuration
public 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
@WebFilter
public 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 实现多种认证形式就完结了,如有谬误,感激斧正。