天天看點

SpringBoot學習筆記(十四:Spring Security安全管理 )一、Spring Security簡介二、整合Spring Security

文章目錄

Spring Security 的前身是 Acegi Security,在被收納為Spring子項目後正式更名為Spring Security。 截止到目前(2020年4月16日),Spring Security已經更新到5.3.2 RELEASE版本,加入了原生OAuth2.0架構,支援更加現代化的密碼加密方式。

應用程式的安全性通常展現在兩個方面:認證和授權:

  • 認證是确認某主體在某系統中是否合法、可用的過程。這裡的主體既可以是登入系統的使用者,也可以是接入的裝置或者其他系統。
  • 授權是指當主體通過認證之後,是否允許其執行某項操作的過程。

這些概念并非Spring Security獨有,而是應用安全的基本關注點。Spring Security可以幫助我們更便捷地完成認證和授權。

Spring Security提供了一組深入的授權功能。有三個主要領域:

  • 對Web 請求進行授權
  • 授權某個方法是否可以被調用
  • 授權通路單個領域對象執行個體

Spring Security原理簡易說明:

一旦啟用了 Spring Security, Spring IoC 容器就會建立一個名稱為 SpringSecurityFiltrChain的Spring Bean 。它的類型為 FilterChainProxy,事實上它也實作了filter 接口,隻是它是個特殊的攔截器。

FilterChainProxy

SpringBoot學習筆記(十四:Spring Security安全管理 )一、Spring Security簡介二、整合Spring Security

Spring Security操作的過程中它會提供Servlet 過濾器DelegatingFilterProxy,這個過濾器會通過 Spring Web IoC 容器去擷取 Spring Security 所自動建立的 FilterChainProxy 對象,這個對象上存在一個攔截器清單( List ),清單上存在使用者驗證的攔截器、跨站點請求僞造等攔截器 ,這樣它就可以提供多種攔截功能。

SpringBoot學習筆記(十四:Spring Security安全管理 )一、Spring Security簡介二、整合Spring Security

于是焦點又落到了FilterChainProxy 對象上,通過它還可以注冊 Filter ,也就允許注冊自定義 Filter 來實作對應的攔截邏輯,以滿足不同的需要。當然 Spring Security 也實作了大部分常用的安全功能,并提供了相應的機制來簡化開發者的工作,是以大部分情況下并不需要自定義開發 ,使用它提供的機制即可。

Multiple SecurityFilterChain

SpringBoot學習筆記(十四:Spring Security安全管理 )一、Spring Security簡介二、整合Spring Security

建立 Spring Boot Web 項目,然後添加 spring-boot-starter-security 依賴即可:

<dependency>
            <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>      

寫一個簡單地hello接口:

@GetMapping("/hello")
    public String hello(){
        return "Hello,it's safe";
    }      

啟動項目,通路hello接口,會發現需要登入,,這個登入頁面是由Spring Security 提供的:

SpringBoot學習筆記(十四:Spring Security安全管理 )一、Spring Security簡介二、整合Spring Security

預設的使用者名是 user ,預設的登入密碼 在每次啟動項目時随機生成 檢視項目啟動日志:

SpringBoot學習筆記(十四:Spring Security安全管理 )一、Spring Security簡介二、整合Spring Security

從項目啟動日志中可以看到預設的登入密碼,登入成功後,就可以通路hello 接口了。

在上面代碼中,使用者名是預設的,密碼是随機生成的,可以對使用者名和密碼以及使用者角色進行配置:

spring.security.user.name=sao
spring.security.user.password=123456
spring.security.user.roles=admin      

為了給 FilterChainProxy 加入自定義的初始化, SpringSecurity 供了 SecurityConfigurer 口,通過它就能夠實作現對 Spring Security 的配置。 有了這個接口還不太友善,因為它隻是能夠提供接口定義的功能,為了更友善 Spring Web 工程還提供了專門的接口 WebSecurityConfigurer, 并且在這個接口的定義上提供了以個抽象類 WebSecurityConfigAdapter—— 開發者通過繼承它就能 得到Spring Security 預設的安全功能。 也可以通過覆寫它提供的方法來自定義自 的安全攔截方案。

