SpringSecurity执行流程分析与使用
一、SpringSecurity过滤器链
-
UsernamePasswordAuthenticationFilter:用户认证相关
-
ExceptionTranslationFilter:异常处理相关
-
FilterSecurityInterceptor:用户授权相关
二、核心过滤器之 UsernamePasswordAuthenticationFilter
此过滤器主要完成用户认证功能。认证流程如下:
1. 调用 attemptAuthentication方法
进行一些前置处理(获取用户输入的表单对象,并封装为UsernamePasswordAuthenticationToken对象,也就是Authentication接口实现类对象)。
2. 调用authenticate方法
核心方法,进入到ProviderManager类中开始进行用户认证
3. 调用retrieveUser方法
进入AbstractUserDetailsAuthenticationProvider类中,检索用户(判断表单用户是否合法)
4. 调用loadUserByUsername方法
进入DaoAuthenticationProvider类中,调用UserDetailsService接口实现类中的loadUserByUsername方法。因此我们只需要实现UserDetailsService接口,在loadUserByUsername方法中写入判断逻辑,就可以通过用户表单输入的用户名来判断该用户是否合法。
5. 实现UserDetailsService接口
开发人员实现此接口,返回系统用户对象(也就是数据库查询出来的用户信息,包括权限信息也可以放入UserDetails接口实现类对象里)
6. 调用additionalAuthenticationChecks方法
回到AbstractUserDetailsAuthenticationProvider类中,调用additionalAuthenticationChecks方法,进行密码校验。
7. 调用createSuccessAuthentication方法
密码校验通过之后。调用createSuccessAuthentication方法,将从数据库中查询得到的该用户的权限信息,赋予表单用户对象
流程总结
1、首先用户输入用户名和密码进行登录,UsernamePasswordAuthenticationFilter过滤器就会将用户输入的表单对象封装成authentication接口实现类对象,然后调用authenticate方法对这个对象进行用户认证; ====> 开始认证
2、authenticate方法最终会调用UserDetailsService中的loadUserByName方法。因此,我们开发人员需要手动编写UserDetailsService的接口实现类对象,重写它的loadUserByName方法,在方法体中进行查询判断,判断用户在表单中输入的用户名是否在数据库中存在(也就是是否为合法用户)。如果是合法用户,将查询得到的正确的用户信息封装为UserDetails进行返回; 如果不是合法用户,说明用户名输入错误,直接抛出异常; ====> 认证阶段一
3、用户输入的用户名如果为合法的用户名,那么框架内部会自动调用additionalAuthenticationChecks方法,进行密码校验,校验用户输入的密码和系统中用户的正确密码是否一致。如果一致,将系统用户(UserDetils对象)的相关权限信息赋值给表单用户(Authentication对象),并进行返回; 如果不一致,说明密码输入错误,直接抛出异常; ====> 认证阶段二
三、项目整合SpringSecurity
虽然springsecurity提供了默认的用户名和密码以及登录页,供我们使用。但是这种不符合我们正常开发的场景,因此,我们需要自定义一些拓展。例如:不使用默认的用户名密码进行登录,而是从数据库中进行验证登录;不使用默认的登录页面等等。
1、引入依赖
// 这里使用的springboot版本为2.7.5
// gradle
implementation 'org.springframework.boot:spring-boot-starter-security:2.7.5'
// maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.5</version>
</dependency>
2、SecurityConfig配置类
@Configuration
public class SecurityConfig {
/**
* springSecurity中内置:用于密码加密。
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 通过 getAuthenticationManager方法 获取 AuthenticationManager 对象,并注入ioc容器中。
* 通过 AuthenticationManager.authenticate 方法进行登录认证。
*/
@Autowired
private AuthenticationConfiguration authenticationConfiguration;
@Bean
public AuthenticationManager getAuthentication() throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
/**
* 配置相关的过滤器链设置,并注入IOC容器中。
* 老版本的写法是:继承WebSecurityConfigurerAdapter类,重写它的configure(HttpSecurity http)方法。
*/
@Autowired
private TokenAuthenticationFilter tokenAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 这里其他api还有很多,可以自行查看,这里只做基本演示。
http
// 关闭csrf防护
.csrf().disable()
// 调整策略,不使用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 不需要认证的请求,允许匿名访问
.antMatchers("/test/login").anonymous()
// 其他请求都需要认证
.anyRequest().authenticated();
// // 自定义登录认证过滤器,在所有拦截器之前执行
http.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
3、自定义UserDetailsService接口实现类
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
/***
* 根据账号获取系统用户信息
* @param username:前台输入的用户名。
* @return: org.springframework.security.core.userdetails.UserDetails
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库中取出用户信息
User user = userService.findUserByName(username);
// 判断用户是否存在
if (Objects.isNull(user)){
throw new UsernameNotFoundException("用户名不存在!");
}
// 将用户信息存入 securityUser 对象,此对象为UserDetails接口的实现类对象
// 这里返回的是数据库中正确的用户信息,之后security会自行验证用户输入的密码是否正确,无需开发人员关注
SecurityUser securityUser = new SecurityUser(user);
return securityUser;
}
}
4、自定义UserDetails接口实现类
@Data
@Slf4j
@NoArgsConstructor
@AllArgsConstructor
public class SecurityUser implements UserDetails {
//当前登录用户(transient表示该属性不能被序列化)
private transient User currentUserInfo;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null; // TODO 这里是权限相关,暂时没整合
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.getName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
5、编写登录接口
controller
@ApiOperation("用户登录")
@PostMapping("/login")
public R login(@RequestBody User user) {
String token = loginService.login(user);
return StringUtils.isNotEmpty(token) ? R.success().message("登录成功!").data("token", token) : R.failed().message("登录失败!");
}
service
public interface LoginService {
/**
* 用户登录
* @param user:前端表单提交过来的对象
* @return:返回token
*/
String login(User user);
}
serviceImpl
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenManager tokenManager;
@Override
public String login(User user) {
// 1、通过authenticate方法进行校验。
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getName(),user.getPassword());
Authentication authentication = authenticationManager.authenticate(authenticationToken);
if (Objects.isNull(authentication)){
throw new RuntimeException("用户名或密码输入错误!"); // authentication对象为null表示登录失败。
}
// 2、校验成功后,生成JWT返回
User successUser = ((SecurityUser) authentication.getPrincipal()).getCurrentUserInfo();
return tokenManager.createToken(successUser.getName());
}
}
6、自定义认证过滤器
/**
* @author zbinyds
* @title: TokenAuthenticationFilter
* @projectName demo
* @description: 认证过滤器。继承OncePerRequestFilter,用户每次请求先走这个过滤器,判断请求头中是否存在token,再走security内置的过滤器。
* @date 2022.11.21 19:00
*/
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private TokenManager tokenManager; // 工具类,用于生成/解析token。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 将用户信息封装authentication对象
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
if (Objects.isNull(authentication)){
filterChain.doFilter(request,response);
return;
}
// 存入SecurityContextHolder中
SecurityContextHolder.getContext().setAuthentication(authentication);
// 放行
filterChain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
// 从请求头中获取token值
String token = request.getHeader("token");
if (!StringUtils.isEmpty(token)) {
try {
// 解析token得到用户名
String userName = tokenManager.getUserNameFromToken(token);
return new UsernamePasswordAuthenticationToken(userName, token, null); // TODO 权限没整合,所以这里返回null
} catch (Exception e) {
throw new RuntimeException("token不合法!");
}
}
return null;
}
}
至此,用户的认证功能就已经完成了。至于权限相关的,暂时还没有引入。
7、其他工具类
tokenManager
/**
* @author zbinyds
* @time 2022/09/13 19:34
* @description:jwt工具类。用户生成token、根据token取值。
*/
@Component
@Slf4j
public class TokenManager {
private long tokenExpiration = 15 * 60 * 1000; // token过期时间(15min有效)
private String tokenSignKey = "zbinyds"; // jwt秘钥
/**
* 根据username生成token
*
* @param username:认证成功的用户名
* @return:token
*/
public String createToken(String username) {
String token = Jwts.builder().setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
return token;
}
/**
* 解析token。根据token获取用户名。
*
* @param token
* @return
*/
public String getUserNameFromToken(String token) throws Exception {
String userName = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
return userName;
}
/**
* 将token设置为过期状态。
* @param token
*/
public void removeToken(String token) {
//jwttoken无需删除,客户端扔掉即可。
}
}