上一章节我们简单介绍了SpringSecurity使用方式及自动配置原理,这一节我们会着重阐述SpringSecurity的配置,并且会基于配置类WebSecurityConfigurerAdapter的三个方法的使用方式及原理向大家介绍
创建 WebSecurityConfig配置类,继承 WebSecurityConfigurerAdapter抽象类,实现 Spring Security在 Web 场景下的自定义配置。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
思考: 是否需要添加@EnableWebSecurity注解?
同样的,我们通过spring-boot-starter-security导入了spring-security依赖,在@EnableAutoConfiguration加载组件的时候默认到spring-boot-autoconfigure下的META-INF/spring.factories下去寻找SecurityAutoConfiguration
SecurityAutoConfiguration
/**
* {@link EnableAutoConfiguration Auto-configuration} for Spring Security.
*
* @author Dave Syer
* @author Andy Wilkinson
* @author Madhura Bhave
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean(AuthenticationEventPublisher.class)
public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
return new DefaultAuthenticationEventPublisher(publisher);
}
}
我们来分析一下上面这波源码
SecurityAutoConfiguration
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
SecurityDataConfiguration.class })
WebSecurityEnablerConfiguration.class就有一个注解@EnableWebSecurity,所以我们就不需要在继承 WebSecurityConfigurerAdapter的类去添加@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnClass(EnableWebSecurity.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity
class WebSecurityEnablerConfiguration {
}
重写 configure(AuthenticationManagerBuilder auth) 方法,实现 AuthenticationManager认证管理器
采用编码的方式去配置用户名密码和角色,注意第9行代码auth.inMemoryAuthentication()等同于我们使用在application.properties/yml文件中定义的用户名密码的方式是相同(前面有分析过运行原理),我们来分析一下configure(AuthenticationManagerBuilder auth) 到底可以干什么
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
String password = passwordEncoder().encode("123456");
auth
// 使用基于内存的 InMemoryUserDetailsManager
.inMemoryAuthentication()
//使用 PasswordEncoder 密码编码器
//.passwordEncoder(passwordEncoder())
// 配置用户
.withUser("fox").password(password).roles("admin")
// 配置其他用户
.and()
.withUser("fox2").password(password).roles("user");
}
@Bean
public PasswordEncoder passwordEncoder(){
//return NoOpPasswordEncoder.getInstance();
return new BCryptPasswordEncoder();
}
}
用于通过允许AuthenticationManager容易地添加来建立认证机制,也就是说用来记录账号,密码,角色信息
AuthenticationManagerBuilder.class的两个方法
inMemoryAuthentication() 基于内存的认证
public InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuthentication()
throws Exception {
return apply(new InMemoryUserDetailsManagerConfigurer<>());
}
userDetailsService() 基于编码的认证
public <T extends UserDetailsService> DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T> userDetailsService(
T userDetailsService) throws Exception {
this.defaultUserDetailsService = userDetailsService;
return apply(new DaoAuthenticationConfigurer<>(userDetailsService));
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 让springsecurity走我们自定义的UserDetailsService,定义的密码解析
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
// 重写springsecurity的userDetailService中的loadUserByUserName方法
@Bean
@Override
public UserDetailsService userDetailsService() {
return username -> {
Admin admin = adminService.getAdminByUserName(username);
if (admin != null) {
admin.setRoles(adminService.getRoles(admin.getId()));
return admin;
}
// 这是由springsecurity提供的异常处理器
throw new UsernameNotFoundException("该账号不存在!");
};
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
configure(HttpSecurity http)网络认证器
允许基于选择匹配在资源级配置基于网络的安全性。以下示例将以/ admin /开头的网址限制为具有ADMIN角色的用户,并声明任何其他网址需要成功验证
也就是对角色的权限——所能访问的路径做出限制
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeUrls()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
}
configure(HttpSecurity http)到底能做什么
springsecurity为我们提供了大量可定制的权限配置,我着重列出了以下几个方法的使用方式以及最佳实践
作用:销毁HttpSession对象,清除认证数据,设置logout成功跳转路径
Spring security默认实现了logout退出,用户只需要向 Spring Security 项目中发送 /logout 退出请求即可。默认的退出 url 为 /logout ,退出成功后跳转到 /login?logout ;当然我们也可以自定义跳转路径
public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<LogoutConfigurer<H>, H> {
private List<LogoutHandler> logoutHandlers = new ArrayList<>();
private SecurityContextLogoutHandler contextLogoutHandler = new SecurityContextLogoutHandler();
private String logoutSuccessUrl = "/login?logout";
private LogoutSuccessHandler logoutSuccessHandler;
private String logoutUrl = "/logout";
// 自定义跳转路径
public LogoutConfigurer<H> logoutUrl(String logoutUrl) {
this.logoutRequestMatcher = null;
this.logoutUrl = logoutUrl;
return this;
}
// 跳转成功
public LogoutConfigurer<H> logoutSuccessUrl(String logoutSuccessUrl) {
this.customLogoutSuccess = true;
this.logoutSuccessUrl = logoutSuccessUrl;
return this;
}
}
自定义退出跳转路径
http.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html");
LogoutSuccessHandler
退出成功处理器,实现 LogoutSuccessHandler 接口 ,可以自定义退出成功处理逻辑。
http.
logoutSuccessHandler(new MyLogoutSuccessHandler());
public LogoutSuccessHandler MyLogoutSuccessHandler() implements LogoutSuccessHandler{
@Override
pubilc void onLogoutSuccess(HttpServletRequest var1, HttpServletResponse var2, Authentication var3) throws IOException, ServletException{
// 自定义内容
}
}
public interface LogoutSuccessHandler {
void onLogoutSuccess(HttpServletRequest var1, HttpServletResponse var2, Authentication var3) throws IOException, ServletException;
}
最佳实践:现在大多数都流行前后端分离,包括职责的分离,以前有很多需要通过后端逻辑去完成的,现在很大一部分都交给了前端去完成,比如页面的跳转,前后端分离前,javaweb都是通过jsp servlet去完成页面跳转,而在前后端分离以后,后端主要接收前端发起的请求并响应对应格式的JSON数据;
所以在前端发起的logout登出请求,页面的跳转包括前端保存在web端的缓存数据(Authorization,token认证,当前登录用户数据等)清除,都由前端逻辑去完成,而后端在使用了SpringSecruity安全框架后,只需要清除保存在SecurityContextHolder中的当前登录用户的数据,然后返回success状态码即可
public LogoutConfigurer<H> logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler) {
this.logoutSuccessUrl = null;
this.customLogoutSuccess = true;
this.logoutSuccessHandler = logoutSuccessHandler;
return this;
}
private LogoutSuccessHandler getLogoutSuccessHandler() {
LogoutSuccessHandler handler = this.logoutSuccessHandler;
if (handler == null) {
handler = createDefaultSuccessHandler();
}
return handler;
}
// 如果我们不自定义LogOutSuccessHandler,则会默认帮我们创建
private LogoutSuccessHandler createDefaultSuccessHandler() {
SimpleUrlLogoutSuccessHandler urlLogoutHandler = new SimpleUrlLogoutSuccessHandler();
urlLogoutHandler.setDefaultTargetUrl(this.logoutSuccessUrl);
if (this.defaultLogoutSuccessHandlerMappings.isEmpty()) {
return urlLogoutHandler;
}
DelegatingLogoutSuccessHandler successHandler = new DelegatingLogoutSuccessHandler(
this.defaultLogoutSuccessHandlerMappings);
successHandler.setDefaultLogoutSuccessHandler(urlLogoutHandler);
return successHandler;
}
CSRF(Cross-site request forgery)跨站请求伪造,防止恶意伪造用户请求访问受信任站点的非法请求访问。
什么是跨域?跨域:只要网络协议,ip 地址,端口中任何一个不相同就是跨域请求。
为什么要禁用CSRF?
CSRF导致的接口访问问题
Could not verify the provided CSRF token because your session was not found in spring security
在开发人员使用Postman调试接口时,已经通过继承WebSecurityConfigurerAdapter过滤了Rest接口拦截的机制,但出现403错误并且提示信息为“Could not verify the provided CSRF token because your session was not found in spring security”。
解决方法
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 使用jwt不需要csrf,关闭csrf保护功能(跨域访问)
.csrf().disable();
}
}
会话控制
我们可以通过以下选项准确控制会话何时创建以及Spring Security如何与之交互:
机制 | 描述 |
---|---|
always | 如果session不存在总是需要创建 |
ifRequired | 如果需要就创建一个session(默认)登录时 |
never | Spring Security 将不会创建session,但是如果应用中其他地方创建了session,那 |
么Spring Security将会使用它 | |
stateless | Spring Security将绝对不会创建session,无状态架构适用于REST API |
最佳实践:前后端分离我们一般会使用无状态的会话协议 ,基于token的鉴权机制类似于http协议是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。所以我们要禁用session
@Override
protected void configure(HttpSecurity http) throws Exception {
// 使用jwt不需要csrf
http.// 基于token不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
请求授权
在使用spring Security时,需要注意authorizeRequests的顺序,如果是以下这种情况的话:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.antMatchers("/hello").permitAll();
}
由于是按照从上往下顺序依次执行,如上所示,当我们访问/hello时,会发现此时仍然需要登录!
所以我们往往会把.anyRequest().authenticated()放在最后
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/hello").hasRole("USER")
.antMatchers("/hi").hasRole("ADMIN")
.antMatchers("/sayHello").permitAll()
.anyRequest().authenticated();
}
如上就表示。访问/hello接口需要USER角色,访问/hi需要ADMIN角色,访问其他接口需要认证
顾名思义就是在指定过滤器之前添加过滤器
@Override
public HttpSecurity addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter) {
return addFilterAtOffsetOf(filter, -1, beforeFilter);
}
private HttpSecurity addFilterAtOffsetOf(Filter filter, int offset, Class<? extends Filter> registeredFilter) {
int order = this.filterOrders.getOrder(registeredFilter) + offset;
this.filters.add(new OrderedFilter(filter, order));
return this;
}
我们可以来分析一下上面这段源码,addFilterBefore(参数a,参数b),参数a是我们要添加到过滤器链中的目标过滤器,参数b是用于确定参数a在过滤器中的定位,a将放置在b之前执行,在通过调用addFilterAtOffsetOf(a,-1,b),this.filterOrders.getOrder(registeredFilter) -1,通过过滤器b获取执行顺序在-1即为过滤器a 的在过滤器链中的执行顺序,并添加到过滤器链中
最佳实践
由于我们前面通过sessionManagement关闭了session,使用无状态的会话协议的token进行认证,在使用springsecurity安全框架后,用户提交用户名密码后首先会被UsernamePasswordAuthenticationFilter所拦截,但是我们前后端分离后,在前端发出请求的请求头都会携带一个token(Authentication)的kv键值对,我们需要先对这个token进行一个验证,验证后在放行
// 添加jwt 登录授权过滤器
http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
用来添加自定义未授权和未登录结果返回
// 添加自定义为授权和未登录结果返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint);
未授权
/**
* 当访问接口没有权限时,自定义返回结果
*/
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
PrintWriter out = httpServletResponse.getWriter();
RespBean bean = RespBean.error("权限不足,请联系管理员!");
bean.setCode(403);
out.write(new ObjectMapper().writeValueAsString(bean));
out.flush();
out.close();
}
}
未登录
/**
* 当未登录或者token失效访问接口时自定义的返回接口
*/
@Component
@Slf4j
public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
PrintWriter out = httpServletResponse.getWriter();
log.info("未登录,请先进行登录");
RespBean bean = RespBean.error("未登录,请先进行登录");
bean.setCode(401);
out.write(new ObjectMapper().writeValueAsString(bean));
out.flush();
out.close();
}
}
configure(WebSecurity)用于影响全局安全性(配置资源,设置调试模式,通过实现自定义防火墙定义拒绝请求)的配置设置。一般用于配置全局的某些通用事物,例如静态资源等
有的时候我们在放行的路径、静态资源太多,可以在configure((HttpSecurity http)中直接拦截所有路径
http
.authorizeRequests()
.anyRequest()
.authenticated();
使用configure(WebSecurity web)放行所有免认证就可以访问的路径,比如注册业务,登录业务,退出业务,静态资源等等
// 不走拦截链去放行路径
@Override
public void configure(WebSecurity web) throws Exception {
// 放行静态资源
web.ignoring().antMatchers(
"/login",
"/logout",
"/css/**",
"/js/**",
"/index.html",
"/favicon.ico",
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**",
"/captcha",
"/ws/**"
);
}
本人知识浅薄,如果有哪里解析有误,欢迎指教