這裡需要 WebSecurityConfigAdapter 中預設存在的方法:

/**
*用來配置使用者簽名服務,主要是 user-details 制,你還可以給予使用者賦予角色
* @param auth 簽名管理器構造器 用于建構使用者具體權限控制
*/
protected roid configure (AuthenticationManagerBuilder auth ); 
/** 
*用來配置Filter鍊
* @param web Spring Web Securty 對象
*/ 
public roid configure (WebSecurity web) ; 
/**
*用來配置攔截保護的請求,比如什麼請求放行,什麼請求需要驗證
*@param http http 安全請求對象
*/ 
protected void configure (HttpSecurity http) throws Exception ;      

繼承自 WebSecurityConfigurerAdapter ,實作基于記憶體的認證,配置方式如下:

/**
 * @Author 三分惡
 * @Date 2020/1/11
 * @Description
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 從 Spring5 開始,強制要求密碼要加密,如果不想加密,可以使用一個過時的 PasswordEncoder 的執行個體 NoOpPasswordEncoder
     * BCryptPasswordEncoder 密碼編碼工具
     * @return
     */
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                //配置了兩個使用者,包括使用者的使用者名、角色、密碼,使用者密碼已經加密(123)
                .withUser("zhangsan")
                   //角色
                   .roles("ADMIN")
                   //密碼已經加密,可以通過 passwordEncoder.encode("123")擷取
                  .password("$2a$10$dQLFreAJHM0F.4XAWQMTA.kB5W3H2.hjA6xBUJFHTFT7iHRzO0flm")
                 //連接配接方法
                .and()
                .withUser("lisi").roles("USER").password("$2a$10$RDdoj3sm/RD7HzqSnU864eEE5kEZZxbyQqnYQJGrO2pgkUGCDutTC");
    }

}      

上面雖然現在可以實作認證功能,但是受保護的資源都是預設的 , 根據實際情況進行角色管理,如果要實作這些功能 就需要重寫 WebSecurityConfigurerAdapter 的另一 方法:

@Override
    protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests()             //開啟登入配置
               .antMatchers("/admin/**")
               //通路"admin/**"模式的URL必須具備ADMIN權限
               .hasRole("ADMIN")
               .antMatchers("/user/**")
               //通路"user/**"模式的 URL必須具備 AD1IN、USER 的角色
               .access( "hasAnyRole('ADMIN','USER') ")
               //除上面配置外的其它url都需要登入
               .anyRequest()
               .authenticated()
               .and()
               //開啟表單登入
               .formLogin()
               //配置登入接口為“/login”
               .loginProcessingUrl("/login")
               //登入相關的接口不需要認證
               .permitAll()
               .and()
               //關閉 csrf
               .csrf()
               .disable();
    }      

在HelloController中添加相應的接口,admin/hello隻能被具備ADMIN角色的使用者通路,user/hello能被具備ADMIN和UAER角色的使用者通路:

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(){
        return "Hello,it's safe";
    }

    @GetMapping("admin/hello")
    public String adminHello(){
        return "Hello,admin is safe";
    }

    @GetMapping("user/hello")
    public String userHello(){
        return "Hello,user is safe";
    }
}
      

登入表單一直使 Spring Security 提供的頁面,登入成功後也是預設的頁面跳轉,但是,前後端分離正在成為企業級應用開發的主流,在前後端分離的開發方式中,前後端的資料互動通過 JSON 進行,這時,登入成功後就不是頁面跳轉了,而是一段 JSON 提示。要實作這些功能,需要繼續完善上文的配置:

