看完這一篇,你就對Spring Security略窺門徑了(SpringBoot實現默認、內存、數據庫)

寫在前面

開發Web應用,對頁面的安全控制通常是必須的。比如:對於沒有訪問權限的用戶需要轉到登錄表單頁面。要實現訪問控制的方法多種多樣,可以通過Aop、攔截器實現,也可以通過框架實現,例如:Apache Shiro、Spring Security。我們這裏要講的Spring Security 就是一個Spring生態中關於安全方面的框架。它能夠爲基於Spring的企業應用系統提供聲明式的安全訪問控制解決方案。

默認認證用戶名密碼

項目pom.xml添加spring-boot-starter-security依賴

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

重啓你的應用。再次打開頁面,你講看到一個登錄頁面

在這裏插入圖片描述
既然跳到了登錄頁面,那麼這個時候我們就會想,這個登錄的用戶名以及密碼是什麼呢?讓我們來從SpringBoot源碼尋找一下。你搜一下輸出日誌,會看到下面一段輸出:
在這裏插入圖片描述
這段日誌是UserDetailsServiceAutoConfiguration類裏面的如下方法輸出的:
在這裏插入圖片描述
通過上面的這個類,我們可以看出,是SecurityProperties這個Bean管理了用戶名和密碼。在SecurityProperties裏面的一個內部靜態類User類裏面,管理了默認的認證的用戶名與密碼。代碼如下

@ConfigurationProperties(
    prefix = "spring.security"
)
public class SecurityProperties {
    public static final int BASIC_AUTH_ORDER = 2147483642;
    public static final int IGNORED_ORDER = -2147483648;
    public static final int DEFAULT_FILTER_ORDER = -100;
    private final SecurityProperties.Filter filter = new SecurityProperties.Filter();
    private SecurityProperties.User user = new SecurityProperties.User();

    public SecurityProperties() {
    }

    public SecurityProperties.User getUser() {
        return this.user;
    }

    public SecurityProperties.Filter getFilter() {
        return this.filter;
    }

    public static class User {
        private String name = "user";
        private String password = UUID.randomUUID().toString();
        private List<String> roles = new ArrayList();
        private boolean passwordGenerated = true;

        public User() {
        }

        public String getName() {
            return this.name;
        }

        public void setName(String name) {
            this.name = name;
        }

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

        public void setPassword(String password) {
            if (StringUtils.hasLength(password)) {
                this.passwordGenerated = false;
                this.password = password;
            }
        }

        public List<String> getRoles() {
            return this.roles;
        }

        public void setRoles(List<String> roles) {
            this.roles = new ArrayList(roles);
        }

        public boolean isPasswordGenerated() {
            return this.passwordGenerated;
        }
    }

    public static class Filter {
        private int order = -100;
        private Set<DispatcherType> dispatcherTypes;

        public Filter() {
            this.dispatcherTypes = new HashSet(Arrays.asList(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST));
        }

        public int getOrder() {
            return this.order;
        }

        public void setOrder(int order) {
            this.order = order;
        }

        public Set<DispatcherType> getDispatcherTypes() {
            return this.dispatcherTypes;
        }

        public void setDispatcherTypes(Set<DispatcherType> dispatcherTypes) {
            this.dispatcherTypes = dispatcherTypes;
        }
    }
}

綜上所述,security默認的用戶名是user, 默認密碼是應用啓動的時候,通過UUID算法隨機生成的,默認的role是"USER"。當然,如果我們想簡單改一下這個用戶名密碼,可以在application.properties配置你的用戶名密碼,例如
在這裏插入圖片描述
當然這只是一個初級的配置,更復雜的配置,可以分不用角色,在控制範圍上,能夠攔截到方法級別的權限控制。

內存用戶名密碼認證

在上面的內容,我們什麼都沒做,就添加了spring-boot-starter-security依賴,整個應用就有了默認的認證安全機制。下面,我們來定製用戶名密碼。寫一個繼承了 WebSecurityConfigurerAdapter的配置類,具體內容如下

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("admin")
                .password(new BCryptPasswordEncoder().encode("1234567"))
                .roles("USER");
    }
}

這裏對上面的代碼進行簡要說明

  • Spring security 5.0中新增了多種加密方式,也改變了默認的密碼格式。需要修改一下configure中的代碼,我們要將前端傳過來的密碼進行某種方式加密,Spring Security 官方推薦的是使用bcrypt加密方式。inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()),這相當於登陸時用BCrypt加密方式對用戶密碼進行處理。以前的".password("123")" 變成了 “.password(new BCryptPasswordEncoder().encode("123"))”,這相當於對內存中的密碼進行Bcrypt編碼加密。如果比對時一致,說明密碼正確,才允許登陸。

  • 通過 @EnableWebSecurity註解開啓Spring Security的功能。使用@EnableGlobalMethodSecurity(prePostEnabled = true)這個註解,可以開啓security的註解,我們可以在需要控制權限的方法上面使用@PreAuthorize@PreFilter這些註解。

  • 繼承 WebSecurityConfigurerAdapter 類,並重寫它的方法來設置一些web安全的細節。我們結合@EnableWebSecurity註解和繼承WebSecurityConfigurerAdapter,來給我們的系統加上基於web的安全機制。

  • configure(HttpSecurity http)方法裏面,我們進入到源碼中,就會看到默認的認證代碼是:

