spring security整合步骤
过滤器链
SpringSecurity的本质就是一个过滤器链,内部包含了提供各种功能的过滤器,基本案例中的过滤器链如下图所示:
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。基本案例的认证工作主要有它负责
ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException
FilterSecurityInterceptor:负责权限校验的过滤器
认证流程
认证流程中的核心类
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中
UsernamePasswordAuthenticationFilter实现类:实现了我们最常用的基于用户名和密码的认证逻辑,封装Authentication对象
DaoAuthenticationProvider实现类:是AuthenticationManager中管理的其中一个Provider,因为是要访问数据库,所以叫Dao
1.自定义 Spring Security 配置
要更好地控制 Spring Security
的行为,你可以创建一个自定义的 SecurityConfig
类,继承自 WebSecurityConfigurerAdapter
。通过覆盖方法,您可以配置认证、授权规则、自定义登录页面、注销等。要加上 @EnableWebSecurity
注解和 @Configuration
java
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/admin/**").authenticated() // 认证所有以 /admin 为前缀的 URL 资源
.anyRequest().permitAll().and() // 其他都需要放行,无需认证
.formLogin().and() // 使用表单登录
.httpBasic(); // 使用 HTTP Basic 认证
}
可以在 .yaml
文件中配置账号密码 spring.security.user.name|password
2.封装 JwtTokenHelper
工具类, 封装所有 JWT
相关的功能
java
@Component
public class JwtTokenHelper implements InitializingBean {
/**
* 签发人
*/
@Value("${jwt.issuer}")
private String issuer;
/**
* 秘钥
*/
private Key key;
/**
* JWT 解析
*/
private JwtParser jwtParser;
/**
* 解码配置文件中配置的 Base 64 编码 key 为秘钥
* @param base64Key
*/
@Value("${jwt.secret}")
public void setBase64Key(String base64Key) {
key = Keys.hmacShaKeyFor(Base64.getDecoder().decode(base64Key));
}
/**
* 初始化 JwtParser
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
// 考虑到不同服务器之间可能存在时钟偏移,setAllowedClockSkewSeconds 用于设置能够容忍的最大的时钟误差
jwtParser = Jwts.parserBuilder().requireIssuer(issuer)
.setSigningKey(key).setAllowedClockSkewSeconds(10)
.build();
}
/**
* 生成 Token
* @param username
* @return
*/
public String generateToken(String username) {
LocalDateTime now = LocalDateTime.now();
// Token 一个小时后失效
LocalDateTime expireTime = now.plusHours(1);
return Jwts.builder().setSubject(username)
.setIssuer(issuer)
.setIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant()))
.setExpiration(Date.from(expireTime.atZone(ZoneId.systemDefault()).toInstant()))
.signWith(key)
.compact();
}
/**
* 解析 Token
* @param token
* @return
*/
public Jws<Claims> parseToken(String token) {
try {
return jwtParser.parseClaimsJws(token);
} catch (SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) {
throw new BadCredentialsException("Token 不可用", e);
} catch (ExpiredJwtException e) {
throw new CredentialsExpiredException("Token 失效", e);
}
}
/**
* 生成一个 Base64 的安全秘钥
* @return
*/
private static String generateBase64Key() {
// 生成安全秘钥
Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512);
// 将密钥进行 Base64 编码
String base64Key = Base64.getEncoder().encodeToString(secretKey.getEncoded());
return base64Key;
}
public static void main(String[] args) {
String key = generateBase64Key();
System.out.println("key: " + key);
}
}
这里 afterPropertiesSet()
是 Spring 提供的生命周期回调方法,用于在 Bean 属性注入后执行初始化逻辑。这里设置签发人和密钥,为分布式环境可以设置不同的签发人和密钥,这里的密钥在setBase64Key中会生成然后给 jwtParser
, 在后面 parseToken
方法中会自动校验,generateToken
和 afterPropertiesSet()
都会设置签发人,一个是产生,一个事检验。
在.yaml
文件中创建好 相应的签发人和密钥
yaml
jwt:
# 签发人
issuer: quanxiaoha
# 秘钥
secret: jElxcSUj38+Bnh73T68lNs0DfBSit6U3whQlcGO2XwnI+Bo3g4xsiCIPg8PV/L0fQMis08iupNwhe2PzYLB9Xg==
3.PasswordEncoder 密码加密
用于存储加密的密码,数据库的数据安全,一般网站都会忘记密码重新设置,而不是找回原来密码
java
@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// BCrypt 是一种安全且适合密码存储的哈希算法,它在进行哈希时会自动加入"盐",增加密码的安全性。
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
System.out.println(encoder.encode("quanxiaoha"));
}
}
初始化了一个 PasswordEncoder
接口的具体实现类 BCryptPasswordEncoder
。BCryptPasswordEncoder
是 Spring Security 提供的密码加密器的一种实现,使用 BCrypt 算法对密码进行加密。BCrypt 是一种安全且适合密码存储的哈希算法,它在进行哈希时会自动加入"盐",增加密码的安全性。
4. 实现 UserDetailsService:Spring Security 用户详情服务
UserDetailsService
是 Spring Security 提供的接口,用于从应用程序的数据源(如数据库、LDAP、内存等)中加载用户信息。它是一个用于将用户详情加载到 Spring Security 的中心机制。UserDetailsService
主要负责两项工作:
加载用户信息: 从数据源中加载用户的用户名、密码和角色等信息。
创建 UserDetails 对象: 根据加载的用户信息,创建一个 Spring Security 所需的 UserDetails 对象,包含用户名、密码、角色和权限等。
这里要重写 loadUserByUsername
方法,实验拿到我们自己的数据
java
@Service
@Slf4j
public class UserDetailServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库中查询
// ...
// 暂时先写死,密码为 quanxiaoha, 这里填写的密文,数据库中也是存储此种格式
// authorities 用于指定角色,这里写死为 ADMIN 管理员
return User.withUsername("quanxiaoha")
.password("$2a$10$n7RJ1q.RnXx5M3O6B0i0he04fZOPjIJpyWcKuicW1bFyFHWhlGose")
.authorities("ADMIN")
.build();
}
}
5. 自定义认证过滤器
接下来,我们自定义一个用于认证的过滤器,新建 /filter 包,并创建 JwtAuthenticationFilter 过滤器,代码如下:
java
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* 指定用户登录的访问地址
*/
public JwtAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
ObjectMapper mapper = new ObjectMapper();
// 解析提交的 JSON 数据
JsonNode jsonNode = mapper.readTree(request.getInputStream());
JsonNode usernameNode = jsonNode.get("username");
JsonNode passwordNode = jsonNode.get("password");
// 判断用户名、密码是否为空
if (Objects.isNull(usernameNode) || Objects.isNull(passwordNode)
|| StringUtils.isBlank(usernameNode.textValue()) || StringUtils.isBlank(passwordNode.textValue())) {
throw new UsernameOrPasswordNullException("用户名或密码不能为空");
}
String username = usernameNode.textValue();
String password = passwordNode.textValue();
// 将用户名、密码封装到 Token 中
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(username, password);
return getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
}
}
此过滤器继承了AbstractAuthenticationProcessingFilter
,用于处理 JWT(JSON Web Token)的用户身份验证过程。
这里的认证流程是:调用了父类 AbstractAuthenticationProcessingFilter 的构造函数,通过 AntPathRequestMatcher 指定了处理用户登录的访问地址。这意味着当请求路径匹配 /login 并且请求方法为 POST 时,该过滤器将被触发。然后从request对象中拿到用户名密码判断合法性,通过Authentication
的实现子类UsernamePasswordAuthenticatonToken
封装Authentication
对象,这里只有用户名密码,还没有权限,再进行AuthenticationManager
的authenticate
方法进行认证,前者是从父类中继承过来的,它提供了一群Provider
,所以调用的就是一群provider
的authenticate
方法进行认证。
6. 自定义用户名或密码不能为空异常
上面过滤器代码中,有个动作是校验用户名、密码是否为空,为空则抛出 UsernameOrPasswordNullException 异常,此类是自定义的得来的。新建包 /exception, 在此包中创建该类:
java
public class UsernameOrPasswordNullException extends AuthenticationException {
public UsernameOrPasswordNullException(String msg, Throwable cause) {
super(msg, cause);
}
public UsernameOrPasswordNullException(String msg) {
super(msg);
}
}
注意,需继承自 AuthenticationException,只有该类型异常,才能被后续自定义的认证失败处理器捕获到。
7. 自定义认证成功处理器
新建 /handler
包,并创建 RestAuthenticationSuccessHandler
类:
java
@Component
@Slf4j
public class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private JwtTokenHelper jwtTokenHelper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 从 authentication 对象中获取用户的 UserDetails 实例,这里是获取用户的用户名
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// 通过用户名生成 Token
String username = userDetails.getUsername();
String token = jwtTokenHelper.generateToken(username);
// 返回 Token
LoginRspVO loginRspVO = LoginRspVO.builder().token(token).build();
ResultUtil.ok(response, Response.success(loginRspVO));
}
}
此类实现了 Spring Security 的 AuthenticationSuccessHandler 接口,用于处理身份验证成功后的逻辑。首先,从 authentication 对象中获取用户的 UserDetails 实例,这里是主要是获取用户的用户名,然后通过用户名生成 Token 令牌,最后返回数据。这里的getPrincipal
:用户的主体信息(通常是 UserDetails 对象,存储用户名、权限等)。
credentials
:用户的凭证(如密码,认证成功后通常会被擦除)。
authorities
:用户的权限集合(如角色 ROLE_USER)。
7. ResultUtil 返参工具类
为了在过滤器中方便的返回 JSON 参数,我们需要封装一个工具类 ResultUtil
, 放置在 /utils
包下,代码如下:
java
public class ResultUtil {
/**
* 成功响参
* @param response
* @param result
* @throws IOException
*/
public static void ok(HttpServletResponse response, Response<?> result) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.OK.value());
response.setContentType("application/json");
PrintWriter writer = response.getWriter();
ObjectMapper mapper = new ObjectMapper();
writer.write(mapper.writeValueAsString(result));
writer.flush();
writer.close();
}
/**
* 失败响参
* @param response
* @param result
* @throws IOException
*/
public static void fail(HttpServletResponse response, Response<?> result) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.OK.value());
response.setContentType("application/json");
PrintWriter writer = response.getWriter();
ObjectMapper mapper = new ObjectMapper();
writer.write(mapper.writeValueAsString(result));
writer.flush();
writer.close();
}
/**
* 失败响参
* @param response
* @param status 可指定响应码,如 401 等
* @param result
* @throws IOException
*/
public static void fail(HttpServletResponse response, int status, Response<?> result) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setStatus(status);
response.setContentType("application/json");
PrintWriter writer = response.getWriter();
ObjectMapper mapper = new ObjectMapper();
writer.write(mapper.writeValueAsString(result));
writer.flush();
writer.close();
}
}
8. 自定义认证失败处理器
在 /handler
包下,创建 RestAuthenticationFailureHandler
认证失败处理器:
java
@Component
@Slf4j
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.warn("AuthenticationException: ", exception);
if (exception instanceof UsernameOrPasswordNullException) {
// 用户名或密码为空
ResultUtil.fail(response, Response.fail(exception.getMessage()));
return;
} else if (exception instanceof BadCredentialsException) {
// 用户名或密码错误
ResultUtil.fail(response, Response.fail(ResponseCodeEnum.USERNAME_OR_PWD_ERROR));
return;
}
// 登录失败
ResultUtil.fail(response, Response.fail(ResponseCodeEnum.LOGIN_FAIL));
}
}
通过自定义了一个实现了 Spring Security 的 AuthenticationFailureHandler 接口类,用于在用户身份验证失败后执行一些逻辑。首先,我们打印了异常日志,方便后续定位问题,然后对异常的类型进行判断,通过 ResultUtil 工具类,返回不同的错误信息,如用户名或者密码为空、用户名或密码错误等,若未判断出异常是什么类型,则统一提示为 登录失败。
这里定义两个枚举异常
java
LOGIN_FAIL("20000", "登录失败"),
USERNAME_OR_PWD_ERROR("20001", "用户名或密码错误"),
9. 自定义 JWT 认证功能配置
完成了以上前置工作后,我们开始配置 JWT 认证相关的配置。在 /config
包下新建 JwtAuthenticationSecurityConfig
, 代码如下:
java
@Configuration
public class JwtAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private RestAuthenticationSuccessHandler restAuthenticationSuccessHandler;
@Autowired
private RestAuthenticationFailureHandler restAuthenticationFailureHandler;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
// 自定义的用于 JWT 身份验证的过滤器
JwtAuthenticationFilter filter = new JwtAuthenticationFilter();
filter.setAuthenticationManager(httpSecurity.getSharedObject(AuthenticationManager.class));
// 设置登录认证对应的处理类(成功处理、失败处理)
filter.setAuthenticationSuccessHandler(restAuthenticationSuccessHandler);
filter.setAuthenticationFailureHandler(restAuthenticationFailureHandler);
// 直接使用 DaoAuthenticationProvider, 它是 Spring Security 提供的默认的身份验证提供者之一
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
// 设置 userDetailService,用于获取用户的详细信息
provider.setUserDetailsService(userDetailsService);
// 设置加密算法
provider.setPasswordEncoder(passwordEncoder);
httpSecurity.authenticationProvider(provider);
// 将这个过滤器添加到 UsernamePasswordAuthenticationFilter 之前执行
httpSecurity.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
}
}
上述代码是一个 Spring Security 配置类,用于配置 JWT(JSON Web Token)的身份验证机制。它继承了 Spring Security 的 SecurityConfigurerAdapter 类,用于在 Spring Security 配置中添加自定义的认证过滤器和提供者。通过重写 configure() 方法,我们将之前写好过滤器、认证成功、失败处理器,以及加密算法整合到了 httpSecurity 中。
10. 应用 JWT 认证功能配置
接下来,我们编辑 Spring Security 配置 WebSecurityConfig 类,修改内容如下:
java
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable(). // 禁用 csrf
formLogin().disable() // 禁用表单登录
.apply(jwtAuthenticationSecurityConfig) // 设置用户登录认证相关配置, 用我们上面自己认证好的规则
.and()
.authorizeHttpRequests()
.mvcMatchers("/admin/**").authenticated() // 认证所有以 /admin 为前缀的 URL 资源
.anyRequest().permitAll() // 其他都需要放行,无需认证
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 前后端分离,无需创建会话
}
}
上述代码中,在 configure()
方法中,首先禁用了 CSRF(Cross-Site Request Forgery)攻击防护。在前后端分离的情况下,通常不需要启用 CSRF 防护。同时,还禁用了表单登录,并应用了 JWT
相关的配置类 JwtAuthenticationSecurityConfig
。最后,配置会话管理这块,将会话策略设置为无状态(STATELESS
),适用于前后端分离的情况,无需创建会话。
11. 从数据库中查询用户信息
前面我们根据用户名查询用户信息这块,是代码中写死的。接下来,我们将其改造为从数据库中查询。首先,我们将 t_user
表中之前用于测试的记录删除干净,并执行如下语句,为用户表添加一条记录,用户名为 "quanxiaoha":
这里我们从数据库获取,编辑 UserDetailServiceImpl
类,改为从数据库中查询:
java
@Service
@Slf4j
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库中查询
UserDO userDO = userMapper.findByUsername(username);
// 判断用户是否存在
if (Objects.isNull(userDO)) {
throw new UsernameNotFoundException("该用户不存在");
}
// authorities 用于指定角色,这里写死为 ADMIN 管理员
return User.withUsername(userDO.getUsername())
.password(userDO.getPassword())
.authorities("ADMIN")
.build();
}
}