赞
踩
注:本文基于
Spring Boot 3.2.1
以及Spring Security 6.2.1
Spring Security 使用起来非常简单,只要引入相关依赖包,然后增加注解@EnableWebSecurity
就可以。同时提供了丰富的扩展点,可以让你自定义权限校验策略。
常见的使用场景分两类:
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
Spring Boot和Spring Security的默认配置在运行时提供了以下行为:
对于任何端点(包括Boot的/error端点),需要经过身份验证的用户。
在启动时,使用生成的密码注册默认用户user(密码记录到控制台;如 8e557245-73e2-4286-969a-ff57fe326336)。
可以通过如下配置修改:
spring.security.user.name=admin
spring.security.user.password=admin
保护密码存储使用BCrypt以及其他方法。
提供基于表单的登录和注销流程。(分别是/login
和 /logout
)
身份验证基于表单登录和HTTP Basic。
提供内容协商;对于Web请求,重定向到登录页面;对于服务请求,返回401未授权。
缓解CSRF攻击。
缓解Session fixation攻击。
写入Strict-Transport-Security以确保HTTPS。
写入X-Content-Type-Options以减少嗅探攻击。
写入缓存控制头以保护已认证的资源。
写入X-Frame-Options以减少点击劫持。
与HttpServletRequest的认证方法集成。
发布身份验证成功和失败事件。
了解Spring Boot如何与Spring Security协调以实现这一点是有帮助的。看一下Boot的安全自动配置,它做了以下事情(为了说明而简化):
@EnableWebSecurity @Configuration public class DefaultSecurityConfig { @Bean @ConditionalOnMissingBean(UserDetailsService.class) InMemoryUserDetailsManager inMemoryUserDetailsManager() { String generatedPassword = // ...; return new InMemoryUserDetailsManager(User.withUsername("user") .password(generatedPassword).roles("USER").build()); } @Bean @ConditionalOnMissingBean(AuthenticationEventPublisher.class) DefaultAuthenticationEventPublisher defaultAuthenticationEventPublisher(ApplicationEventPublisher delegate) { return new DefaultAuthenticationEventPublisher(delegate); } }
- 添加了
@EnableWebSecurity
注解。 (除此之外,这还将Spring Security的默认过滤器链发布为@Bean
)- 发布一个用户详细服务
@Bean
,用户名为user,并使用随机生成的密码记录到控制台- 发布一个
AuthenticationEventPublisher
@Bean用于发布身份验证事件
只要添加注解@EnableWebSecurity
就可以开启权限校验
@EnableWebSecurity
@Configuration
public class BasicSecurityConfig {
}
配置登录账号密码:
spring.security.user.name=admin
spring.security.user.password=admin
1)启动工程,会自动打开默认的登录页面
输入账号密码即可登录。
登录成功后,通过SecurityContextHolder.getContext().getAuthentication();
获取当前用户信息
2)输入地址 http://localhost:8080/logout 打开默认的退出页面,点击按钮 Log Out 退出
这里以thymeleaf模板为例,制作登录页面。
1)添加 thymeleaf 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2)分别添加登录页面 login.html 和首页 index.html,放在目录 resources/templates 中
登录页面 login.html ,发送post
请求到/login
地址
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Login</title> <style> .sr-only{width:80px;display:inline-block;text-align: match-parent} .form-control{width: 120px} .form-signin{margin: 0 auto;width:220px;} </style> </head> <body> <div class="container" style=""> <form class="form-signin" method="post" th:action="@{/login}"> <h2>用户登录</h2> <p> <label for="username" class="sr-only">用户名</label> <input type="text" id="username" name="username" class="form-control" placeholder="Username"> </p> <p> <label for="password" class="sr-only">密码</label> <input type="password" id="password" name="password" class="form-control" placeholder="Password"> </p> <button type="submit">登录</button> </form> </div> </body> </html>
首页 index.html,退出功能发送post
请求到/logout
地址
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>index</title> <style> .form-signin{margin: 0 auto;width:220px;} </style> </head> <body> <form class="form-signin" method="post" th:action="@{/logout}"> <h2>确认退出?</h2> <button class="btn btn-lg btn-primary btn-block" type="submit">退出</button> </form> </body> </html>
3)配置 HttpSecurity
@EnableWebSecurity @Configuration public class BasicSecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize // 放行登录页面 .requestMatchers("/login").permitAll() // 拦截其他所有请求 .anyRequest().authenticated() ) // 退出时,让session失效 .logout(logout -> logout.invalidateHttpSession(true)) // 配置登录页面 和 登录成功后页面 .formLogin(form -> form.loginPage("/login").permitAll() .loginProcessingUrl("/login").defaultSuccessUrl("/index")); // 开启csrf 保护 http.csrf(Customizer.withDefaults()); return http.build(); } }
4)启动工程测试
登录页面
首页,包含退出按钮
用户登录成功时,返回token,后续每次请求都带上token。token设置过期时间,提供token刷新功能。
后台服务端解析token,判断是否有效,如果有效取得token中存储的用户信息,并调用SecurityContextHolder.getContext().setAuthentication(authentication)
存储用户信息。
JWT 和相关jar包有很多,这里直接使用 hutool 的工具类
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
创建 自定义登录接口服务类,实现接口 UserDetailsService
即可
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity entity = new UserEntity();
entity.setId(100L);
entity.setPassword("{noop}123456");
entity.setUsername("admin");
entity.setAuthorities(AuthorityUtils.createAuthorityList("ROLE_ADMIN"));
return entity;
}
}
创建自定义的UserEntity类,实现接口 UserDetails
,可以根据情况增加自己需要的属性
@Data
public class UserEntity implements UserDetails {
private Long id;
private String password;
private String username;
private List<GrantedAuthority> authorities;
private boolean accountNonExpired = true;
private boolean accountNonLocked = true;
private boolean credentialsNonExpired = true;
private boolean enabled = true;
}
在httpSecurity中配置 userDetailsService
http.userDetailsService(userDetailService)
这样在登录的时候,就会调用MyUserDetailsServiceImpl.loadUserByUsername()
方法
配置用户登录成功时逻辑处理,返回需要的token
http.formLogin(form -> form.loginPage("/login").permitAll() .loginProcessingUrl("/login") .successHandler((request, response, authentication) -> { log.info("登录成功:{}", authentication); UserEntity principal = (UserEntity) authentication.getPrincipal(); String secret = "0123456789"; Map<String, Object> payload = new HashMap<>(); payload.put("id", principal.getId()); payload.put("username", principal.getUsername()); String token = JWTUtil.createToken(payload, secret.getBytes()); response.setContentType("application/json;charset=UTF-8"); Map<String, Object> map = new HashMap<>(); map.put("token", token); response.getWriter().write(JSONUtil.toJsonStr(map)); }).failureHandler((request, response, authentication) -> { log.info("登录失败:{}", authentication); }));
配置禁用session
// CSRF 禁用,因为不使用 Session
http.csrf(AbstractHttpConfigurer::disable);
// 基于 token 机制,所以不需要 Session
http.sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
创建自定义Jwt验证过滤器,并配置
@Component public class JwtTokenFilter extends OncePerRequestFilter { @Autowired private MyUserDetailsServiceImpl userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 验证token是否有效 String token = request.getHeader("token"); if (StrUtil.isNotEmpty(token)) { String secret = "0123456789"; boolean verify = JWTUtil.verify(token, secret.getBytes()); if (!verify) { response.setContentType("application/json;charset=utf-8"); response.getWriter().write("{\"code\":401,\"msg\":\"token无效\"}"); return; } else { //认证成功,设置用户信息 UserEntity user = JWTUtil.parseToken(token).getPayloads().toBean(UserEntity.class); // 模拟获取用户信息,实际情况应该是从数据库查询 UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername()); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities()); //设置用户信息 SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } filterChain.doFilter(request, response); } }
配置执行位置,在UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
至此,基于Jwt的spring security 配置完成,实际项目应该是从数据库查询用户以及角色权限
1)用postman测试,先用login方法(post请求),获取token
2)然后复制token当作header参数传给其他接口
JwtSecurityConfig
配置类完整代码如下:
@EnableWebSecurity @Configuration @Slf4j public class JwtSecurityConfig { @Autowired private MyUserDetailsServiceImpl userDetailService; @Autowired private JwtTokenFilter jwtTokenFilter; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize .requestMatchers("/login").permitAll() .anyRequest().authenticated() ) .userDetailsService(userDetailService) .exceptionHandling(ex -> ex.authenticationEntryPoint((request, response, authException) -> { log.error("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), authException); ServletUtils.writeJSON(response, authException.getMessage()); }).accessDeniedHandler((request, response, accessDeniedException) -> { log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), "", accessDeniedException); })) .logout(logout -> logout.invalidateHttpSession(true)) // 配置登录页面 .httpBasic(Customizer.withDefaults()) .formLogin(form -> form.loginPage("/login").permitAll() .loginProcessingUrl("/login") .successHandler((request, response, authentication) -> { log.info("登录成功:{}", authentication); UserEntity principal = (UserEntity) authentication.getPrincipal(); // 登录成功,返回token给前端 String secret = "0123456789"; Map<String, Object> payload = new HashMap<>(); payload.put("id", principal.getId()); payload.put("username", principal.getUsername()); String token = JWTUtil.createToken(payload, secret.getBytes()); response.setContentType("application/json;charset=UTF-8"); Map<String, Object> map = new HashMap<>(); map.put("token", token); response.getWriter().write(JSONUtil.toJsonStr(map)); }).failureHandler((request, response, authentication) -> { log.info("登录失败", authentication); })); // CSRF 禁用,因为不使用 Session http.csrf(AbstractHttpConfigurer::disable); // 基于 token 机制,所以不需要 Session http.sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。