在這裏插入圖片描述
從方法名我們基本可以看懂這些方法的功能。上面的那個默認的登錄頁面,就是SpringBoot默認的用戶名密碼認證的login頁面。我們使用SpringBoot默認的配置super.configure(http),它通過 authorizeRequests() 定義哪些URL需要被保護、哪些不需要被保護。默認配置是所有訪問頁面都需要認證,纔可以訪問。

  • 通過 formLogin() 定義當需要用戶登錄時候,轉到的登錄頁面。

  • configureGlobal(AuthenticationManagerBuilder auth) 方法,在內存中創建了一個用戶,該用戶的名稱爲root,密碼爲root,用戶角色爲USER。這個默認的登錄頁面是怎麼冒出來的呢?是的,SpringBoot內置的,SpringBoot甚至給我們做好了一個極簡的登錄頁面。這個登錄頁面是通過Filter實現的。具體的實現類是org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter。同時,這個DefaultLoginPageGeneratingFilter也是SpringBoot的默認內置的Filter。

輸入用戶名,密碼,點擊Login。不過,我們發現,SpringBoot應用的啓動日誌還是打印瞭如下一段:
在這裏插入圖片描述
但實際上,已經使用了我們定製的用戶名密碼了。如果我們要配置多個用戶,多個角色,可參考使用如下示例的代碼:

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("admin")
                .password(new BCryptPasswordEncoder().encode("1234567"))
                .roles("USER")
                .and()
                .withUser("admin1")
                .password(new BCryptPasswordEncoder().encode("123"))
                .roles("ADMIN", "USER");
    }

角色權限控制

當我們的系統功能模塊當需求發展到一定程度時,會不同的用戶,不同角色使用我們的系統。這樣就要求我們的系統可以做到,能夠對不同的系統功能模塊,開放給對應的擁有其訪問權限的用戶使用。Spring Security提供了Spring EL表達式,允許我們在定義URL路徑訪問(@RequestMapping)的方法上面添加註解,來控制訪問權限。在標註訪問權限時,根據對應的表達式返回結果,控制訪問權限:

true,表示有權限
fasle,表示無權限

Spring Security可用表達式對象的基類是SecurityExpressionRoot。

public abstract class SecurityExpressionRoot implements SecurityExpressionOperations {
    protected final Authentication authentication;
    private AuthenticationTrustResolver trustResolver;
    private RoleHierarchy roleHierarchy;
    private Set<String> roles;
    private String defaultRolePrefix = "ROLE_";
    public final boolean permitAll = true;
    public final boolean denyAll = false;
    private PermissionEvaluator permissionEvaluator;
    public final String read = "read";
    public final String write = "write";
    public final String create = "create";
    public final String delete = "delete";
    public final String admin = "administration";

    public SecurityExpressionRoot(Authentication authentication) {
        if (authentication == null) {
            throw new IllegalArgumentException("Authentication object cannot be null");
        } else {
            this.authentication = authentication;
        }
    }

    public final boolean hasAuthority(String authority) {
        return this.hasAnyAuthority(authority);
    }

    public final boolean hasAnyAuthority(String... authorities) {
        return this.hasAnyAuthorityName((String)null, authorities);
    }

    public final boolean hasRole(String role) {
        return this.hasAnyRole(role);
    }

    public final boolean hasAnyRole(String... roles) {
        return this.hasAnyAuthorityName(this.defaultRolePrefix, roles);
    }

    private boolean hasAnyAuthorityName(String prefix, String... roles) {
        Set<String> roleSet = this.getAuthoritySet();
        String[] var4 = roles;
        int var5 = roles.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            String role = var4[var6];
            String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
            if (roleSet.contains(defaultedRole)) {
                return true;
            }
        }