//開啟表單登入
                .formLogin()
                //配置登入接口為“/login”
                .loginProcessingUrl("/login")
                //登入頁面
                .loginPage("/login_page")
                //自定義認證所需的使用者名和密碼的參數名
                .usernameParameter("username")
                .passwordParameter("password")
                //定義登入成功的處理邏輯,這裡是模拟前後端分離的情況,傳回一段json,不前後端分離的話可以直接傳回頁面
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException, ServletException {
                        Object principal = auth.getPrincipal();
                        response.setContentType("application/json;charset=utf-8");
                        PrintWriter out = response.getWriter();
                        response.setStatus(200);
                        Map<String, Object> map = new HashMap<>();
                        map.put("status", 200);
                        map.put("msg", principal);
                        ObjectMapper om = new ObjectMapper();
                        out.write(om.writeValueAsString(map));
                        out.flush();
                        out.close();
                    }
                })
                //定義登入失敗的處理邏輯
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
                        response.setContentType("application/json;charset=utf-8");
                        PrintWriter out = response.getWriter();
                        response.setStatus(401);
                        Map<String, Object> map = new HashMap<>();
                        map.put("status", 401);
                        if (e instanceof LockedException) {
                            map.put("msg", "賬号被鎖定,登入失敗 !");
                        } else if (e instanceof BadCredentialsException) {
                            map.put("msg", "賬戶名或密碼輸入錯誤, 登入失敗!");
                        } else if (e instanceof DisabledException) {
                            map.put("msg", "賬戶被禁用,登入失敗!");
                        } else if (e instanceof AccountExpiredException) {
                            map.put("msg", "賬戶已過期,登入失敗!");
                        } else {
                            map.put("msg", "登入失敗!");
                        }
                        ObjectMapper om = new ObjectMapper ();
                        out .write(om.writeValueAsString(map));
                        out.flush();
                        out.close();
                    }
                })
                //登入相關的接口不需要認證
                .permitAll()
                .and()      

配置完成後,使用 Postman 進行登入測試,登入成功後傳回使用者的基本資訊 密碼已經過濾掉了。如果登入失敗, 會有相應的提示。

登入成功

SpringBoot學習筆記(十四:Spring Security安全管理 )一、Spring Security簡介二、整合Spring Security

登入失敗

SpringBoot學習筆記(十四:Spring Security安全管理 )一、Spring Security簡介二、整合Spring Security

如果想要登出登入 也隻需要提供簡單的配置即可:

//登出登入
                .logout()
                //登出登入請求url
                .logoutUrl("/logout")
                //清除身份認證資訊
                .clearAuthentication(true)
                //使 Session失效
                .invalidateHttpSession(true)
                //定義登出成功的業務邏輯,這裡傳回一段json
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        out.write("logout success");
                        out.flush();
                    }
                })
                .permitAll()
                .and()      

上面介紹的認證與授權都是基于 URL 的,也可以通過注解來靈活地配置方法安全,要使用相關注解,首先要通過@EnableGloba!MethodSecurity 注解開啟基于注解的安全配置:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

}      
  • prePostEnabled=true會解鎖@PreAuthorize和@PostAuthorize兩個注解,@PreAuthorize會在執行方法前驗證,@PostAuthorize會在執行方法後驗證。
  • securedEnabled=true會解鎖@Secured 注解。
@Service
public class MethodService {
    //示通路該方法需要 ADMIN 角色
    @Secured("ROLE ADMIN")
    public String admin () {
        return "hello admin ";
    }

    //通路該方法既需要ADMIN角色又需要USER角色
    @PreAuthorize("hasRole ('ADMIN') and hasRole ('USER ')")
        public String user(){
        return "Hello User";
    }

    //通路該方法需要ADMIN 或 USER角色
    @PreAuthorize("hasAnyRole('ADMIN','USER')")
    public String any(){
        return "Hello Every One";
    }

}      

密碼加密一般會用到散列函數,又稱雜湊演算法、哈希函數,這是一種從任何資料中建立數字“指紋”的方法。散列函數把消息或資料壓縮成摘要,使得資料量變小,将資料的格式固定下來,然後将資料打亂混合,重新建立一個散列值。散列值通常用一個短的随機字母和數字組成的字元串來代表。好的散列函數在輸入域中很少出現散列沖突。在散清單和資料進行中,不抑制沖突來差別資料會使得資料庫記錄更難找到。我們常用的散列函數有 MD5 消息摘要算法、安全雜湊演算法(Secure Hash Algorithm )。

123456 ---MD5---> e10adc3949ba59abbe56e057f20f883e      

