Spring Security 认证流程

参考资料

官方指引
官方文档
白话让你理解什么是oAuth2协议
最简单易懂的Spring Security 身份认证流程讲解
Spring Security零基础入门之一
SpringSecurity+JWT认证流程解析
How Spring Security Authentication works - Java Brains(这个视频教程讲的超级详细!!!整个流程的理解可以看它)

Spring Security 采用 AOP,基于 Servlet 过滤器实现的安全框架。它提供了完善的认证机构和授权功能,而在做系统的时候,一般做的第一个模块就是认证与授权模块,因为这是一个系统的入口,也是一个系统最重要最基础的一环,在认证与授权服务设计搭建好了之后,剩下的模块才得以安全访问。

Spring Security 主要功能如下

  • 认证
  • 授权
  • 攻击防护

整个 Spring Security 的类图

这篇笔记主要讲 Spring Security 的认证流程,下一篇笔记才是鉴权流程

Authentication 和 Authorization

认证 (Authentication): 你是谁。

Authentication(认证) 是验证您的身份的凭据(例如用户名/用户ID和密码),通过这个凭据,系统得以知道你就是你,也就是说系统存在你这个用户。所以,Authentication 被称为身份/用户验证。

授权 (Authorization): 你有权限干什么。

Authorization(授权) 发生在 Authentication(认证) 之后。授权嘛,光看意思大家应该就明白,它主要掌管我们访问系统的权限。比如有些特定资源只能具有特定权限的人才能访问比如 admin,有些对系统资源操作比如删除、添加、更新只能特定人才具有。

常用的 Http 认证授权技术

1、基于表单的认证(Cookie & Session):基于表单的认证并不是在 HTTP 协议中定义的,而是服务器自己实现的认证方式,安全程度取决于实现程度。一般用 Cookie 来管理 Session 会话,是最常用的认证方式之一。它的安全程度取决于服务器的实现程度,客户端在Cookie中携带认证信息,服务器解析并返回结果。

2、基于JWT(Json Web Token)的认证:App和服务端常用的认证方式,用户ID和密码传输到服务器上验证,服务器验证通过以后生成加密的JWT Token返回给客户端,客户端再发起请求时携带返回的Token进行认证。(多了个防篡改)

3、Http Basic 认证:最早的 Http 认证方式,用户 ID 和密码以分号连接,经过 Base64 编码后存储到 Authorization 字段,发送到服务端进行认证 ;用户 ID/密码 以明文形式暴露在网络上,安全性较差。(如果没有使用 SSL/TLS 这样的传输层安全的协议,那么以明文传输的密钥和口令很容易被拦截)

4、Http Digest 认证:在 HttpBasic 的基础上,进行了一些安全性的改造,用户ID, 密码 , 服务器/客户端随机数,域,请求信息,经过 MD5 加密后存储到 Authorization 字段,发送到服务端进行认证;密码经过 MD5 加密,安全性比 Basic 略高。

5、其他认证方式(Oauth 认证,单点登陆,HMAC 认证):通过特定的加密字段和加密流程,对客户端和服务端的信息进行加密生成认证字段,放在 Authorization 或者是消息体里来实现客户信息的认证

主要 jar 包功能介绍

Spring Security 主要 jar 包功能介绍

spring-security-core.jar 核心包,任何 Spring Security 功能都需要此包。
spring-security-web.jar web 工程必备,包含过滤器和相关的 Web 安全基础结构代码。
spring-security-config.jar 用于解析 xml 配置文件,用到 Spring Security 的 xml 配置文件的就要用到此包。
spring-security-taglibs.jar Spring Security 提供的动态标签库,jsp 页面可以用。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Security 认证流程

参考资料 Spring Security(一)–Architecture Overview(这是一系列教程)
参考资料 SpringSecurity+JWT认证流程解析

登录认证(Authentication)和访问授权(Authorization)

认证流程如下图所示

这里的 AuthenticationFilter 采用的是责任链设计模式(请求层层上报,直到有人解决为止),一个 web 请求会经过一条过滤器链,在经过过滤器链的过程中会完成认证与授权,如果中间发现这条请求未认证或者未授权,会根据被保护 API 的权限去抛出异常,然后由异常处理器去处理这些异常。

过滤链如下图所示

