写在前面的话

更多Spring与微服务相关的教程请戳这里 ​​火力全开系列 Spring与微服务教程合集 持续更新​

 

 

1、Spring Security如何灵活集成多种认证技术?

首先是javax.security.auth.Subject类,而一个Subject类包含多个javax.security.Principal

Principal类源码:

public interface Principal {
public boolean equals(Object another);
public String toString();
public int hashCode();
public String getName();
public default boolean implies(Subject subject) {
if (subject == null)
return false;
return subject.getPrincipals().contains(this);
}
}

 

spring security对principal做了一层封装,用Authentication表示

Authentication类源码:

public interface Authentication extends Principal, Serializable {
//获取主体权限集合
Collection<? extends GrantedAuthority> getAuthorities();

//获取主体凭证,通常被称为用户密码
Object getCredentials();

//获取主体携带的详细信息
Object getDetails();

//获取主体,通常为用户名
Object getPrincipal();

//主体是否验证成功
boolean isAuthenticated();

void setAuthenticated(boolean var1) throws IllegalArgumentException;

}

 

由于大部分场景下身份验证都是基于用户名和密码进行的,所以Spring Security提供了一个UsernamePasswordAuthenticationToken类Authentication的子类)用于代指这一类证明

也可以用SSH KEY进行登录,但它不属于用户名和密码登录这个范畴,如有必要,也可以自定义实现

在使用表单登录中,每一个登录用户都被包装为一个 UsernamePasswordAuthenticationToken,从而在Spring Security的各个AuthenticationProvider中流动

AuthenticationProvider被Spring Security定义为一个验证过程

AuthenticationProvider接口的源码:

public interface AuthenticationProvider {
//成功则返回一个验证完成的Authentication
Authentication authenticate(Authentication var1) throws AuthenticationException;

//是否支持验证当前的Authentication类型
boolean supports(Class<?> var1);

}

 

一次完整的认证可以包含多个AuthenticationProvider,一般由ProviderManager即AuthenticationManager的子类)管理

如下是ProviderManager的authenticate方法源码,当有多个AuthenticationProvider时,循环验证,只要有一个验证通过则通过

Iterator var8 = this.getProviders().iterator();

while(var8.hasNext())

 

2、如何自定义Provider?

Spring Security提供了多种常见的认证技术,包括但不限于以下几种:

  • HTTP层面的认证技术,包括HTTP基本认证和HTTP摘要认证两种
  • 基于LDAP的认证技术(Lightweight Directory Access Protocol,轻量目录访问协议)
  • 聚焦于证明用户身份的OpenID认证技术
  • 聚焦于授权的OAuth认证技术
  • 系统内维护的用户名和密码认证技术

其中,使用最为广泛的是由系统维护的用户名和密码认证技术,通常会涉及数据库访问。为了更好地按需定制,Spring Security 并没有直接糅合整个认证过程,而是提供了一个抽象的

AbstractUserDetailsAuthenticationProvider(是AuthenticationProvider的子类),在 AbstractUserDetailsAuthenticationProvider 中实现了基本的认证流程,通过继承AbstractUserDetailsAuthenticationProvider,

并实现retrieveUseradditionalAuthenticationChecks两个抽象方法即可自定义核心认证过程

DaoAuthenticationProvider就是继承了AbstractUserDetailsAuthenticationProvider

 

3、用户名密码是如何登陆的?

验证流程:

  1. 用户请求首先会进入到UsernamePasswordAuthenticationFilter中,此时的Authentication是未认证的
  2. 接着通过ProviderManager找到匹配的Provider,此处找到是DaoAuthenticationProvider,DaoAuthenticationProvider会根据UserDetailsService去查找UserDetails,接着执行校验逻辑
  3. 如果校验成功,则Authentication变成已认证的。最后沿着调用链返回时会经过SecurityContexPersistenceFilter,该过滤器在过滤器链的最前面,当请求过来的时候检查Session里是否有SecurityContex,有的话则拿出来放到线程里,如果没有则通过
     

Spring Security教程 第二弹 spring security核心源码分析_spring

 

UsernamePasswordAuthenticationFilter类的源码:

package org.springframework.security.web.authentication;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;

public class UsernamePasswordAuthenticationFilter 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 = "username";
private String passwordParameter = "password";
private boolean postOnly = true;

public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}

@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}

@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}

protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}

public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}

public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}

public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}

public final String getUsernameParameter() {
return this.usernameParameter;
}

public final String getPasswordParameter() {
return this.passwordParameter;
}
}

 

在该类中,可以看到如下信息:

  • 登陆接口是post请求
  • 登陆接口url为/login
  • 参数key分别为username和password

