一、前言
eladmin采用Spring Security + JWT作为安全框架。因此我们先了解一下Spring Security的认证流程。
二、Spring Security 认证流程
这块内容网上资料较多,可参考:
这里借用一下毕竟尹稳健老哥梳理的Spring Security认证流程图作为参考。
三、 eladmin 认证流程
类比Spring Security的认证流程,在eladmin中的落地实践: 大致流程如图所示,和Spring Security大同小异,主要区别是多了个TokenFilter
少了UsernamePasswordAuthenticationFilter
。由于eladmin通过SpringSecurityConfig
自定义了认证流程,默认的UsernamePasswordAuthenticationFilter
失效了。UsernamePasswordAuthenticationFilter
的主要作用其实就是把用户名密码封装为UsernamePasswordAuthenticationToken
对象,并传给身份验证管理器进行身份验证,这个步骤在eladmin中写在了AuthorizationController
的登入接口中,如下图:
下图是eladmin的授权认证模块。接下来我们从TokenFilter
开始抽丝剥茧一步一步分析下面这些类是如何搭配使用的。
3.1 TokenFilter(重要)
TokenFilter:顾名思义token的过滤器,它继承了GenericFilterBean
成为了过滤器链中的一环。
1. GenericFilterBean
GenericFilterBean
是 Spring Framework 中的一个抽象类,用于实现自定义的 Servlet 过滤器,GenericFilterBean
提供了一个方便的基类,使得创建自定义过滤器变得简单,只需要继承 GenericFilterBean
并实现 doFilter
方法即可。
2. TokenProvider
Token提供者,提供了下列方法用于管理Token:
- createToken(): 创建Token 设置永不过期,Token 的时间有效性转到Redis 维护。这样可以灵活地管理用户的登录状态,包括主动注销、强制下线等操作。
- getAuthentication(String token): 依据Token 获取鉴权信息。值得注意的是该方法的返回值:
java
Authentication getAuthentication(String token) {
Claims claims = getClaims(token);
User principal = new User(claims.getSubject(), "******", new ArrayList<>());
// 这个构造方法会执行super.setAuthenticated(true)即不需要再次认证。
return new UsernamePasswordAuthenticationToken(principal, token, new ArrayList<>());
}
- checkRenewal(String token):处理Token续期,更新redis中token的过期时间。其中
SecurityProperties
类,该类记录了JWT的参数配置。通过@ConfigurationProperties
从yml配置文件中读取,既能解决硬编码问题,也能实现灵活配置。 - getToken(HttpServletRequest request):从请求头中获取token。
- loginKey(String token):进一步封装token,作为用户登入的key存在redis中。
补充:InitializingBean
TokenProvider
实现了InitializingBean
接口。InitializingBean
是Spring提供的拓展性接口,InitializingBean
接口为bean提供了属性初始化后的处理方法,它只有一个afterPropertiesSet
方法,凡是继承该接口的类,在bean的属性初始化后都会执行该方法。和它功能类似的还有@PostConstruct
。在此处,TokenProvider
类的afterPropertiesSet
方法,对jwtParser
,jwtBuilder
两个属性做了初始化。
3. SecurityProperties
用于做Jwt参数的配置类。在application.yml中进行如下配置:
yml
#jwt
jwt:
header: Authorization
# 令牌前缀
token-start-with: Bearer
# 必须使用最少88位的Base64对该令牌进行编码
base64-secret: ZmQ0ZGI5NjQ0MDQwY2I4MjMxY2Y3ZmI3MjdhN2ZmMjNhODViOTg1ZGE0NTBjMGM4NDA5NzYxMjdjOWMwYWRmZTBlZjlhNGY3ZTg4Y2U3YTE1ODVkZDU5Y2Y3OGYwZWE1NzUzNWQ2YjFjZDc0NGMxZWU2MmQ3MjY1NzJmNTE0MzI=
# 令牌过期时间 此处单位/毫秒 ,默认2小时,可在此网站生成 https://www.convertworld.com/zh-hans/time/milliseconds.html
token-validity-in-seconds: 7200000
# 在线用户key
online-key: "online-token:"
# 验证码
code-key: "captcha-code:"
# token 续期检查时间范围(默认30分钟,单位默认毫秒),在token即将过期的一段时间内用户操作了,则给用户的token续期
detect: 1800000
# 续期时间范围,默认 1小时,这里单位毫秒
renew: 3600000
然后在ConfigBeanConfiguration
中注入SecurityProperties
bean对象。
java
/**
* @apiNote 配置文件转换Pojo类的 统一配置 类
* @author: liaojinlong
* @date: 2020/6/10 19:04
*/
@Configuration
public class ConfigBeanConfiguration {
@Bean
@ConfigurationProperties(prefix = "login")
public LoginProperties loginProperties() {
return new LoginProperties();
}
@Bean
@ConfigurationProperties(prefix = "jwt")
public SecurityProperties securityProperties() {
return new SecurityProperties();
}
}
4. OnlineUserService
在线用户服务,提供了下列方法用于管理在线用户信息:
- save(JwtUserDto jwtUserDto, String token, HttpServletRequest request):把认证通过的用户信息记录到redis中,记录的信息有"用户名","token","浏览器","IP","登入时间"等,这些信息都封装在
OnlineUserDto
中。其中token使用对称加密算法,再次加密了一次,提高安全性。 - getAll(String username):查询某个账号下的所有在线登入信息。当不传username时,获取所有账号下的在线登入信息。
- kickOutForUsername(String username):踢掉之前登入的用户。可选,如果用户禁止多人登入同一个账号的话就会调用这个方法。值得注意的是 该方法上加了
@Ansy
注解,意味着该方法将被异步执行,而未在项目中找到继承AsyncConfigurer
的类,意味着用的是默认线程池SimpleAsyncTaskExecutor
。
5. UserCacheManager
管理用户缓存用于减少数据库查询,提供了下列方法:
- addUserCache(String userName, JwtUserDto user): 添加
JwtUserDto
到redis中。 - getUserCache(String userName):从redis中获取缓存的
JwtUserDto
。 - cleanUserCache(String userName):删除缓存的
JwtUserDto
。
3.2 UserDetailsServiceImpl(重要)
UserDetailsServiceImpl
是 Spring Security 框架中的一个实现了 UserDetailsService
接口的类。它主要负责从数据源(如数据库)中加载用户信息,并将这些信息封装成一个实现了 UserDetails
接口的对象,以供 Spring Security 在进行身份认证和授权时使用。我们看看eladmin中是如何实现UserDetailsService
接口的:
1. UserService
UserService
用于管理eladmin的系统用户,提供不限于如下方法:
- updatePass(String username, String encryptPassword):更新用户密码
- create(User resources):创建用户
- findById(long id):根据ID查询用户。值得注意的是该方法上添加了
@Cacheable(key = "'id:' + #p0")
注解,意味着每次查询到的结果都会缓存起来,这样就能提高下次查询效率了。在eladmin项目中RedisConfig
继承CachingConfigurerSupport
,重写了keyGenerator
自定义了缓存key生成策略。
在UserDetailsServiceImpl
中UserService
用于从数据源中查询用户是否存在。
2. RoleService
RoleService
用于管理eladmin中的角色信息(eladmin权限控制采用 RBAC
思想,这一点在鉴权流程中细说),提供不限于如下方法:
- create(Role resources):创建角色
- void delete(Set ids):删除角色
- findByUsersId(Long id):查询某个用户有哪些角色
- updateMenu(Role resources, RoleDto roleDTO):修改绑定的菜单
在UserDetailsServiceImpl
中RoleService
用于查询用户权限信息。并封装到JwtUserDto
类中。如下图:
其中mapToGrantedAuthorities()
方法就是把形如user:list、roles:list
这样的用户权限信息封装进List<AuthorityDto>
集合中。
3. DataService
DataService
数据权限服务类,只提供了一个方法:
- getDeptIds(UserDto user): 查询用户的数据权限。
3.3 JwtAuthenticationEntryPoint
该类继承了AuthenticationEntryPoint
,处理经身份验证的用户尝试访问安全的资源时返回的响应。 补充:当用户没有提供任何凭据尝试访问安全的资源时,会调用AuthenticationEntryPoint的commence()方法,该方法负责发送一个401 Unauthorized的响应给用户,提示用户需要进行身份验证才能继续访问该资源。
3.4 AuthorizationController
AuthorizationController
作为认证接口层,提供了登入,登出、获取验证码等接口。这个类中的成员变量前文都已介绍,只有AuthenticationManagerBuilder
第一次出场。
1.AuthenticationManagerBuilder
AuthenticationManagerBuilder是Spring Security提供的一个配置类,用于构建和配置AuthenticationManager(身份验证管理器)。AuthenticationManagerBuilder提供了一系列方法,可以用于定义如何进行用户身份验证。它允许开发人员通过链式调用来配置不同的身份验证方式,例如内存验证、数据库验证、LDAP验证等。
以下是一些常用的AuthenticationManagerBuilder方法:
- inMemoryAuthentication(): 配置基于内存的用户认证。可以使用该方法添加用户、密码和角色信息。
- jdbcAuthentication(): 配置基于JDBC的用户认证。可以使用该方法指定数据源、查询用户信息的SQL语句以及查询用户权限的SQL语句。
- userDetailsService(): 指定自定义的UserDetailsService实现类,用于加载用户信息。可以使用该方法自定义用户信息的加载逻辑,例如从数据库、LDAP等获取用户信息。
- authenticationProvider(): 添加自定义的身份验证提供者。可以使用该方法将自定义的AuthenticationProvider实现类添加到身份验证流程中。
通过使用这些方法,可以根据应用程序的需求来配置合适的身份验证方式。在配置完成后,AuthenticationManagerBuilder会构建出一个AuthenticationManager实例,用于处理身份验证请求。
总结
上述类相互配合,便完成了eladmin的认证流程。接下来将通过流程图展示这些类之间是如何协调,完成认证的: