本人菜鸟一枚,这篇文章算是我学习 Spring Security 的记录吧.
这个可以参考官方的文档1,我的配置如下:
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.MessageDigestPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.study.myspringsecurity.security.filter.RestAuthenticationFilter;
import org.study.myspringsecurity.security.handler.AuthenticationHandler;
import java.util.Map;
import static org.springframework.security.config.Customizer.withDefaults;
/**
* spring security 配置类
*/
@Slf4j
@EnableWebSecurity(debug = true)
public class MySecurityConfig {
private final ObjectMapper objectMapper;
private final AuthenticationHandler handler;
private AuthenticationManagerBuilder authenticationManagerBuilder;
public MySecurityConfig(ObjectMapper objectMapper, AuthenticationHandler handler, ObjectPostProcessor<Object> objectPostProcessor) {
this.objectMapper = objectMapper;
this.handler = handler;
// 这里创建了一个 AuthenticationManagerBuilder。注意,直接注入一个 AuthenticationManagerBuilder 运行会报错
this.authenticationManagerBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
}
/**
* rest 登录的过滤器链配置
*/
@Bean
@Order(10)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.antMatcher("/api/**")
// 适于于无状态的接口
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.antMatchers("/api/login").permitAll()
.antMatchers("/api/**").hasRole("USER")
.anyRequest().authenticated())
// 自定义的过滤器
.addFilterAt(restAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// rest 接口不需要 csrf 处理
.csrf(AbstractHttpConfigurer::disable)
// 禁用表单登录
.formLogin(AbstractHttpConfigurer::disable)
// 允许 http basic
.httpBasic(withDefaults());
return http.build();
}
/**
* 表单登录的过滤器链
*/
@Bean
@Order(20)
public SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception {
http
// 如果这里不设置 authorizeHttpRequests, 那么任何请求都不需要登录
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
// 使用自定义的登录页面, 注意这里需要 permitAll() 否则访问 login.html 页面的时候也会需要认证
.formLogin(form -> form.loginPage("/login").defaultSuccessUrl("/").permitAll())
// 退出登录的配置
.logout(logout -> logout.logoutUrl("/my-logout"))
// 记住我的设置. 这里注入一个 userDetailService , 因为没有的话, 启动会报错
.rememberMe(rememberMe -> rememberMe.key("myKey").tokenValiditySeconds(7 * 24 * 3600)
.userDetailsService(myUserDetailsService()))
// 允许 http basic
.httpBasic(withDefaults());
;
return http.build();
}
/**
* 自定义的认证过滤器
*/
private RestAuthenticationFilter restAuthenticationFilter() throws Exception {
RestAuthenticationFilter filter = new RestAuthenticationFilter(objectMapper);
filter.setAuthenticationSuccessHandler(handler.jsonAuthenticationSuccessHandler());
filter.setAuthenticationFailureHandler(handler.jsonAuthenticationFailureHandler());
filter.setAuthenticationManager(myAuthenticationManager());
filter.setFilterProcessesUrl("/api/login");
return filter;
}
/**
* 配置要忽略的路径
*/
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
// 忽略 /error 页面
return web -> web.ignoring().antMatchers("/error")
// 忽略常见的静态资源路径
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
/**
* 获取用户业务逻辑
*/
@Bean
UserDetailsService myUserDetailsService() {
log.info("init userDetailsService");
try {
return authenticationManagerBuilder.inMemoryAuthentication()
.withUser("user")
// 密码是 password
.password("{bcrypt}$2a$10$8Lnu.pNYLUMspcSJuRfyROzg1OyiRVp81.YXaQpxOKHUlOx3ELZB.")
.roles("USER")
.and()
.withUser("admin")
// 密码是 password
.password(
"{SHA-1}{jdIB7T8lvztpz60HGhwWXQP8dkJyNq/oE+UVixGXwGc=}9cc577b587921547a6c0dcf1a4d736e02a2c339b")
.roles("ADMIN", "USER")
.and()
.getUserDetailsService();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 构造一个认证管理器
*/
@Bean
AuthenticationManager myAuthenticationManager() throws Exception {
log.info("init authenticationManager");
return authenticationManagerBuilder.build();
}
/**
* 密码编码器
* password bcrypt 加密之后是
* {bcrypt}$2a$10$8Lnu.pNYLUMspcSJuRfyROzg1OyiRVp81.YXaQpxOKHUlOx3ELZB.
* password SHA-1 加密之后是
* {SHA-1}{jdIB7T8lvztpz60HGhwWXQP8dkJyNq/oE+UVixGXwGc=}9cc577b587921547a6c0dcf1a4d736e02a2c339b
*/
@Bean
PasswordEncoder passwordEncoder() {
log.info("init passwordEncoder");
// 可以使用比较老的 MD5 SHA-1 SHA-256 等等
// return new MessageDigestPasswordEncoder("SHA-1");
// 也可以使用的密码编码器有 BCryptPasswordEncoder SCryptPasswordEncoder
// Pbkdf2PasswordEncoder
// return new BCryptPasswordEncoder();
// 也可以让多种编码器共存, 方便后续运维
String defaultId = "bcrypt";
val map = Map.of("bcrypt", new BCryptPasswordEncoder(),
"SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
return new DelegatingPasswordEncoder(defaultId, map);
}
}
然后就既可以通过表单登录,也可以通过api接口登录。表单登录如下图所示:
api接口登录如下所示:
### login rest 登录测试
POST http://localhost:8080/api/login
Content-Type: application/json
{
"username": "admin",
"password": "password"
}
响应为:
{
"authorities": [
{
"authority": "ROLE_ADMIN"
},
{
"authority": "ROLE_USER"
}
],
"details": {
"remoteAddress": "127.0.0.1",
"sessionId": null
},
"authenticated": true,
"principal": {
"password": null,
"username": "admin",
"authorities": [
{
"authority": "ROLE_ADMIN"
},
{
"authority": "ROLE_USER"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},
"credentials": null,
"name": "admin"
}