注:这里 Security 提供两种过滤器类:

UsernamePasswordAuthenticationFilter 表示表单登陆过滤器
BasicAuthenticationFilter 表示 httpBasic 方式登陆过滤器

如上图,一个请求想要访问到 API 就会以从左到右的形式经过蓝线框框里面的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分负责异常处理,橙色部分则是负责授权。

不过上述的两个过滤器是 Spring Security 对 form 表单认证和 Basic 认证内置的两个 Filter,而 JWT 认证方式用不上。

不过这两个过滤器有两个自带的叫 formLogin 和 httpBasic 的配置项

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .and()
        .formLogin() // formLogin 对应着 form 表单认证方式,即 UsernamePasswordAuthenticationFilter
        .and()
        .httpBasic(); // httpBasic 对应着 Basic 认证方式,即 BasicAuthenticationFilter
}

换言之,配置了这两种认证方式,过滤器链中才会加入它们,否则它们是不会被加到过滤器链中去的。

因为 Spring Security 自带的过滤器中是没有针对 JWT 的认证方式,所以需要自己写一个 JWT 的认证过滤器,然后放在绿色的位置进行认证工作。

一些重要的组件

  • SecurityContext:上下文对象,Authentication 对象会放在里面。
  • SecurityContextHolder:用于拿到上下文对象的静态工具类。
  • Authentication:认证接口,定义了认证对象的数据形式。
  • AuthenticationManager:用于校验 Authentication,返回一个认证完成后的 Authentication 对象。

这里建议先看下面 “整合上面四个组件认证流程” 了解下各个组件具体是如何执行的,再回来详细的看各个组件的细节

SecurityContext

上下文对象,认证后的数据就放在这里面,接口定义如下:

public interface SecurityContext extends Serializable {
 // 获取Authentication对象
 Authentication getAuthentication();

 // 放入Authentication对象
 void setAuthentication(Authentication authentication);
}

这个接口里面只有两个方法,其主要作用就是 get or set Authentication

SecurityContextHolder

用于拿到上下文对象的静态工具类。

SecurityContextHolder 用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限… 这些都被保存在 SecurityContextHolder 中。SecurityContextHolder 默认使用 ThreadLocal 策略来存储认证信息。

public class SecurityContextHolder {

 public static void clearContext() {
  strategy.clearContext();
 }

 public static SecurityContext getContext() {
  return strategy.getContext();
 }
    
 public static void setContext(SecurityContext context) {
  strategy.setContext(context);
 }

}

可以说是 SecurityContext 的工具类,用于 get or set or clear SecurityContext,默认会把数据都存储到当前线程中。

从这个 SecurityContextHolder 取得 UserDetail 的实例:

public static String getLoginAccount() {
    // getPrincipal 返回值需要强转为 UserDetail 具体原因看下面的 Authentication那节
    return ((UserDetail) SecurityContextHolder
    .getContext()
    .getAuthentication() // 返回:Authentication
    .getPrincipal()) // 这里就是 Authentication 内部保存的 UserDetail 对象
    .getUsername();
}

Authentication

认证接口,定义了认证对象的数据形式。

public interface Authentication extends Principal, Serializable {
 
 Collection<? extends GrantedAuthority> getAuthorities();
 Object getCredentials();
 Object getDetails();

 // AuthenticationManager 实现通常会返回一个包含更丰富信息的 Authentication 作为供应用程序使用的主体。 
 // 许多身份验证提供程序将创建一个 UserDetails 对象作为主体
 // 所以如果 AuthenticationManager 使用的是 ProviderManager 则这里返回值需要强转为 UserDetails
 Object getPrincipal();

 boolean isAuthenticated();
 void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

这几个方法效果如下:

  • getAuthorities: 获取用户权限,一般情况下获取到的是用户的角色信息。
  • getCredentials: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。
  • getDetails: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)。
  • getPrincipal: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails。
  • isAuthenticated: 获取当前 Authentication 是否已认证。
  • setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。

Authentication 只是定义了一种在 SpringSecurity 进行认证过的数据的数据形式应该是怎么样的,要有权限,要有密码,要有身份信息,要有额外信息。

AuthenticationManager ⭐

public interface AuthenticationManager {
 // 认证方法
 Authentication authenticate(Authentication authentication)
   throws AuthenticationException;
}

