package org.springframework.security.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
Attempts to authenticate the passed Authentication object, returning a fully populated Authentication object (including granted authorities) if successful...
以下是该类注释文档中的翻译内容
尝试验证传递的身份验证对象,如果成功则返回完全填充的身份验证对象(包括授予的权限)。
AuthenticationManager必须遵守以下关于例外的合同:
如果帐户被禁用,AuthenticationManager可以测试此状态,则必须抛出DisabledException。
如果帐户被锁定,AuthenticationManager可以测试帐户锁定,则必须抛出LockedException。
如果提供了不正确的凭据,则必须抛出BadCredentialsException异常。虽然上述异常是可选的,但AuthenticationManager必须始终测试凭证。
应该按照上述顺序测试异常并抛出异常(即,如果帐户被禁用或锁定,则立即拒绝身份验证请求,并且不执行凭据测试过程)。这可以防止针对禁用或锁定的帐户测试凭据。
参数: (Authentication)
身份验证——身份验证请求对象
返回:
一个经过完全身份验证的对象,包括凭证
抛出:
AuthenticationException -如果认证失败
总的来说
AuthenticationManager这个接口方法非常奇特,入参和返回值的类型都是Authentication。该接口的作用是对用户的未授信凭据进行认证,认证通过则返回授信状态的凭据,否则将抛出认证异常AuthenticationException。
AuthenticationManager的实现类
AuthenticationManager是怎么实现对用户的未授信凭据进行认证的呢?
首先我们来分析以下认证的过程:
1. 用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
2. 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
3. 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
4. SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道web表单的对应的 DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。
下面看一段代码帮助我们理解以下前三步:
这里的JwtLoginAuthenticationFilter继承了AbstractAuthenticationProcessingFilter,起到的作用和UsernamePasswordAuthenticationFilter作用相同
package com.itheima.stock.security.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import com.itheima.stock.pojo.vo.LoginReqVo;
import com.itheima.stock.pojo.vo.LoginRespVoExt;
import com.itheima.stock.pojo.vo.R;
import com.itheima.stock.pojo.vo.ResponseCode;
import com.itheima.stock.security.detail.LoginUserDetail;
import com.itheima.stock.security.utils.JwtTokenUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.MediaType;
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.core.GrantedAuthority;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
/**
* @author Mr.huang
* @version 1.0
* @description 自定义登录过滤器
* @date 2023/2/8 21:09
*/
public class JwtLoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private RedisTemplate redisTemplate;
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/***
* @description 通过构造器传入自定义的登录地址
* @param loginUrl
* @return
* @author huang
* @date 2023/2/8 21:30
*/
public JwtLoginAuthenticationFilter(String loginUrl) {
super(loginUrl);
}
/***
* @description 用户认证处理的方法
* @param httpServletRequest
* @param httpServletResponse 我们约定请求方式必须是post方式,且请求的数据是json格式
* 约定请求是账户:username 密码:password
* @return org.springframework.security.core.Authentication
* @author huang
* @date 2023/2/8 21:31
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
//验证请求是否符合约定:post ajax json
//APPLICATION_JSON_VALUE = "application/json"
if (!(httpServletRequest.getMethod().equalsIgnoreCase("POST") && (httpServletRequest.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) || httpServletRequest.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE))) {
//如果不符合约定,抛出认证服务异常
throw new AuthenticationServiceException("Authentication method not supported: " + httpServletRequest.getMethod());
}
//获取servlet中的数据
//public abstract class ServletInputStream extends InputStream
//这个类定义了一个用来读取客户端的请求信息的输入流。这是一个 Servlet 引擎提供的抽象类。
// 一个 Servlet 通过使用ServletRequest 接口获得了对一个 ServletInputStream 对象的说明。
// 这个类的子类必须提供一个从 InputStream 接口读取有关信息的方法。
ServletInputStream in = httpServletRequest.getInputStream();
/*
Jackson ObjectMapper类(com.fasterxml.jackson.databind.ObjectMapper)是使用Jackson解析JSON最简单的方法。
Jackson ObjectMapper可以从字符串、流或文件解析JSON,并创建Java对象或对象图来表示已解析的JSON。将JSON解析为Java对象也称为从JSON反序列化Java对象
Jackson ObjectMapper也可以从Java对象创建JSON. 从Java对象生成JSON的过程也被称为序列化Java对象到JSON
Jackson对象映射器(Object Mapper)可以把JSON解析为用户自定义类对象, 或者解析为JSON内置的树模型的对象
readValue()方法的第一个参数是JSON数据源(字符串, 流或者文件), 第二个参数是解析目标Java类
*/
LoginReqVo vo = new ObjectMapper().readValue(in, LoginReqVo.class);
//设置响应数据类型和响应字符编码
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpServletResponse.setCharacterEncoding("UTF-8");
//判断参数是否合法
/*
StringUtils的isBlank()方法可以一次性校验这三种情况,返回值都是true,否则为false
如:StringUtils.isBlank(image);
如果image为空,返回值为true
如果返回值不为空,返回值为false
*/
if (vo == null || StringUtils.isBlank(vo.getUsername()) || StringUtils.isBlank(vo.getPassword()) || StringUtils.isBlank(vo.getRkey()) || Strings.isNullOrEmpty(vo.getCode())) {
R<Object> error = R.error(ResponseCode.USERNAME_OR_PASSWORD_ERROR.getMessage());
/*
1、writeValue()
2、writeValueAsString()
两种不同的方法都是把Java对象(student)转为Json格式的数据响应到页面中,writeValue没有返回值,但它实际上也是把student转为String类型的数据,
并且是以Json的格式进行存储,数据返回到前端中可以直接进行解析。
首先,writeValue的使用方法:
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(),Object obj);
其次,writeValueAsString
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(Object obj);
response.getWriter().write(json);
*/
String respStr = new ObjectMapper().writeValueAsString(error);
httpServletResponse.getWriter().write(respStr);
return null;
}
String rCode = (String) redisTemplate.opsForValue().get(vo.getRkey());
if (Strings.isNullOrEmpty(rCode) || !Objects.equals(rCode, vo.getCode())) {
R<Object> error = R.error(ResponseCode.CHECK_CODE_ERROR.getMessage());
String respStr = new ObjectMapper().writeValueAsString(error);
httpServletResponse.getWriter().write(respStr);
return null;
}
redisTemplate.delete(vo.getRkey());
//组装UsernamePasswordAuthenticationToken
/*
1. 用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到,
封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
*/
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(vo.getUsername(), vo.getPassword());
//调用认证管理器进行认证处理
/*
2. 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
*/
return this.getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
}
/***
* @description 用户认证成功后回调的方法
* 认证成功后,响应前端token信息
* @param request
* @param response
* @param chain
* @param authResult
* @return void
* @author huang
* @date 2023/2/9 11:35
*/
/*
3. 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,
身份信息,细节信息,但密码通常会被移除) Authentication 实例。
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//获取用户详情信息
LoginUserDetail userDetail = (LoginUserDetail) authResult.getPrincipal();
//获取用户名称
String username = userDetail.getUsername();
//获取权限集合对象
List<GrantedAuthority> authorities = userDetail.getAuthorities();
String auStrList = authorities.toString();
//复制userDetail属性值到LoginRespVoExt对象即可
LoginRespVoExt resp = new LoginRespVoExt();
BeanUtils.copyProperties(userDetail,resp);
super.successfulAuthentication(request, response, chain, authResult);
//生成token字符串:将用户名称和权限信息价格生成token字符串
String tokenStr = JwtTokenUtil.createToken(username, auStrList);
resp.setAccessToken(tokenStr);
R<LoginRespVoExt> r = R.ok(resp);
String respStr = new ObjectMapper().writeValueAsString(r);
//设置响应数据格式
response.setCharacterEncoding("UTF-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(respStr);
}
/***
* @description 认证失败后,回调的方法
* @param request
* @param response
* @param failed
* @return void
* @author huang
* @date 2023/2/9 11:55
*/
@Override
public void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
R<Object> r = R.error(ResponseCode.USERNAME_OR_PASSWORD_ERROR);
String respStr = new ObjectMapper().writeValueAsString(r);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter().write(respStr);
}
}
认证管理器(AuthenticationManager)委托 AuthenticationProvider完成认证工作,那 AuthenticationProvider怎样完成认证工作的呢?
先看看AuthenticationProvider的源码和实现结构
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
authenticate()方法定义了认证的实现过程,它的参数是一个Authentication,里面包含了登录用户所提交的用 户、密码等。而返回值也是一个Authentication,这个Authentication则是在认证成功后,将用户的权限及其他信 息重新组装后生成。
Spring Security中维护着一个 List 列表,存放多种认证方式,不同的认证方式使用不同的AuthenticationProvider。如使用用户名密码登录时,使用AuthenticationProvider1,短信登录时使用 AuthenticationProvider2等等这样的例子很多。
每个AuthenticationProvider需要实现supports()方法来表明自己支持的认证方式,如我们使用表单方式认证, 在提交请求时Spring Security会生成UsernamePasswordAuthenticationToken,它是一个Authentication,里面 封装着用户提交的用户名、密码信息。而对应的,哪个AuthenticationProvider来处理它?
我们在DaoAuthenticationProvider的父类AbstractUserDetailsAuthenticationProvider发现以下代码:
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
也就是说当web表单提交用户名密码时,AuthenticationManager初始化委托AuthenticationProvider进行处理表单,AuthenticationProvider找到对应的DaoAuthenticationProvider进行处理。
对AuthenticationManager的认证处理过程就追溯到这里,接下来我将收集资料,继续分析SpringSecurity框架中的其他接口/类的基本原理.