【Spring Boot】25.安全

簡介

安全是咱們開發中一直須要考慮的問題,例如作身份認證、權限限制等等。市面上比較常見的安全框架有:html

  • shiro
  • spring security

shiro比較簡單,容易上手。而spring security功能比較強大,可是也比較難以掌握。springboot集成了spring security,咱們此次來學習spring security的使用。java

spring security

應用程序的兩個主要區域是「認證」和「受權」(或者訪問控制)。這兩個主要區域是Spring Security 的兩個目標。git

  • 「認證」(Authentication),是創建一個他聲明的主體的過程(一個「主體」通常是指用戶,設備或一些能夠在你的應用程序中執行動做的其餘系統)。github

  • 「受權」(Authorization)指肯定一個主體是否容許在你的應用程序執行一個動做的過程。爲了抵達須要受權的店,主體的身份已經有認證過程創建。web

這個概念是通用的而不僅在Spring Security中。spring

Spring Security是針對Spring項目的安全框架,也是Spring Boot底層安全模塊默認的技術選型。他能夠實現強大的web安全控制。對於安全控制,咱們僅需引入spring-boot-starter-security模塊,進行少許的配置,便可實現強大的安全管理。須要注意幾個類:數據庫

  • WebSecurityConfigurerAdapter:自定義Security策略
  • AuthenticationManagerBuilder:自定義認證策略
  • @EnableWebSecurity:開啓WebSecurity模式

測試使用

搭建基本測試環境

  1. 引入thymeleaf和security場景啓動器
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
  1. 編寫幾個簡單的html頁面,咱們將其分別放在不一樣的模板文件夾子目錄user,admin以及super中,預備給咱們的3種不一樣的角色訪問適用。
++template
----index.html
++++user
------user1.html
------user2.html
------user3.html
++++admin
------admin1.html
------admin2.html
------admin3.html
++++super
------super1.html
------super2.html
------super3.html

每一個html都寫一點簡單的內容,相似於this is xxxx.html。例如admin/admin3.html的內容以下:瀏覽器

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>admin-3</title>
</head>
<body>
this is admin-3 file.
</body>
</html>

爲了方便查看,你也能夠將title標籤體內容修改成一致的名稱,如admin-3安全

  1. 編寫controller,對咱們的訪問路徑進行映射:
package com.example.dweb.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {
    @GetMapping("/")
    public String user1(){
        return "/index";
    }

    @GetMapping("/user/user1")
    public String user1(){
        return "/user/user1";
    }
    @GetMapping("/user/user2")
    public String user2(){
        return "/user/user2";
    }
    @GetMapping("/user/user3")
    public String user3(){
        return "/user/user3";
    }


    @GetMapping("/admin/admin1")
    public String admin1(){
        return "/admin/admin1";
    }
    @GetMapping("/admin/admin2")
    public String admin2(){
        return "/admin/admin2";
    }
    @GetMapping("/admin/admin3")
    public String admin3(){
        return "/admin/admin3";
    }


    @GetMapping("/super/super1")
    public String super1(){
        return "/super/super1";
    }
    @GetMapping("/super/super2")
    public String super2(){
        return "/super/super2";
    }
    @GetMapping("/super/super3")
    public String super3(){
        return "/super/super3";
    }
}
  1. 運行項目,測試咱們對各個頁面的訪問是否正常。在運行項目以前,先在pom文件中將spring-security場景啓動器刪除,避免security對咱們進行訪問攔截:
<!--<dependency>-->
        <!--<groupId>org.springframework.boot</groupId>-->
        <!--<artifactId>spring-boot-starter-security</artifactId>-->
    <!--</dependency>

默認狀況下,springsecurity會生成登陸頁面要求用戶進行登陸,默認用戶名爲user,密碼爲啓動項目時控制檯info級別打印出的一串uuid,可查看源碼瞭解String password = UUID.randomUUID().toString()springboot

安全配置編寫