AuthenticationManager (接口)是认证相关的核心接口,它定义了一个认证方法,它将一个未认证的 Authentication 传入,返回一个已认证的 Authentication。它的默认实现类是 ProviderManager

注:AuthenticationManager 有多个认证类 AuthenticationManager,ProviderManager ,AuthenticationProvider …

整合上面四个组件认证流程

将这四个部分,串联起来,构成 Spring Security 进行认证的流程:

1、先是一个请求带着身份信息进来,用户名和密码被过滤器获取到,封装成 Authentication,通常情况下是 UsernamePasswordAuthenticationToken 这个实现类。

2、这个 Authentication 经过 AuthenticationManager 的认证(身份管理器负责验证)认证成功后,AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication 实例。

3、SecurityContextHolder 安全上下文容器将上面填充了信息的 Authentication,通过 SecurityContextHolder.getContext().setAuthentication(…) 方法,设置到 SecurityContext 其中。

下面编写一个例子,具体的描述这个流程

public class AuthenticationExample {
    private static final AuthenticationManager am = new SampleAuthenticationManager();

    public static void main(String[] args) throws Exception {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

        while(true) {
            System.out.println("Please enter your username:");
            String name = in.readLine();
            System.out.println("Please enter your password:");
            String password = in.readLine();

            try {
                // 1、封装一个 UsernamePasswordAuthenticationToken 对象
                Authentication request = new UsernamePasswordAuthenticationToken(name, password);

                // 2、经过 AuthenticationManager 的认证,如果认证失败会抛出一个 AuthenticationException 错误
                Authentication result = am.authenticate(request);

                // 3、将这个认证过的 Authentication 填入 SecurityContext 里面
                SecurityContextHolder.getContext().setAuthentication(result);
                break;
            } catch(AuthenticationException e) {
                System.out.println("Authentication failed:" + e.getMessage());
            }
        }

        System.out.println("Successfully authenticated. Security context contains:\n" +
                SecurityContextHolder.getContext().getAuthentication());
    }
}

// 实现一个简单 AuthenticationManager 用于认证
class SampleAuthenticationManager implements AuthenticationManager {
    static final List<GrantedAuthority> AUTHORITIES = new ArrayList<>();

    static {
        AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
    }

    // 关键认证部分
    @Override
    public Authentication authenticate(Authentication auth) throws AuthenticationException {

        // getCredentials 返回的是密码,这里随便写了,直接用户名和密码一致就算登陆成功
        if (auth.getName().equals(auth.getCredentials())) {
            // 认证成功返回一个已经认证的 UsernamePasswordAuthenticationToken 的对象,并把这个用户的权限填入
            return new UsernamePasswordAuthenticationToken(auth.getName(),
                    auth.getCredentials(), AUTHORITIES);
        }
        throw new BadCredentialsException("Bad Credentials");
    }
}

输出为:

注意:上述这段代码只是为了让大家了解 Spring Security 的工作流程而写的,不是什么源码。在实际使用中,整个流程会变得更加的复杂,但是基本思想,和上述代码如出一辙。

ProviderManager

ProviderManager 是 AuthenticationManager 的默认实现类,其实很大一部分工具类都是围绕着这个 ProviderManager 实现类来的,由他衍生出来的 AuthenticationProvider 接口

下面来看下只保留了关键认证部分的 ProviderManager 源码:(主要看这里的 providers 的注释)

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

    // 这里维护着一个 AuthenticationProvider 列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。
    // 也就是说,核心的认证入口始终只有一个:AuthenticationManager
    // 例如下面不同的认证方式,对应了三个 AuthenticationProvider:
    // 1、用户名 + 密码(UsernamePasswordAuthenticationToken),
    // 2、邮箱 + 密码,
    // 3、手机号码 + 密码登录
    // 在默认策略下,只需要通过一个 AuthenticationProvider 的认证,即可被认为是登录成功。
    private List<AuthenticationProvider> providers = Collections.emptyList();


    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;

        // ProviderManager 中的 List(providers),会依照次序去认证,认证成功则立即返回,
        // 若认证失败则返回 null,下一个 AuthenticationProvider 会继续尝试认证,如果所有
        // 认证器都无法认证成功,则 ProviderManager 会抛出一个 ProviderNotFoundException 异常。
        for (AuthenticationProvider provider : getProviders()) {
            
            // 这个 supports 方法用来判断此AuthenticationProvider 是否支持当前的 Authentication 对象
            if (!provider.supports(toTest)) {
                continue;
            }
            try {
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
          ...
          catch (AuthenticationException e) {
                lastException = e;
            }
        }

        // 如果有 Authentication 信息,则直接返回
        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                // 移除密码
                ((CredentialsContainer) result).eraseCredentials();
            }
            // 发布登录成功事件
            eventPublisher.publishAuthenticationSuccess(result);
            return result;
        }
	   ...

        // 执行到此,说明没有认证成功,包装异常信息
        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage(
                    "ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() },
                    "No AuthenticationProvider found for {0}"));
        }
        prepareException(lastException, authentication);
        throw lastException;
    }
}

