(十三)Spring Security自定義實現

如何實現自定義的認證和受權

Spring Security重寫WebSecurityConfigurerAdapter方法能夠實現自定義的認證和受權,有兩種方式可用java

  • configure(HttpSecurity http)方法種調用HttpSecurity 提供的3種認證方法開啓不一樣的認證方式web

  • configure(HttpSecurity http)方法種調用HttpSecurity apply()方法,將自定義的SecurityConfigurerAdapter(包含了你自定義的認證和受權)導入Spring Securityapp

ps:HttpSecurity 提供了ide

  • openidLogin() 基於 OpenId 的驗證
  • formLogin() 基於表單的身份驗證
  • oauth2Login() 基於外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份驗證

上述兩種自定義方式本質上都是調用了HttpSecurity 對象的apply()方法。第一種方式實際上是將提早寫好的SecurityConfigurerAdapter實現類做爲參數傳到apply()方法種ui

關於WebSecurityConfigurerAdapterSecurityConfigurerAdapter之後再說this

WebSecurityConfigurerAdapter

這裏簡單說一下WebSecurityConfigurerAdapterurl

經過重寫WebSecurityConfigurerAdapter的三個configure()方法實現自定以認證和受權.net

void configure(HttpSecurity http);code

void configure(AuthenticationManagerBuilder auth);orm

void configure(WebSecurity web);

一般重寫第一個方法

開啓並自定義表單登陸認知功能

重寫WebSecurityConfigurerAdapterconfigure(HttpSecurity http);方法,調用http.formLogin()

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 關閉csrf
    http.csrf().disable();

    // 建立一個SecurityConfigurerAdapter的子類,並將其應用到http中
    http.apply(customUsernamePasswordAuthenticationConfig);

    http.formLogin() // 開啓表單登陸認證方式
        .loginPage("/login") // 若是檢查到沒有登陸,就調用這個接口
            .loginProcessingUrl("/login") // UserNamePasswordAuthenticationFilter默認攔截的url是login,這裏能夠自定義它攔截的url
            .defaultSuccessUrl("/successLogin") // 成功登陸後調用的接口(成功經過)
            // .successForwardUrl("/hello") 
            // defaultSuccessUrl和successForwardUrl的區別參考 https://www.xttblog.com/?p=4994
        	// .successHandler()  // 設置一個成功處理器,認證成功會執行此對象的方法
            // .failureHandler()  // 設置一個失敗處理器,認證失敗會執行此對象的方法
            .permitAll() // 放行登陸接口
        	.and()
        .logout() // 開啓登出功能,建立DefaultLogoutPageGeneratingFilter
            .logoutUrl("/logout")// 設置執行登出服務的url
            .logoutRequestMatcher(getLogoutRequestMatcher()) // 默認登出只支持get方式的"/logout",這裏設置使用POST和GET方式的/logout都能響應登出
            .logoutSuccessUrl("/successLogout")
            .and()
        //                .rememberMe() // 建立相關過濾器,開啓記住我功能,可是須要表單提交一個name爲'remember-me'的單選框
        //                .tokenRepository() // 自定義token存儲業務
        //                .tokenValiditySeconds() // 自定義token存儲時長
        //                .and()
        .authorizeRequests() 
            .antMatchers("/hello") 
            .permitAll() // 放行/hello接口
            .anyRequest()
            .authenticated(); // 其餘接口都保護起來
}

自定義認證流程

下面將模仿UsernamePasswordAuthenticationFilter建立一套認證使用的組件

  • 寫一個Filter過濾器,能夠繼承AbstractAuthenticationProcessingFilter(默認只攔截POST方式的'/login'的請求,具體見doFilter()方法)也能夠繼承GenericFilterBean
  • AuthenticationProvider的實現類,這裏使用的」認證主體「實現類是內置的UsernamePasswordAuthenticationToken,你也能夠自定義一個
  • 寫一個UserDetialsService的實現類並注入到自定義的AuthenticationProvider的實現類中
  • 寫一個SecurityConfigurerAdapter的子類,將自定義的FilterAuthenticationProvider加入到HttpSecurity對象中