配置編寫過程能夠參考官方網站文檔:前往,以及springsecurity的官方文檔:前往

注意版本號,這裏是2.1.x版本的適用文檔。

  1. 中止項目,將咱們以前註釋掉的springsecutiry場景啓動器源碼還原.
<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
  1. 編寫配置類config/MySecurityConfig控制請求的訪問權限
package com.example.dweb.config;

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;

@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
       // super.configure(http);
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/user/**").hasAnyRole("user","admin","super")
                .antMatchers("/admin/**").hasAnyRole("admin","super")
                .antMatchers("/super/**").hasRole("super");
    }
}

能夠看到,咱們定製了以下的訪問規則:

  • 對於首頁,容許任何人訪問
  • /user/** 這樣的請求,只有角色爲user、admin以及super的人才能訪問;
  • /admin/** 這樣的請求,角色類型爲admin和super的能夠訪問
  • /super/** 這樣的請求,角色類型爲super的能夠訪問

假設認證用戶只有這三種角色類型的話,那麼super擁有最高的訪問權限,admin次之,而user最小。

別忘了爲該配置類添加@EnableWebSecurity註解. 接下來咱們啓動項目,訪問/能夠正常訪問,可是咱們訪問/user/user1等以後會報錯:

...
There was an unexpected error (type=Forbidden, status=403).
Access Denied

權限禁止,達成了咱們的目的。

登陸

  1. 接下來咱們經過用戶登陸,來實現對不一樣角色的訪問限制。咱們前面註釋掉了super.configure(http);這樣的代碼,這裏是默認的安全配置,其中就指定了默認的登陸界面。咱們能夠本身來開啓自動配置的登陸功能(http.formLogin();)。看其源碼:
/**
	 * Specifies to support form based authentication. If
	 * {@link FormLoginConfigurer#loginPage(String)} is not specified a default login page
	 * will be generated.
	 *
	 * <h2>Example Configurations</h2>
	 *
	 * The most basic configuration defaults to automatically generating a login page at
	 * the URL "/login", redirecting to "/login?error" for authentication failure. The
	 * details of the login page can be found on
	 * {@link FormLoginConfigurer#loginPage(String)}
	 *
	 * <pre>
	 * &#064;Configuration
	 * &#064;EnableWebSecurity
	 * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter {
	 *
	 * 	&#064;Override
	 * 	protected void configure(HttpSecurity http) throws Exception {
	 * 		http.authorizeRequests().antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;).and().formLogin();
	 * 	}
	 *
	 * 	&#064;Override
	 * 	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	 * 		auth.inMemoryAuthentication().withUser(&quot;user&quot;).password(&quot;password&quot;).roles(&quot;USER&quot;);
	 * 	}
	 * }
	 * </pre>
	 *
	 * The configuration below demonstrates customizing the defaults.
	 *
	 * <pre>
	 * &#064;Configuration
	 * &#064;EnableWebSecurity
	 * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter {
	 *
	 * 	&#064;Override
	 * 	protected void configure(HttpSecurity http) throws Exception {
	 * 		http.authorizeRequests().antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;).and().formLogin()
	 * 				.usernameParameter(&quot;username&quot;) // default is username
	 * 				.passwordParameter(&quot;password&quot;) // default is password
	 * 				.loginPage(&quot;/authentication/login&quot;) // default is /login with an HTTP get
	 * 				.failureUrl(&quot;/authentication/login?failed&quot;) // default is /login?error
	 * 				.loginProcessingUrl(&quot;/authentication/login/process&quot;); // default is /login
	 * 																		// with an HTTP
	 * 																		// post
	 * 	}
	 *
	 * 	&#064;Override
	 * 	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	 * 		auth.inMemoryAuthentication().withUser(&quot;user&quot;).password(&quot;password&quot;).roles(&quot;USER&quot;);
	 * 	}
	 * }
	 * </pre>
	 *
	 * @see FormLoginConfigurer#loginPage(String)
	 *
	 * @return the {@link FormLoginConfigurer} for further customizations
	 * @throws Exception
	 */
	public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
		return getOrApply(new FormLoginConfigurer<>());
	}