AuthenticationManager 的默认实现类 ProviderManager,可以发现就是它的内部就是通过注册一些 AuthenticationProvider,然后维护一个 List<AuthenticationProvider> 依次对 Authentication 进行比对

把上面的代码整理成图,流程如下所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fl2C45O8-1618590250111)(https://images.alsritter.icu/images/2021/03/26/20210326110139.png)]

ProviderManager 体系类图

这整个 ProviderManager 体系的类图如下

AuthenticationProvider 接口 ⭐

这里 AuthenticationProvider 接口就下面两个实现类

public interface AuthenticationProvider {

    // 认证
    Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
    
    // 这个 supports 方法用来判断此AuthenticationProvider 是否支持当前的 Authentication 对象
    boolean supports(Class<?> authentication);
}

就是输入一个凭证,输出一个 Principal(输入输出都是封装在 Authentication 里面的)


而它们有可以有多个类型的认证方式

如何组装这些认证呢?所以这时就需要 AuthenticationManager 上马工作了,当然并不是直接使用它,而是使用它的实现类 ProviderManager 它会执行这里注册进来的每一个 AuthenticationProvider 直到认证通过为止(全部都无法通过表示认证失败)

那这些 AuthenticationProvider 需要做什么工作呢?首先通过 username 取得数据库里面的用户数据,所以这时就可以使用这个 UserDetailService 来取得数据

这个 UserDetailService 会将用户数据填充到 Authentication 里面去

最后的最后,为了让其它的过滤器也能获取到这个已经认证过的 Authentication,Spring Security 会将其存到上下文中(内部是一个 ThreadLocal)

ProviderManager 如何维护 AuthenticationProvider

所以实际原理就是 ProviderManager 维护了一个 AuthenticationProvider 数组,它们都可以用来进行认证,当前用户的凭证在某个 AuthenticationProvider 无法通过时就换下一个继续,直到全部都无法通过才算认证失败,而当有一个成功了则表示认证成功

AuthenticationProvider 接口的实现较多,有:

所以,认证流程,总体上是这样的:

DaoAuthenticationProvider

这个 DaoAuthenticationProvider 就是 AuthenticationProvider 的最常实现类,下面对其进行一些说明。

顾名思义,Dao 正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。

按照我们最直观的思路,怎么去认证一个用户呢?

用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名,提交的密码和保存的密码是否相同便是了。

在 Spring Security 中。提交的用户名和密码,被封装成了 UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了 UserDetailsService

取得用户

在 DaoAuthenticationProvider 中,对应的根据用户名加载用户的方法便是 retrieveUser。

// 虽然有两个参数,但是 retrieveUser 只有第一个参数(username)起主要作用,返回一个 UserDetails。
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
		throws AuthenticationException {
	prepareTimingAttackProtection();
	try {
		UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
		if (loadedUser == null) {
			throw new InternalAuthenticationServiceException(
					"UserDetailsService returned null, which is an interface contract violation");
		}
		return loadedUser;
	}
	catch (UsernameNotFoundException ex) {
		mitigateAgainstTimingAttack(authentication);
		throw ex;
	}
	catch (InternalAuthenticationServiceException ex) {
		throw ex;
	}
	catch (Exception ex) {
		throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
	}
}
比对密码

还需要完成 UsernamePasswordAuthenticationToken 和 UserDetails 密码的比对,这便是交给 additionalAuthenticationChecks 方法完成的(注意,这个方法是在父类 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法中被调用的),如果这个 void 方法没有抛异常,则认为比对成功。

// 虽然这个方法启用了,但是好像执行的还是这个方法
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
		UsernamePasswordAuthenticationToken authentication)
		throws AuthenticationException {
	if (authentication.getCredentials() == null) {
		logger.debug("Authentication failed: no credentials provided");
		throw new BadCredentialsException(messages.getMessage(
				"AbstractUserDetailsAuthenticationProvider.badCredentials",
				"Bad credentials"));
	}
	String presentedPassword = authentication.getCredentials().toString();
	if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
		logger.debug("Authentication failed: password does not match stored value");
		throw new BadCredentialsException(messages.getMessage(
				"AbstractUserDetailsAuthenticationProvider.badCredentials",
				"Bad credentials"));
	}
}

