1 安全认证的基本概念
两个基本概念
-
认证(Authentication)
-
授权(Authorization)
1.1 认证
认证就是根据用户名密码登录的过程,就是所谓的登录认证 对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验:
-
如果校验通过,则:正常返回数据。
-
如果校验未通过,则:抛出异常,告知其需要先进行登录。
那么,判断会话是否登录的依据是什么?先来简单分析一下登录访问流程:
-
用户提交
name
+password
参数,调用登录接口。 -
登录成功,返回这个用户的 Token 会话凭证。
-
用户后续的每次请求,都携带上这个 Token。
-
服务器根据 Token 判断此会话是否登录成功。
所谓登录认证,指的就是服务器校验账号密码,为用户颁发 Token 会话凭证的过程,这个 Token 也是我们后续判断会话是否登录的关键所在。
1.2 授权(鉴权)
所谓权限认证,核心逻辑就是判断一个账号是否拥有指定权限:
-
有,就让你通过。
-
没有,那么禁止访问!
深入到底层数据中,就是每个账号都会拥有一组权限码集合,框架来校验这个集合中是否包含指定的权限码。
例如:当前账号拥有权限码集合 ["user-add", "user-delete", "user-get"]
,这时候我来校验权限 "user-update"
,则其结果就是:验证失败,禁止访问。
2 Spring Security 核心组件
Spring Security 核心组件有 Authentication(认证/身份验证) 、 AuthenticationProvider(认证提供者) 、 AuthenticationManager(认证管理者) 比较重要的两个感念
-
Authentication:认证,其实就是身份的识别,也就是登录
-
Authorization:授权(鉴权),根据角色进行权限控制
2.1 凭证 Authentication
Authentication 直译是“认证”的意思,在 Spring Security 中, Authentication 接口用来表示凭证或者令牌,可以理解为用户的用户名、密码、权限等信息。
Authentication 的代码如下:
public interface Authentication extends Principal, Serializable {
//权限集合
//可使用 AuthorityUtils.commaSeparatedStringToAuthorityList("admin, ROLE_ADMIN")进行初始化
Collection<? extends GrantedAuthority> getAuthorities();
//用户名和密码认证时,可以理解为密码
Object getCredentials();
//认证时包含的一些详细信息,可以是一个包含用户信息的 POJO 实例
Object getDetails();
//用户名和密码认证时,可以理解为用户名
Object getPrincipal();
//是否认证通过,通过为 true
boolean isAuthenticated();
//设置是否认证通过
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Spring Security常见的内置的实现类
-
UsernamePasswordAuthenticationToken: 用户名、密码认证的场景中作为验证的凭证
-
RememberMeAuthenticationToken: “记住我”的身份认证场景
-
AnonymousAuthenticationToken: 匿名访问的用户
除了以上内置凭证类外,还可以通过实现 Authentication 定制自己的身份认证实现类。
2.2 认证提供者 AuthenticationProvider
AuthenticationProvider 是一个接口,包含两个函数 authenticate 和 supports,用于完成对凭证进行身份认证操作。
public interface AuthenticationProvider {
//对实参 authentication 进行身份认证操作
Authentication authenticate(Authentication authentication) throws AuthenticationException;
//判断是否支持该 authentication
boolean supports(Class<?> authentication);
}
AuthenticationProvider 接口常见内置的实现类:
-
AbstractUserDetailsAuthenticationProvider:对UsernamePasswordAuthenticationToken 类型的凭证/令牌进行验证的认证提供者类,用于“用户名+密码”验证的场景。
-
RememberMeAuthenticationProvider: 对 RememberMeAuthenticationToken 类型的凭证/令牌进行验证的认证提供者类,用于“记住我”的身份认证场景。
-
AnonymousAuthenticationProvider: 这是一个对 AnonymousAuthenticationToken 类型的凭证/令牌进行验证的认证提供者类,用于匿名身份认证场景。
除此之外,可以通过实现 AuthenticationProvider 接口来扩展出自定义的认证提供者。
2.3 认证管理者 AuthenticationManager
AuthenticationManager 是一个接口,其唯一的 authenticate 验证方法是认证流程的入口,接收一个 Authentication 令牌对象作为参数。
public interface AuthenticationManager {
//认证流程的入口
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationManager 的一个实现类名为 ProviderManager,该类有一个 providers 成员变量,负责管理一个提供者清单列表,其源码如下:
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
//提供者清单
private List<AuthenticationProvider> providers = Collections.emptyList();
//迭代提供者清单,找出支持令牌的提供者,交给提供者去执行令牌验证
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
}
}
认证管理者 ProviderManager 在进行令牌验证时,会对提供者列表进行迭代,找出支持令牌的认证提供者,并交给认证提供者去执行令牌验证。如果该认证提供者的 supports 方法返回 true,就会调用该提供者的 authenticate 方法。 如果验证成功, 那么整个认证过程结束;如果不成功, 那么继续处理列表中的下一个提供者。只要有一个验证成功, 就会认证成功。
3 Spring Security入门案例
-
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
-
HelloController
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "欢迎访问 hangge.com";
}
}
- 启动
生成密码如上图,用户名user
-
配置用户名和密码
如果对默认的用户名和密码不满意,可以在 application.properties 中配置默认的用户名、密码和角色。
spring.security.user.name=lxs
spring.security.user.password=1
spring.security.user.roles=admin
4 Spring Security进阶
4.1 简单认证流程实战
简单认证的处理流程大致包括以下步骤:
-
定制一个凭证/令牌类。
-
定制一个认证提供者类和凭证/令牌类进行配套,并完成对自制凭证/令牌实例的验证。
-
定制一个过滤器类,从请求中获取用户信息组装成定制凭证/令牌,交给认证管理者。
-
定制一个 HTTP 的安全认证配置类(AbstractHttpConfigurer 子类),将上一步定制的过滤器加入请求的过滤链。
-
定义一个 Spring Security 安全配置类(WebSecurityConfigurerAdapter 子类), 对 Web容器的 HTTP 安全认证机制进行配置。
案例:当系统资源被访问时,过滤器从 HTTP 的 token 请求头获取用户名和密码,然后与系统中的用户信息进行匹配,如果匹配成功, 就可以访问系统资源,否则返回 403 响应码,表示未授权。
-
DemoToken
定义自定义令牌类代码如下:
public class DemoToken extends AbstractAuthenticationToken
{
//用户名称
private String userName;
//密码
private String password;
...
}
-
DemoAuthProvider
与DemoToken匹配的验证提供者DemoAuthProvider代码如下:
public class DemoAuthProvider implements AuthenticationProvider {
public DemoAuthProvider(){
}
//模拟的数据源,实际场景从 DB 中获取
private Map<String, String> map = new LinkedHashMap<>();
//初始化模拟的数据源,放入两个用户
{
map.put("zhangsan", "123456" );
map.put("lisi", "123456" );
}
//具体的验证令牌方法
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
DemoToken token = (DemoToken) authentication;
//从数据源 map 中获取用户密码
String rawPass = map.get(token.getUserName());
//验证密码,如果不相等,就抛出异常
if (!token.getPassword().equals(rawPass))
{
token.setAuthenticated(false);
throw new BadCredentialsException("认证有误:令牌校验失败" );
}
//验证成功
token.setAuthenticated(true);
return token;
}
/**
*判断令牌是否被支持
*@param authentication 这里仅仅 DemoToken 令牌被支持
*@return
*/
@Override
public boolean supports(Class<?> authentication)
{
return authentication.isAssignableFrom(DemoToken.class);
}
}
-
DemoAuthFilter
定制一个过滤器类DemoAuthFilter,从请求头中获取 token 字段,解析之后组装成 DemoToken 令牌实例,提交给 AuthenticationManager 进行验证。
public class DemoAuthFilter extends OncePerRequestFilter
{
//认证失败的处理器
private AuthenticationFailureHandler failureHandler = new AuthFailureHandler();
...
//authenticationManager 是认证流程的入口,接收一个 Authentication 令牌对象作为参数
private AuthenticationManager authenticationManager;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException
{
...
AuthenticationException failed = null;
try
{
Authentication returnToken=null;
boolean succeed=false;
//从请求头中获取认证信息
String token = request.getHeader(SessionConstants.AUTHORIZATION_HEAD);
String[] parts = token.split(",");
//组装令牌
DemoToken demoToken = new DemoToken(parts[0],parts[1]);
//提交给 AuthenticationManager 进行令牌验证
returnToken = (DemoToken) this.getAuthenticationManager().authenticate(demoToken);
//获取认证成功标志
succeed=demoToken.isAuthenticated();
if (succeed)
{
//认证成功,设置上下文令牌
SecurityContextHolder.getContext().setAuthentication(returnToken);
//执行后续的操作
filterChain.doFilter(request, response);
return;
}
} catch (Exception e)
{
logger.error("认证有误", e);
failed = new AuthenticationServiceException("请求头认证消息格式错误",e );
}
if(failed == null)
{
failed = new AuthenticationServiceException("认证失败");
}
//认证失败了
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, failed);
}
...
}
-
AbstractHttpConfigurer
为了使得过滤器能够生效,必须将过滤器加入 Web 容器的 HTTP 过滤处理责任链,此项工作可以通过实现一个 AbstractHttpConfigurer 配置类来完成。
public class DemoAuthConfigurer<T extends DemoAuthConfigurer<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T,B>
{
//创建认证过滤器
private DemoAuthFilter authFilter = new DemoAuthFilter();
//将过滤器加入 http 过滤处理责任链
@Override
public void configure(B http) throws Exception
{
//获取 Spring Security 共享的 AuthenticationManager 认证管理者实例
//将其设置到认证过滤器
authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
DemoAuthFilter filter = postProcess(authFilter);
//将过滤器加入 http 过滤处理责任链
http.addFilterBefore(filter, LogoutFilter.class);
}
}
-
DemoWebSecurityConfig
定义一个 Spring Security 安全配置类(WebSecurityConfigurerAdapter 子类), 对 Web 容器的 HTTP 安全认证机制进行配置。有两项工作:
-
应用DemoAuthConfigurer 配置类;
-
构造 AuthenticationManagerBuilder 认证管理者实例。
@EnableWebSecurity
public class DemoWebSecurityConfig extends WebSecurityConfigurerAdapter
{
//配置 HTTP 请求的安全策略,应用 DemoAuthConfigurer 配置类实例
protected void configure(HttpSecurity http) throws Exception
{
http.csrf().disable()
...
.and()
//应用 DemoAuthConfigurer 配置类
.apply(new DemoAuthConfigurer<>())
.and()
.sessionManagement().disable();
}
//配置认证 Builder,由其负责构造 AuthenticationManager 认证管理者实例
//Builder 将构造 AuthenticationManager 实例,并且作为 HTTP 请求的共享对象存储
//在代码中可以通过 http.getSharedObject(AuthenticationManager.class) 来获取管理者实例
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
//加入自定义的 Provider 认证提供者实例
auth.authenticationProvider(demoAuthProvider());
}
//自定义的认证提供者实例
@Bean("demoAuthProvider" )
protected DemoAuthProvider demoAuthProvider()
{
return new DemoAuthProvider();
}
}
-
执行测试
通过swagger-ui界面直接访问返回403,表示认证失败
输入用户名,密码(zhangsan,123456),认证成功
4.2 基于数据源认证流程实战
4.2.1 常见内置类
生产场景中,用户信息都存储在某个数据源(如数据库) 中,认证过程中涉及从数