SpringSecurity+JWT认证流程解析
纸上得来终觉浅,觉知此事要躬行。
楔子
本文适合: 对Spring Security有一点了解或者跑过简单demo但是对整体运行流程不明白的同学,对SpringSecurity有兴趣的也可以当作你们的入门教程,示例代码中也有很多注释。
本文代码: 码云地址 GitHub地址
大家在做系统的时候,一般做的第一个模块就是认证与授权模块,因为这是一个系统的入口,也是一个系统最重要最基础的一环,在认证与授权服务设计搭建好了之后,剩下的模块才得以安全访问。
市面上一般做认证授权的框架就是shiro
和Spring Security
,也有大部分公司选择自己研制。出于之前看过很多Spring Security
的入门教程,但都觉得讲的不是太好,所以我这两天在自己鼓捣Spring Security
的时候萌生了分享一下的想法,希望可以帮助到有兴趣的人。
Spring Security
框架我们主要用它就是解决一个认证授权功能,所以我的文章主要会分为两部分:
- 第一部分认证(本篇)
- 第二部分授权(放在下一篇)
我会为大家用一个Spring Security + JWT + 缓存的一个demo来展现我要讲的东西,毕竟脑子的东西要体现在具体事物上才可以更直观的让大家去了解去认识。
学习一件新事物的时候,我推荐使用自顶向下的学习方法,这样可以更好的认识新事物,而不是盲人摸象。
注:只涉及到用户认证授权不涉及oauth2之类的第三方授权。
1. 📖SpringSecurity的工作流程
想上手 Spring Security 一定要先了解它的工作流程,因为它不像工具包一样,拿来即用,必须要对它有一定的了解,再根据它的用法进行自定义操作。
我们可以先来看看它的工作流程:
在Spring Security的
官方文档上有这么一句话:
Spring Security’s web infrastructure is based entirely on standard servlet filters.
Spring Security 的web基础是Filters。
这句话展示了Spring Security
的设计思想:即通过一层层的Filters来对web请求做处理。
放到真实的Spring Security
中,用文字表述的话可以这样说:
一个web请求会经过一条过滤器链,在经过过滤器链的过程中会完成认证与授权,如果中间发现这条请求未认证或者未授权,会根据被保护API的权限去抛出异常,然后由异常处理器去处理这些异常。
用图片表述的话可以这样画,这是我在百度找到的一张图片:
如上图,一个请求想要访问到API就会以从左到右的形式经过蓝线框框里面的过滤器,其中绿色部分是我们本篇主要讲的负责认证的过滤器,蓝色部分负责异常处理,橙色部分则是负责授权。
图中的这两个绿色过滤器我们今天不会去说,因为这是Spring Security对form表单认证和Basic认证内置的两个Filter,而我们的demo是JWT认证方式所以用不上。
如果你用过Spring Security
就应该知道配置中有两个叫formLogin
和httpBasic
的配置项,在配置中打开了它俩就对应着打开了上面的过滤器。
formLogin
对应着你form表单认证方式,即UsernamePasswordAuthenticationFilter。httpBasic
对应着Basic认证方式,即BasicAuthenticationFilter。
换言之,你配置了这两种认证方式,过滤器链中才会加入它们,否则它们是不会被加到过滤器链中去的。
因为Spring Security
自带的过滤器中是没有针对JWT这种认证方式的,所以我们的demo中会写一个JWT的认证过滤器,然后放在绿色的位置进行认证工作。
2. 📝SpringSecurity的重要概念
知道了Spring Security的大致工作流程之后,我们还需要知道一些非常重要的概念也可以说是组件:
- SecurityContext:上下文对象,
Authentication
对象会放在里面。 - SecurityContextHolder:用于拿到上下文对象的静态工具类。
- Authentication:认证接口,定义了认证对象的数据形式。
- AuthenticationManager:用于校验
Authentication
,返回一个认证完成后的Authentication
对象。
1.SecurityContext
上下文对象,认证后的数据就放在这里面,接口定义如下:
public interface SecurityContext extends Serializable {
// 获取Authentication对象
Authentication getAuthentication();
// 放入Authentication对象
void setAuthentication(Authentication authentication);
}
这个接口里面只有两个方法,其主要作用就是get or set Authentication
。
2. SecurityContextHolder
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
,默认会把数据都存储到当前线程中。
3. Authentication
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
这几个方法效果如下:
getAuthorities
: 获取用户权限,一般情况下获取到的是用户的角色信息。getCredentials
: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。getDetails
: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)。getPrincipal
: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails。isAuthenticated
: 获取当前Authentication
是否已认证。setAuthenticated
: 设置当前Authentication
是否已认证(true or false)。
Authentication
只是定义了一种在SpringSecurity进行认证过的数据的数据形式应该是怎么样的,要有权限,要有密码,要有身份信息,要有额外信息。
4. AuthenticationManager
public interface AuthenticationManager {
// 认证方法
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager
定义了一个认证方法,它将一个未认证的Authentication
传入,返回一个已认证的Authentication
,默认使用的实现类为:ProviderManager。
接下来大家可以构思一下如何将这四个部分,串联起来,构成Spring Security进行认证的流程:
1. 👉先是一个请求带着身份信息进来
2. 👉经过AuthenticationManager
的认证,
3. 👉再通过SecurityContextHolder
获取SecurityContext
,
4. 👉最后将认证后的信息放入到SecurityContext
。
3. 📃代码前的准备工作
真正开始讲诉我们的认证代码之前,我们首先需要导入必要的依赖,数据库相关的依赖可以自行选择什么JDBC框架,我这里用的是国人二次开发的myabtis-plus。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
接着,我们需要定义几个必须的组件。
由于我用的Spring-Boot是2.X所以必须要我们自己定义一个加密器:
1. 定义加密器Bean
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
这个Bean是不必可少的,Spring Security
在认证操作时会使用我们定义的这个加密器,如果没有则会出现异常。
2. 定义AuthenticationManager
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
这里将Spring Security
自带的authenticationManager
声明成Bean,声明它的作用是用它帮我们进行认证操作,调用这个Bean的authenticate
方法会由Spring Security
自动帮我们做认证。
3. 实现UserDetailsService
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private RoleInfoService roleInfoService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("开始登陆验证,用户名为: {}",s);
// 根据用户名验证用户
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(UserInfo::getLoginAccount,s);
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;
}
}
实现UserDetailsService
的抽象方法并返回一个UserDetails对象,认证过程中SpringSecurity会调用这个方法访问数据库进行对用户的搜索,逻辑什么都可以自定义,无论是从数据库中还是从缓存中,但是我们需要将我们查询出来的用户信息和权限信息组装成一个UserDetails返回。
UserDetails 也是一个定义了数据形式的接口,用于保存我们从数据库中查出来的数据,其功能主要是验证账号状态和获取权限,具体实现可以查阅我仓库的代码。
4. TokenUtil
由于我们是JWT的认证模式,所以我们也需要一个帮我们操作Token的工具类,一般来说它具有以下三个方法就够了:
- 创建token
- 验证token
- 反解析token中的信息
在下文我的代码里面,JwtProvider充当了Token工具类的角色,具体实现可以查阅我仓库的代码。
4. ✍代码中的具体实现
有了前面的讲解之后,大家应该都知道用SpringSecurity
做JWT认证需要我们自己写一个过滤器来做JWT的校验,然后将这个过滤器放到绿色部分。
在我们编写这个过滤器之前,我们还需要进行一个认证操作,因为我们要先访问认证接口拿到token,才能把token放到请求头上,进行接下来请求。
如果你不太明白,不要紧,先接着往下看我会在这节结束再次梳理一下。
1. 认证方法
访问一个系统,一般最先访问的是认证方法,这里我写了最简略的认证需要的几个步骤,因为实际系统中我们还要写登录记录啊,前台密码解密啊这些操作。
@Override
public ApiResult login(String loginAccount, String password) {
// 1 创建UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken usernameAuthentication = new UsernamePasswordAuthenticationToken(loginAccount, password);
// 2 认证
Authentication authentication = this.authenticationManager.authenticate(usernameAuthentication);
// 3 保存认证信息
SecurityContextHolder.getContext().setAuthentication(authentication);
// 4 生成自定义token
UserDetail userDetail = (UserDetail) authentication.getPrincipal();
AccessToken accessToken = jwtProvider.createToken((UserDetails) authentication.getPrincipal());
// 5 放入缓存
caffeineCache.put(CacheName.USER, userDetail.getUsername(), userDetail);
return ApiResult.ok(accessToken);
}
这里一共五个步骤,大概只有前四步是比较陌生的:
- 传入用户名和密码创建了一个
UsernamePasswordAuthenticationToken
对象,这是我们前面说过的Authentication
的实现类,传入用户名和密码做构造参数,这个对象就是我们创建出来的未认证的Authentication
对象。 - 使用我们先前已经声明过的Bean-
authenticationManager
调用它的authenticate
方法进行认证,返回一个认证完成的Authentication
对象。 - 认证完成没有出现异常,就会走到第三步,使用
SecurityContextHolder
获取SecurityContext
之后,将认证完成之后的Authentication
对象,放入上下文对象。 - 从
Authentication
对象中拿到我们的UserDetails
对象,之前我们说过,认证后的Authentication
对象调用它的getPrincipal()
方法就可以拿到我们先前数据库查询后组装出来的UserDetails
对象,然后创建token。 - 把
UserDetails
对象放入缓存中,方便后面过滤器使用。
这样的话就算完成了,感觉上很简单,因为主要认证操作都会由authenticationManager.authenticate()
帮我们完成。
接下来我们可以看看源码,从中窥得Spring Security是如何帮我们做这个认证的(省略了一部分):
// AbstractUserDetailsAuthenticationProvider
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);
}
}
看了源码之后你会发现和我们平常写的一样,其主要逻辑也是查数据库然后对比密码。
登录之后效果如下:
我们返回token之后,下次请求其他API的时候就要在请求头中带上这个token,都按照JWT的标准来做就可以。
2. JWT过滤器
有了token之后,我们要把过滤器放在过滤器链中,用于解析token,因为我们没有session,所以我们每次去辨别这是哪个用户的请求的时候,都是根据请求中的token来解析出来当前是哪个用户。
所以我们需要一个过滤器去拦截所有请求,前文我们也说过,这个过滤器我们会放在绿色部分用来替代UsernamePasswordAuthenticationFilter
,所以我们新建一个JwtAuthenticationTokenFilter
,然后将它注册为Bean,并在编写配置文件的时候需要加上这个:
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
return new JwtAuthenticationTokenFilter();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(jwtAuthenticationTokenFilter(),
UsernamePasswordAuthenticationFilter.class);
}
addFilterBefore
的语义是添加一个Filter到XXXFilter之前,放在这里就是把JwtAuthenticationTokenFilter
放在UsernamePasswordAuthenticationFilter
之前,因为filter的执行也是有顺序的,我们必须要把我们的filter放在过滤器链中绿色的部分才会起到自动认证的效果。
接下来我们可以看看JwtAuthenticationTokenFilter
的具体实现了:
@Override
protected void doFilterInternal(@NotNull HttpServletRequest request,
@NotNull HttpServletResponse response,
@NotNull FilterChain chain) throws ServletException, IOException {
log.info("JWT过滤器通过校验请求头token进行自动登录...");
// 拿到Authorization请求头内的信息
String authToken = jwtProvider.getToken(request);
// 判断一下内容是否为空且是否为(Bearer )开头
if (StrUtil.isNotEmpty(authToken) && authToken.startsWith(jwtProperties.getTokenPrefix())) {
// 去掉token前缀(Bearer ),拿到真实token
authToken = authToken.substring(jwtProperties.getTokenPrefix().length());
// 拿到token里面的登录账号
String loginAccount = jwtProvider.getSubjectFromToken(authToken);
if (StrUtil.isNotEmpty(loginAccount) && SecurityContextHolder.getContext().getAuthentication() == null) {
// 缓存里查询用户,不存在需要重新登陆。
UserDetail userDetails = caffeineCache.get(CacheName.USER, loginAccount, UserDetail.class);
// 拿到用户信息后验证用户信息与token
if (userDetails != null && jwtProvider.validateToken(authToken, userDetails)) {
// 组装authentication对象,构造参数是Principal Credentials 与 Authorities
// 后面的拦截器里面会用到 grantedAuthorities 方法
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
// 将authentication信息放入到上下文对象中
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("JWT过滤器通过校验请求头token自动登录成功, user : {}", userDetails.getUsername());
}
}
}
chain.doFilter(request, response);
}
代码里步骤虽然说的很详细了,但是可能因为代码过长不利于阅读,我还是简单说说,也可以直接去仓库查看源码:
- 拿到
Authorization
请求头对应的token信息 - 去掉token的头部(Bearer )
- 解析token,拿到我们放在里面的登陆账号
- 因为我们之前登陆过,所以我们直接从缓存里面拿我们的
UserDetail
信息即可 - 查看是否UserDetail为null,以及查看token是否过期,
UserDetail
用户名与token中的是否一直。 - 组装一个
authentication
对象,把它放在上下文对象中,这样后面的过滤器看到我们上下文对象中有authentication
对象,就相当于我们已经认证过了。
这样的话,每一个带有正确token的请求进来之后,都会找到它的账号信息,并放在上下文对象中,我们可以使用SecurityContextHolder
很方便的拿到上下文对象中的Authentication
对象。
完成之后,启动我们的demo,可以看到过滤器链中有以下过滤器,其中我们自定义的是第5个:
🐱🏍就酱,我们登录完了之后获取到的账号信息与角色信息我们都会放到缓存中,当带着token的请求来到时,我们就把它从缓存中拿出来,再次放到上下文对象中去。
结合认证方法,我们的逻辑链就变成了:
登录👉拿到token👉请求带上token👉JWT过滤器拦截👉校验token👉将从缓存中查出来的对象放到上下文中
这样之后,我们认证的逻辑就算完成了。
4. 💡代码优化
认证和JWT过滤器完成后,这个JWT的项目其实就可以跑起来了,可以实现我们想要的效果,如果想让程序更健壮,我们还需要再加一些辅助功能,让代码更友好。
1. 认证失败处理器
当用户未登录或者token解析失败时会触发这个处理器,返回一个非法访问的结果。
2. 权限不足处理器
当用户本身权限不满足所访问API需要的权限时,触发这个处理器,返回一个权限不足的结果。
3. 退出方法
用户退出一般就是清除掉上下文对象和缓存就行了,你也可以做一下附加操作,这两步是必须的。
4. token刷新
JWT的项目token刷新也是必不可少的,这里刷新token的主要方法放在了token工具类里面,刷新完了把缓存重载一遍就行了,因为缓存是有有效期的,重新put可以重置失效时间。
后记
这篇文我从上周日就开始构思了,为了能讲的老妪能解,修修改改了几遍才发出来。
Spring Security
的上手的确有点难度,在我第一次去了解它的时候看的是尚硅谷的教程,那个视频的讲师拿它和Thymeleaf结合,这就导致网上也有很多博客去讲Spring Security
的时候也是这种方式,而没有去关注前后端分离。
也有教程做过滤器的时候是直接继承UsernamePasswordAuthenticationFilter
,这样的方法也是可行的,不过我们了解了整体的运行流程之后你就知道没必要这样做,不需要去继承XXX,只要写个过滤器然后放在那个位置就可以了。
好了,认证篇结束后,下篇就是动态鉴权了,这是我在掘金的第一篇文,我的第一次知识输出,希望大家持续关注。
你们的每个点赞收藏与评论都是对我知识输出的莫大肯定,如果有文中有什么错误或者疑点或者对我的指教都可以在评论区下方留言,一起讨论。
我是耳朵,一个一直想做知识输出的人,下期见。
如果不能谈情说爱,我们可以自怜自爱。
楔子
上一篇文我们讲过了SpringSecurity
的认证流程,相信大家认真读过了之后一定会对SpringSecurity
的认证流程已经明白个七八分了,本期是我们如约而至的动态鉴权篇,看这篇并不需要一定要弄懂上篇的知识,因为讲述的重点并不相同,你可以将这两篇看成两个独立的章节,从中撷取自己需要的部分。
祝有好收获。
1. 📖SpringSecurity的鉴权原理
上一篇文我们讲认证的时候曾经放了一个图,就是下图:
整个认证的过程其实一直在围绕图中过滤链的绿色部分,而我们今天要说的动态鉴权主要是围绕其橙色部分,也就是图上标的:FilterSecurityInterceptor
。
1. FilterSecurityInterceptor
想知道怎么动态鉴权首先我们要搞明白SpringSecurity的鉴权逻辑,从上图中我们也可以看出:FilterSecurityInterceptor
是这个过滤链的最后一环,而认证之后就是鉴权,所以我们的FilterSecurityInterceptor
主要是负责鉴权这部分。
一个请求完成了认证,且没有抛出异常之后就会到达FilterSecurityInterceptor
所负责的鉴权部分,也就是说鉴权的入口就在FilterSecurityInterceptor
。
我们先来看看FilterSecurityInterceptor
的定义和主要方法:
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
Filter {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
}
上文代码可以看出FilterSecurityInterceptor
是实现了抽象类AbstractSecurityInterceptor
的一个实现类,这个AbstractSecurityInterceptor
中预先写好了一段很重要的代码(后面会说到)。
FilterSecurityInterceptor
的主要方法是doFilter
方法,过滤器的特性大家应该都知道,请求过来之后会执行这个doFilter
方法,FilterSecurityInterceptor
的doFilter
方法出奇的简单,总共只有两行:
第一行是创建了一个FilterInvocation
对象,这个FilterInvocation
对象你可以当作它封装了request,它的主要工作就是拿请求里面的信息,比如请求的URI。
第二行就调用了自身的invoke
方法,并将FilterInvocation
对象传入。
所以我们主要逻辑肯定是在这个invoke
方法里面了,我们来打开看看:
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// 进入鉴权
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
invoke
方法中只有一个if-else,一般都是不满足if中的那三个条件的,然后执行逻辑会来到else。
else的代码也可以概括为两部分:
- 调用了
super.beforeInvocation(fi)
。 - 调用完之后过滤器继续往下走。
第二步可以不看,每个过滤器都有这么一步,所以我们主要看super.beforeInvocation(fi)
,前文我已经说过, FilterSecurityInterceptor
实现了抽象类AbstractSecurityInterceptor
, 所以这个里super其实指的就是AbstractSecurityInterceptor
, 那这段代码其实调用了AbstractSecurityInterceptor.beforeInvocation(fi)
, 前文我说过AbstractSecurityInterceptor
中有一段很重要的代码就是这一段, 那我们继续来看这个beforeInvocation(fi)
方法的源码:
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
Authentication authenticated = authenticateIfRequired();
try {
// 鉴权需要调用的接口
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
}
源码较长,这里我精简了中间的一部分,这段代码大致可以分为三步:
- 拿到了一个
Collection<ConfigAttribute>
对象,这个对象是一个List,其实里面就是我们在配置文件中配置的过滤规则。 - 拿到了
Authentication
,这里是调用authenticateIfRequired
方法拿到了,其实里面还是通过SecurityContextHolder
拿到的,上一篇文章我讲过如何拿取。 - 调用了
accessDecisionManager.decide(authenticated, object, attributes)
,前两步都是对decide
方法做参数的准备,第三步才是正式去到鉴权的逻辑,既然这里面才是真正鉴权的逻辑,那也就是说鉴权其实是accessDecisionManager
在做。
2. AccessDecisionManager
前面通过源码我们看到了鉴权的真正处理者:AccessDecisionManager
,是不是觉得一层接着一层,就像套娃一样,别急,下面还有。先来看看源码接口定义:
public interface AccessDecisionManager {
// 主要鉴权方法
void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
InsufficientAuthenticationException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
}
AccessDecisionManager
是一个接口,它声明了三个方法,除了第一个鉴权方法以外,还有两个是辅助性的方法,其作用都是甄别 decide
方法中参数的有效性。
那既然是一个接口,上文中所调用的肯定是他的实现类了,我们来看看这个接口的结构树:
别辜负生命,别辜负自己。
楔子
前面两期我讲了SpringSecurity认证流程和SpringSecurity鉴权流程,今天是第三期,是SpringSecurity
的收尾工作,讲SpringSecurity
的启动流程。
就像很多电影拍火了之后其续作往往是前作的前期故事一样,我这个第三期要讲的SpringSecurity启动流程
也是不择不扣的"前期故事",它能帮助你真正认清SpringSecurity
的整体全貌。
在之前的文章里,在说到SpringSecurity
中的过滤器链的时候,往往是把它作为一个概念了解的,就是我们只是知道有这么个东西,也知道它到底是干什么用的,但是我们却不知道这个过滤器链是由什么类什么时候去怎么样创建出来的。
今天这期就是要了解SpringSecurity
的自动配置到底帮我们做了什么,它是如何把过滤器链给创建出来的,又是在默认配置的时候怎么加入了我们的自定义配置。
祝有好收获(边赞边看,法力无限)。
1. 📚EnableWebSecurity
我们先来看看我们一般是如何使用SpringSecurity
的。
我们用SpringSecurity
的时候都会先新建一个SpringSecurity
相关的配置类,用它继承WebSecurityConfigurerAdapter
,然后打上注解@EnableWebSecurity
,然后我们就可以通过重写 WebSecurityConfigurerAdapter
里面的方法来完成我们自己的自定义配置。
就像这样:
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
}
}
我们已经知道,继承WebSecurityConfigurerAdapter
是为了重写配置,那这个注解是做了什么呢?
从它的名字@EnableWebSecurity
我们可以大概猜出来,它就是那个帮我们自动配置了SpringSecurity
的好心人。
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,
SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
/**
* Controls debugging support for Spring Security. Default is false.
* @return if true, enables debug support with Spring Security
*/
boolean debug() default false;
}
emmm,我猜大家应该有注解相关的知识吧,ok,既然你们都有注解相关的知识,我就直接讲了。
这个@EnableWebSecurity
中有两个地方是比较重要的:
-
一是
@Import
注解导入了三个类,这三个类中的后两个是SpringSecurity
为了兼容性做的一些东西,兼容SpringMVC
,兼容SpringSecurityOAuth2
,我们主要看的其实是第一个类,导入这个类代表了加载了这个类里面的内容。 -
二是
@EnableGlobalAuthentication
这个注解,@EnableWebSecurity
大家还没搞明白呢,您这又来一个,这个注解呢,其作用也是加载了一个配置类-AuthenticationConfiguration
,看它的名字大家也可应该知道它加载的类是什么相关的了吧,没错就是AuthenticationManager
相关的配置类,这个我们可以以后再说。
综上所述,@EnableWebSecurity
可以说是帮我们自动加载了两个配置类:WebSecurityConfiguration
和AuthenticationConfiguration
(@EnableGlobalAuthentication
注解加载了这个配置类)。
其中WebSecurityConfiguration
是帮助我们建立了过滤器链的配置类,而AuthenticationConfiguration
则是为我们注入AuthenticationManager
相关的配置类,我们今天主要讲的是WebSecurityConfiguration
。
2. 📖源码概览
既然讲的是WebSecurityConfiguration
,我们照例先把源码给大家看看,精简了一下无关紧要的:
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
private WebSecurity webSecurity;
private Boolean debugEnabled;
private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers;
private ClassLoader beanClassLoader;
@Autowired(required = false)
private ObjectPostProcessor<Object> objectObjectPostProcessor;
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build();
}
@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(
ObjectPostProcessor<Object> objectPostProcessor,
@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
throws Exception {
webSecurity = objectPostProcessor
.postProcess(new WebSecurity(objectPostProcessor));
if (debugEnabled != null) {
webSecurity.debug(debugEnabled);
}
webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE);
Integer previousOrder = null;
Object previousConfig = null;
for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
if (previousOrder != null && previousOrder.equals(order)) {
throw new IllegalStateException(
"@Order on WebSecurityConfigurers must be unique. Order of "
+ order + " was already used on " + previousConfig + ", so it cannot be used on "
+ config + " too.");
}
previousOrder = order;
previousConfig = config;
}
for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
webSecurity.apply(webSecurityConfigurer);
}
this.webSecurityConfigurers = webSecurityConfigurers;
}
}
如代码所示,首先WebSecurityConfiguration
是个配置类,类上面打了@Configuration
注解,这个注解的作用大家还知道吧,在这里就是把这个类中所有带@Bean
注解的Bean给实例化一下。
这个类里面比较重要的就两个方法:springSecurityFilterChain
和setFilterChainProxySecurityConfigurer
。
springSecurityFilterChain
方法上打了@Bean
注解,任谁也能看出来就是这个方法创建了springSecurityFilterChain
,但是先别着急,我们不能先看这个方法,虽然它在上面。
3. 📄SetFilterChainProxySecurityConfigurer
我们要先看下面的这个方法:setFilterChainProxySecurityConfigurer
,为啥呢?
为啥呢?
因为它是@Autowired
注解,所以它要比springSecurityFilterChain
方法优先执行,从系统加载的顺序来看,我们需要先看它。
@Autowired
在这里的作用是为这个方法自动注入所需要的两个参数,我们先来看看这两个参数:
-
参数
objectPostProcessor
是为了创建WebSecurity
实例而注入进来的,先了解一下即可。 -
参数
webSecurityConfigurers
是一个List,它实际上是所有WebSecurityConfigurerAdapter
的子类,那如果我们定义了自定义的配置类,其实就是把我们的配置也读取到了。这里其实有点难懂为什么参数中
SecurityConfigurer<Filter, WebSecurity>
这个类型可以拿到WebSecurityConfigurerAdapter
的子类?因为
WebSecurityConfigurerAdapter
实现了WebSecurityConfigurer<WebSecurity>
接口,而WebSecurityConfigurer<WebSecurity>
又继承了SecurityConfigurer<Filter, T>
,经过一层实现,一层继承关系之后,WebSecurityConfigurerAdapter
终于成为了SecurityConfigurer
的子类。而参数中
SecurityConfigurer<Filter, WebSecurity>
中的两个泛型参数其实是起到了一个过滤的作用,仔细查看我们的WebSecurityConfigurerAdapter
的实现与继承关系,你可以发现我们的WebSecurityConfigurerAdapter
正好是这种类型。
ok,说完了参数,我觉得我们可以看看代码了:
@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(
ObjectPostProcessor<Object> objectPostProcessor,
@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
throws Exception {
// 创建一个webSecurity实例
webSecurity = objectPostProcessor
.postProcess(new WebSecurity(objectPostProcessor));
if (debugEnabled != null) {
webSecurity.debug(debugEnabled);
}
// 根据order排序
webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE);
Integer previousOrder = null;
Object previousConfig = null;
for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
if (previousOrder != null && previousOrder.equals(order)) {
throw new IllegalStateException(
"@Order on WebSecurityConfigurers must be unique. Order of "
+ order + " was already used on " + previousConfig + ", so it cannot be used on "
+ config + " too.");
}
previousOrder = order;
previousConfig = config;
}
// 保存配置
for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
webSecurity.apply(webSecurityConfigurer);
}
// 成员变量初始化
this.webSecurityConfigurers = webSecurityConfigurers;
}
根据我们的注释,这段代码做的事情可以分为以为几步:
-
创建了一个webSecurity实例,并且赋值给成员变量。
-
紧接着对
webSecurityConfigurers
通过order
进行排序,order
是加载顺序。 -
进行判断是否有相同
order
的配置类,如果出现将会直接报错。 -
保存配置,将其放入
webSecurity
的成员变量中。
大家可以将这些直接理解为成员变量的初始化,和加载我们的配置类配置即可,因为后面的操作都是围绕它初始化的webSecurity
实例和我们加载的配置类信息来做的。
这些东西还可以拆出来一步步的来讲,但是这样的话真是一篇文章写不完,我也没有那么大的精力能够事无巨细的写出来,我只挑选这条痕迹清晰的主脉络来讲,如果大家看完能明白它的一个加载顺序其实就挺好了。
就像Spring的面试题会问SpringBean的加载顺序,SpringMVC则会问SpringMVC一个请求的运行过程一样。
全部弄得明明白白,必须要精研源码,在初期,我们只要知道它的一条主脉络,在之后的使用中,哪出了问题你可以直接去定位到可能是哪有问题,这样就已经很好了,学习是一个循环渐进的过程。
4. 📃SpringSecurityFilterChain
初始化完变量,加载完配置,我们要开始创建过滤器链了,所以先走setFilterChainProxySecurityConfigurer
是有原因的,如果我们不把我们的自定义配置加载进来,创建过滤器链的时候怎么知道哪些过滤器需要哪些过滤器不需要。
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build();
}
springSecurityFilterChain
方法逻辑就很简单了,如果我们没加载自定义的配置类,它就替我们加载一个默认的配置类,然后调用这个build
方法。
看到这熟悉的方法名称,你就应该知道这是建造者模式,不管它什么模式,既然调用了,我们点进去就是了。
public final O build() throws Exception {
if (this.building.compareAndSet(false, true)) {
this.object = doBuild();
return this.object;
}
throw new AlreadyBuiltException("This object has already been built");
}
build()
方法是webSecurity
的父类AbstractSecurityBuilder
中的方法,这个方法又调用了doBuild()
方法。
@Override
protected final O doBuild() throws Exception {
synchronized (configurers) {
buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
// 空方法
beforeInit();
// 调用init方法
init();
buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
// 空方法
beforeConfigure();
// 调用configure方法
configure();
buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
// 调用performBuild
O result = performBuild();
buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
return result;
}
}
通过我的注释可以看到beforeInit()
和beforeConfigure()
都是空方法, 实际有用的只有init()
,configure()
和performBuild()
方法。
我们先来看看init()
,configure()
方法。
private void init() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
for (SecurityConfigurer<O, B> configurer : configurers) {
configurer.init((B) this);
}
for (SecurityConfigurer<O, B> configurer : configurersAddedInInitializing) {
configurer.init((B) this);
}
}
private void configure() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
for (SecurityConfigurer<O, B> configurer : configurers) {
configurer.configure((B) this);
}
}
源码中可以看到都是先获取到我们的配置类信息,然后循环调用配置类自己的init()
,configure()
方法。
前面说过,我们的配置类是继承了WebSecurityConfigurerAdapter
的子类,而WebSecurityConfigurerAdapter
又是SecurityConfigurer
的子类,所有SecurityConfigurer
的子类都需要实现init()
,configure()
方法。
所以这里的init()
,configure()
方法其实就是调用WebSecurityConfigurerAdapter
自己重写的init()
,configure()
方法。
其中WebSecurityConfigurerAdapter
中的configure()
方法是一个空方法,所以我们只需要去看WebSecurityConfigurerAdapter
中的init()
方法就好了。
public void init(final WebSecurity web) throws Exception {
final HttpSecurity http = getHttp();
web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
FilterSecurityInterceptor securityInterceptor = http
.getSharedObject(FilterSecurityInterceptor.class);
web.securityInterceptor(securityInterceptor);
});
}
这里也可以分为两步:
-
执行了
getHttp()
方法,这里面初始化加入了很多过滤器。 -
将
HttpSecurity
放入WebSecurity
,将FilterSecurityInterceptor
放入WebSecurity
,就是我们鉴权那章讲过的FilterSecurityInterceptor
。
那我们主要看第一步getHttp()
方法:
protected final HttpSecurity getHttp() throws Exception {
http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
sharedObjects);
if (!disableDefaults) {
// @formatter:off
http
.csrf().and()
.addFilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and()
.headers().and()
.sessionManagement().and()
.securityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi().and()
.apply(new DefaultLoginPageConfigurer<>()).and()
.logout();
// @formatter:on
ClassLoader classLoader = this.context.getClassLoader();
List<AbstractHttpConfigurer> defaultHttpConfigurers =
SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
http.apply(configurer);
}
}
// 我们一般重写这个方法
configure(http);
return http;
}
getHttp()
方法里面http
调用的那一堆方法都是一个个过滤器,第一个csrf()
很明显就是防止CSRF攻击的过滤器,下面还有很多,这就是SpringSecurity
默认会加入过滤器链的那些过滤器了。
其次,还有一个重点就是倒数第二行代码,我也加上了注释,我们一般在我们自定义的配置类中重写的就是这个方法,所以我们的自定义配置就是在这里生效的。
所以在初始化的过程中,这个方法会先加载自己默认的配置然后再加载我们重写的配置,这样两者结合起来,就变成了我们看到的默认配置。(如果我们不重写configure(http)
方法,它也会一点点的默认配置,大家可以去看源码,看了就明白了。)
init()
,configure()
(空方法)结束之后,就是调用performBuild()
方法。
protected Filter performBuild() throws Exception {
int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList<>(
chainSize);
for (RequestMatcher ignoredRequest : ignoredRequests) {
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
}
// 调用securityFilterChainBuilder的build()方法
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
securityFilterChains.add(securityFilterChainBuilder.build());
}
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
if (httpFirewall != null) {
filterChainProxy.setFirewall(httpFirewall);
}
filterChainProxy.afterPropertiesSet();
Filter result = filterChainProxy;
postBuildAction.run();
return result;
}
这个方法主要需要看的是调用securityFilterChainBuilder
的build()
方法,这个securityFilterChainBuilder
是我们在init()
方法中add的那个,所以这里的securityFilterChainBuilder
其实就是HttpSecurity
,所以这里其实是调用了HttpSecurity
的bulid()
方法。
又来了,WebSecurity
的bulid()
方法还没说完,先来了一下HttpSecurity
的bulid()
方法。
HttpSecurity
的bulid()
方法进程和之前的一样,也是先init()
然后configure()
最后performBuild()
方法,值得一提的是在HttpSecurity
的performBuild()
方法里面,会对过滤器链中的过滤器进行排序:
@Override
protected DefaultSecurityFilterChain performBuild() {
filters.sort(comparator);
return new DefaultSecurityFilterChain(requestMatcher, filters);
}
HttpSecurity
的bulid()
方法执行完了之后将DefaultSecurityFilterChain
返回给WebSecurity
的performBuil()
方法,performBuil()
方法再将其转换为FilterChainProxy
,最后WebSecurity
的performBuil()
方法执行结束,返回一个Filter
注入成为name="springSecurityFilterChain"
的Bean
。
经过以上这些步骤之后,springSecurityFilterChain
方法执行完毕,我们的过滤器链就创建完成了,SpringSecurity
也可以跑起来了。
后记
看到这的话,其实你已经很有耐性了,但可能还觉得云里雾里的,因为SpringSecurity
(Spring大家族)这种工程化极高的项目项目都是各种设计模式和编码思想满天飞,看不懂的时候只能说这什么玩意,看得懂的时候又该膜拜这是艺术啊。
这些东西它不容易看懂但是比较解耦容易扩展,像一条线下来的代码就容易看懂但是不容易扩展了,福祸相依。
而且这么多名称相近的类名,各种继承抽象,要好好理解下来的确没那么容易,这篇其实想给这个SpringSecurity
来个收尾,逼着自己写的,我这个人喜欢有始有终,这段东西也的确复杂,接下来的几篇打算写几个实用的有意思的也轻松的放松一下。
如果你对SpringSecurity
源码有兴趣可以跟着来我这个文章,点开你自己的源码点一点,看一看,加油。
自从上篇征文发了之后,感觉多了很多前端的关注者,掘金果然还是前端多啊,没事,虽然我不怎么写前端,说不定哪天改行了呢哈哈。
我也不藏着掖着,其实我现在是写后端的,我对前端呢只能说是略懂略懂,不过无聊了也可以来看看我的文章,点点赞刷刷阅读干干啥的👍,说不定某一天突然看懂了某篇文还前端劝退后端入行,加油了大家。
别辜负生命,别辜负自己。
你们的每个点赞收藏与评论都是对我知识输出的莫大肯定,如果有文中有什么错误或者疑点或者对我的指教都可以在评论区下方留言,一起讨论。
我是耳朵,一个一直想做知识输出的伪文艺程序员,下期见。
「和耳朵」SpringSecurity是如何代理过滤器链的? - 掘金 (juejin.cn)
从去年的七月份开始我陆陆续续写了三篇SpringSecurity
相关的文章,幸得各位掘友捧场,反响还算不错,这也给了我极大的信心和动力,本来是打算继续写下去做成一个SpringSecurity系列,后来因为换工作的缘故,导致暂时搁置了,今天开始继续更新相关内容,希望大家多多支持👍。
今天的主题是:SpringSecurity如何进行过滤器链代理?
1. 外部容器与内嵌容器
在前文SpringSecurity自动配置这一章我们已经说过SpringSecurity会初始化一个名为springSecurityFilterChain
的Bean,我们的请求实际就会被这个Bean进行处理。
但是SpringSecurity不会直接拿这个Bean进行处理,而是通过一个Filter进行代理,在这个Filter内部调用了这个Bean进行doFilter
处理(实际是不是一层代理,而是多层)。
那我们其实可以将流程简化为下图:
看起来很简单,Spring只需要注册一个Filter再调用过滤器链就行了,但其实Spring不会直接注册一个Filter,它会把这个Filter注册成为一个Bean交给容器管理,再注册到Filter中。
这里涉及到一个问题,我们传统的注册Filter是直接定义Filter,而要把一个Filter先注册成Bean再注册到Filter中无疑会做些额外的事,所以Spring就提供了两个接口:
- WebApplicationInitializer - 打包为war包使用外部容器时使用
- ServletContextInitializer - 打包为jar包使用内嵌容器时使用
这两个接口的作用都是可以在Servlet容器启动后注册自定义的Filter/Listener
并交给Bean工厂管理,唯一不同的就是在不同外部环境下会选取不同的生效接口。
一般来说,框架中会同时包含这两种接口的实现,保证在不同外部环境下都有一致的表现,比如SpringSecurity中就具有以下两个实现:
- AbstractSecurityWebApplicationInitializer - 外部容器处理类
- DelegatingFilterProxyRegistrationBean - 内嵌容器处理类
我们今天的内容也是以这两个实现类为入口进行深入了解,不过上文也说到了他们仅仅是注册Filter的入口不一样,注册Filter之后的后续代理流程还是一模一样的。
2. 内嵌容器方式注册Filter
现在都是微服务大行其道的时候了,一般来说我们的应用都是通过内嵌容器的方式进行运行,所以我们先来说说内嵌容器运行时的整个启动链路。
我们知道在SpringBoot中很重要的一个特性就是自动配置,在org.springframework.boot.autoconfigure.security
包下放着SpringSecurity的自动配置类,我们着重来看SecurityFilterAutoConfiguration
,这里面只有一个方法:
public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(SecurityProperties securityProperties) {
DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean("springSecurityFilterChain", new ServletRegistrationBean[0]);
registration.setOrder(securityProperties.getFilter().getOrder());
registration.setDispatcherTypes(this.getDispatcherTypes(securityProperties));
return registration;
}
通过这个方法的方法名:securityFilterChainRegistration
我们可以看出来该方法的作用就是为了注册过滤器链,同时我们看它的方法内容可以看到它创建了一个上文中提到过的DelegatingFilterProxyRegistrationBean的实例,传入的参数就是过滤器链的名称,由于此类是ServletContextInitializer的实现类,所以它最终会通过它的getFilter()
方法创建一个Filter实例。
public DelegatingFilterProxy getFilter() {
return new DelegatingFilterProxy(this.targetBeanName, this.getWebApplicationContext()) {
protected void initFilterBean() throws ServletException {
}
};
}
这个DelegatingFilterProxy
就是创建出的Filter实例,我们可以再深一步的看看它的实例化过程:
public DelegatingFilterProxyRegistrationBean(String targetBeanName, ServletRegistrationBean<?>... servletRegistrationBeans) {
super(servletRegistrationBeans);
Assert.hasLength(targetBeanName, "TargetBeanName must not be null or empty");
this.targetBeanName = targetBeanName;
this.setName(targetBeanName);
}
实例化的过程比较简单,就是将我们要代理的Bean的名字放在targetBeanName
属性上面,然后这个Filter就相当于在我们容器的Filter链中了。
这其实这个时候我们要代理的Bean的实例还没被放到这个代理类中,目前这个代理类中只有targetBeanName
属性。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized(this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = this.findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
}
delegateToUse = this.initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
this.invokeDelegate(delegateToUse, request, response, filterChain);
}
代理Bean最终的实例化是在第一次请求到达,调用doFilter
方法时,判断之后发现目前并没有被代理对象,就会调用initDelegate
方法:
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
String targetBeanName = this.getTargetBeanName();
Assert.state(targetBeanName != null, "No target bean name set");
Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
if (this.isTargetFilterLifecycle()) {
delegate.init(this.getFilterConfig());
}
return delegate;
}
这个过程也很简单,就是从Bean工厂中找到这个Bean,然后赋值给代理对象。
最后,将会调用invokeDelegate
方法,调用被代理对象的doFilter方法:
protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
其实到这就已经和之前的文章内容串联起来了,剩下的就可以交给代理Bean去做了,这个代理Bean就是我们在SpringSecurity自动配置这一章讲到过的springSecurityFilterChain
。
然而其实我们可以继续深入,在那一章讲过这个Bean的类型是FilterChainProxy
,我们可以看看它内部的doFilter又到底是怎么做的?过滤器链真的是一个链条吗?FilterChainProxy是直接代理这个链条吗?
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (!clearContext) {
this.doFilterInternal(request, response, chain);
} else {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
this.doFilterInternal(request, response, chain);
} catch (RequestRejectedException var9) {
this.requestRejectedHandler.handle((HttpServletRequest)request, (HttpServletResponse)response, var9);
} finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
}
这是FilterChainProxy的doFilter方法,上来就是一个if-else
,但是他们会调用同一个核心方法doFilterInternal
:
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest)request);
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse)response);
List<Filter> filters = this.getFilters((HttpServletRequest)firewallRequest);
if (filters != null && filters.size() != 0) {
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> {
return "Securing " + requestLine(firewallRequest);
}));
}
FilterChainProxy.VirtualFilterChain virtualFilterChain = new FilterChainProxy.VirtualFilterChain(firewallRequest, chain, filters);
virtualFilterChain.doFilter(firewallRequest, firewallResponse);
} else {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.of(() -> {
return "No security for " + requestLine(firewallRequest);
}));
}
firewallRequest.reset();
chain.doFilter(firewallRequest, firewallResponse);
}
}
在这个方法中会根据当前的请求去匹配能匹配到的Filters,具体操作在getFilters
方法中,它的作用就是拿到FilterChainProxy的filterChains成员变量中的所有Filter,然后进行匹配,将匹配到的Filter封装成一个List。
乍听一下filterChains你可能已经不知道了,我们来看看它的类型:SecurityFilterChain
,没错这是一个类,不要和前面的Bean搞混了,如果之前的文章你还记得那么可以知道这其实是由我们的自定义配置构建出的一个类,里面放了我们自定义相关的Filter。
如果我们有多个自定义配置文件就会有多个这样的对象,他们共同组成了filterChains。
如果能匹配到对应的Filter那么它会组成一个新的类:FilterChainProxy.VirtualFilterChain
,这是一个内部类,然后进行doFilter操作,所以我们平常所说的过滤器链实际上是这个,它是真正起效果的那个类。
为了防止大家这一段看不懂、不理解、我特地画了一个图来形象的表示整个链路,只要对照我这个图再根据相关代码,相信可以很快的理解这一段的链路:
3. 外部容器方式注册Filter
刚说完了内嵌容器的方式,再来说说外部容器的方式,我在文章开头也说过,其实就是实现类的不同,注册完Bean后面的流程都是一样的,我先放个流程图给大家看看:
大家可以发现,其实只有最上面的两块发生了变化,所以如果上一节的内容好好看的话这一节的很快就能理解。
外部容器的情况下SpringSecurity的入口在AbstractSecurityWebApplicationInitializer,我再来重述一下它的作用:
- 继承了
WebApplicationInitializer
- Servlet容器启动后会调用其实现类的
onStartup
方法,在这里可以进行Filter的注册
在这个类中它通过调用了insertSpringSecurityFilterChain
方法进行Filter的注册:
private void insertSpringSecurityFilterChain(ServletContext servletContext) {
String filterName = "springSecurityFilterChain";
DelegatingFilterProxy springSecurityFilterChain = new DelegatingFilterProxy(filterName);
String contextAttribute = this.getWebApplicationContextAttribute();
if (contextAttribute != null) {
springSecurityFilterChain.setContextAttribute(contextAttribute);
}
this.registerFilter(servletContext, true, filterName, springSecurityFilterChain);
}
这个方法其实也就是实例化了DelegatingFilterProxy
,后续的流程都是一样了~~~
4. 结语
这三篇加上本篇一共四篇,大致已经将SpringSecurity的轮廓和关键流程勾勒出来了,工作中其实这么多内容也够了,我们公司就是用的SpringSecurity作为认证鉴权框架,相关代码链路也都能看懂😄
🏷️日后会支持输出更多后端内容,欢迎大家点赞收藏👏
Spring Security + JWT + Swagger2 登录验证小结 - 掘金 (juejin.cn)
Spring Security + JWT + Swagger2 登录验证一套流程
主要是三个框架的集成配置,以及各个独立的配置(主要是 JWT + Security 的登录验证)。
流程:
- 构建 Spring Boot 基本项目,准备数据库表 User —— 用于存放登录实体类信息。
- 配置 Security 和 Swagger2 环境,确保没有什么问题。
- 构建
RespBean——公共返回实体类
,JwtTokenUtil——JWT token 工具类
,User——登录实体类
- 让 User 实现
UserDetails
接口,重写部分方法。 - 配置 Security 实现重写
UserDetailsService
方法,以及PasswordEncoder——密码凭证器
并加上@Bean
注解。这两个主要用于设置 Security 的认证。 - 构建
jwtAuthenticationTokenFilter
类——自定义 JWT Token 拦截器,并在SecurityConfig
的授权方法中添加此拦截器。 - 在
Swagger2Config
配置类中,配置有关 Security 的 Token 认证。 - 启动项目查看代码是否准确。
1. 构建 Spring Boot 基本项目,准备数据库——User
项目子模块:authority-security
,父模块已引入 Spring boot 依赖 2.3.0
1.1 导入依赖
<dependencies>
<!-- web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- mysql 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatis-plus 依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</dependency>
<!-- swagger2 依赖 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<!-- swagger 第三方 UI 依赖 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT 依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- commons-pool2 对象池依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
构建数据库表:user
create table user(
id int primary key auto_increment,
username varchar not null,
password varchar not null,
info varchar(200),
enabled tinyint(1) default 1
)
insert into user values(default,"admin","$2a$10$Himwt.wu3MPOLnNQ9YUH8O2quxgi7bMuomiNeFsVKRay87.qG5dgy","管理员 info ...",default)
username:admin;password:123
配置 application.yml
文件参数:
server:
port: 8082
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/dbtest16?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: admin
password: admin
hikari:
# 连接池名字
pool-name: DateHikari
# 最小空闲连接数
minimum-idle: 5
# 空闲连接存活最大事件,默认10分钟(600000)
idle-timeout: 180000
# 最大连接数:默认 10
maximum-pool-size: 10
# 从连接池返回的连接自动提交
auto-commit: true
# 连接最大存活时间,0 表示永久存活,默认 1800000(30 min)
max-lifetime: 1800000
# 连接超时事件 30 s
connection-timeout: 30000
# 测试连接是否可用的查询语句
connection-test-query: SELECT 1
# MP 配置
mybatis-plus:
# 配置 Mapper 映射文件
mapper-locations: classpath*:/mapper/*Mapper.xml
# 实体类的别名包
type-aliases-package: com.cnda.pojo
configuration:
# 自动驼峰命名
map-underscore-to-camel-case: false
# MyBatis 的 SQL 打印是方法接口所在的包
logging:
level:
com.cnda.mapper: debug
# JWT 配置
jwt:
# JWT 存储的请求头
tokenHeader: Authorization
# JWT 加密使用的密钥
secret: test-cnda-secret
# JWT 的有效时间 (60*60*24)
expiration: 604800
# JWT 负载中拿到开头 规定
tokenHead: Bearer
User 实体类代码:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String username;
private String password;
private String info;
private Boolean enabled;
}
2. 配置 Security 和 Swagger2 的配置
先配置好这两个确保没有什么问题,因为重点是 JWT,这两个配置比较简单,当搭配了 JWT 之后,Swagger2 也需要与两者集成一些配置,这个后面再说,现在只配置基本设置。
2.1 配置 SecurityConfig
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/hello",
// 下面是对静态资源以及 swagger2 UI 的放行。
"/css/**",
"/js/**",
"/img/**",
"/index.html",
"favicon.ico",
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**",
"/ws/**"
);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
}
上面使用 WebSecurity
放行了 /hello
请求,在 LoginController
中。
@RestController
public class LoginController {
@RequestMapping("/hello")
public String hello(){
return "Hello Word!";
}
}
这意味除了 localhost:8082/hello
会被放行,其他请求都会被 Security 拦截重定向到 /login
(这个请求 Security 内部已经实现了包括相关页面)。
2.2 配置 Swagger2Config
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo()) // 配置 apiInfo
.select() // 选择那些路径和api会生成document
.apis(RequestHandlerSelectors.basePackage("com.cnda.controller")) // // 对哪些 api进行监控,RequestHandlerSelectors.basePackage 基于包扫描
.paths(PathSelectors.any()) // 对所有路径进行监控
.build();
}
private ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("在线接口文档")
.description("在线接口文档")
.contact(new Contact("cnda","http://localhost:8082/doc.html","xxx@xxx.com"))
.build();
}
}
运行效果:
修改一下 Rustful 风格,并加了一个 /hello1
请求,不放行,打印内容相同。
可以看到 Security 和 Swagger2 基本配置完成。
3. 构建 JWT 工具类、公共响应对象
JWT 工具类主要用于生成 JWT,判断 JWT 是否有效,刷新 JWT 等方法。
公共响应对象——RespBean
,返回的都已 JSON 格式返回。
3.1 JwtUtil
@Component
public class JwtUtil {
// 准备两个存放在荷载的内容
private static final String CLAIM_KEY_SUB = "sub";
private static final String CLAIM_KEY_CREATE = "ibt";
// 提取 application.yml 中 JWT 的参数:
// 1. expiration Long
@Value("${jwt.expiration}")
private Long expiration;
// 2. secret String
@Value("${jwt.secret}")
private String secret; // 密钥
// 根据用户名构建 token
public String foundJWT(UserDetails userDetails) {
String username = userDetails.getUsername();
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_SUB, username);
claims.put(CLAIM_KEY_CREATE, new Date());
return foundJWT(claims);
}
// 根据荷载 map 构建 token
private String foundJWT(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(getExpiration()) // 过期时间
.signWith(SignatureAlgorithm.HS512, secret) // 设置签名算法和密钥
.compact();
}
// 判断 token 是否有效
public boolean validateToken(String token,UserDetails userDetails){
// 从 token 中获取 username 与 userDetails 中的username 对比
String username = getUsernameInToken(token);
// 判断 username 是否一致以及 token 是否过期
return username.equals(userDetails.getUsername()) && !isExpired(token);
}
// 判断 token 是否过期
// true 过期 false 没过期
private boolean isExpired(String token) {
Date expiration = getClaimsInToken(token).getExpiration();
return expiration.before(new Date());
}
// 从 token 中提取荷载信息
public Claims getClaimsInToken(String token){
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
e.printStackTrace();
}
return claims;
}
// 从 token 中提取用户名信息
public String getUsernameInToken(String token){
String username;
try {
username = getClaimsInToken(token).getSubject();
}catch (Exception e){
username = null;
}
return username;
}
// token 是否能刷新
public boolean tokenCanRef(String token){
return !isExpired(token); // 有效地 token 才能被刷新
}
// 刷新 token
public String refToken(String token){
Claims claimsInToken = getClaimsInToken(token);
claimsInToken.put(CLAIM_KEY_CREATE,new Date());
return foundJWT(claimsInToken);
}
// 设置过期时间
private Date getExpiration() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
}
3.2 RespBean 公共返回对象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RespBean {
private long code;
private String message;
private Object obj;
/**
* 返回响应结果
*/
private static RespBean result(long code, String message, Object obj) {
return new RespBean(code, message, obj);
}
/*
返回成功响应
*/
public static RespBean success(String message) {
return result(200, message, null);
}
/*
返回成功响应以及数据体
*/
public static RespBean success(String message, Object obj) {
return result(200, message, obj);
}
/*
返回错误响应
*/
public static RespBean error(String message) {
return result(500, message, null);
}
}
4. 让 User 实体类实现 UserDetails 的方法成为 Security 验证的用户核心主体
由于 Security 框架的性质,自定义授权和认证时,一般情况下会自定义 UserDetails。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private String info;
private Boolean enabled;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { // 权限角色
return null;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() { // 这里数据库实现了该字段,直接用即可
return this.enabled;
}
}
5. 重写 UserDetailsServer 和 PasswordEncoder
5.1 重写 UserDetailsServer
这个类就只有一个方法:
loadUserByUsername(UserDetails details)
,该方法用于根据用户名加载用户信息,用作于 Security 的后续认证,同时也可以用一个类去实现该接口,这里为了方便,同时也是 Lambda 表达式。
注意:这里的 UserMapper
没有代码展示了,就一个根据用户名查询用户信息的 SQL。
@Resource
private UserMapper mapper;
@Bean
@Override
protected UserDetailsService userDetailsService() {
return username -> {
User user = mapper.find(username);
if (user!=null){
return user;
}
throw new UsernameNotFoundException("用户名或密码不正确");
};
}
5.2 PasswordEncoder——密码凭证器
这个类主要用于验证表单提交的密码是否和 重写之后的 UserDetailsServer 得到的 UserDetails 中的加密密码一致。
@Bean
public PasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
5.3 配置到 SecurityConfig 的认证中
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(encoder());
}
6. 配置 JWT 的拦截器
public class JwtTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService service;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 获取请求头中的指定的值
String headerToken = httpServletRequest.getHeader(tokenHeader);
// 保证 header中的 token 不为 null,且以指定字串开头——Bearer
if (headerToken!=null && headerToken.startsWith(tokenHead)){
// 截取有效 token
String jwtToken = headerToken.substring(tokenHead.length());
String username = jwtUtil.getUsernameInToken(jwtToken);
// 判断 UserDetails 中的用户主体是否为null
if (username!=null && SecurityContextHolder.getContext().getAuthentication() == null){
// SecurityContextHolder.getContext().getAuthentication() == null 代表着此时 Security 中没有登录的用户主体
// 此时可以使用有效地 jwtToken 进行用户认证
UserDetails userDetails = service.loadUserByUsername(username);
// 判断 token 是否有效
if (jwtUtil.validateToken(jwtToken,userDetails)){
// 如果有效则使用 token 中的信息进行登录
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null, userDetails.getAuthorities());
// 根据请求设置 Details,包含了部分请求信息和主体信息。具体效果不清楚...坑
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
// 将 authenticationToken 设置到 SecurityContext 中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
6.1 将 JWT 拦截器设置到 SecurityConfig 的授权方法中。
@Override
protected void configure(HttpSecurity http) throws Exception {
// 由于我们使用的是 JWT 令牌的形式来验证用户,所以可以将 csrf 防御关闭
// JWT 能有效防止 csrf 攻击,强行使用 csrf 可能导致令牌泄露
http.csrf()
.disable()
// 基于 token,不需要使用 Session 了
.sessionManagement() // Session 管理
// 管理 Session 创建策略
// ALWAYS, 总是创建HttpSession
// NEVER, 只会在需要时创建一个HttpSession
// IF_REQUIRED, 不会创建HttpSession,但如果它已经存在,将可以使用HttpSession
// STATELESS; 永远不会创建HttpSession,它不会使用HttpSession来获取SecurityContext
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests() // 授权请求
// 除了上面的请求,其他所有请求都需要认证
.anyRequest()
.authenticated()
.and()
// 禁止缓存
.headers()
.cacheControl();
// 自定义拦截器 JWT 过滤器
http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); // 将过滤器按照一定顺序加入过滤器链。
}
@Bean
public JwtTokenFilter jwtTokenFilter() {
return new JwtTokenFilter();
}
7. 完善 LoginController 请求,运行项目。
LoginController
@RestController
public class LoginController {
@Autowired
private UserService service;
@GetMapping("/hello")
public String hello(){
return "Hello Word!";
}
@GetMapping("/hello1")
public String hello1(){
return "Hello1 Word!";
}
@PostMapping("/login")
public RespBean loginUser(@RequestBody User user, HttpServletRequest request){
return service.login(user.getUsername(),user.getPassword(),request);
}
}
UserService
,使用的时 MVC 模式,所以只展示实现类:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtUtil jwtUtil;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Override
public RespBean login(String username, String password, HttpServletRequest request) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails==null || !passwordEncoder.matches(password,userDetails.getPassword())){
return RespBean.error("用户名或密码错误!");
}
if (!userDetails.isEnabled()){
return RespBean.error("用户状态异常!");
}
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetails,null, userDetails.getAuthorities());
String jwt = jwtUtil.foundJWT(userDetails);
SecurityContextHolder.getContext().setAuthentication(token);
Map<String,String> msg = new HashMap<>();
msg.put("tokenHead",tokenHead);
msg.put("token", jwt);
return RespBean.success("登录成功!",msg);
}
}
7.1 完善 Swagger2Config 配置
由于 JWT 的加入,所以 Swagger2 的方法请求也是需要带入 JWT 令牌,提供了 Security 的全局认证。
只展示了修改的部分。
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo()) // 配置 apiInfo
.select() // 选择那些路径和api会生成document
.apis(RequestHandlerSelectors.basePackage("com.cnda.controller")) // // 对哪些 api进行监控,RequestHandlerSelectors.basePackage 基于包扫描
.paths(PathSelectors.any()) // 对所有路径进行监控
.build()
// 添加和 Security 相关的配置。
.securityContexts(securityContexts())
.securitySchemes(securitySchemes());
}
// 以下方法相对于给 Swagger 添加了一个在 Security 的全局授权,并且以正则的形式设置了授权的请求 url
/**
* securityContexts
* 请求体内容
*/
private List<SecurityContext> securityContexts(){
List<SecurityContext> securityContexts = new ArrayList<>();
securityContexts.add(getContextByPath("/hello/.*"));
return securityContexts;
}
// 通过正则表达式来设置哪些路径
// 通过 Path 获取到对应的 SecurityContext
private SecurityContext getContextByPath(String pathRegex) {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex(pathRegex)) // 按照 String 的 matches 方法进行匹配
.build();
}
/**
* 配置默认的全局鉴权策略;其中返回的 SecurityReference 中,reference 即为 ApiKey 对象里面的name,保持一致才能开启全局鉴权
* @return SecurityReference
*/
private List<SecurityReference> defaultAuth() {
List<SecurityReference> references = new ArrayList<>();
// scope 参数:
AuthorizationScope authorizationScope = new AuthorizationScope("global","accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
references.add(new SecurityReference("Authorization",authorizationScopes));
return references;
}
/**
* securitySchemes
* 安全体方案
*/
private List<SecurityScheme> securitySchemes(){
List<SecurityScheme> apiKeys = new ArrayList<>();
// 设置请求头信息
apiKeys.add(new ApiKey("Authorization","Authorization","Header"));
return apiKeys;
}
修改的部分直接 CV 大法即可。
7.2 运行项目查看效果:
可以看到利用 Swagger2 的调试,返回 JWT Token 令牌成功!
{
"code": 200,
"message": "登录成功!",
"obj": {
"tokenHead": "Bearer",
"token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImlidCI6MTY3Nzk4NzIwNjgyMSwiZXhwIjoxNjc4NTkyMDA2fQ.p_GUqevx8gvCK2txxeEX-RQFm69yDCxCYNlZbeHgVIizSUDO6gaT3a2jGXvzXqofH2uxkQBgN4WfeSIlGydiNA"
}
}
将令牌设置到 Swagger2 中
这样之前的 /hello1
就可以请求成功了:
说明 Swagger2 设置 JWT 也成功了,每次发送请求,头部都会携带 JWT 令牌。
总结
还是对 Security 不太熟悉,Swagger2 的配置比较固定
JWT 主要也是两个点:
- JWT Token Utile 工具类,主要用于管理 JWT 令牌。
- JWT Token Filter JWT 拦截器,这个就是 Security 和 JWT 的集成了,以及请求发来的时候解析 JWT 从而完成免登录这一操作。