比对密码的过程,用到了 PasswordEncoder 和 SaltSource,密码加密和盐的概念相信不用我赘述了,它们为保障安全而设计,都是比较基础的概念。(这里的 PasswordEncoder 和 UserDetailsService 就是传入进来的)

注意,虽然注解显示这个 additionalAuthenticationChecks 方法弃用了,但是官方还是用这个比对密码的(如下执行时的断点)

总之就是 DaoAuthenticationProvider:它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。

UserDetails

上面不断提到了 UserDetails 这个接口,它代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。

public interface UserDetails extends Serializable {

   // 取得权限
   Collection<? extends GrantedAuthority> getAuthorities();

   String getPassword();

   String getUsername();

   boolean isAccountNonExpired();

   boolean isAccountNonLocked();

   boolean isCredentialsNonExpired();

   boolean isEnabled();
}

它和 Authentication 接口很类似,比如它们都拥有 username,authorities,区分他们也是本文的重点内容之一。Authentication 的 getCredentials() 与 UserDetails 中的 getPassword() 需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。

Authentication 中的 getAuthorities() 实际是由 UserDetails 的 getAuthorities() 传递而形成的。

还记得 Authentication 接口中的 getUserDetails() 方法吗?其中的 UserDetails 用户详细信息便是经过了 AuthenticationProvider 之后被填充的。

UserDetailsService

参考资料 Spring Security Custom Authentication - AuthenticationProvider vs UserDetailsService

public interface UserDetailsService {
   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetailsService 和 AuthenticationProvider 两者的职责常常被人们搞混,UserDetailsService 它纯粹是一个用于用户数据的 DAO,除了向框架内的其他组件提供该数据之外,没有其他功能。特别是,它不对用户进行身份验证,这是由 AuthenticationManager 完成的。所以在多数情况下,如果需要自定义身份验证过程,直接实现 AuthenticationProvider 更有意义。

UserDetailsService 常见的实现类有

  • JdbcDaoImpl 从数据库加载用户
  • InMemoryUserDetailsManager 从内存中加载用户
  • 也可以自己实现 UserDetailsService,通常这更加灵活。

具体实现流程

定义加密器Bean

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

这个 Bean 是不必可少的,Spring Security 在认证操作时会使用我们定义的这个加密器,如果没有则会出现异常。

AuthenticationManager

@Bean
public AuthenticationManager authenticationManager() throws Exception {
    return super.authenticationManager();
}

这里将 Spring Security 自带的 authenticationManager 声明成 Bean,声明它的作用是用它帮我们进行认证操作,调用这个 Bean 的 authenticate 方法会由 Spring Security 自动帮我们做认证。

UserInfo 用户信息表

创建一个实体来保存用户信息

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class UserInfo implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id")
    private String id;

    private String username;

    private String password;

    private Integer activeStatus;

    private LocalDateTime createTime;
}

RoleInfo 权限表

创建一个实体来保存权限表

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class RoleInfo implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id")
    private String id;

    private String roleName;

    private String roleCode;

    private String roleRemark;

    private Integer activeStatus;

    private LocalDateTime createTime;
}

自定义 UserDetail

这个 UserDetail 用来保存用户信息

上面已经说过了 Authentication 中的 getAuthorities() 实际是由 UserDetails 的 getAuthorities() 传递而形成的。所以需要实现这个 getAuthorities 方法来填充权限