實際上,上面的執行個體在現實使用中還存在着一個不小的問題。雖然 MD5 算法是不可逆的,但是因為它對同一個字元串計算的結果是唯一 的,是以一些人可能會使用“字典攻擊”的方式來攻破 MD5 加密的系統。這雖然屬于暴力解密,卻十分有效,因為大多數系統的使用者密碼都不會很長。

為了解決這個問題,我們可以使用鹽值加密“salt-source”,所謂加鹽加密,是指在加密之前,為原文附上額外的随機值,再進行加密。具體實作方法并不固定。

Spring Security内置了密碼加密機制,隻需使用一個PasswordEncoder接口即可。

PasswordEncoder接口定義了encode和matches兩個方法,當用資料庫存儲使用者密碼時,加密過程用 encode方法,matches方法用于判斷使用者登入時輸入的密碼是否正确。

Spring Security 還内置了幾種常用的 PasswordEncoder 接口,例如, StandardPasswordEncoder中的正常摘要算法(SHA-256等)、BCryptPasswordEncoder加密,以及類似 BCrypt的慢散列加密Pbkdf2PasswordEncoder等,官方推薦使用BCryptPasswordEncoder。

配置密碼加密非常簡單:

@Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }      

同樣可以自定義加密方式,例如不想使用推薦的BCryptPasswordEncoder,想使用其它的加密方法,例如MD5加密,很簡單,我們自己實作一個PasswordEncoder,在配置中使用自定義的加密類即可。

public class Md5PasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence charSequence) {
        //省略md5加密過程
        return md5String;
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        //省略比對過程
        return false;
    }
}      
@Bean
    PasswordEncoder passwordEncoder() {
        return new Md5PasswordEncoder();
    }      

在真實項目中,使用者的基本資訊以及角色等都存儲在資料庫中,是以需要從資料庫中擷取資料進行認證。

一共三張表,分别是使用者表、角色表、使用者_角色關聯表。

SpringBoot學習筆記(十四:Spring Security安全管理 )一、Spring Security簡介二、整合Spring Security

