(二)基於數據庫的認證與授權

環境準備

controller

@RestController
@RequestMapping("/api/admin")
public class AdminController {

    @GetMapping("/hello")
    public String hello(){
        return "hello! this is admin page";
    }
}

/**----------------------分割線------------------------*/

@RestController
@RequestMapping("/api/user")
public class UserController {

    @GetMapping("/hello")
    public String hello(){
        return "hello! this is user page!";
    }
}

/**----------------------分割線------------------------*/

@RestController
@RequestMapping("/api/public")
public class PublicController {

    @GetMapping("/hello")
    public String hello(){
        return "hello! this is public page";
    }
}

資源權限配置

@EnableWebSecurity
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/api/user/**").hasAnyRole("user")  //user 角色訪問/api/user/開頭的路由
                .antMatchers("/api/admin/**").hasAnyRole("admin") //admin角色訪問/api/admin/開頭的路由
                .antMatchers("/api/public/**").permitAll()                 //允許所有可以訪問/api/public/開頭的路由
                .and()
            .formLogin();
    }
}

antMatchers()是一個採用ANT模式的URL匹配器:

  • * 表示匹配0或任意數量的字符
  • ** 表示匹配0或者更多的目錄。antMatchers("/admin/api/**")相當於匹配了/admin/api/下的所有API。

配置默認用戶,密碼和角色信息

#默認登錄用戶
spring.security.user.name=user
#默認user用戶密碼
spring.security.user.password=user
#默認user用戶所屬角色
spring.security.user.roles=user

啓動服務進行驗證

直接方訪問localhost:8080/api/public/hello,沒問題,正常訪問:
在這裏插入圖片描述
訪問localhost:8080/api/user/hello,跳轉到了登錄驗證頁面:
在這裏插入圖片描述
正確使用user用戶進行登錄後,也能夠正常訪問:
在這裏插入圖片描述
訪問localhost:8080/api/admin/hello,跳轉到了登錄驗證頁面:

此時正確使用user用戶進行登錄後,發現提示了403
在這裏插入圖片描述

頁面顯示403錯誤,表示該用戶授權失敗,401代表該用戶認證失敗;本次訪問已經通過了認證環節,只是在授權的時候被駁回了。

基於內存的多用戶支持

到目前爲止,我們仍然只有一個可登錄的用戶,怎樣引入多用戶呢?非常簡單,我們只需實現一個自定義的UserDetailsService即可:

    @Bean
    public UserDetailsService userDetailsService(){
        UserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        //創建用戶user01,密碼user01,角色user
        userDetailsManager.createUser(User.withUsername("user01").password("user01").roles("user").build());
        //創建用戶admin01,密碼admin01,角色admin
        userDetailsManager.createUser(User.withUsername("admin01").password("admin01").roles("admin").build());
        return userDetailsManager;
    }

其中InMemoryUserDetailsManagerUserDetailManager的實現類,它將用戶數據源寄存在內存裏,在一些不需要引入數據庫這種重數據源的系統中很有幫助。
UserDetailManager 繼承了 UserDetailService;

重啓服務,使用新創建的用戶admin01去訪問localhost:8080/api/admin/hello,我們可以發現,依舊未能夠正確認證並授權:
在這裏插入圖片描述

這塊兒在陳木鑫老師的書中是已經能夠正確認證並授權通過了,現在爲什麼沒有成功呢?
因爲Spring security 5.0中新增了多種加密方式,也改變了密碼的格式。詳見:https://blog.csdn.net/canon_in_d_major/article/details/79675033

我們這裏針對性對userDetailsService進行修改,指明使用默認的passwordEncoder

    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();

        //創建用戶user01,密碼user01,角色user
        userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
        //創建用戶admin01,密碼admin01,角色admin
        userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());

        return userDetailsManager;
    }

這樣我們就可以正確的認證訪問了:
在這裏插入圖片描述

基於默認數據庫模型的認證與授權

除了InMemoryUserDetailsManager,Spring Security還提供另一個UserDetailsService實現類:JdbcUserDetailsManager
JdbcUserDetailsManager幫助我們以JDBC的方式對接數據庫和Spring Security。

JdbcUserDetailsManager設定了一個默認的數據庫模型,只要遵從這個模型,在簡便性上,JdbcUserDetailsManager甚至可以媲美InMemoryUserDetailsManager

準備數據庫

我這裏使用的是PostgreSQL數據庫,在這塊兒使用其他數據庫(例如MySQL)都是一樣的,這塊兒看個人愛好吧。

(1)在工程中引入jdbcPostgreSQL兩個必要依賴:

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

(2)在application.properties配置文件中配置數據庫連接信息:

#數據庫連接信息
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/springsecuritydemo?schema=public
spring.datasource.data-username=postgres
spring.datasource.data-password=aaaaaa

(3)預製表結構和數據
JdbcUserDetailsManager設定了一個默認的數據庫模型,Spring Security將該模型定義在/org/springframework/security/core/userdetails/jdbc/users.ddl內:

create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null);
create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);

JdbcUserDetailsManager需要兩個表,其中users表用來存放用戶名、密碼和是否可用三個信息,authorities表用來存放用戶名及其權限的對應關係。

我們現在使用sql創建這兩張表,在Pg數據庫中執行上面的語句,我們發現以下錯誤:
在這裏插入圖片描述
因爲該語句是用hsqldb創建的,而PostgreSQL不支持
varchar_ignorecase這種類型。怎麼辦呢?很簡單,將varchar_ignorecase改爲PostgreSQL支持的varchar即可:

create table users(username varchar(50) not null primary key,password varchar(500) not null,enabled boolean not null);
create table authorities (username varchar(50) not null,authority varchar(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
配置完成後,重啓環境,正常啓動。

編碼實現

下面我們修改一下userDetailService bean,使用JdbcUserDetailsManager實現,讓Spring Security使用數據庫來管理用戶:

    @Bean
    public UserDetailsService userDetailsService(DataSource dataSource){
        JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager();
        userDetailsManager.setDataSource(dataSource);

        //創建用戶user01,密碼user01,角色user
        userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
        //創建用戶admin01,密碼admin01,角色admin
        userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());

        return userDetailsManager;
    }

JdbcUserDetailsManagerInMemoryUserDetailsManager在用法上沒有太大區別,只是多了設置DataSource的環節。Spring Security 通過DataSource執行設定好的命令。例如,此處的createUser函數實際上就是執行了下面的SQL語句:

insert into users (username,password,enabled) values(?,?,?)

查看 JdbcUserDetailsManager 的源代碼可以看到更多定義好的 SQL 語句,諸如deleteUserSqlupdateUserSql等,這些都是JdbcUserDetailsManager與數據庫實際交互的形式。當然,JdbcUserDetailsManager 也允許我們在特殊情況下自定義這些 SQL 語句,如有必要,調用對應的setXxxSql方法即可。
在這裏插入圖片描述
現在重啓服務,我們發現看看Spring Security在數據庫中生成了下面這些數據:
users表:
在這裏插入圖片描述
authorities表:
在這裏插入圖片描述
重啓服務後,使用user01用戶和admin01用戶都能夠正常合理訪問接口,與預期的行爲一致。

到目前爲止,一切都工作得很好,但是隻要我們重啓服務,應用就會報錯。這是因爲users表在創建語句時,username字段爲主鍵,主鍵是唯一不重複的,但重啓服務後會再次創建admin和user,導致數據庫報錯(在內存數據源上不會出現這種問題,因爲重啓服務後會清空username字段中的內容)。
所以如果需要在服務啓動時便生成部分用戶,那麼建議先判斷用戶名是否存在。

    @Bean
    public UserDetailsService userDetailsService(DataSource dataSource){
        JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager();
        userDetailsManager.setDataSource(dataSource);

        //創建用戶user01,密碼user01,角色user
        if (!userDetailsManager.userExists("user01")) { //判斷user01是否存在
            userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
        }
        //創建用戶admin01,密碼admin01,角色admin
        if (!userDetailsManager.userExists("admin01")) {//判斷admin01是否存在
            userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());
        }
        return userDetailsManager;
    }

補充

WebSecurityConfigurer Adapter定義了三個configure:

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        this.disableLocalConfigureAuthenticationBldr = true;
    }
    public void configure(WebSecurity web) throws Exception {
    }

    protected void configure(HttpSecurity http) throws Exception {
        this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
        ((HttpSecurity)((HttpSecurity)((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated().and()).formLogin().and()).httpBasic();
    }

我們只用到了一個參數,用來接收 HttpSecurity 對象的配置方法。另外兩個參數也有各自的用途,其中,AuthenticationManagerBuilder的configure同樣允許我們配置認證用戶:

@EnableWebSecurity
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()
                .antMatchers("/api/user/**").hasAnyRole("user")  //user 角色訪問/api/user/開頭的路由
                .antMatchers("/api/admin/**").hasAnyRole("admin") //admin 角色訪問/api/admin/開頭的路由
                .antMatchers("/api/public/**").permitAll()                 //允許所有可以訪問/api/public/開頭的路由
                .and()
            .formLogin();
    }

//    @Bean
//    public UserDetailsService userDetailsService(){
//        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
//
//        //創建用戶user01,密碼user01,角色user
//        userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
//        //創建用戶admin01,密碼admin01,角色admin
//        userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());
//
//        return userDetailsManager;
//    }

//    @Bean
//    public UserDetailsService userDetailsService(DataSource dataSource){
//        JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager();
//        userDetailsManager.setDataSource(dataSource);
//
//        //創建用戶user01,密碼user01,角色user
//        if (!userDetailsManager.userExists("user01")) { //判斷user01是否存在
//            userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
//        }
//        //創建用戶admin01,密碼admin01,角色admin
//        if (!userDetailsManager.userExists("admin01")) {//判斷admin01是否存在
//            userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());
//        }
//        return userDetailsManager;
//    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
                .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("user1")
                .password(new BCryptPasswordEncoder().encode("user01"))
                .roles("user")
                .and()
                .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("admin01")
                .password(new BCryptPasswordEncoder().encode("admin01"))
                .roles("admin");
    }
}

自定義數據庫模型的認證於授權

InMemoryUserDetailsManagerJdbcUserDetailsManager兩個類都是UserDetailsService的實現類,自定義數據庫結構實際上也僅需實現一個自定義的UserDetailsService
UserDetailsService僅定義了一個loadUserByUsername方法,用於獲取一個UserDetails對象。UserDetails對象包含了一系列在驗證時會用到的信息,包括用戶名、密碼、權限以及其他信息,Spring Security 會根據這些信息判定驗證是否成功。

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

也就是說,不管數據庫結構如何變化,只要能構造一個UserDetails即可。

自定義實現UserDetail

1.編寫實體User實現UserDetail

public class User implements UserDetails {
    private Long id;
    private String userName;
    private String password;
    private Boolean enable;
    private String roles;
    private List<GrantedAuthority> authentications;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getRoles() {
        return roles;
    }

    public void setRoles(String roles) {
        this.roles = roles;
    }

    public Boolean getEnable() {
        return enable;
    }

    public void setEnable(Boolean enable) {
        this.enable = enable;
    }

    public List<GrantedAuthority> getAuthentications() {
        return authentications;
    }

    public void setAuthentications(List<GrantedAuthority> authentications) {
        this.authentications = authentications;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.getAuthentications();
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.userName;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return this.enable;
    }
}

實現UserDetails定義的幾個方法:

  • isAccountNonExpiredisAccountNonLockedisCredentialsNonExpired 暫且用不到,統一返回rue,否則Spring Security會認爲賬號異常。
  • isEnabled對應enable字段,將其代入即可。
  • getAuthorities方法本身對應的是roles字段,但由於結構不一致,所以此處新建一個,並在後續進行填充。

2.數據庫持久層

這裏使用JPA 實現實體關係型映射,建立實體與數據庫的關係:

(1)需要引入spring-boot-stater-data-jpa依賴

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

(2)更改User爲數據庫映射實體類:

@Entity
@Table
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    private String userName;
    private String password;
    private Boolean enable;
    private String roles;
    
    @Transient
    private List<GrantedAuthority> authentications;
	....
	....

(3)新建UserRepository

public interface UserRepository extends JpaRepository<User,Long> {
    User findByUserName(String userName);
}

自定義實現UserDetailsService

@Service
public class MyUserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //(1)從數據庫獲取用戶
        User user = userRepository.findByUserName(username);
        if (user==null)//用戶不存在
            throw new RuntimeException("用戶"+username+"不存在!");
        //(2)將數據庫中的roles解析爲UserDetail的權限集
        String roles = user.getRoles();
        List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(roles);
        user.setAuthentications(grantedAuthorities);
        return user;
    }
}

AuthorityUtils.commaSeparatedStringToAuthorityList(String list) 是spring security 提供的將逗號隔開的權限集字符串切割爲權限對象列表,當然上面代碼中我們也可以自己實現來代替:

    List<GrantedAuthority> getGrantedAuthorities(String roles){
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        String[] split = StringUtils.split(roles, ";");
        for (int i = 0;i<split.length;i++){
            if (!StringUtils.isEmpty(split[i])){
                SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority(split[i]);
                grantedAuthorities.add(grantedAuthority);
            }
        }
        return grantedAuthorities;
    }

至此,我們就實現了Spring Security的自定義數據庫結構認證工程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章