spring security(4)https://developer.aliyun.com/article/1531015
六,权限校验的方法
6. hasAuthority方法
我们最早使用@PreAuthorize注解是在前面笔记的 ‘授权’ 的 ‘3. 自定义访问路径的权限’。可以回去看看,当时并没有详细学习@PreAuthorize注解的hasAuthority方法,只是直接使用,我们下面就来学习一下hasAuthority方法的原理,然后再学习hasAnyAuthority、hasRole、hasAnyRole方法就容易理解了
hasAuthority方法: 执行到了SecurityExpressionRoot的hasAuthority,内部其实是调用authentication的getAuthorities方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源
自定义权限校验的方法
security校验权限的PreAuthorize注解,其实就是获取用户权限,然后跟业务接口的权限进行比较,最后返回一个布尔类型。自定义一个权限校验方法的话,就需要新建一个类,在类里面定义一个方法,按照前面学习的三种方法的定义格式,然后返回值是布尔类型。如下
package com.sucurity.expression; import com.sucurity.domain.LoginUser; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import java.util.List; @Component("MyExpressionRoot") //自定义权限校验的方法 public class MyExpressionRoot { //自定义权限校验的方法 public boolean MyHasAuthority(String authority){ //获取用户具有的权限字符串,有可能用户具有多个权限字符串,所以获取后是一个集合 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); //LoginUser是我们在domain目录写好的实体类 LoginUser loginUser = (LoginUser) authentication.getPrincipal(); List<String> permissions = loginUser.getPermissions(); //判断用户权限集合中,是否存在跟业务接口(业务接口的权限字符串会作为authority形参传进来)一样的权限 return permissions.contains(authority); } }
之后只需要这样调用即可
@RestController public class HelloController { @RequestMapping("/hello") @PreAuthorize("@MyExpressionRoot.MyHasAuthority('system:test:list')") public String hello() { return "<h1>Hello World</h1>"; } }
使用配置类配置权限
把SecurityConfig类修改为如下,主要就是添加权限控制相关的配置
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) //实现Security提供的WebSecurityConfigurerAdapter类,就可以改变密码校验的规则了 public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean //把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验 //注意也可以注入PasswordEncoder,效果是一样的,因为PasswordEncoder是BCry..的父类 public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } //---------------------------认证过滤器的实现---------------------------------- @Autowired //注入我们在filter目录写好的类 private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; //---------------------------登录接口的实现------------------------------------ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http //由于是前后端分离项目,所以要关闭csrf .csrf().disable() //由于是前后端分离项目,所以session是失效的,我们就不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() //指定让spring security放行登录接口的规则 .authorizeRequests() // 对于登录接口 anonymous表示允许匿名访问 .antMatchers("/user/login").anonymous() //基于配置的的权限控制。指定接口的地址,为HelloController类里面的/configAuth接口,指定权限为system:dept:list .antMatchers("/configAuth").hasAuthority("system:dept:list") //上一行的hasAuthority方法就是security官方提供的4种权限控制的方法之一 // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); //---------------------------认证过滤器的实现------------------------------- //把token校验过滤器添加到过滤器链中 //第一个参数是上面注入的我们在filter目录写好的类,第二个参数表示你想添加到哪个过滤器之前 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //---------------------------👇 异常处理的相关配置 👇------------------------ http.exceptionHandling() //配置认证失败的处理器 .authenticationEntryPoint(authenticationEntryPoint) //配置授权失败的处理器 .accessDeniedHandler(accessDeniedHandler); //---------------------------👇 设置security运行跨域访问 👇------------------ http.cors(); //---------------------------👆 设置security运行跨域访问 👆------------------ } //---------------------------👇 异常处理的相关配置 👇----------------------------- @Autowired //注入Security提供的认证失败的处理器,这个处理器里面的AuthenticationEntryPointImpl实现类,用的不是官方的了, //而是用的是我们在handler目录写好的AuthenticationEntryPointImpl实现类,因为我们也是添加到容器把官方的这个实现类覆盖了 private AuthenticationEntryPoint authenticationEntryPoint; @Autowired //注入Security提供的授权失败的处理器,这个处理器里面的AccessDeniedHandlerImpl实现类,用的不是官方的了, //而是用的是我们在handler目录写好的AccessDeniedHandlerImpl实现类,因为我们也是添加到容器把官方的这个实现类覆盖了 private AccessDeniedHandler accessDeniedHandler; //---------------------------👆 异常处理的相关配置 👆---------------------------- }
cors
https://blog.csdn.net/freeking101/article/details/86537087
7 . 自定义处理器
7.1 自定义成功处理器
[!note]
在调用chain.doFilter(request, response);之后的代码,通常会在当前过滤器处理完请求并传递给下一个过滤器后执行。这部分代码通常是过滤器中在所有后续过滤器都完成处理后需要执行的清理工作或者某些特定逻辑。
例如:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 在此进行预处理操作 chain.doFilter(request, response); // 调用此行后,执行过滤链中的下一个过滤器 // 这里的代码将在所有后续过滤器完成后执行 // 可能包含如:资源清理、记录日志、修改响应内容等操作 }
AbstractAuthenticationProcessingFilter
调用requiresAuthentication方法确定请求是否用于身份验证,并应由此过滤器处理。如果是身份验证请求,将调用attemptAuthentication来执行身份验证。有三种可能的结果。
返回一个Authentication对象。在调用successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)方法之后,将调用配置的SessionAuthenticationStrategy(以处理任何与会话相关的行为,例如创建新会话以防止会话固定攻击)
身份验证过程中发生AuthenticationException异常。将调用unsuccessfulAuthentication方法
返回Null,表示身份验证过程不完整。假设子类已经完成了继续身份验证过程所需的工作(例如重定向),那么该方法将立即返回。假设该方法将接收稍后的请求,并且返回的Authentication对象不是null。
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } try { Authentication authenticationResult = attemptAuthentication(request, response); if (authenticationResult == null) { // return immediately as subclass has indicated that it hasn't completed return; } this.sessionStrategy.onAuthentication(authenticationResult, request, response); // Authentication success if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } successfulAuthentication(request, response, chain, authenticationResult); } catch (InternalAuthenticationServiceException failed) { this.logger.error("An internal error occurred while trying to authenticate the user.", failed); unsuccessfulAuthentication(request, response, failed); } catch (AuthenticationException ex) { // Authentication failed unsuccessfulAuthentication(request, response, ex); } }
调用的UsernamePasswordAuthenticationFilter的方法
UsernamePasswordAuthenticationFilter
默认处理与表单登录相关的POST请求,这是通过该过滤器内部的配置决定的。具体而言,在UsernamePasswordAuthenticationFilter
中,有两个重要的属性控制了它处理请求的方式:
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); username = (username != null) ? username : ""; username = username.trim(); String password = obtainPassword(request); password = (password != null) ? password : ""; UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); }
成功的调用
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { SecurityContextHolder.getContext().setAuthentication(authResult); if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult)); } this.rememberMeServices.loginSuccess(request, response, authResult); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } this.successHandler.onAuthenticationSuccess(request, response, authResult); }
成功处理器也就是successHandler的类型
public interface AuthenticationSuccessHandler { /** * Called when a user has been successfully authenticated. * @param request the request which caused the successful authentication * @param response the response * @param chain the {@link FilterChain} which can be used to proceed other filters in * the chain * @param authentication the <tt>Authentication</tt> object which was created during * the authentication process. * @since 5.2.0 */ default void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException { onAuthenticationSuccess(request, response, authentication); chain.doFilter(request, response); } /** * Called when a user has been successfully authenticated. * @param request the request which caused the successful authentication * @param response the response * @param authentication the <tt>Authentication</tt> object which was created during * the authentication process. */ void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException; }
successHandler成功处理器的类型是一个接口
如果要自定义成功处理器,
那么就只需要新建某个类去实现这个接口,然后配置给security就可以了
import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component //官方提供的LogoutSuccessHandler接口的实现类,用于自定义'登出成功的处理器' public class MyLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { //我们只验证登出成功之后被调用到下面那行代码就行,如果是要自定义'登出成功的处理器',那么就在下面写具体代码即可 System.out.println("退出登录成功"); } }
第二步: 把刚刚创建的MyLogoutSuccessHandler实现类,配置给security。把 SecurityConfig 类,修改为如下,主要就是添加了登出成功的处理器
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; //SpringSecurity的配置类 public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired //注入security官方提供的AuthenticationSuccessHandler接口 private AuthenticationSuccessHandler xxsuccessHandler; @Autowired //注入security官方提供的AuthenticationSuccessHandler接口 private AuthenticationFailureHandler xxfailureHandler; @Autowired //注入security官方提供的LogoutSuccessHandler接口 private LogoutSuccessHandler xxlogoutSuccessHandler; @Override protected void configure(HttpSecurity http) throws Exception { //super.configure(http); //默认写的就这一行 //上面注入的接口作为successHandler方法的参数写进来。注意最重要的是'formLogin登录表单',不然链式编程写不下去 //这样就相当于把刚刚在MySuccessHandler类里面的"自定义'登录成功的处理器'",配置给security了 http.formLogin() //登录认证成功的处理器 .successHandler(xxsuccessHandler) //登录认证失败的处理器 .failureHandler(xxfailureHandler); //登出成功的处理器的配置 http.logout() //登出成功的处理器 .logoutSuccessHandler(xxlogoutSuccessHandler); //其它默认的认证接口,例如业务接口的认证限制,要配,因为你重写自定义了之后,原有的配置都被覆盖,不写的话业务接口就没有security认证拦截的功能了 //我的截图里面没有下面这一行的代码,希望你们不要漏写 http.authorizeRequests().anyRequest().authenticated(); } }