一、代码先行
1、设计模式
SpringSecurity 采用的是 责任链 的设计模式,是一堆过滤器链的组合,它有一条很长的过滤器链。
不过我们不需要去仔细了解每一个过滤器的含义和用法,只需要搞定以下几个问题即可:怎么登录、怎么校验账户、认证失败处理。
2、 POM依赖
没啥好说的,maven导入即可。
不写版本号,默认就会下载 最新的 版本。
<!--springSecurity-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3、登录
不管你用哪种权限框架,第一个要解决的问题就是登录。就是在我们的登录接口中,将账户密码委托给权限框架接管,让权限框架帮我们做校验和权限认证。
新建一个登录方法,目的是 验证用户的 用户名 和 密码,并在验证成功后为该用户生成一个JWT。
@GetMapping("/login")
public String securityLogin(String username,String password){
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username,password);
// 使用authenticationManager调用loadUserByUsername获取数据库中的用户信息,
Authentication authentication = authenticationManager.authenticate(authToken);
if(authentication == null) {
throw new RuntimeException("登录失败啦呀");
}
//获取符合security规范的User
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
String token = jwtUtil.createToken(securityUser.getUser());
return token;
}
UsernamePasswordAuthenticationToken
就是 我们委托 框架 帮我们托管 的 登录凭证。
然后是:
单词重点说明: authenticate (v) 证明…是事实; authentication (n)证明
Authentication authentication = authenticationManager.authenticate(authToken);
这就是spring security
帮我们 执行 认证 和 授权 的方法,最终返回一个 认证结果。
大家思考一下,正常登录的逻辑无非是 4步走:
- 输入账号密码
- 根据账号从 DB数据库 中获取用户实体
- 校验密码是否正确
- 校验成功,将用户生成token后返回
我们再回过来看 上面的代码,第2步和第3步没见到。
肯定是spring security
已经帮我们做了,但是,这并不代表我们可以省略这两步,只是需要我们写在别的地方,仅此而已。
4、根据账号从DB中获取用户实体
这个步骤是不可能不写的,只是写到了别处。
说明一点,spring security
中的 用户概念,有自己的一套规则,不能直接用 我们系统里面的 User类。
因此如果我们要用spring security
,就得 实现 他的 用户接口:UserDetails
。
所以,我们新建一个SecurityUser类:
@Data
@NoArgsConstructor
public class SecurityUser implements UserDetails {
// 使用聚合模式,将我们自己的User对象聚合到SecurityUser中
// user字段存储了用户的一些信息,例如用户名和密码等
private User user;
// 覆盖UserDetails接口中的getAuthorities方法,返回用户的权限集合
// 这里返回null,表示未实现获取权限的逻辑
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
// 覆盖UserDetails接口中的getPassword方法,返回用户的密码
// 这里通过调用user对象的getPwd方法获取密码
@Override
public String getPassword() {
return user.getPwd();
}
// 覆盖UserDetails接口中的getUsername方法,返回用户的用户名
// 这里通过调用user对象的getUserName方法获取用户名,并注释说明有的地方可能会用email作为用户名,这里还是使用userName
@Override
public String getUsername() {
// 用户名:有的地方可能会用email作为用户名,我们这还是userName
return user.getUserName();
}
// 覆盖UserDetails接口中的isAccountNonExpired方法,判断账户是否过期
// 这里直接返回true,表示账户不过期
@Override
public boolean isAccountNonExpired() {
return true;
}
// 覆盖UserDetails接口中的isAccountNonLocked方法,判断账户是否被锁定
// 这里直接返回true,表示账户未被锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
// 覆盖UserDetails接口中的isCredentialsNonExpired方法,判断凭证是否过期
// 这里直接返回true,表示凭证不过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 覆盖UserDetails接口中的isEnabled方法,判断用户是否启用
// 这里直接返回true,表示用户启用状态为true
@Override
public boolean isEnabled() {
return true;
}
}
虽然我们系统里面有自己的 user 了,但是为了适配,所以就 聚合 进来。
然后是如何查询DB,肯定得有个 Service 去查询,我们已经有自己的UserService了,但是很可惜,spring security
这个地方也有自己的规范,我们自己写的user Service,人家不认,唉。
没办法,重新写个Service,自己写都写了,也不能丢了,所以 依照上面的操作,还是聚合进来。
@Service
@Slf4j
public class UserDetailService implements UserDetailsService {
@Resource
UserService userService;
/**
* 根据用户名直接从DB中查询用户数据,作为登录校验的依据
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.getByUsername(username);
if (user == null) {
log.info("username not found");
throw new UsernameNotFoundException("username not found");
}
SecurityUser securityUser = new SecurityUser();
securityUser.setUser(user);
return securityUser;
}
}
核心逻辑 是,我们还是用之前的方法拿到 User,但为了适配,就塞到 SecurityUser 里面去。
UserDetailsService 是spring security
认可的接口,我们得实现这个接口,并且实现loadUserByUsername
方法,这个方法在spring security
的认证逻辑里面会用到。目的就是拿到DB中真实的User,跟我们登录的账号密码进行比对。
5、校验密码是否正确
很多时候我们的密码是要进行加密的,但是我们登录肯定传的是明文密码,所以需要转换后再去比对。
这个 校验密码 的逻辑,需要写在spring security
的 配置类 中。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
UserDetailService userService;
/**
* 新增security账户
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.equals(encodedPassword);
}
});
}
}
因为我们的项目并没有对密码加密,所以就直接比较了。
在 LoginController 中,我们用到了AuthenticationManager
这个对象,需要在配置类中注册。
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
// 身份验证管理器, 直接继承即可.
return super.authenticationManagerBean();
}
AuthenticationManager 是 Spring Security框架中的一个核心接口,它负责处理身份验证请求。
在认证过程中,用户提交身份验证信息(如用户名和密码),AuthenticationManager 会验证这些信息的有效性。
最后是 路由 的相关配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 禁用跨站请求伪造保护
.csrf().disable()
// 设置会话管理策略为无会话,因为我们使用token作为信息传递介质
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 进行认证请求的配置
.authorizeRequests()
// 将所有登入和注册的接口放开,这些都是无需认证就访问的
.antMatchers("/security/login").anonymous()
// 除了上面的那些,剩下的任何接口请求都需要经过认证
.anyRequest().authenticated()
.and()
// 允许跨域请求
.cors()
;
// 在UsernamePasswordAuthenticationFilter之前添加JWT认证过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
这段代码是Spring Security框架中用于配置HTTP安全的部分。它主要涉及到跨站请求伪造(CSRF)的禁用、会话管理策略的设置、认证请求的配置以及跨域请求的允许。同时,还添加了一个JWT(JSON Web Token)认证过滤器。
因为我们项目用到了jwt,所以在进行账号密码验证之前,要先走jwt的过滤器。
jwt过滤器代码如下:
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
JWTUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ServletException, IOException {
//获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//token为空的话, 就不管它, 让SpringSecurity中的其他过滤器处理请求
//请求放行
filterChain.doFilter(request, response);
return;
}
//token不为空时, 解析token
User user = null;
try {
user = jwtUtil.verify(token);
} catch (Exception e) {
// 过滤器中抛出的异常,无法被统一异常捕获,所以在这里直接返回
e.printStackTrace();
Result result = new Result();
result.setCode(403);
result.setMsg("Token无效:" + e.getMessage());
WebUtils.response(response,result);
return;
}
SecurityUser securityUser = new SecurityUser();
securityUser.setUser(user);
//将用户安全信息存入SecurityContextHolder, 在之后SpringSecurity的过滤器就不会拦截
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(securityUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
流程参考这个图:
如果校验成功了,那么在登录方法中就会往下走,生成token返回,结束。
6、全局异常返回
认证过程中,难免出现各种异常,我们一般会做一个通用的返回
Result
@Data
public class Result implements Serializable {
private int code;
private String msg;
private Object data;
public static Result succ(Object data) {
return success(200, "操作成功", data);
}
public static Result error(String msg) {
return error(400, msg, null);
}
public static Result success (int code, String msg, Object data) {
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
public static Result error (int code, String msg, Object data) {
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
}
GlobalExceptionHandler
/**
* 全局异常处理
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 400 错误:运行时异常
* @param e
* @return
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = RuntimeException.class)
public Result handler(RuntimeException e) {
log.error("运行时异常:----------------{}", e.getMessage());
return Result.error(e.getMessage());
}
/**
* 403 错误:权限不足
* @param e
* @return
*/
@ResponseStatus(HttpStatus.FORBIDDEN)
@ExceptionHandler(value = AccessDeniedException.class)
public Result handler(AccessDeniedException e) {
log.info("security权限不足:----------------{}", e.getMessage());
return Result.error("权限不足");
}
/**
* 400 错误:异常请求-方法参数不匹配
* @param e
* @return
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result handler(MethodArgumentNotValidException e) {
log.info("实体校验异常:----------------{}", e.getMessage());
BindingResult bindingResult = e.getBindingResult();
ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
return Result.error(objectError.getDefaultMessage());
}
/**
* 400 错误:异常请求-非法参数
* @param e
* @return
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IllegalArgumentException.class)
public Result handler(IllegalArgumentException e) {
log.error("Assert异常:----------------{}", e.getMessage());
return Result.error(e.getMessage());
}
}
二、原理亮明
Spring Security 它专注于身份验证和授权,并可根据需求进行配置和自定义。在实际应用中,了解身份验证组件的概念对于使用 Spring Security 进行实现和自定义非常有帮助。
1、没有使用 Spring Security
如果我们没有使用 Spring Security,那么请求将被 DispatcherServlet
拦截。
- DispatcherServlet
服务分发器
它拦截任何 HTTP 请求并将其转发到正确的控制器。
在 Spring Boot 中,DispatcherServlet 会被自动配置。
2、DispatcherServlet 是如何工作的
在深入了解 Spring Security 之前,让我们先了解一下 DispatcherServlet 如何分发请求。
当我们在一个端点(比如 /hello/world)发出请求时,DispatcherServlet 会如何处理它?
DispatcherServlet 创建了一个 IOC 容器,它是 Spring Framework 的核心组件之一,用于管理 bean 的创建和依赖关系。DispatcherServlet 创建的是 WebApplicationContext,这是一个专门用于 Web 应用程序的 IOC 容器。WebApplicationContext 是根据配置文件由 DispatcherServlet 进行配置的。
该 IOC 容器 会创建控制器 bean 的实例。当请求到达时,DispatcherServlet 会使用 IOC 容器 查找适当的 控制器 bean
,并 委托
给它来 处理请求。
3、使用 Spring Security
当将 Spring Security 添加到 Spring Boot 应用程序中时,所有请求在到达 DispatcherServlet 之前 都会被 Spring Security 拦截。
4、认证 流程
身份验证请求和响应
虽然 Spring Security 可以与不同类型的身份验证方法一起使用,但在本文中,我们讨论的是 用户名和密码 方式的 身份验证,以便深入了解完整的身份验证流程。
-
Filter Chain 在将请求转发给 Dispatcher Servlet 之前拦截传入请求。
-
请求进入认证过滤器(其中一个过滤器是 UsernamePasswordAuthenticationFilter)。
-
过滤器从请求(HttpServletRequest 对象)中提取用户名和密码。
-
然后使用凭据创建 UsernamePasswordAuthenticationToken。
-
调用 AuthenticationManager 的 authenticate() 方法。
-
AuthenticationManager 的 authenticate() 方法实现将尝试使用其拥有的 AuthenticationProvider 之一进行身份验证。
-
如果一个身份验证提供程序能够成功进行身份验证,它将返回一个包含凭据和权限的完整的 UsernamePasswordAuthenticationToken。
-
提供程序返回的此令牌用于在 Spring Security 上下文中将用户设置为已认证。
-
一旦用户通过身份验证,请求就会被转发到处理请求的 DispatcherServlet。
5、认证流程 中涉及到的 组件
5.1 什么是过滤器
Spring Filter 是一个组件,可以拦截任何传入请求,并在将其传递给 DispatcherServlet 之前执行某些操作。
过滤器可以处理请求,然后将其转发到 Filter Chain 中的下一个过滤器,或者可以停止并发送回 HTTP 响应。其中一个过滤器是存在于 FilterChain 中的 UsernamePasswordAuthenticationFilter。此过滤器尝试查找 HTTP Post 请求中传递的用户名和密码,如果找到,则尝试使用凭据对用户进行身份验证。
我们可以创建自己的 Filter 并将其添加到 SecurityFilterChain 中,以提供自己的逻辑来处理请求。
5.2 什么是 AuthenticationManager
AuthenticationManager 是一个接口,用于处理 身份验证 的过程。它只有一个方法 authenticate
(Authentication authentication),该方法将一个身份验证对象作为参数,并返回 已经认证的
身份验证对象。身份验证对象通常是一个包含用户名和密码等凭据的 AuthenticationToken 对象。
Authentication authenticate(Authentication authentication) throws AuthenticationException;
AuthenticationManager 的 实现类 是 ProviderManager 类,它提供了 authenticate() 方法的逻辑。
我们可以提供我们自己的 AuthenticationProvider 实现类 或 使用默认实现。
5.3 什么是 ProviderManager
该类 实现 了 AuthenticationManager 接口 并覆盖了 authenticate() 方法。
它使用一组 AuthenticationProvider 来验证 Authentication 对象中发送的凭据。如果 AuthenticationProvider 支持身份验证类型,则将用于验证用户。如果没有提供者支持身份验证类型,则将抛 AuthenticationException。
每个身份验证提供程序的 supports() 方法用于检查它是否可以支持所需的身份验证类型。
5.4 什么是 AuthenticationProvider
AuthenticationProvider 是一个接口,用于定义验证用户的协议。它负责接收 Authentication 对象(表示用户凭据)并返回已经认证的 Authentication 对象,如果凭据有效。如果凭据无效,则 AuthenticationProvider 应该抛出 AuthenticationException。
AuthenticationProvider 接口有两个方法:
-
authenticate():此方法接受 Authentication 对象作为输入,并在凭据有效时返回已认证的 Authentication 对象。如果凭据无效,则 AuthenticationProvider 应该抛出 AuthenticationException。
-
supports():此方法接受 Authentication 对象作为输入,并且如果 AuthenticationProvider 可以验证该对象,则返回 true。如果 AuthenticationProvider 无法验证该对象,则应返回 false。
Spring Security 中的默认 AuthenticationProvider 是 DaoAuthenticationProvider。此提供程序使用 UserDetailsService 从数据库加载用户详细信息。如果用户凭据与数据库中的详细信息匹配,则 DaoAuthenticationProvider 将返回已经认证的 Authentication 对象。
我们可以添加我们自己的 AuthenticationProvider 实现来提供不同的身份验证方法。
5.5 身份验证和 UsernamePasswordAuthenticationToken
Authentication 和 UsernamePasswordAuthenticationToken 是什么?
- Authentication
这是 Spring Security 中的一个接口,表示传入身份验证请求的令牌或已认证的主体(表示一个实体,比如个人)AuthenticationManager.authenticate() 方法。
提供的一些方法为:
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
- UsernamePasswordAuthenticationToken
此类扩展了 AbstractAuthenticationToken 类(身份验证对象的基类),可用于用户名/密码身份验证请求。
此类有两个构造函数:
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
第一个构造函数可用于传入请求以创建未经身份验证的 Authentication 对象。
Authentication authentication = new UsernamePasswordAuthenticationToken(username,password);
第二个构造函数可用于创建完全经过身份验证的 Authentication 对象。
Authentication authToken = new UsernamePasswordAuthenticationToken(username, password, userAuthorities);
然后,此完全经过身份验证的 Authentication 对象从 AuthenticationProvider/AuthenticationManager 返回并表示已认证的用户。然后将此已认证的对象设置在 SecurityContext 中。Spring Security 中的 SecurityContext 是当前执行线程的安全上下文的表示。它包含有关当前已认证用户的信息,例如其用户名、权限和会话信息。