:security实现多种方式登陆
spring security 的核心功能主要包括
认证
授权
攻击防护
其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式。
在说多种认证方式之前,咱们先简单过下单认证方式是如何配置的,也说下Spring Security的各个配置类的作用。
1、UsernamePasswordAuthenticationFilter
Spring Security 默认认证过滤器,处于拦截器链当中,继承AbstractAuthenticationProcessingFilter,咱们看一下源码
可以看出里面构造方法指定了默认拦截地址 /login,attemptAuthentication是父类AbstractAuthenticationProcessingFilter抽象方法的实现方法,在父类中doFilter方法里调用,可以看到方法实现是从request里取得用户名密码,最后构建成UsernamePasswordAuthenticationToken,然后调用AuthenticationManager的 authenticate 方法作为参数传进去进行认证。
2、UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken没什么好讲的,在其实就是对认证参数用户名密码的封装,当然后续登录成功之后会作为用户认证信息的封装。
3、AuthenticationManager
authenticationManager是AbstractAuthenticationProcessingFilter的一个成员变量,从上面可以看出,这个参数是必须赋值的,采用默认的过滤器认证,spring security会默认给一个实现类ProviderManager,看下代码
从源码看到,管理器会遍历所有注册的认证器集合,调用每个认证器的authenticate认证,此时会有疑惑,如果多个登录方式,肯定会有多个认证器,每次都遍历认证所有的认证器是否不太合理?关键在于以下这个判断代码
这个 toTest 参数就是过滤器传进来的 UsernamePasswordAuthenticationToken
Class<? extends Authentication> toTest = authentication.getClass();
if (!provider.supports(toTest)) {
continue;
}
会调用每个认证器的supports方法,只有此方法返回true,才会执行认证,(由此想到如果自定义认证器,此方法一定要重写),此方法如何实现,咱们看一下此方法的默认实现,会判断
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
由此看出,参数必须为 UsernamePasswordAuthenticationToken 类或者其子类的字节码,此参数又是由UsernamePasswordAuthenticationFilter 里传过来的
由此得知,每个过滤器都需要一个AbstractAuthenticationToken的子类绑定
4、AuthenticationProvider
这个是重点配置,具体认证方法都是在这里实现的,因此我们要自定义我们的认证方法,都需要实现这个接口,这个接口只有两个方法,authenticate用来认证,supports 用来决定启用条件
具体实现方法根据自己的业务需要,一般是查询数据库,对比密码,看下我的实现类,一般我们会注入一个自定义的UserDetailService实现类,重写 loadUserByUsername,具体根据用户名查询用户信息,认证成功将用户信息包装成 Authentication 返回
5、AuthenticationSuccessHandler、AuthenticationFailureHandler
看过滤器源码,认证结束后,会根据认证成功或失败,分别调用两个成功失败处理器
successHandler.onAuthenticationSuccess(request, response, authResult);
failureHandler.onAuthenticationFailure(request, response, failed);
因此,我们可以自定义这两个处理器,来自己处理认证成功失败
6、配置文件
最后,Spring Security的配置文件
根据上面的介绍和代码,我们大概可以知道原理了,现在开始撸码吧!
pom文件添加security的依赖
${spring-boot.version} 2.3.1.RELEASE
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>
实现UsernamePasswordAuthenticationFilter
该步骤作为过滤器适用,根据判断是否存在对应session,以提示是否需要重新登陆
@Slf4j
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private static final List<String> forIndexList = Lists.newArrayList();
static {
forIndexList.add("/");
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
//根路径,直接跳转到index页面
if (forIndexList.contains(request.getServletPath())) {
response.sendRedirect(request.getRequestURL() + "index.html");
return;
}
log.info("===>URL={} , IP={}", request.getRequestURL(), request.getRemoteAddr());
// 如果是接口请求验证登录情况
if (!request.getRequestURL().toString().contains(".html")
&& !"/login".equals(request.getServletPath())
&& !"/mobileLogin".equals(request.getServletPath())
&& !"/wxLogin".equals(request.getServletPath())) {
//用户是否登录
LoginUserDetail currentUser = SessionUtils.getCurrentUser(request);
log.info("current user {}", JSON.toJSONString(currentUser));
if (currentUser == null) {
response.setContentType("application/json;charset=UTF-8");
PrintWriter printWriter = response.getWriter();
Response httpResponse = new Response(401, "请先登录!");
printWriter.write(JSON.toJSONString(httpResponse));
printWriter.flush();
printWriter.close();
return;
}
}
chain.doFilter(request, response);
}
}
实现UserDetails,作为session的信息实体
public class LoginUserDetail implements UserDetails, Serializable {
private Long id;
private String username;
private String password;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return 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;
}
}
登陆方式1:账号密码登陆
public class UsernameAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
public UsernameAuthenticationProcessingFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.