【詳解】核心元件之UserDetailService

貓毛·波拿巴發表於2018-08-04

簡介

  • UserDetails => Spring Security基礎介面,包含某個使用者的賬號,密碼,許可權,狀態(是否鎖定)等資訊。只有getter方法。
  • Authentication => 認證物件,認證開始時建立,認證成功後儲存於SecurityContext
  • principal => 使用者資訊物件,是一個Object,通常可轉為UserDetails

UserDetails介面

用於表示一個principal,但是一般情況下是作為(你所使用的使用者資料庫)和(Spring Security 的安全上下文需要保留的資訊)之間的介面卡。

實際上就是相當於定義一個規範,Security這個框架不管你的應用時怎麼儲存使用者和許可權資訊的。只要你取出來的時候把它包裝成一個UserDetails物件給我用就可以了。

package org.springframework.security.core.userdetails;

import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;

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

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

UserDetails用來做什麼?為什麼還要帶上許可權集合?

如果我們不用認證框架,我們是怎麼手動實現登入認證的?

基本上就是根據前端提交上來的使用者名稱從資料庫中查詢這個賬號的資訊,然後比對密碼。再進一步,可能還會新增一個欄位來判斷,當前使用者是否已被鎖定。這個介面就是這麼用的。即把這些資訊取出來,然後包裝成一個物件交由框架去認證。

為什麼還要帶上許可權?

因為登入成功後也不是什麼都能訪問的,還要根據你所擁有的許可權進行判斷。有許可權你才能訪問特定的物件。Security框架是這樣設計的,即認證成功後,就把使用者資訊和擁有的許可權都儲存在SecurityContext中,當訪問受保護資源(某個物件/方法)的時候,就把許可權拿出來比對,看看是否滿足。

框架提供的UserDetails預設實現

UserDetails有一個預設實現(框架提供的),User。使用者可以從自己的資料庫中取出此使用者的賬號,密碼,以及相關許可權,然後用構造方法填充建立一個User物件即可。 
注:實現CredentialsContainer介面是為了在登入成功後,清除使用者資訊中的密碼。(登入成功後會將使用者資訊儲存在SecurityContext中)
public class User implements UserDetails, CredentialsContainer {
    private static final long serialVersionUID = 500L;
    private static final Log logger = LogFactory.getLog(User.class);
    private String password;
    private final String username;
    private final Set<GrantedAuthority> authorities;
    private final boolean accountNonExpired;
    private final boolean accountNonLocked;
    private final boolean credentialsNonExpired;
    private final boolean enabled;

    public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this(username, password, true, true, true, true, authorities);
    }

    public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        if (username != null && !"".equals(username) && password != null) {
            this.username = username;
            this.password = password;
            this.enabled = enabled;
            this.accountNonExpired = accountNonExpired;
            this.credentialsNonExpired = credentialsNonExpired;
            this.accountNonLocked = accountNonLocked;
            this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
        } else {
            throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
        }
    }
    //省略部分程式碼
}

什麼時候提供UserDetails資訊,怎麼提供?

UserDetailsService介面

那肯定是認證的時候。其實認證的操作,框架都已經幫你實現了,它所需要的只是,你給我提供獲取資訊的方式。所以它就定義一個介面,然後讓你去實現,實現好了之後再注入給它。

框架提供一個UserDetailsService介面用來載入使用者資訊。如果要自定義實現的話,使用者可以實現一個CustomUserDetailsService的類,然後把你的應用中的UserService和AuthorityService注入到這個類中,使用者獲取使用者資訊和許可權資訊,然後在loadUserByUsername方法中,構造一個User物件(框架的類)返回即可。

package org.springframework.security.core.userdetails;

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

框架提供的UserDetailsService介面預設實現

  • InMemoryDaoImpl => 儲存於記憶體
  • JdbcDaoImpl => 儲存於資料庫(磁碟)

其中,如果你的資料庫設計符合JdbcDaoImpl中的規範,你也就不用自己去實現UserDetailsService了。但是大多數情況是不符合的,因為你使用者表不一定就叫users,可能還有其他字首什麼的,比如叫tb_users。或者欄位名也跟它不一樣。如果你一定要使用這個JdbcDaoImpl,你可以通過它的setter方法修改它的資料庫查詢語句。

注:它是利用Spring框架的JdbcTemplate來查詢資料庫的

public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService, MessageSourceAware {
    public static final String DEF_USERS_BY_USERNAME_QUERY = "select username,password,enabled from users where username = ?";
    public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY = "select username,authority from authorities where username = ?";
    public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY = "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id";
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private String authoritiesByUsernameQuery = "select username,authority from authorities where username = ?";
    private String groupAuthoritiesByUsernameQuery = "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id";
    private String usersByUsernameQuery = "select username,password,enabled from users where username = ?";
    private String rolePrefix = "";
    private boolean usernameBasedPrimaryKey = true;
    private boolean enableAuthorities = true;
    private boolean enableGroups;
    //省略方法
}

注入到哪裡去呢? 

那肯定是注入到認證處理類中的,框架利用AuthenticationManager(介面)來進行認證。而Security為了支援多種方式認證,它提供ProviderManager類,這個實現了AuthenticationManager介面。它擁有多種認證方式,可以根據認證的型別委託給對應的認證處理類進行處理,這個處理類實現了AuthenticationProvider介面。

所以,最終UserDetailsService是注入到AuthenticationProvider的實現類中。

誤解

1.UserDetailService 負責認證使用者
    實際上:UserDetailService只單純地負責存取使用者資訊,除了給框架內的其他元件提供資料外沒有其他功能。而認證過程是由AuthenticationManager來完成的。(大多數情況下,可以通過實現AuthenticationProvider介面來自定義認證過程
 
 

相關文章