Spring Security 入門 (二)
我們在篇(一)中已經談到了預設的登入頁面以及預設的登入賬號和密碼。
在這一篇中我們將自己定義登入頁面及賬號密碼。
我們先從簡單的開始吧:設定自定義的賬號和密碼(並非從資料庫讀取),雖然意義不大。
上一篇中,我們僅僅重寫了 configure(HttpSecurity http) 方法,該方法是用於完成使用者授權的。
為了完成自定義的認證,我們需要重寫 configure(AuthenticationManagerBuilder auth) 方法。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO Auto-generated method stub auth.inMemoryAuthentication().withUser("Hello").password("{noop}World").roles("USER"); } @Override protected void configure(HttpSecurity http) throws Exception { // TODO Auto-generated method stub http.authorizeRequests() .antMatchers("/login").permitAll() .antMatchers("/user").hasRole("USER") .anyRequest().authenticated() .and() .formLogin().defaultSuccessUrl("/hello"); } }
這個就是新的 WebSecurityConfig 類,控制器裡面的方法我就不寫了,仿照(一)很容易寫出來,執行結果你們自己測試吧。
configure(AuthenticationManagerBuilder auth) 方法中,AuthenticationManagerBuilder 的 inMemoryAuthentication() 方法
可以新增使用者,並給使用者指定許可權,它還有其他的方法,我們以後用到再講。
在 Password 的地方我們需要注意了:
Spring 5.0 之後為了更加安全,修改了密碼儲存格式,密碼儲存格式為{id}encodedPassword。
id 是一個識別符號,用於查詢是哪個 PasswordEncoder,也就是密碼加密的格式所對應的 PasswordEncoder。
encodedPassword 是指原始密碼經過加密之後的密碼。id 必須在密碼的開始,id前後必須加 {}。
如果 id 找不到,id 則會為空,會丟擲異常:There is no PasswordEncoder mapped for id "null"。
好啦,重點來啦,我們現在開始設定自定義登入頁面,並從資料庫讀取賬號密碼。
一般來講,我們先講認證原理及流程比較好,不過這個地方我也說不太清楚。那我們還是從例子說起吧。
我用的是 MyBaits 框架操作 Mysql 資料庫。為了支援它們,我們需要在原來的 pom.xml 中新增依賴。
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
好啦,現在我們首先定義一個使用者物件,為了簡單,我們只有三個屬性:id,username,password。
package security.pojo; public class User { private int id; private String username; private String password; private String roles; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getRoles() { return roles; } public void setRoles(String roles) { this.roles = roles; } }
然後,為了根據使用者名稱找到使用者,我們定義一個 Mapper:
package security.mapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import security.pojo.User; @Mapper public interface UserMapper { @Select("select * from users where username = #{username}") public User findByUsername(String username); }
而這樣的一個 Mapper 是不會載入到 Bean 中去的,我們需要對這個類進行掃描:
package security; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("security.mapper") public class SecurityApplication { public static void main(String[] args) { SpringApplication.run(SecurityApplication.class, args); } }
好啦,這個 Mapper 已經成為一個 Bean 了,下面的將是重點:來自 《Spring Boot 2 企業應用實戰》
1、UserDetails
UserDetails 是 Spring Security 的一個核心介面。其中定義了一些可以獲取使用者名稱、密碼、許可權等與認證相關資訊的方法。
Spring Security 內部使用的 UserDetails 實現類大都是內建的 User 類,要使用 UserDetails,也可以直接使用該類。
在 Spring Security 內部,很多需要使用使用者資訊的時候,基本上都是使用 UserDetails,比如在登入認證的時候。
UserDetails 是通過 UserDetailsService 的 loadUserByUsername() 方法進行載入的。
我們也需要實現自己的 UserDetailsService 來載入自定義的 UserDetails 資訊。
2、UserDetailsService
Authentication.getPrincipal() 的返回型別是 Object,但很多情況下返回的其實是一個 UserDetails 的例項。
登入認證的時候 Spring Security 會通過 UserDetailsService 的 loadByUsername() 方法獲取相對應的 UserDetails
進行認證,認證通過後會將改 UserDetails 賦給認證通過的 Authentication 的 principal,
然後再把該 Authentication 存入 SecurityContext。之後如果需要使用使用者資訊,
可以通過 SecurityContextHolder 獲取存放在 SecurityContext 中的 Authentication 的 principal。
3、Authentication
Authentication 用來表示使用者認證資訊,在使用者登入認證之前,
Spring Security 會將相關資訊封裝為一個 Authentication
具體實現類的物件,在登入認證成功之後又會生成一個資訊更全面、包含使用者許可權等資訊的 Authentication 物件,
然後把它儲存在 SpringContextHolder 所持有的 SecurityContext 中,供後續的程式進行呼叫,如訪問許可權的鑑定等。
4、SecurityContextHolder
SecurityContextHolder 是用來儲存 SecurityContext 的。SecurityContext 中含有當前所訪問系統的使用者的詳細資訊。
預設情況下,SecurityContextHolder 將使用 ThreadLocal 來儲存 SecurityContext。
這也就意味著在處於同一執行緒的方法中,可以從 ThreadLocal 獲取到當前 SecurityContext。
好啦,這個地方就到這兒啦,沒弄懂也不要緊,我們能看懂例子就行了:
package security.service; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import security.mapper.UserMapper; import security.pojo.User; @Service public class UserService implements UserDetailsService { @Autowired UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // TODO Auto-generated method stub User user = userMapper.findByUsername(username); if(user == null) { throw new UsernameNotFoundException("使用者名稱不存在"); } List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority(user.getRoles())); return new org.springframework.security.core.userdetails.User(user.getUsername(),user.getPassword(),authorities); } }
在這個類中,我們實現了 UserDetailsService 介面,然後重寫了 loadUserByUsername(String username) 方法。
之後自動注入了一個根據使用者名稱查詢使用者的 Mapper,再將查詢的使用者物件複製給 user。
當存在這個使用者的時候,我們獲取它的許可權新增到許可權列表中,然後把這個列表以及使用者名稱,密碼存入到 UserDetails 物件中。
因為一個使用者的許可權可能不止一個,所以是一個許可權列表。另外由於之前定義的類名叫 User,所以在 return 那個地方需要這麼寫。
你們可以把 User 類名改成其他的,再 import org.springframework.security.core.userdetails; 然後 return new User(..) 。
在這個類裡有個這個型別 GrantedAuthority:
Authentication 的 getAuthority() 可以返回當前 Authentication 物件所擁有的許可權,即當前使用者所擁有的許可權,
其返回值是一個 GrantedAuthority 型別的陣列,每一個 GrantedAuthority 物件代表賦予給當前使用者的一種許可權。
GrantedAuthority 是一個介面,其通常是通過 UserDetailsService 進行載入,然後賦予 UserDetails 的。
GrantedAuthority 中只定義了一個 getAuthority() 方法,該方法返回一個字串,表示對應的許可權。
如果對應的許可權不能用字串表示,則應當返回 null。
最後我們到了配置環節了:
package security.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 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 security.service.UserService; @SuppressWarnings("deprecation") @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Autowired private AuthenticationProvider authenticationProvider; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO Auto-generated method stub auth.authenticationProvider(authenticationProvider); } @Override protected void configure(HttpSecurity http) throws Exception { // TODO Auto-generated method stub http.authorizeRequests() .antMatchers("/css/**","/images/*","/js/**","/login").permitAll() .antMatchers("/index").hasRole("USER") .anyRequest().authenticated() .and() .formLogin().loginPage("/login") .usernameParameter("username") .passwordParameter("password") .defaultSuccessUrl("/success") .failureUrl("/failure"); } @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(userService); provider.setPasswordEncoder(NoOpPasswordEncoder.getInstance()); return provider; } }
這個類中我們重寫了兩個 configure() 方法。其中一個我們之前談過,不過並沒有講全,現在補充一下:
在 formLogin() 下還有 .usernameParameter() 和 .passwordParameter() 這兩個函式。
這兩個函式是用於指定登入頁面使用者名稱及密碼的標識的。
failureUrl 是指定登入失敗顯示的頁面的。
還有其他的一些我們以後用到再講。
另一個 configure() 方法是用於認證的。我們這裡僅僅只寫了一行程式碼。
我們把之前的 @Service 的那個類注入到了 userService 中,再把 @Bean 的那個 Bean 注入到了 authenticationProvider 中。
在這個 Bean 裡面有個 DaoAuthenticationProvider 類:
Spring Security 預設會使用 DaoAuthenticationProvider 實現 AuthenticationProvider 介面,專門進行使用者認證處理。
DaoAuthenticationProvider 在進行認證處理的時候需要一個 UserDetailsService 來獲取使用者的資訊 UserDetails,
其中包括使用者名稱,密碼和所擁有的許可權等。
看到這些程式碼,可以知道我們寫的程式碼都有聯絡了。我們還差一個控制器的程式碼:
package security.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class SecurityController { @RequestMapping("/login") public String login() { return "login"; } @RequestMapping("/success") public String success() { return "success"; } @RequestMapping("/failure") public String failure() { return "failure"; } @RequestMapping("/index") public String index() { return "index"; } }
好啦,到此 java 程式碼就結束了,我們就差幾個頁面沒寫,這裡僅寫一個重要的 login 頁面作為演示:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <form th:action="@{/login}" method="post"> <input th:name="username" type="text"> <input th:name="password" type="password"> <input type="submit" value="login"> </form> </body> </html>
前面我們設定了 usernameParameter("username"),passwordParameter("password"),
在這裡我們用的是 thymeleaf 模板,所以使用者名稱和密碼就分別用 th:name="username",th:name="password"。
最後我們看下資料庫:
熬,對啦,連線資料庫的地方需要寫在 application.properties 檔案裡:
注意了,那個 url 資料庫(security)後面一定要寫上 ?serverTimezone=UTC&characterEncoding=utf-8 這樣的,不然會出錯的。
至此,入門專案就結束了,所有的原始碼都在上面啦,覺得可以的話點個贊啦!
想要原始碼的話可以點此下載:Download (CSDN)
或者百度雲:
連結:https://pan.baidu.com/s/1X1kTs6OpyidZv_627Xadiw&shfl=sharepset
提取碼:j