天天看點

基于spring的安全管理架構-Spring Security

基于spring的安全管理架構-Spring Security

什麼是spring security?

spring security是基于spring的安全架構.它提供全面的安全性解決方案,同時在Web請求級别和調用級别确認和授權.在Spring Framework基礎上,spring security充分利用了依賴注入(DI)和面向切面程式設計(AOP)功能,為應用系統提供聲明式的安全通路控制功能,建曬了為企業安全控制編寫大量重複代碼的工作,是一個輕量級的安全架構,并且很好內建Spring MVC

spring security的核心功能有哪些?

1 認證 :認證使用者

2 驗證: 驗證使用者是否有哪些權限,可以做哪些事情

spring security基于哪些技術實作?

Filter,Servlet,AOP實作

架構技術準備:

IDEA 2017.3 ,MAVEN 3+ ,springboot 2.2.6 spring security 5.2.2, JDK 8+

spring security初步內建使用

建立一個基于Maven的spring boot項目,引入必需依賴

父級依賴

<groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-parent</artifactId>
  <version>2.2.6.RELEASE</version>           

springboot項目內建spring security的起步依賴

springboot web項目的起步依賴

<groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
           

我們啟動springboot項目的主類

大家可以看到,此刻我們已經實作了spring security最簡單的功能,上面截圖的最下方就是spring sceurity給我們随機生成的密碼

我們此刻可以建立一個最簡單的controller層來測試通路安全控制

@RestController

public class HelloController {

@RequestMapping("/sayHello")
public String sayHello() {
    System.out.println("Hello,spring security");
    return "hello,spring security";
}
           

}

接下來我們通過調用這個sayHello接口,我們會得到一個登入界面

此刻我們輸入預設的使用者名user ,密碼就是控制台随機生成的一串字元 2dddf218-48c7-454c-875d-f7283e8457c1

我們就可以以成功通路:  hello,spring security

當然,我們也可以在spring的配置檔案中去配置自定義的使用者名和密碼,這樣也可以實作同樣的效果,配置如下圖所示.

如果我們不想使用spring security的通路控制功能,我們可以在Springboot的啟動類注解上排除spring security的自動配置

@SpringBootApplication(exclude ={SecurityAutoConfiguration.class})

這樣我們再次通路接口,就不會要求我們登陸就可以直接通路了.

Spring Security 基于記憶體配置:

去除上述所有配置,我們重新配置一個配置類去繼承WebSecurityConfigurerAdapter,這個擴充卡類有很多方法,我們需要重寫configure(AuthenticationManagerBuilder auth)方法

@Configuration //配置類

@EnableWebSecurity //啟用spring security安全架構功能

public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    PasswordEncoder passwordEncoder = passwordEncoder();
    auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("123456"))
            .roles();
}

/**
 * spring security自帶的加密算法PasswordEncoder,我們使用其中一種算法來對密碼加密 BCryptPasswordEncoder方法采用SHA-256
 * +随機鹽+密鑰對密碼進行加密,過程不可逆 不加密高版本會報錯
 */
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}           

這樣我們就在記憶體配置了使用者admin,密碼采用加密算法去實作記憶體中的使用者登入認證.

在實際的場景中一個使用者可能有多個角色,接下來看一下基于記憶體角色的使用者認證

首先我們在配置類上需要添加注解啟用方法級别的使用者角色認證@EnableGlobalMethodSecurity(prePostEnabled = true)

@EnableGlobalMethodSecurity(prePostEnabled = true)

//啟用方法級别的認證 prePostEnabled boolean預設false,true表示可以使用 @PreAuthorize注解 和 @PostAuthorize注解

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    PasswordEncoder passwordEncoder = passwordEncoder();
    auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("123456"))
            .roles("super", "normal");
    auth.inMemoryAuthentication().withUser("normal").password(passwordEncoder.encode("123456"))
            .roles("normal");
}

/**
 * spring security自帶的加密算法PasswordEncoder,我們使用其中一種算法來對密碼加密 BCryptPasswordEncoder方法采用SHA-256
 * +随機鹽+密鑰對密碼進行加密,過程不可逆 不加密高版本會報錯
 */
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}           

此刻我們在記憶體中建立了兩個使用者,一個normal使用者,隻有normal權限,一個admin使用者,擁有super權限和normal權限.

我們建立三個通路路徑,分别對應super,normal和 super,normal都可以通路

@RequestMapping("/super")
@PreAuthorize(value = "hasRole('super')")
public String saySuper() {
    System.out.println("Hello,super!");
    return "Hello,super";
}

@RequestMapping("/normal")
@PreAuthorize(value = "hasRole('normal')")
public String sayNormal() {
    System.out.println("Hello,normal!");
    return "hello,normal";
}

@RequestMapping("/all")
@PreAuthorize(value = "hasAnyRole('normal','super')")
public String sayAll() {
    System.out.println("Hello,super,normal!");
    return "Hello,super,normal";
}
           

我們會發現,normal使用者可以通路2,3 admin可以通路 1,2,3,由此可以看出,此刻權限控制是OK的