        return false;
    }

    public final Authentication getAuthentication() {
        return this.authentication;
    }

    public final boolean permitAll() {
        return true;
    }

    public final boolean denyAll() {
        return false;
    }

    public final boolean isAnonymous() {
        return this.trustResolver.isAnonymous(this.authentication);
    }

    public final boolean isAuthenticated() {
        return !this.isAnonymous();
    }

    public final boolean isRememberMe() {
        return this.trustResolver.isRememberMe(this.authentication);
    }

    public final boolean isFullyAuthenticated() {
        return !this.trustResolver.isAnonymous(this.authentication) && !this.trustResolver.isRememberMe(this.authentication);
    }

    public Object getPrincipal() {
        return this.authentication.getPrincipal();
    }

    public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
        this.trustResolver = trustResolver;
    }

    public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
        this.roleHierarchy = roleHierarchy;
    }

    public void setDefaultRolePrefix(String defaultRolePrefix) {
        this.defaultRolePrefix = defaultRolePrefix;
    }

    private Set<String> getAuthoritySet() {
        if (this.roles == null) {
            Collection<? extends GrantedAuthority> userAuthorities = this.authentication.getAuthorities();
            if (this.roleHierarchy != null) {
                userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities);
            }

            this.roles = AuthorityUtils.authorityListToSet(userAuthorities);
        }

        return this.roles;
    }

    public boolean hasPermission(Object target, Object permission) {
        return this.permissionEvaluator.hasPermission(this.authentication, target, permission);
    }

    public boolean hasPermission(Object targetId, String targetType, Object permission) {
        return this.permissionEvaluator.hasPermission(this.authentication, (Serializable)targetId, targetType, permission);
    }

    public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) {
        this.permissionEvaluator = permissionEvaluator;
    }

    private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
        if (role == null) {
            return role;
        } else if (defaultRolePrefix != null && defaultRolePrefix.length() != 0) {
            return role.startsWith(defaultRolePrefix) ? role : defaultRolePrefix + role;
        } else {
            return role;
        }
    }
}

通過閱讀源碼,我們可以更加深刻的理解其EL寫法,並在寫代碼的時候正確的使用。變量defaultRolePrefix硬編碼約定了role的前綴是"ROLE_"。同時,我們可以看出hasRole跟hasAnyRole是一樣的。hasAnyRole是調用的hasAnyAuthorityName(defaultRolePrefix, roles)。所以,我們在學習一個框架或者一門技術的時候,最準確的就是源碼。通過源碼,我們可以更好更深入的理解技術的本質。

SecurityExpressionRoot爲我們提供的使用Spring EL表達式總結如下:

表達式 描述
hasRole([role]) 當前用戶是否擁有指定角色。
hasAnyRole([role1,role2]) 多個角色是一個以逗號進行分隔的字符串。如果當前用戶擁有指定角色中的任意一個則返回true。
hasAuthority([auth]) 等同於hasRole
hasAnyAuthority([auth1,auth2]) 等同於hasAnyRole
Principle 代表當前用戶的principle對象
authentication 直接從SecurityContext獲取的當前Authentication對象
permitAll 總是返回true,表示允許所有的
denyAll 總是返回false,表示拒絕所有的
isAnonymous() 當前用戶是否是一個匿名用戶
isRememberMe() 表示當前用戶是否是通過Remember-Me自動登錄的
isAuthenticated() 表示當前用戶是否已經登錄認證成功了。
isFullyAuthenticated() 如果當前用戶既不是一個匿名用戶,同時又不是通過Remember-Me自動登錄的,則返回true。

在Controller方法上添加@PreAuthorize這個註解,value="hasRole('ADMIN')")是Spring-EL expression,當表達式值爲true,標識這個方法可以被調用。如果表達式值是false,標識此方法無權限訪問。

在Spring Security裏面獲取當前登錄認證通過的用戶信息

如果我們想要在前端頁面顯示當前登錄的用戶怎麼辦呢?在在Spring Security裏面怎樣獲取當前登錄認證通過的用戶信息?下面我們就來探討這個問題。其實很好辦。我們添加一個LoginFilter,默認攔截所有請求,把當前登錄的用戶放到系統session中即可。在Spring Security中,用戶信息保存在SecurityContextHolder中。Spring Security使用一個Authentication對象來持有所有系統的安全認證相關的信息。這個信息的內容格式如下:

{
    "accountNonExpired":true,
    "accountNonLocked":true,
    "authorities":[{
        "authority":"ROLE_ADMIN"
    },{
        "authority":"ROLE_USER"
    }],
    "credentialsNonExpired":true,
    "enabled":true,
    "username":"root"
}

這個Authentication對象信息其實就是User實體的信息,類似如下(當然,密碼沒放進來)。

public class User implements UserDetails, CredentialsContainer {
    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;
        ....
}

我們可以使用下面的代碼(Java)獲得當前身份驗證的用戶的名稱:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
    String username = ((UserDetails)principal).getUsername();
} else {
    String username = principal.toString();
}

通過調用getContext()返回的對象是SecurityContext的實例對象,該實例對象保存在ThreadLocal線程本地存儲中。使用Spring Security框架,通常的認證機制都是返回UserDetails實例,通過如上這種方式,我們就可以拿到認證登錄的用戶信息。

用數據庫存儲用戶和角色,實現安全認證