而这个 UserDetail 最终会被存入 Authentication 里面,可以通过 Authentication 的 getPrincipal() 方法取得这个存入的 UserDetail

@Data
public class UserDetail implements Serializable, UserDetails {
    private static final long serialVersionUID = 1L;

    // 这一块都是自定义的需要保存的数据,只需要能把 UserDetails 需要的数据返回出去,这里存的是啥都没关系
    private UserInfo userInfo;
    private List<RoleInfo> roleInfoList;
    private Collection<? extends GrantedAuthority> grantedAuthorities;
    private List<String> roles;

    public String getUserId() {
        return this.userInfo.getId();
    }

    // 返回当前用户信息的权限
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (grantedAuthorities != null) return this.grantedAuthorities;

        // 从 roleInfoList 中取出当前用户拥有的权限,填充进数组
        List<SimpleGrantedAuthority> grantedAuthorities = new ArrayList<>();
        // 使用 String 来保存权限
        List<String> authorities = new ArrayList<>();
        roleInfoList.forEach(role -> {
            authorities.add(role.getRoleCode());
            grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleCode()));
        });

        this.grantedAuthorities = grantedAuthorities;
        this.roles = authorities;
        return this.grantedAuthorities;
    }

    @Override
    public String getPassword() {
        return this.userInfo.getPassword();
    }

    @Override
    public String getUsername() {
        return this.userInfo.getUsername();
    }

    /**
     * 账户是否没过期
     *
     * @return boolean
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 账户是否没被锁定
     *
     * @return boolean
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 账户凭据是否没过期
     *
     * @return boolean
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 账户是否启用
     *
     * @return boolean
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

实现 UserDetailsService

这个 UserDetailsService 是在 AuthenticationProvider(DaoAuthenticationProvider 这个实现类的代码看上面) 里面的 retrieveUser 方法里面调用

实现 UserDetailsService 的抽象方法并返回一个 UserDetails 对象,认证过程中 SpringSecurity 会调用这个方法访问数据库进行对用户的搜索(传入一个用户名),逻辑什么都可以自定义,无论是从数据库中还是从缓存中,但是我们需要将我们查询出来的用户信息和权限信息组装成一个 UserDetails 返回

说白了,查询用户是否存在就是这步

UserDetails 也是一个定义了数据形式的接口,用于保存我们从数据库中查出来的数据,其功能主要是验证账号状态和获取权限,具体看上面。

@Slf4j
@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleInfoService roleInfoService;

    @Override
    public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
        log.debug("开始登陆验证,用户名为: {}", name);

        // 根据用户名验证用户
        QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(UserInfo::getLoginAccount, name);

        UserInfo userInfo = userService.getOne(queryWrapper);
        if (userInfo == null) {
            throw new UsernameNotFoundException("用户名不存在,登陆失败。");
        }

        // 构建UserDetail对象
        UserDetail userDetail = new UserDetail();
        userDetail.setUserInfo(userInfo);
        List<RoleInfo> roleInfoList = roleInfoService.listRoleByUserId(userInfo.getUserId());
        userDetail.setRoleInfoList(roleInfoList);
        return userDetail;
    }
}

编写 Controller

这里没什么好讲的,就是将登陆请求转发 authService,这个 api 地址会在下面访问控制那里注册进去

注:这里的 ApiResult 是自定义的标准返回对象,这里就不细述了

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthService authService;
    @Autowired
    private JwtProvider jwtProvider;

    @PostMapping("/login")
    public ApiResult login(@Valid @RequestBody LoginInfo loginInfo) {
        // 这里的 loginAccount 就是 username
        return authService.login(loginInfo.getLoginAccount(), loginInfo.getPassword());
    }

    @PostMapping("/logout")
    public ApiResult logout() {
        return authService.logout();
    }

    @PostMapping("/refresh")
    public ApiResult refreshToken(HttpServletRequest request) {
        return authService.refreshToken(jwtProvider.getToken(request));
    }
}

编写认证服务 ⭐

先创建一个 AuthService 接口

public interface AuthService {
    // 这里的 loginAccount 就是 username
    ApiResult login(String loginAccount, String password);

    ApiResult logout();

    ApiResult refreshToken(String token);
}

编写实现类,这里的 Cache 就是封装了一下 CacheManager,细节直接看 原作者的源码,实际上使用 Redis 当缓存也是可以这么玩的,这里学习一下

这里具体的 Jwt 细节就不写了,都大同小异,关键在于下面的 login 执行流程

@Slf4j
@Service
public class AuthServiceImpl implements AuthService {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private JwtProvider jwtProvider;
    @Autowired
    private Cache caffeineCache;


    @Override
    public ApiResult login(String loginAccount, String password) {

        log.debug("进入login方法");
        // 1 创建 UsernamePasswordAuthenticationToken 这里的 loginAccount 就是 username
        UsernamePasswordAuthenticationToken usernameAuthentication = new UsernamePasswordAuthenticationToken(loginAccount, password);
        // 2 认证
        Authentication authentication = this.authenticationManager.authenticate(usernameAuthentication);
        // 3 保存认证信息
        SecurityContextHolder.getContext().setAuthentication(authentication);
        // 4 生成自定义token
        AccessToken accessToken = jwtProvider.createToken((UserDetails) authentication.getPrincipal());
        UserDetail userDetail = (UserDetail) authentication.getPrincipal();

        // 5 放入缓存
        caffeineCache.put(CacheName.USER, userDetail.getUsername(), userDetail);
        return ApiResult.ok(accessToken);
    }

    @Override
    public ApiResult logout() {
        caffeineCache.remove(CacheName.USER, AuthProvider.getLoginAccount());
        SecurityContextHolder.clearContext();
        return ApiResult.ok();
    }

    @Override
    public ApiResult refreshToken(String token) {
        AccessToken accessToken = jwtProvider.refreshToken(token);
        UserDetail userDetail = caffeineCache.get(CacheName.USER, accessToken.getLoginAccount(), UserDetail.class);
        caffeineCache.put(CacheName.USER, accessToken.getLoginAccount(), userDetail);
        return ApiResult.ok(accessToken);
    }
}
具体认证步骤

1、传入用户名和密码创建了一个 UsernamePasswordAuthenticationToken 对象,这是前面说过的 Authentication 的实现类,传入用户名和密码做构造参数,这个对象就是我们创建出来的未认证的 Authentication 对象。

2、使用先前已经声明过的 Bean-authenticationManager 调用它的 authenticate 方法进行认证,返回一个认证完成的 Authentication 对象。

3、认证完成没有出现异常,就会走到第三步,使用 SecurityContextHolder 获取 SecurityContext 之后,将认证完成之后的 Authentication 对象,放入上下文对象。

4、从 Authentication 对象中拿到我们的 UserDetails 对象,之前我们说过,认证后的 Authentication 对象调用它的 getPrincipal() 方法就可以拿到我们先前数据库查询后组装出来的 UserDetails 对象,然后创建 token。

5、把 UserDetails 对象放入缓存中,方便后面过滤器使用。

这样的话就算完成了,感觉上很简单,因为主要认证操作都会由 authenticationManager.authenticate() 帮我们完成。

authenticate 方法

这个 authenticate 方法就是认证的核心方法,它位于 AbstractUserDetailsAuthenticationProvider 这个抽象类里面,而大部分具体的 AuthenticationProvider 都是先继承自这个抽象类,具体看上面的类图

接下来我们可以看看源码,从中窥得 Spring Security 是如何帮我们做这个认证的(省略了一部分):

// AbstractUserDetailsAuthenticationProvider
// 注意这个 AbstractUserDetailsAuthenticationProvider 类是上面的 DaoAuthenticationProvider 的父类,
// 认证部分都是在这个父类做的
public Authentication authenticate(Authentication authentication) {

    // 校验未认证的Authentication对象里面有没有用户名
    String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
            : authentication.getName();

    boolean cacheWasUsed = true;
    // 从缓存中去查用户名为XXX的对象
    UserDetails user = this.userCache.getUserFromCache(username);
    // 如果没有就进入到这个方法
    if (user == null) {
        cacheWasUsed = false;
        try {
            // 调用我们重写 UserDetailsService 的 loadUserByUsername 方法
            // 拿到我们自己组装好的 UserDetails 对象
            user = retrieveUser(username,
                    (UsernamePasswordAuthenticationToken) authentication);

        } catch (UsernameNotFoundException notFound) {
            logger.debug("User '" + username + "' not found");
            if (hideUserNotFoundExceptions) {
                throw new BadCredentialsException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "Bad credentials"));
            } else {
                throw notFound;
            }
        }
        Assert.notNull(user,
                "retrieveUser returned null - a violation of the interface contract");
    }
    try {
        // 校验账号是否禁用
        preAuthenticationChecks.check(user);
        // 校验数据库查出来的密码,和我们传入的密码是否一致
        additionalAuthenticationChecks(user,
                (UsernamePasswordAuthenticationToken) authentication);
    }
}

添加 JWT 过滤器

有了 token 之后,我们要把过滤器放在过滤器链中,用于解析 token,因为我们没有 session,所以我们每次去辨别这是哪个用户的请求的时候,都是根据请求中的 token 来解析出来当前是哪个用户。

所以我们需要一个过滤器去拦截所有请求,前文我们也说过,这个过滤器我们会放在绿色部分用来替代 UsernamePasswordAuthenticationFilter,所以我们新建一个 JwtAuthenticationTokenFilter,然后将它注册为 Bean,并在编写配置文件的时候需要加上这个:

@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
    return new JwtAuthenticationTokenFilter();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 将自定义的 JWT 过滤器放到过滤链中
    http.addFilterBefore(jwtAuthenticationTokenFilter(),
                    UsernamePasswordAuthenticationFilter.class);
}

addFilterBefore 的语义是添加一个 Filter 到 XXXFilter 之前,放在这里就是把 JwtAuthenticationTokenFilter 放在 UsernamePasswordAuthenticationFilter 之前,因为 filter 的执行也是有顺序的,我们必须要把我们的 filter 放在过滤器链中绿色的部分才会起到自动认证的效果。

具体实现代码就不写了,可以直接看 作者的源码

配置访问控制

最后把上面的都配置进来

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 注册自定义的 JWT 过滤器
    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
        return new JwtAuthenticationTokenFilter();
    }

    // 注册上面自定义实现的 UserDetailsService
    @Bean
    public CustomUserDetailsService  customUserDetailsService() {
        return new CustomUserDetailsService();
    }

    // 注意,如果直接调用了 AuthenticationManager 来做认证,需要将其注册进去
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    // 注意这里配置了默认的 AuthenticationManager
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserDetailsService()).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()
                // 放行所有OPTIONS请求
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                // 放行登录方法
                .antMatchers("/api/auth/login").permitAll()
                // 认证只需上面的 /api/auth/login 就行了,下面的属于鉴权部分,看下一篇笔记
                .and()
                // 将自定义的JWT过滤器放到过滤链中
                .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class)
                // 打开Spring Security的跨域
                .cors()
                .and()
                // 关闭CSRF
                .csrf().disable()
                // 关闭Session机制
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Security认证流程可以简要描述如下: 1. 当一个请求进入系统时,会经过一个过滤器链,其中包括一个实现了AbstractAuthenticationProcessingFilter的过滤器。 2. 这个过滤器会首先判断请求的URI是否需要认证,如果需要认证,则执行attemptAuthentication方法进行认证。 3. attemptAuthentication方法会调用AuthenticationManager进行认证,AuthenticationManager是一个接口,具体的实现类是ProviderManager。 4. ProviderManager内部包含了一个List<AuthenticationProvider>对象,通过AuthenticationProvider接口的实现类来扩展不同的认证提供者。 5. 在认证过程中,AuthenticationManager会依次调用每个AuthenticationProvider的authenticate方法进行认证。 6. 如果认证成功,会将认证后的Authentication对象存放到SecurityContext中。 7. 如果认证失败,会通过认证失败处理器AuthenticationFailureHandler进行处理。 8. 认证成功后,会执行successfulAuthentication方法,将已认证的Authentication存放到SecurityContext中。 9. 这样,下一个请求进来时,系统就能知道该请求是否已经通过认证。 总结起来,Spring Security认证流程包括了过滤器链、认证管理器认证提供者和认证失败处理器等组件,通过这些组件的协作,实现了对请求的认证和授权。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *2* [SpringSecurity认证流程分析](https://blog.csdn.net/chisuisi5702/article/details/126281839)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Spring Security认证过程](https://blog.csdn.net/weixin_38927257/article/details/102960752)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值