這樣簡單地基于記憶體的使用者權限認證就完成了,但是記憶體中的使用者資訊是不穩定不可靠的,我們需要從資料庫讀取,那麼spring security又是如何幫我們去完成的呢?

spring security基于資料庫使用者資訊的安全通路控制

當我們把使用者資訊加入到資料庫,需要實作架構提供的UserDetailsService接口,去通過調用資料庫去擷取我們需要的使用者和角色資訊

@Autowired
private MyUserDetailService userDetailService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    PasswordEncoder passwordEncoder = passwordEncoder();
    //        auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("123456"))
    //                .roles("super", "normal");
    auth.userDetailsService(userDetailService).passwordEncoder(new BCryptPasswordEncoder());
}
/**
 * spring security自帶的加密算法PasswordEncoder,我們使用其中一種算法來對密碼加密 BCryptPasswordEncoder方法采用SHA-256
 * +随機鹽+密鑰對密碼進行加密,過程不可逆 不加密高版本會報錯
 */
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}           

自定義實作的接口,去通過資料庫查詢使用者資訊,此處需要注意兩個地方,

1:我們資料庫的密碼是通過new BCryptPasswordEncoder().encode("123456")生成的,明文密碼是不可以的,因為我們已經指定了密碼加密規則BCryptPasswordEncoder,

2:我們若有多個角色怎麼辦?循環周遊放入list中,注意:角色必須以ROLE_開頭

@Component

public class MyUserDetailService implements UserDetailsService {

@Resource
private UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
    org.springframework.security.core.userdetails.User user = null;
    User userInfo = null;
    if (!StringUtils.isEmpty(userName)) {
        userInfo = userMapper.getUserInfoByName(userName);
        if (userInfo != null) {
            List<GrantedAuthority> list = new ArrayList<>();
            String role = userInfo.getRole();
            GrantedAuthority authority = new SimpleGrantedAuthority(
                    "ROLE_" + userInfo.getRole());
            list.add(authority);
            //建立User對象傳回
            user = new org.springframework.security.core.userdetails.User(userInfo.getName(),
                    userInfo.getPassword(), list);
        }
    }
    return user;
}           

這裡的接口給予了使用者極大的擴充空間,我們最終建立User對象傳回,User對象有兩個構造方法,根據需要選取,參數含義參考源碼對照就行

這樣我們就通過查詢資料庫擷取使用者的登入使用者名和密碼以及角色資訊是否比對和具有通路權限.

基于角色的權限

認證和授權:

認證(authentication):認證通路者是誰?是否是目前系統的有限使用者

授權(authorization):目前使用者可以做什麼?

我們就以RBAC(Role-Based Access controll),這樣我們就需要設計出最少五張表去完成權限控制

user 表(存儲使用者資訊)

user_role(使用者角色資訊關系表)

role表(角色資訊)

role_permission(角色權限資訊關系表)

permission(授權資訊,可以存儲通路url路徑等)

這樣的權限設計模型,權限授予角色,角色授予使用者,管理起來清晰明了

接下來我們需要再次重寫MyWebSecurityConfig中的兩個configure方法

我們如果想忽略控制某些資源,不加通路攔截,我們就可以在WebSecurity方法配置忽略請求的url,一般會設定登入路徑,擷取圖形驗證碼路徑,靜态資源等

@Override

public void configure(WebSecurity web) throws Exception {
    //設定忽略攔截的路徑比對,這些請求無需攔截,直接放行
    web.ignoring().antMatchers("/index.html", "/static/**", "/login_p", "/getPicture");
}           

接下來我們就重點講一下重新的下一個方法HttpSecurity,這個方法裡面配置了我們對于權限的處理

protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests() //authorizeRequests() 允許基于使用HttpServletRequest限制通路
            .withObjectPostProcessor(postProcessor())  //請求都會經過此方法配置的過濾器*****重點******,出了WebSecurity配置的忽略請求
            .and()  //傳回HttpSecurity對象-----------------------------------
            .formLogin()  //指定基于表單的身份驗證沒指定,則将生成預設登入頁面
            .loginPage("/login_p") //指定跳轉登入頁
            .loginProcessingUrl("/login") //登入路徑
            .usernameParameter("username") //使用者名參數名
            .passwordParameter("password")//密碼參數名
            .failureHandler(customAuthenticationFailureHandler()) //自定義失敗處理
            .successHandler(customAuthenticationSuccessHandler()) //自定義成功處理
            .permitAll().and() //傳回HttpSecurity對象----------------------------------------
            .logout()//
            .logoutUrl("/logout").logoutSuccessHandler(customLogoutSuccessHandler())
            .permitAll()//
            .and()  //傳回HttpSecurity對象----------------------------------------
            .csrf().disable() //預設會開啟CSRF處理,判斷請求是否攜帶了token,如果沒有就拒絕通路  我們此處設定禁用
            .exceptionHandling()//
            .authenticationEntryPoint(customAuthenticationEntryPoint()) //認證入口
            .accessDeniedHandler(customAccessDeniedHandler()); //通路拒絕處理
}
           

