SpringSecurity框架原理浅谈之AuthenticationManager

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框架中的其他接口/类的基本原理.

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr.huang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值