认证
1. 登录校验流程
通常而言,一个系统的登录校验流程可用下图表示:

2. 入门案例认证流程

Authentication
接口:表示当前访问系统的用户,实现类中封装了用户相关信息AuthenticationManager
接口:定义了认证的方法UserDetailsService
接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法UserDetails
接口:提供核心用户信息。通过UserDetailsService
根据用户名获取要处理的用户信息,并封装成UserDetails
对象返回,然后将这些信息封装到Authentication
对象中。
3. 一种“认证-校验”方案
3.1 方案示意

登录认证:
- 自定义登录接口:调用
ProviderManager
的方法进行认证,如果认证通过生成 jwt,同时把用户信息存入 redis 中 - 自定义
UserDetailsService
,在其中查询数据库来获取用户信息。
校验:
- 定义 jwt 认证过滤器:获取 token、解析 token 获取其中的 userId、从 redis 中获取用户信息、存入
SecurityContextHolder
3.2 数据结构假设
User
(DAO 层实体):@Data public class User { private long id; private String username; private String password; }
LoginUser
(适配 Spring Security 的一种“DTO”):@Data @NoArgsConstructor @AllArgsConstructor public class LoginUser implements UserDetails { private User user; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return user.getPassword(); } ... }
3.3 自定义登录接口
如果我们不用 Spring Security 提供的默认登录接口(POST http://localhost:8080/login
),直接自定义一套 controller,然后在对应的登录方法中调用 AuthenticationManager#authenticate
方法来触发 Spring Security 的用户认证即可。
代码如下:
LoginController
:为了省事,这里就直接在 controller 写 service 层的业务逻辑了。
@RestController public class LoginController { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; @PostMapping("/user/login") public String login(@RequestBody User user) { // AuthenticationManager authenticate 进行用户认证 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()); Authentication authentication = authenticationManager.authenticate(authenticationToken); // 如果认证没通过,给出对应提示 if (authentication == null) { throw new RuntimeException("登录失败"); } // 如果认证通过,使用 userId 生成一个 jwt,将 jwt 返回 LoginUser loginUser = (LoginUser) authentication.getPrincipal(); long userId = loginUser.getUser().getId(); String jwt = JwtUtils.createJWT(String.valueOf(userId)); redisCache.setCacheObject("login:" + userId, loginUser); return jwt; } }
配置类:
- 为了注入上述 controller 中依赖的 bean,需要在配置类中建造出相应的 bean
- 此外,作为登录接口,其本身显然不能够再被 Spring Security 要求进行认证拦截,应该直接被放行,故而也需要进行相应的配置
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @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() .authorizeRequests() // 对于登录接口,允许匿名访问 .antMatchers("/user/login").anonymous() // 除了上述请求,所有请求全都需要鉴权认证 .anyRequest().authenticated(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
3.4 密码加密存储
实际项目中,我们不会把密码明文存储在数据库中。
默认使用的PasswordEncoder
要求数据库中的密码格式为:{id}password
,它会根据id
去判断密码的加密方式。但一般我们不会采用这种方式,所以就需要替换PasswordEncoder
,我们一般使用 Spring Security 提供的BCryptPasswordEncoder
。
在使用BCryptPasswordEncoder
时,只需要把BCryptPasswordEncoder
对象注入到 Spring 容器中即可,Spring Security 会自动使用该PasswordEncoder
来进行密码校验。此外,在定义这个 Spring 配置类时,Spring Security 要求这个配置类要继承WebSecurityConfigurerAdapter
,即:
注意,
WebSecurityConfigurerAdapter
这个类已经标记为废弃,详情见 Spring Security 即将弃用配置类 WebSecurityConfigurerAdapter。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3.5 自定义UserDetailsService
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查询用户信息
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName, username);
User user = userMapper.selectOne(wrapper);
// 如果查询不到数据就通过抛出异常来给出提示
if (Objects.isNull(user)) {
throw new RuntimeException("用户名或密码错误");
}
// TODO 根据用户查询权限信息,添加到 LoginUser 中
// 封装成 UserDetails 对象返回
return new LoginUser(user);
}
}
3.6 定义 jwt 校验过滤器
需要实现一个过滤器,然后配置它在过滤器链中的顺序。这里就暂略了。.....
3.7 退出登陆
只需要定义一个退出接口,然后获取SecurityContextHolder
中的认证信息,同时删除 redis 中的对应缓存即可。
略了......