public ObjectPostProcessor postProcessor() {

ObjectPostProcessor<FilterSecurityInterceptor> obj = new ObjectPostProcessor<FilterSecurityInterceptor>() { //此方法
        @Override
        public <O extends FilterSecurityInterceptor> O postProcess(O object) {
            object.setSecurityMetadataSource(metadataSource); //通過請求位址擷取改位址需要的使用者角色
            object.setAccessDecisionManager(
                    accessDecisionManager); //判斷是否登入,是否目前使用者是否具有通路目前url的角色
            return object;
        }
    };
    return obj;
}
           

在這裡我們需要實作兩個接口FilterInvocationSecurityMetadataSource ,AccessDecisionManager

首先是FilterInvocationSecurityMetadataSource,我們在這個接口實作類裡面getAttributes()方法主要做的就是擷取請求路徑url,然後去資料庫查詢哪些角色具有此路徑的通路權限,然後把角色資訊傳回List,很巧,SecurityConfig已經提供了一個方法createList,我們直接調用此方法傳回就可以

public class CustomMetadataSource implements FilterInvocationSecurityMetadataSource {

@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {           

    String requestUrl = ((FilterInvocation)o).getRequestUrl();

List<String> list = new ArrayList();
    if (list.size() > 0) {           

    //僞代碼 比對到具有該url的角色放入集合

String[] values = new String[list.size()];
        return SecurityConfig.createList(values);
    }
    //沒有比對上的資源,都是登入通路
    return SecurityConfig.createList("ROLE_LOGIN");
}

@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
    return null;
}

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

下面我們需要通過使用者所擁有的角色和url所需角色作比對,比對可以通路,不比對抛出異常AccessDeniedException,這裡更巧的一點是

我們可以通過Authentication擷取使用者所擁有的的角色,我們在上面實作類放入的角色集合也通過參數形式再次傳了進來,我們可以循環比對目前使用者是否有足夠權限

public class UrlAccessDecisionManager implements AccessDecisionManager {

@Override
public void decide(Authentication auth, Object o, Collection<ConfigAttribute> cas){
    Iterator<ConfigAttribute> iterator = cas.iterator();
    while (iterator.hasNext()) {
        ConfigAttribute ca = iterator.next();
        //目前請求需要的權限
        String needRole = ca.getAttribute();
        if ("ROLE_LOGIN".equals(needRole)) {
            if (auth instanceof AnonymousAuthenticationToken) {
                throw new BadCredentialsException("未登入");
            } else
                return;
        }
        //目前使用者所具有的權限
        Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
        for (GrantedAuthority authority : authorities) {
            if (authority.getAuthority().equals(needRole)) {
                return;
            }
        }
    }
    throw new AccessDeniedException("權限不足!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
    return true;
}
@Override
public boolean supports(Class<?> aClass) {
    return true;
}           

當我們把這兩個接口自定義實作了方法之後,後面每一步的自定義處理資訊,我們都可以根據業務需要去處理,比如

自定義身份驗證處理器: 根據異常去響應會不同資訊或者跳轉url,其他自定義處理器同理

下面給大家一個處理器demo,下面自定義處理器custom**的都可以參考做不同情況處理傳回值等來完成處理,前後端分離可以響應資料,不分離的可以跳轉頁面

public AuthenticationFailureHandler customAuthenticationFailureHandler() {

AuthenticationFailureHandler failureHandler = new AuthenticationFailureHandler() {
        @Override
        public void onAuthenticationFailure(HttpServletRequest httpServletRequest,
                HttpServletResponse resp, AuthenticationException e)
                throws IOException, ServletException {
            resp.setContentType("application/json;charset=utf-8");
            RespBean respBean = null;
            if (e instanceof BadCredentialsException
                    || e instanceof UsernameNotFoundException) {
                respBean = RespBean.error("賬戶名或者密碼輸入錯誤!");
            } else if (e instanceof LockedException) {
                respBean = RespBean.error("賬戶被鎖定,請聯系管理者!");
            } else if (e instanceof CredentialsExpiredException) {
                respBean = RespBean.error("密碼過期,請聯系管理者!");
            } else if (e instanceof AccountExpiredException) {
                respBean = RespBean.error("賬戶過期,請聯系管理者!");
            } else if (e instanceof DisabledException) {
                respBean = RespBean.error("賬戶被禁用,請聯系管理者!");
            } else {
                respBean = RespBean.error("登入失敗!");
            }
            resp.setStatus(401);
            ObjectMapper om = new ObjectMapper();
            PrintWriter out = resp.getWriter();
            out.write(om.writeValueAsString(respBean));
            out.flush();
            out.close();
        }
    };
    return failureHandler;
}
           

當我們把表建立好,實作上面的不同接口處理器,完成上述配置,我們就可以實作安全通路控制,至于spring security更深層級的用法,歡迎大家一起探讨!有時間我會分享一下另一個主流的安全通路控制架構 Apache shiro.其實我們會發現,所有的安全架構都是基于RBAC模型來實作的,根據架構的接口去做自定義實作來完成權限控制.

原文位址

https://www.cnblogs.com/zhaoletian/p/12747628.html