很多時候,我們需要的是實現一個用數據庫存儲用戶和角色,實現系統的安全認證。爲了簡化講解,本例中在權限角色上,我們簡單設計兩個用戶角色:USER,ADMIN。我們設計頁面的權限如下:

  • 首頁/ : 所有人可訪問
  • 登錄頁 /login: 所有人可訪問
  • 普通用戶權限頁 /httpapi, /httpsuite: 登錄後的用戶都可訪問
  • 管理員權限頁 /httpreport : 僅管理員可訪問
  • 無權限提醒頁: 當一個用戶訪問了其沒有權限的頁面,我們使用全局統一的異常處理頁面提示。

配置Spring Security

我們首先使用Spring Security幫我們做登錄、登出的處理,以及當用戶未登錄時只能訪問: http://localhost:8080/ 以及 http://localhost:8080/login 兩個頁面。同樣的,我們要寫一個繼承WebSecurityConfigurerAdapter的配置類:

import com.springboot.in.action.service.LightSwordUserDetailService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

/**
 * Created by jack on 2017/4/27.
 */

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
//使用@EnableGlobalMethodSecurity(prePostEnabled = true)
// 這個註解,可以開啓security的註解,我們可以在需要控制權限的方法上面使用@PreAuthorize,@PreFilter這些註解。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    @Bean
    public UserDetailsService userDetailsService() { //覆蓋寫userDetailsService方法 (1)
        return new AdminUserDetailService();

    }

    /**
     * If subclassed this will potentially override subclass configure(HttpSecurity)
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //super.configure(http);
        http.csrf().disable();

        http.authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/amchart/**",
                "/bootstrap/**",
                "/build/**",
                "/css/**",
                "/dist/**",
                "/documentation/**",
                "/fonts/**",
                "/js/**",
                "/pages/**",
                "/plugins/**"
            ).permitAll() //默認不攔截靜態資源的url pattern (2)
            .anyRequest().authenticated().and()
            .formLogin().loginPage("/login")// 登錄url請求路徑 (3)
            .defaultSuccessUrl("/httpapi").permitAll().and() // 登錄成功跳轉路徑url(4)
            .logout().permitAll();

        http.logout().logoutSuccessUrl("/"); // 退出默認跳轉頁面 (5)

    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //AuthenticationManager使用我們的 Service來獲取用戶信息,Service可以自己寫,其實就是簡單的讀取數據庫的操作
        auth.userDetailsService(()); //6}

}

上面的代碼只做了基本的配置,其中:

  • 覆蓋寫userDetailsService方法,具體的AdminUserDetailsService實現類,就是之前說的獲取用戶信息的service層類。
  • 默認不攔截靜態資源的url pattern。我們也可以用下面的WebSecurity這個方式跳過靜態資源的認證。
public void configure(WebSecurity web) throws Exception {
    web
        .ignoring()
        .antMatchers("/resourcesDir/**");
}
  • 跳轉登錄頁面url請求路徑爲/login,我們需要定義一個Controller把路徑映射到login.html。
  • 登錄成功後跳轉的路徑爲/httpapi
  • 退出後跳轉到的url爲/
  • 認證鑑權信息的Bean,採用我們自定義的從數據庫中獲取用戶信息的AdminUserDetailService類。

我們同樣使用@EnableGlobalMethodSecurity(prePostEnabled = true)這個註解,開啓security的註解,這樣我們可以在需要控制權限的方法上面使用@PreAuthorize@PreFilter這些註解。

用戶退出

我們在configure(HttpSecurity http)方法裏面定義了任何權限都允許退出,當然SpringBoot集成Security的默認退出請求是/logout

http.logout().logoutSuccessUrl("/"); // 退出默認跳轉頁面 (4)

配置錯誤處理頁面

訪問發生錯誤時,跳轉到系統統一異常處理頁面。我們首先添加一個GlobalExceptionHandlerAdvice,使用@ControllerAdvice註解:

import org.springframework.web.bind.annotation.{ControllerAdvice, ExceptionHandler}
import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.ModelAndView

/**
  * Created by jack on 2017/4/27.
  */
@ControllerAdvice
class GlobalExceptionHandlerAdvice {
  @ExceptionHandler(value = Exception.class)//表示捕捉到所有的異常,你也可以捕捉一個你自定義的異常
    public ModelAndView exception(Exception exception, WebRequest request){
        ModelAndView modelAndView = new ModelAndView("/error");
        modelAndView.addObject("errorMessage", exception.getMessage());
        modelAndView.addObject("stackTrace", exception.getStackTrace());
        return modelAndView;
    }
}

其中,@ExceptionHandler(value = Exception.class),表示捕捉到所有的異常,這裏你也可以捕捉一個你自定義的異常。比如說,針對安全認證的Exception,我們可以單獨定義處理。此處不再贅述。

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