@Component
public class CustomUsernamePasswordAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private CustomUsernamePasswordAuthenticationProvider customUsernamePasswordAuthenticationProvider;

    // 這個方法用到的HttpSecurity對象和自定義的WebSecurityConfigurerAdapter的子類使用的HttpSecurity對象是同一個
    @Override
    public void configure(HttpSecurity http) throws Exception {

        CustomUsernamePasswordAuthenticationFilter filter = new CustomUsernamePasswordAuthenticationFilter();

        // 設置AuthenticaionManager,這裏獲取到的對象是http中保存的對象
        filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));

        // 設置記住我服務
        //        filter.setRememberMeServices();
        // 設置成功失敗處理器
        //        filter.setAuthenticationSuccessHandler();
        //        filter.setAuthenticationFailureHandler();

        http
            // 將自定義的AuthenticationProvider加到http中,http會後會將其加入到AuthenticationManager中
            // 既然是自定義的Filter其實也能夠不經過調用AuthenticationManager進行驗證,能夠直接將自定義的AuthenticationProvider注入到Filter中寫建立並校驗Authentication的流程
            .authenticationProvider(customUsernamePasswordAuthenticationProvider)
            // 把自定義的過濾器放在UsernamePasswordAuthenticationFilter前
            .addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
    }
}
public class CustomUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    private static final String username = "username";
    private static final String password = "password";

    protected CustomUsernamePasswordAuthenticationFilter() {
        // 設置攔截器攔截/login
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {

        // 獲取用戶名,密碼
        String username = this.obtainUsername(request);
        String password = this.obtainPassword(request);

        // 判空
        if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){
            throw new UsernameNotFoundException("用戶名或密碼爲空");
        }

        // 這裏可使用自定義AuthenticationToken
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);

        return this.getAuthenticationManager().authenticate(authenticationToken);
    }

    private String obtainPassword(HttpServletRequest request) {
        return request.getParameter(password);
    }

    private String obtainUsername(HttpServletRequest request) {
        return request.getParameter(username);
    }

}
@Component
public class CustomUsernamePasswordAuthenticationProvider implements AuthenticationProvider {

    // 自定義的UserDetailsService
    @Qualifier("customUserDetailsService")
    @Autowired
    private UserDetailsService userDetailsService;

    // 自定義的PasswordEncoder
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        // 獲取用戶名和密碼
        String username = authentication.getPrincipal().toString();
        String password = authentication.getCredentials().toString();

        // 獲取user,一般自定義的UserDetailsService實現類會包含相似用username查詢user對象的邏輯
        UserDetails user = userDetailsService.loadUserByUsername(username);

        if (user == null) {
            throw new InternalAuthenticationServiceException("找不到用戶");
        }

        // 省略校驗user帳戶是否過時的流程

        // 直接校驗密碼
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new BadCredentialsException("用戶名或密碼出錯");
        }

        // 到這裏尚未出錯,就建立一個已經被認證的Authentication
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());

        return result;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

自定義權限

一般將鑑權流程交給FilterSecurityInterceptor就夠了,咱們要作的是賦予用戶不一樣的權限和爲接口設置權限

爲用戶賦予權限的流程在UserDetailsServiceUserDetails loadUserByUsername(String username)方法中

建立一個User對象,並將權限賦值給它,在用戶認證成功後便可完成權限賦值

爲接口設置權限有兩種方式

  • Java-configure 寫Java代碼配置
  • 註解配置

Java-configure配置

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .antMatchers("/user").hasRole("ROLE_USER") // 有ROLE_USER權限的用戶才能訪問'/user'接口
        .antMatchers("/admin").hasRole("ROLE_ADMIN")  // 有ROLE_ADMIN權限的用戶才能訪問'/admin'接口
        .anyRequest()
        .authenticated();
}

註解配置

  1. 在自定義的WebSecurityConfigurerAdapter的子類上添加@EnableGlobalMethodSecurity開啓註解式配置
  2. 在接口方法上使用註解並指定訪問接口所須要的權限
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
@PreAuthorize("hasAnyRole('ROLE_USER')")
@GetMapping("/user")
@ResponseBody
public UserDetails user(@AuthenticationPrincipal UserDetails userDetails){
    return userDetails;
}

@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
@GetMapping("/admin")
@ResponseBody
public String admin(){
    return "admin";
}

ps:關於hasRole()方法和 hasAuthority()方法的區別看這裏

相關文章
相關標籤/搜索