SpringSecurity+JWT认证流程解析
楔⼦
本⽂适合: 对Spring Security有⼀点了解或者跑过简单demo但是对整体运⾏流程不明⽩的同学,对SpringSecurity有兴趣的也可以当作你们的⼊门教程,⽰例代码中也有很多注释。
⼤家在做系统的时候,⼀般做的第⼀个模块就是认证与授权模块,因为这是⼀个系统的⼊⼝,也是⼀个系统最重要最基础的⼀环,在认证与授权服务设计搭建好了之后,剩下的模块才得以安全访问。
市⾯上⼀般做认证授权的框架就是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() {
Context();
}
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的认证,
validation框架
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 = One(queryWrapper);
if (userInfo == null) {
throw new UsernameNotFoundException("⽤户名不存在,登陆失败。");
}
// 构建UserDetail对象
UserDetail userDetail = new UserDetail();
userDetail.setUserInfo(userInfo);
List<RoleInfo> roleInfoList = roleInfoService.UserId());
userDetail.setRoleInfoList(roleInfoList);
return userDetail;
}
}
复制代码
实现UserDetailsService的抽象⽅法并返回⼀个UserDetails对象,认证过程中SpringSecurity会调⽤这个⽅法访问数据库进⾏对⽤户的搜索,逻辑什么都可以⾃定义,⽆论是从数据库中还是从缓存中,但是我们需要将我们查询出来的⽤户信息和权限信息组装成⼀个UserDetails返回。