建立表并插入一些測試資料:

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `rolename` varchar(50) NOT NULL COMMENT '角色名',
  `note` varchar(255) NOT NULL COMMENT '角色描述',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='角色表';

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'ROLE_ADMIN', '管理者');
INSERT INTO `role` VALUES ('2', 'ROLE_DBA', '資料庫管理者');
INSERT INTO `role` VALUES ('3', 'ROLE_USER', '使用者');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `username` varchar(50) NOT NULL COMMENT '使用者名',
  `password` varchar(255) NOT NULL COMMENT '密碼',
  `enabled` tinyint(1) NOT NULL COMMENT '是否可用,1表示可用,0表示不可用',
  `locked` tinyint(1) NOT NULL COMMENT '是否上鎖,1表示上鎖,0表示未上鎖',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='關聯表';

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'admin', '$2a$10$7zE.mjxSWV6w6jyh8iL8Q.5ouGokPg1PuL531YKPmRWCysN30I.qO', '1', '0');
INSERT INTO `user` VALUES ('2', 'root', '$2a$10$7zE.mjxSWV6w6jyh8iL8Q.5ouGokPg1PuL531YKPmRWCysN30I.qO', '1', '0');
INSERT INTO `user` VALUES ('3', 'laosan', '$2a$10$7zE.mjxSWV6w6jyh8iL8Q.5ouGokPg1PuL531YKPmRWCysN30I.qO', '1', '0');

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uid` int(11) NOT NULL COMMENT '使用者id',
  `rid` int(11) NOT NULL COMMENT '角色id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT='使用者-角色關聯表';

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('1', '1', '1');
INSERT INTO `user_role` VALUES ('2', '2', '2');
INSERT INTO `user_role` VALUES ('3', '3', '3');
INSERT INTO `user_role` VALUES ('4', '1', '3');
INSERT INTO `user_role` VALUES ('5', '1', '2');      
  • 角色名有一個預設的字首"ROLE_"

這裡選擇MyBatis作為持久層架構,添加依賴:

<dependency>
            <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>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.12</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>      

資料庫連接配接和MyBatis相關配置:

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/demo_security?serverTimezone=CTT&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true

mybatis.type-aliases-package=edu.hpu.pojo
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
      

使用者實體類需要實作UserDetails接口:

public class User implements UserDetails {
    private Integer id;
    private  String username;
    private  String password;
    private Boolean enabled;
    private Boolean locked;
    private List<Role> roles;

    /**
     * 擷取目前使用者對象所具有的角色資訊
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>() ;
        for (Role role : roles){
            authorities.add (new SimpleGrantedAuthority (role.getRolename()) ) ;
        }
        return authorities;
    }

    /**
     * 擷取目前使用者的密碼
     * @return
     */
    @Override
    public String getPassword() {
        return password;
    }

    /**
     * 擷取d目前使用者的使用者名
     * @return
     */
    @Override
    public String getUsername() {
        return username;
    }

    /**
     * 目前賬戶是否未過期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 目前賬戶是否未鎖定
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return !locked;
    }

    /**
     * 目前賬戶密碼是否未過期
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 目前賬戶是否可用
     * @return
     */
    @Override
    public boolean isEnabled() {
        return enabled;
    }

    //省略getter、setter
}      
  • 實作了UserDetails接口的七個方法,方法作用見注釋
  • 使用者根據實際情況設直這個方法的傳回值 因為預設情況下不需要開發者自己進行密碼角色等資訊的比對,開發者隻需要提供相關資訊即可。例如 getPassword()方法傳回的密碼和使用者輸入的登入密碼不比對,會自動抛出BadCredentialsException 異常。
  • getAuthorities()方法用來擷取目前使用者所具有的角色資訊。

角色實體類:

public class Role {
    private  Integer id;
    private  String rolename;
    private  String note;
    //省略getter、setter
}      

接口:

@Mapper
public interface UserMapper {
    User loadUserByUsername(String username);
    List<Role> getUserRolesByUid (Integer id) ;
}
      

對應的映射檔案:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="edu.hpu.mapper.UserMapper">

    <select id="loadUserByUsername" resultType="User">
            select * from user where username=#{username}
    </select>

    <select id="getUserRolesByUid" resultType="Role">
        select r.* from role r
           join user_role ur on  r.id=ur.rid
           join user u on  u.id=ur.uid
        where  u.id=#{id}
    </select>
</mapper>      

UserService需要實作UserDetailsService接口

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);
        if (user==null){
            throw new UsernameNotFoundException("賬号不存在");
        }
        user.setRoles(userMapper.getUserRolesByUid(user.getId()));
        return user;
    }
}      
  • 實作 UserDetailsService 接口,并實作該接口中的 loadUserByUsername 方法,該方法的參數就是使用者登入時輸入的使用者名,通過使用者名去資料庫中查找使用者,如果沒有查找到使用者,就抛出 個賬戶不存在的異常,如果查找到了使用者,就繼續查找該使用者所具有的角色資訊,并将擷取到的 user 對象傳回,再由系統提供的 DaoAuthenticationProvider 類去比對密碼是否正确。
  • loadUserByUsername 方法将在使用者登入時自動調用。

這裡是一個比較精簡的配置,使用者名和密碼從資料庫中擷取:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserService userService;

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService (userService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/db/**").hasRole("DBA")
                .antMatchers("/user/**").hasRole("USER")
                .and()
                .formLogin()
                .loginProcessingUrl("/login").permitAll()
                .and()
                .csrf().disable();
    }
}      

根據配置類編寫不同權限的接口:

@RestController
public class HelloController {

    @GetMapping("/user/hello")
    public String userHello(){
        return "你好,普通使用者!";
    }

    @GetMapping("/admin/hello")
    public String adminHello(){
        return "你好,管理者!";
    }

    @GetMapping("/db/hello")
    public String dbaHello(){
        return "你好,資料庫管理者!";
    }
}      

啟動項目,就可以通路不同權限的接口進行測試了。

在上面的執行個體中中定義了三種角色,但是這三種角色之間不具備任何關系,一 般來說角色之間是有關系的,例如 ROLE_ADMIN 一般既具有 ADMIN 的權限,又具有 USER 的權限。那麼如何配置這種角色繼承關系呢?在 pring Security 中隻需要開發者提供一個 RoleHierarchy 即可。

假設 ROLE_DBA是終極大 Boss ,具有所有的權限, ROLE_ADMIN具有 ROLE_USER權限, ROLE_USER是一個公共角色,即 ROLE_ADMIN繼承 ROLE_USER, ROLE_DBA繼承ROLE_ADMIN ,要描述這種繼承關系,隻需要開發者在 Spring Security 的配置類中提供RoleHierarchy 即可,代碼如下:

@Bean
    RoleHierarchy roleHierarchy(){
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        String hireachy = "ROLE_DBA > ROLE_ADMIN> ROLE_USER";
        roleHierarchy.setHierarchy(hireachy);
        return roleHierarchy;
    }      

配置完 RoleHierarchy 之後,具有 ROLE_DBA 角色的使用者就可以通路所有資源了, 具有

ROLE_ADMIN 角色的使用者也可以通路具有 ROLE_USER 角色才能通路的資源。

使用 ttpSecurity 配置的認證授權規則還是不夠靈活,無法實作資源和角色之間的動态調整,要實作動态配置 URL 權限,就需要我們自定義權限配置,在第4節的基礎上進行改造。

這裡的資料庫在4資料庫的基礎上再增加一張資源表和資源角色關聯表,資源表中定義了使用者能夠通路的 URL 模式,資源角色表則定義了通路該模式的 URL 需要什麼樣的角色。

添加兩張表之後的資料庫表結構如下:

SpringBoot學習筆記(十四:Spring Security安全管理 )一、Spring Security簡介二、整合Spring Security

建立資源表和資源角色關聯表并插入一些測試資料:

DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `pattern` varchar(50) NOT NULL COMMENT '路徑',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='資源表';

-- ----------------------------
-- Records of menu
-- ----------------------------
INSERT INTO `menu` VALUES ('1', '/db/**');
INSERT INTO `menu` VALUES ('2', '/admin/**');
INSERT INTO `menu` VALUES ('3', '/user/**');

-- ----------------------------
-- Table structure for menu_role
-- ----------------------------
DROP TABLE IF EXISTS `menu_role`;
CREATE TABLE `menu_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `mid` int(11) NOT NULL COMMENT '資源id',
  `rid` int(11) NOT NULL COMMENT '角色id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='資源_角色關聯表';