註釋已經說明了一切,咱們能夠總結一下:

  • /login 請求能夠來到登陸頁面;
  • 若登陸錯誤,會重定向到login?error;
  • 其餘的咱們能夠根據本身的需求查詢,還算是比較全面的;
  1. 如今咱們的配置類以下所示:
// super.configure(http);
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/user/**").hasAnyRole("user","admin","super")
                .antMatchers("/admin/**").hasAnyRole("admin","super")
                .antMatchers("/super/**").hasRole("super");

        // open auto login
        http.formLogin();
  1. 啓動項目,再次訪問受限的頁面如/user/user1,這一次,系統將會將頁面定向到登陸頁面了。如今系統提供的只是一個位於內存中默認用戶名爲user,密碼爲一串隨即UUID的帳戶。顯然這對於實際開發沒有什麼意義,所以咱們通常是鏈接數據庫等進行用戶數據對比的。

接下來,咱們能夠自定義用戶的認證過程,但爲了演示方便,咱們就使用內存帳戶進行認證了。

  1. 要定製自定義用戶認證過程
package com.example.dweb.config;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
       // super.configure(http);
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/user/**").hasAnyRole("user","admin","super")
                .antMatchers("/admin/**").hasAnyRole("admin","super")
                .antMatchers("/super/**").hasRole("super");

        // open auto login
        http.formLogin();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        super.configure(auth);
        String password = "123456";
        auth.inMemoryAuthentication()
                .withUser("user").password(password).roles("user")
                .and()
                .withUser("admin").password(password).roles("admin")
                .and()
                .withUser("super").password(password).roles("super");

    }

    // use my password encoder, it like original password.
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence rawPassword) {
                return rawPassword.toString();
            }

            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return encodedPassword.equals(rawPassword);
            }
        };
    }

}

咱們定義了3個帳戶,其用戶名和角色名一致,密碼都爲123456.

啓動項目,咱們試試測試效果。

能夠看到,訪問結果和咱們預期的同樣。

  • 若登陸用戶是user,則只能訪問/user/**
  • 若登陸用戶是admin,則能訪問/admin/**以及/user/**;
  • 若登陸帳戶是super,則能訪問全部頁面;

若是你使用的是高版本(5.X)的spring security 可能會遇到java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"這樣的錯誤,解決方案就是使用密碼編碼器,即PasswordEncorder實例,所以,咱們須要在此處提供PasswordEncorder類型的bean passwordEncorder,spring security提供了很多實例供咱們使用,能夠本身去查看,例如BCryptPasswordEncoder等等,不過要注意有很多已經標註了@Deprecated,好比LdapShaPasswordEncoderStandardPasswordEncoder,使用以前參考一下源代碼好些。我這裏爲了偷懶,本身用了一個明文驗證的PasswordEncoder。

NoOpPasswordEncoder也是一個spring security 提供的PasswordEncorder,可是請注意,他已經被標註@Deprecated,即不推薦使用的註解。因此若是須要明文驗證,本身定義一個PasswordEncoder的bean就能夠了。

註銷

註銷和登陸同樣,咱們須要先卡其自動配置的註銷功能:

@Override
    protected void configure(HttpSecurity http) throws Exception {
       // super.configure(http);
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/user/**").hasAnyRole("user","admin","super")
                .antMatchers("/admin/**").hasAnyRole("admin","super")
                .antMatchers("/super/**").hasRole("super");

        // open auto login function.
        http.formLogin();
        // open auto logout function.
        http.logout();
    }

查看該方法的源碼:

/**
	 * Provides logout support. This is automatically applied when using
	 * {@link WebSecurityConfigurerAdapter}. The default is that accessing the URL
	 * "/logout" will log the user out by invalidating the HTTP Session, cleaning up any
	 * {@link #rememberMe()} authentication that was configured, clearing the
	 * {@link SecurityContextHolder}, and then redirect to "/login?success".
	 *
	 * <h2>Example Custom Configuration</h2>
	 *
	 * The following customization to log out when the URL "/custom-logout" is invoked.
	 * Log out will remove the cookie named "remove", not invalidate the HttpSession,
	 * clear the SecurityContextHolder, and upon completion redirect to "/logout-success".
	 *
	 * <pre>
	 * &#064;Configuration
	 * &#064;EnableWebSecurity
	 * public class LogoutSecurityConfig extends WebSecurityConfigurerAdapter {
	 *
	 * 	&#064;Override
	 * 	protected void configure(HttpSecurity http) throws Exception {
	 * 		http.authorizeRequests().antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;).and().formLogin()
	 * 				.and()
	 * 				// sample logout customization
	 * 				.logout().deleteCookies(&quot;remove&quot;).invalidateHttpSession(false)
	 * 				.logoutUrl(&quot;/custom-logout&quot;).logoutSuccessUrl(&quot;/logout-success&quot;);
	 * 	}
	 *
	 * 	&#064;Override
	 * 	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	 * 		auth.inMemoryAuthentication().withUser(&quot;user&quot;).password(&quot;password&quot;).roles(&quot;USER&quot;);
	 * 	}
	 * }
	 * </pre>
	 *
	 * @return the {@link LogoutConfigurer} for further customizations
	 * @throws Exception
	 */
	public LogoutConfigurer<HttpSecurity> logout() throws Exception {
		return getOrApply(new LogoutConfigurer<>());
	}

