前言
自界说登录流程是整合 SpringSecurity 开发必不可少的一步。上篇文章咱们介绍了整合数据库的登录,本篇文章在此基础上整理了 SpringSecurity + JWT + Redis 的登录流程。
全体流程图
登录及认证的全体流程如下图:

依赖
除了 SpringSecurity 的相关依赖外,还需求 Redis 和 hutool (强壮且全面的工具包,本篇文章中 JWT 的相关类也来自该包) 的依赖。
<!-- springboot整合的redis依赖,里边集成了 spring-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.13</version>
</dependency>
装备
在整个流程中,咱们用到了 SpringSecurity + JWT + Redis ,需求装备的是SpringSecurity 和 Redis。
SpringSecurity 装备
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private JwtFilter jwtFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 注入 AuthenticationManager 目标,用于调用认证办法
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 放行登录接口
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 关闭csrf
.csrf().disable()
// 不经过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 把jwt过滤器放到UsernamePasswordAuthenticationFilter前,便于先判别用户是否登录,再决议是否登录
.authorizeRequests()
// 对于登录接口答应匿名访问
.antMatchers("/user/login").anonymous()
// 除上面外的一切恳求全部需求鉴权认证
.anyRequest().authenticated();
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
}
在装备类中,咱们注入了 AuthenticationManager 目标。这个目标在上篇文章咱们提到过,用于调用认证办法。但是在父类 WebSecurityConfigurerAdapter 中并没有将它注入到容器中,而咱们又需求在自己的登录接口中调用它,因而需求重写 authenticationManagerBean 办法并将回来的目标注入到容器中。 configure(HttpSecurity http) 办法用于装备路由,只敞开登录接口,其他接口都需求认证。
Redis装备
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
// 设置序列化
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(
Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 装备redisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
RedisSerializer<?> stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);// key序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// value序列化
redisTemplate.setHashKeySerializer(stringSerializer);// Hash key序列化
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);// Hash value序列化
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
在 Redis 装备中大部分是常规的序列化装备,特殊的是 ObjectMapper ,这个装备是为因为存储在 Redis 的实体类中除了成员变量的 get 办法外,其他办法不能有回来值,否则会导致反序列化反常,而 ObjectMapper 就是为了解决这个问题。 反序列化反常的问题可以经过下面的代码验证
@Test
public void test(){
RedisTestEntity entity = new RedisTestEntity();
entity.setName("111");
redisTemplate.opsForValue().set("name", entity);
}
@Test
public void get(){
Object name = redisTemplate.opsForValue().get("name");
System.out.println(name);
}
@Data
public class RedisTestEntity {
private String name;
public Integer requireAge(){
return 18;
}
}
界说登录接口
参数的接纳
@Data
public class UserLogin {
private String username;
private String password;
}
controller部分
@PostMapping("/login")
public R<String> login(@RequestBody UserLogin userLogin){
String jwt = securityService.login(userLogin);
return R.success().data(jwt);
}
controller 仅仅担任匹配路由和回来数据,业务经过 service 的相关办法完结,因而 controller 中没有太多代码
service部分
@Resource
private AuthenticationManager manager;
@Resource
private RedisTemplate redisTemplate;
@Override
public String login(UserLogin userLogin) {
Authentication userAuthentication = new UsernamePasswordAuthenticationToken(userLogin.getUsername(), userLogin.getPassword());
Authentication authenticate = manager.authenticate(userAuthentication);
// 假如认证成功则进入生成token的逻辑
if (authenticate.isAuthenticated()) {
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
SysUser user = loginUser.getUser();
// 将登录成功的目标存入redis
redisTemplate.opsForValue().set(KeyUtil.getLoginUserKey(user.getUserId()), loginUser);
// 生成token
String token = JWT
.create()
.setPayload(userLoginId, user.getUserId())
// (签发时刻)---------(生效时刻)---------(当前时刻)---------(失效时刻)
.setIssuedAt(new Date())
// 过期时刻七天
.setExpiresAt(new Date(System.currentTimeMillis() + DateUnit.WEEK.getMillis()))
// // 设置HS256为加密算法,以用户的密码为盐(密钥)
.setSigner("HMD5", salt.getBytes(StandardCharsets.UTF_8))
.sign();
return token;
}
throw new RuntimeException("用户名或密码过错");
}
service 完结了登录的主要流程,包括:
- 调用 AuthenticationManager 实例的 authenticate 办法对用户的账号密码进行验证,该办法会调用到咱们上篇文章自界说的办法,经过查询数据库的数据完结校验
- 假如校验成功,则将用户信息存入 Redis 并生成相应 token ,同时将用户 id 存入 token 的荷载中,失利则抛出反常
登录过滤器
完结登录后,在今后的每次恳求都需求在恳求头中带上 token 以便于认证,认证操作经过过滤器完结(关于 jwt 的具体常识本篇文章不做讨论,不熟悉请自行查阅相关资料)。
@Component
public class JwtFilter extends OncePerRequestFilter {
@Resource
private RedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
// 没有token,去走登录流程
if (StrUtil.isBlank(token)) {
filterChain.doFilter(request, response);
return;
}
// token不能为空
JWT jwt = JWTUtil.parseToken(token);
// 验证token是否合法
HMacJWTSigner singer = new HMacJWTSigner(AlgorithmUtil.getAlgorithm("HMD5"), salt.getBytes(StandardCharsets.UTF_8));
boolean common = jwt.verify(singer);
// 验证时刻,失利会抛出反常
try {
JWTValidator.of(jwt).validateDate(DateUtil.date());
} catch (ValidateException exception) {
throw new TokenInvalidException("token反常");
}
if (common){
NumberWithFormat userIdObj = (NumberWithFormat)jwt.getPayload(userLoginId);
Integer userId = userIdObj.intValue();
LoginUser loginUser = (LoginUser) redisTemplate.opsForValue().get(KeyUtil.getLoginUserKey(userId));
// 假如用户不存在,阐明token反常
if (loginUser == null) {
throw new TokenInvalidException("token反常");
}
// 将用户信息存入 SecurityContextHolder ,以便本次在恳求中运用
UsernamePasswordAuthenticationToken authenticationLoginUser = new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationLoginUser);
filterChain.doFilter(request, response);
}
}
}
咱们自界说的过滤器类继承了 OncePerRequestFilter 类偏重写了 doFilterInternal 办法,然后在 SpringSecurity 的装备类中将其添加到 UsernamePasswordAuthenticationFilter 前面(校验账号密码之前),对应装备类中的如下代码
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
在过滤器中,咱们首要校验了 token 是否存在。假如不存在则直接放行,然后在后续校验权限时会被主动阻拦下来。存在则验证 token 的合法性,经往后从 token 拿到用户 id ,从 Redis 中获取到用户信息,假如不存在阐明用户的登录状况反常(可能是退出了登录状况,后文会说)。从 Redis 中取到用户信息后,就可以将用户信息存到 SecurityContextHolder 中,方便后续进行认证、授权以及运用。最后放行,履行后续操作。
退出登录
在做完前面的操作后,退出登录的操作就很简单了。
@PostMapping("/logout")
public R<String> logout(){
Boolean delete = securityService.logout();
return R.success().data(delete);
}
@Override
public Boolean logout() {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Integer userId = loginUser.getUser().getUserId();
Boolean delete = redisTemplate.delete(KeyUtil.getLoginUserKey(userId));
return delete;
}
controller 和 service 的代码分别如图。 在 service 中咱们删除了 Redis 中的用户信息,这也就解释了为什么过滤器中会存在 token 合法但是用户信息不存在的状况(退出登录),当然也可能有其它的特殊状况。
结语
本篇文章就到这了,下一篇应该是关于授权的内容,咱们下次再会。