当然,这些信息我们可以通过自定义AbstractAuthenticationProcessingFilter 来个性化定制

 

4、自定义AuthenticationProvider与前后端分离登录实战

4.1、自定义AuthenticationProvider类

注意:自定义UserDetailsService见​​​​https://blog.51cto.com/u_14643435/2853405​​​中的4.3节

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

@Component
public class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

@Autowired
private UserDetailsService userDetailsService;

@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if(userDetails.getPassword().equals(authentication.getCredentials())){
System.out.println("密码校验成功");
}else{
System.out.println("密码校验错误");
//必须抛出异常才能表示验证失败
throw new BadCredentialsException("密码校验错误");
}
}

@Override
protected UserDetails retrieveUser(String s, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(s);
return userDetails;
}
}

 

4.2、自定义登录成功处理类

import com.bobo.group.springsecuritybase.util.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@Component
public class JsonAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
ObjectMapper mapper = new ObjectMapper();
response.setHeader("Content-Type", "application/json;charset=utf-8");
response.setStatus(200);
PrintWriter writer = response.getWriter();
String val = mapper.writeValueAsString(new Result<String>(200, "登录成功!", ""));
writer.write(val);
writer.flush();
writer.close();
}
}

 

4.3、自定义登录失败处理类

import com.bobo.group.springsecuritybase.util.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@Component
public class JsonAuthenticationFailureHandler implements AuthenticationFailureHandler {


@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
ObjectMapper mapper = new ObjectMapper();
response.setHeader("Content-Type", "application/json;charset=utf-8");
response.setStatus(200);
PrintWriter writer = response.getWriter();
String val = mapper.writeValueAsString(new Result<String>(401, "登录失败!", ""));
writer.write(val);
writer.flush();
writer.close();
}
}

 

4.4、统一响应实体类

public class Result<T> {

private Integer code;
private String desc;
private T data;
public Result(Integer code, String desc, T data) {
this.code = code;
this.desc = desc;
this.data = data;
}
public Integer getCode() {return code;}
public void setCode(Integer code) {this.code = code;}
public String getDesc() {return desc;}
public void setDesc(String desc) {this.desc = desc;}
public T getData() {return data;}
public void setData(T data) {this.data = data;}
}

 

4.5、WebSecurityConfig类

import com.bobo.group.springsecuritybase.handler.CustomAuthenticationProvider;
import com.bobo.group.springsecuritybase.handler.JsonAuthenticationFailureHandler;
import com.bobo.group.springsecuritybase.handler.JsonAuthenticationSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

//以下3个类用于自定义authenticationProvider
@Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
@Autowired
private JsonAuthenticationSuccessHandler jsonAuthenticationSuccessHandler;
@Autowired
private JsonAuthenticationFailureHandler jsonAuthenticationFailureHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//ant风格通配符。?匹配单个字符;*匹配0或任意个字符;**匹配0或任意多目录
//需要认证+授权,且有admin(区分大小写)角色才能访问
.antMatchers("/admin/**").hasRole("ADMIN")
//需要认证+授权,且有user角色才能访问;spring security默认用户的角色就是user
.antMatchers("/user/**").hasRole("USER")
//公开资源,不需要认证+授权就能访问
.antMatchers("/app/**","/**/*.html","/**/*.js").permitAll()
.anyRequest().authenticated()
.and().csrf().disable()
.formLogin()
.loginProcessingUrl("/auth/doLogin")
.successHandler(jsonAuthenticationSuccessHandler)
.failureHandler(jsonAuthenticationFailureHandler)
.and()
//当没有权限,响应码设为403,而不是默认的302.如果需要返回json数据也可以定义
.exceptionHandling().authenticationEntryPoint(new Http403ForbiddenEntryPoint());
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//自定义authenticationProvider
//其实也自定义了userDetailsService,因为在自定义的authenticationProvider类中,可以看到注入了userDetailsService
auth.authenticationProvider(customAuthenticationProvider);
}

}

 

4.6、前端登录页面

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="jquery-3.4.1.js"></script>
<script>
$(function () {
$("#login").click(function () {
var username=$("#username").val();
var password=$("#password").val();
$.ajax({
url: "http://localhost:8140/spring-security-base/auth/doLogin",
data:{
"username" : username,
"password" : password
},
method : "post",
success:function (result) {
alert(result.desc)
}
});
});
});

</script>
</head>
<body>
<h1>Spring Security前后端分离登录认证测试</h1>
<div>
用户名:<input type="text" id="username"><br>
密码:<input type="password" id="password"><br>
<button id="login">登录</button>
</div>

</body>
</html>

 

至此,前后端分离登录与自定义AuthenticationProvider就完成了。我们可以访问login.html,输入数据库中对应的用户名与密码进行登录认证。