很是詳細,咱們大體能夠了解到:

  1. 訪問/logout會清空session以及全部的認證信息。
  2. 註銷成功後會跳轉到頁面/login?success

咱們給首頁添加一個退出按鈕:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
this is index file.

<form th:action="@{/logout}" method="post">
<input type="submit" value="註銷" />
</form>
</body>
</html>

運行項目以後咱們登陸以後進入首頁,點擊退出按鈕,發現來到了登陸界面。

這是默認的,咱們也能夠定製退出頁面的位置,只需以下設置便可。

http.logout().logoutSuccessUrl("/");

點擊註銷感受仍是沒反應(實際上是刷新了一下),由於當前頁面和退出頁面是同樣的。

thymeleaf整合安全模塊

咱們有時候有不少需求須要和用戶的角色綁定,例如管理員會顯示一些多餘的菜單等等,有一種解決方案就是經過thymeleaf和spring security的整合模塊來完成。

<dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
            <version>3.0.4.RELEASE</version>
        </dependency>

官方文檔能夠查看地址:文檔

如今咱們來完善一下以前的註銷按鈕的問題,他應該出如今已登陸帳戶的頁面纔是,而不該該出如今未登陸的用戶的首頁。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
<div sec:authorize="isAuthenticated()">
    <h1><span sec:authentication="name"></span>,您好,您的角色是<span sec:authentication="principal.authorities"></span></h1>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="註銷" />
    </form>
</div>
<hr>
<div sec:authorize="!isAuthenticated()">
    你好,您當前未登陸,請預先<a th:href="@{/login}">登陸</a>
</div>

<hr>
this is index file.

</body>
</html>

注意sec:authorize以及sec:authentication的區別。

其實該插件基本都是操做一些內置對象,例如authentication等的,所以,有的地方也能夠用thymeleaf的基礎語法直接訪問。 例如如下兩端代碼輸出是同樣的:

<h1 th:text="${#authentication.getName()}"></h1>
<h1 sec:authentication="name"></h1>

記住我

記住我功能也是比較經常使用的登陸便利條款,開啓記住我只需這樣配置:

http.rememberMe();

如今咱們的配置類以下所示:

package com.example.dweb.config;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
       // super.configure(http);
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/user/**").hasAnyRole("user","admin","super")
                .antMatchers("/admin/**").hasAnyRole("admin","super")
                .antMatchers("/super/**").hasRole("super");

        // open auto login function.
        http.formLogin();
        // open auto logout function.
        http.logout().logoutSuccessUrl("/");
        // open remember me function.
        http.rememberMe();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        super.configure(auth);
        String password = "123456";
        auth.inMemoryAuthentication()
                .withUser("user").password(password).roles("user")
                .and()
                .withUser("admin").password(password).roles("admin")
                .and()
                .withUser("super").password(password).roles("super");

    }

    // use my password encoder, it like original password.
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence rawPassword) {
                return rawPassword.toString();
            }

            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return encodedPassword.equals(rawPassword);
            }
        };
    }

}

重啓項目以後來到登陸頁面,會發現自動爲咱們添加了帶有文本Remember me on this computer的複選框按鈕,經過勾選該按鈕以後登陸,即使咱們關掉瀏覽器,訪問咱們的網站的時候就無需再次登陸了,至關於記住了咱們的認證信息。

咱們來探究一下其實現原理: 打開瀏覽器的控制檯(我使用的是google),找到application選項卡的Cookies菜單欄,會發現裏面有兩個數據

  • JSESSIONID
  • remember-me

有效時間14天左右,瀏覽器經過remember-me和服務器進行交互,檢查以後就無需登陸了。

當咱們點擊註銷按鈕,則會刪除這個Cookie。

定製登錄頁

正常狀況下咱們確定是不能依靠springboot爲咱們提供的login頁面的,須要本身定製,定製方式也很簡單,在開啓登陸功能的地方定製便可。

// open auto login function.
    http.formLogin().loginPage("/login");

在模板目錄下放置一個登陸界面:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
<form method="post" action="@{/userLogin}">
    <input type="text" name="username" value="user"/>
    <input type="text" name="password" value="123456"/>
    <input type="submit" value="login">
</form>

</body>
</html>

IndexController中添加對login的映射:

@GetMapping("/login")
public String login(){
    return "/login";
}

默認狀況下post形式的/login表明處理登陸,默認證字段就是username和password,固然,你也能夠修改

// open auto login function.
http.formLogin().loginPage("/login").usernameParameter("username").passwordParameter("password");

留意loginPage的源碼註釋部分:

* If "/authenticate" was passed to this method it update the defaults as shown below:
	 *
	 * <ul>
	 * <li>/authenticate GET - the login form</li>
	 * <li>/authenticate POST - process the credentials and if valid authenticate the user
	 * </li>
	 * <li>/authenticate?error GET - redirect here for failed authentication attempts</li>
	 * <li>/authenticate?logout GET - redirect here after successfully logging out</li>
	 * </ul>

也就說,一旦咱們定製了登陸頁面,那麼其餘的規則也會受到影響,大體就是

  • /page Get 處理前往登陸頁面
  • /page post 處理登陸請求
  • /page?success GET 登錄成功請求
  • /page?error GET 登錄失敗請求

固然,咱們也能夠修改處理登陸頁面的地址:

http.formLogin().loginPage("/login").usernameParameter("username").passwordParameter("password").loginProcessingUrl("/login");

注意是post方式。

一樣的,rememberme也支持自定義配置。

// open remember me function.
http.rememberMe().rememberMeParameter("remember-me");

name咱們設置爲默認的就好,這樣就無需配置了,查看源碼能夠知道默認是什麼:

private static final String DEFAULT_REMEMBER_ME_NAME = "remember-me";

這樣,咱們在登錄頁面添加rememberme按鈕

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
<form method="post" th:action="@{/login}">
    <input type="text" name="username" value="user"/>
    <input type="text" name="password" value="123456"/>
    <input th:type="checkbox" name="remember-me"> 記住我
    <input type="submit" value="login">
</form>

</body>
</html>

而後測試其效果,應該和默認的登陸界面是如出一轍的。

相關文章
相關標籤/搜索