-- ----------------------------
-- Records of menu_role
-- ----------------------------
INSERT INTO `menu_role` VALUES ('1', '1', '2');
INSERT INTO `menu_role` VALUES ('2', '2', '1');
INSERT INTO `menu_role` VALUES ('3', '3', '3');      

Menu.java:

public class Menu {
    private Integer id;
    private String pattern;
    private List<Role> roles;
    //省略getter、setter
}
      

MenuMapper.java:

@Mapper
public interface MenuMapper {
    List<Menu> getAllMenus();
}      

MenuMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="edu.hpu.mapper.MenuMapper">
    <resultMap id="BaseResultMap" type="edu.hpu.pojo.Menu">
        <id property="id" column="id" />
        <result property="pattern" column="pattern"/>
        <collection property="roles" ofType="edu.hpu.pojo.Role">
            <id property="id" column="id" />
            <result property="rolename" column="rolename"/>
            <result property="note" column="note"/>
        </collection>
    </resultMap>

    <select id="getAllMenus" resultMap="BaseResultMap">
        SELECT
          m.*,
          r.id AS rid,
          r.rolename AS rolename,
          r.note AS note
        FROM
         menu m
        LEFT JOIN menu_role mr ON m.id=mr.mid
        LEFT JOIN role r ON mr.rid= r.id
    </select>
</mapper>      

要實作動态配置權限,首先要自定義 FilterlnvocationSecurityMetadataSource,Spring Security中通過FilterlnvocationSecurityMetadataSource接口的getAttributes方法來确定一個請求需要哪些角色,接口的預設實作類是DefaultFilterlnvocationSecurityMetadataSource,參考DefaultFilterlnvocationSecurityMetadataSource,可以自定義FilterlnvocationSecurityMetadataSource接口實作類。

