安全是咱們開發中一直須要考慮的問題,例如作身份認證、權限限制等等。市面上比較常見的安全框架有:html
shiro比較簡單,容易上手。而spring security功能比較強大,可是也比較難以掌握。springboot集成了spring security,咱們此次來學習spring security的使用。java
應用程序的兩個主要區域是「認證」和「受權」(或者訪問控制)。這兩個主要區域是Spring Security 的兩個目標。git
「認證」(Authentication),是創建一個他聲明的主體的過程(一個「主體」通常是指用戶,設備或一些能夠在你的應用程序中執行動做的其餘系統)。github
「受權」(Authorization)指肯定一個主體是否容許在你的應用程序執行一個動做的過程。爲了抵達須要受權的店,主體的身份已經有認證過程創建。web
這個概念是通用的而不僅在Spring Security中。spring
Spring Security是針對Spring項目的安全框架,也是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-thymeleaf</artifactId> </dependency>
++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安全
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"; } }
<!--<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版本的適用文檔。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
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"); } }
能夠看到,咱們定製了以下的訪問規則:
假設認證用戶只有這三種角色類型的話,那麼super擁有最高的訪問權限,admin次之,而user最小。
別忘了爲該配置類添加@EnableWebSecurity註解. 接下來咱們啓動項目,訪問/能夠正常訪問,可是咱們訪問/user/user1等以後會報錯:
... There was an unexpected error (type=Forbidden, status=403). Access Denied
權限禁止,達成了咱們的目的。
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> * @Configuration * @EnableWebSecurity * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter { * * @Override * protected void configure(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin(); * } * * @Override * protected void configure(AuthenticationManagerBuilder auth) throws Exception { * auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); * } * } * </pre> * * The configuration below demonstrates customizing the defaults. * * <pre> * @Configuration * @EnableWebSecurity * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter { * * @Override * protected void configure(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin() * .usernameParameter("username") // default is username * .passwordParameter("password") // default is password * .loginPage("/authentication/login") // default is /login with an HTTP get * .failureUrl("/authentication/login?failed") // default is /login?error * .loginProcessingUrl("/authentication/login/process"); // default is /login * // with an HTTP * // post * } * * @Override * protected void configure(AuthenticationManagerBuilder auth) throws Exception { * auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); * } * } * </pre> * * @see FormLoginConfigurer#loginPage(String) * * @return the {@link FormLoginConfigurer} for further customizations * @throws Exception */ public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception { return getOrApply(new FormLoginConfigurer<>()); }
註釋已經說明了一切,咱們能夠總結一下:
// 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();
接下來,咱們能夠自定義用戶的認證過程,但爲了演示方便,咱們就使用內存帳戶進行認證了。
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/**
/admin/**
以及/user/**
;若是你使用的是高版本(5.X)的spring security 可能會遇到java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
這樣的錯誤,解決方案就是使用密碼編碼器,即PasswordEncorder實例,所以,咱們須要在此處提供PasswordEncorder類型的bean passwordEncorder,spring security提供了很多實例供咱們使用,能夠本身去查看,例如BCryptPasswordEncoder
等等,不過要注意有很多已經標註了@Deprecated
,好比LdapShaPasswordEncoder
、StandardPasswordEncoder
,使用以前參考一下源代碼好些。我這裏爲了偷懶,本身用了一個明文驗證的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> * @Configuration * @EnableWebSecurity * public class LogoutSecurityConfig extends WebSecurityConfigurerAdapter { * * @Override * protected void configure(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin() * .and() * // sample logout customization * .logout().deleteCookies("remove").invalidateHttpSession(false) * .logoutUrl("/custom-logout").logoutSuccessUrl("/logout-success"); * } * * @Override * protected void configure(AuthenticationManagerBuilder auth) throws Exception { * auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); * } * } * </pre> * * @return the {@link LogoutConfigurer} for further customizations * @throws Exception */ public LogoutConfigurer<HttpSecurity> logout() throws Exception { return getOrApply(new LogoutConfigurer<>()); }
很是詳細,咱們大體能夠了解到:
咱們給首頁添加一個退出按鈕:
<!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和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菜單欄,會發現裏面有兩個數據
有效時間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>
也就說,一旦咱們定製了登陸頁面,那麼其餘的規則也會受到影響,大體就是
固然,咱們也能夠修改處理登陸頁面的地址:
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>
而後測試其效果,應該和默認的登陸界面是如出一轍的。