@Component
public class CustomFilterinvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    //建立一個AntPathMatcher執行個體,主要用來實作ant風格的URL比對。
    AntPathMatcher antPathMatcher =new AntPathMatcher() ;
    @Autowired
    MenuMapper menuMapper;

    /**
     *
     * @param o
     * 參數是一個FilterInvocation,可以從中去除請求的url
     * @return Collection<ConfigAttribute>:表示目前請求 URL 所需的角色。
     * @throws IllegalArgumentException
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        //從FilterInvocation 中提取出目前請求的url
        String requestUtl=( (FilterInvocation) o).getRequestUrl();
        //從資料庫中取出資源資訊
        List<Menu> menus=menuMapper.getAllMenus();
        for (Menu menu:menus) {
            if (antPathMatcher.match(menu.getPattern(),requestUtl)){
                //擷取目前請求的 URL 所需要的角色資訊
                List<Role> roles=menu.getRoles();
                String[] roleArr =new String[roles.size()];
                for (int i=0;i<roleArr.length;i++){
                    roleArr[i] = roles.get(i).getRolename();
                }
                //傳回角色資訊
                return SecurityConfig.createList(roleArr);
            }
        }
        //如果不存在比對的角色資訊,傳回ROLE_LOGIN,即登入就可通路
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    /**
     *
     * @return 傳回所有定義好的權限資源, Spring Security 在啟動時會校驗
     * 相關配置是否正确 ,如果不需要校驗,那麼該方法直接傳回 null 即可
     */
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    /**
     *
     * @param aClass
     * @return 傳回類對象是否支援校驗
     */
    @Override
    public boolean supports(Class<?> aClass) {
        return  FilterInvocation.class.isAssignableFrom(aClass) ;
    }
}      

當一個請求走完FilterlnvocationSecurityMetadataSource的getAttributes方法之後,會

來到AccessDecisionManager類中進行角色資訊的比對,自定義AccessDecisionManager類如下:

@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {

    /**
     * 判斷目前登入的使用者是否具備目前請求 URL 所需要的角色資訊,如果不具備,就抛出AccessDeniedException異常
     * @param authentication
     * 參數1:目前登入使用者的資訊
     * @param o
     * 參數2:Filterlnvocation對象,可以取目前請求對象等
     * @param collection
     * 參數3:FilterlnvocationSecurityMetadataSource中getAttributes 方法的傳回值,即目前請求URL所需的角色
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        Collection <? extends GrantedAuthority> auths = authentication.getAuthorities();
        //角色資訊對比
        for(ConfigAttribute configAttribute:collection){
            //目前URL權限為ROLE_LOGIN,登入即可通路
            if ("ROLE_LOGIN".equals(configAttribute.getAttribute())&& authentication instanceof UsernamePasswordAuthenticationToken){
               return;
            }
            //全新判斷
            for (GrantedAuthority authority:auths){
                //目前使用者具備通路目前URL的權限
                if (configAttribute.getAttribute().equals(authority.getAuthority())){
                    return;
                }
            }
            //目前使用者不具備通路目前URL的權限,抛出異常
            throw new AccessDeniedException("權限不足");
        }
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}
      

  • 重寫configure(HttpSecurity http)方法,根據資料庫中的資料動态配置設定權限
  • 将寫的兩個類的執行個體設定進去
/**
     * 動态配置通路權限
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //定義FilterSecurityInterceptor将自定義的兩個類放進去
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setSecurityMetadataSource(cfisms());
                        o.setAccessDecisionManager(cadm());
                        return o;
                    }
                })
                .and()
                .formLogin()
                .loginProcessingUrl("/login").permitAll()
                .and()
                .csrf().disable() ;
    }

    @Bean
    CustomFilterinvocationSecurityMetadataSource cfisms(){
        return new CustomFilterinvocationSecurityMetadataSource () ;
    }

    @Bean
    CustomAccessDecisionManager cadm (){
        return  new CustomAccessDecisionManager();
    }      

重新啟動,和4是一樣的效果。