咱們在篇(一)中已經談到了默認的登陸頁面以及默認的登陸帳號和密碼。css
在這一篇中咱們將本身定義登陸頁面及帳號密碼。html
咱們先從簡單的開始吧:設置自定義的帳號和密碼(並不是從數據庫讀取),雖然意義不大。java
上一篇中,咱們僅僅重寫了 configure(HttpSecurity http) 方法,該方法是用於完成用戶受權的。mysql
爲了完成自定義的認證,咱們須要重寫 configure(AuthenticationManagerBuilder auth) 方法。web
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 類,控制器裏面的方法我就不寫了,仿照(一)很容易寫出來,運行結果大家本身測試吧。spring
configure(AuthenticationManagerBuilder auth) 方法中,AuthenticationManagerBuilder 的 inMemoryAuthentication() 方法sql
能夠添加用戶,並給用戶指定權限,它還有其餘的方法,咱們之後用到再講。數據庫
在 Password 的地方咱們須要注意了:apache
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 企業應用實戰》
一、UserDetails
UserDetails 是 Spring Security 的一個核心接口。其中定義了一些能夠獲取用戶名、密碼、權限等與認證相關信息的方法。
Spring Security 內部使用的 UserDetails 實現類大都是內置的 User 類,要使用 UserDetails,也能夠直接使用該類。
在 Spring Security 內部,不少須要使用用戶信息的時候,基本上都是使用 UserDetails,好比在登陸認證的時候。
UserDetails 是經過 UserDetailsService 的 loadUserByUsername() 方法進行加載的。
咱們也須要實現本身的 UserDetailsService 來加載自定義的 UserDetails 信息。
二、UserDetailsService
Authentication.getPrincipal() 的返回類型是 Object,但不少狀況下返回的實際上是一個 UserDetails 的實例。
登陸認證的時候 Spring Security 會經過 UserDetailsService 的 loadByUsername() 方法獲取相對應的 UserDetails
進行認證,認證經過後會將改 UserDetails 賦給認證經過的 Authentication 的 principal,
而後再把該 Authentication 存入 SecurityContext。以後若是須要使用用戶信息,
能夠經過 SecurityContextHolder 獲取存放在 SecurityContext 中的 Authentication 的 principal。
三、Authentication
Authentication 用來表示用戶認證信息,在用戶登陸認證以前,
Spring Security 會將相關信息封裝爲一個 Authentication
具體實現類的對象,在登陸認證成功以後又會生成一個信息更全面、包含用戶權限等信息的 Authentication 對象,
而後把它保存在 SpringContextHolder 所持有的 SecurityContext 中,供後續的程序進行調用,如訪問權限的鑑定等。
四、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")
.loginProcessingUrl("/login") .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() 以及 .loginProcessingUrl("/login") 這三個函數。
前兩個函數是用於指定登陸頁面用戶名及密碼的標識的,後面的一個是用於表單請求的 action 參數。
defaultSuccessUrl 是指定登陸成功顯示的頁面,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"),
另外因爲默認的登陸頁面表單請求的 action="/login",用戶名參數和密碼分別爲 "username","password"。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; private String usernameParameter = "username"; private String passwordParameter = "password"; private boolean postOnly = true; public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); } // ... ... }
若是用 thymeleaf 模板的話,這三個參數就分別用 th:action="{/login}" ,th:name="username",th:name="password"。
如果咱們想自定義的話,好比登陸頁面爲 signin.html,登陸請求的 action 爲 "/signin",
用戶名參數爲 uname,密碼參數爲 pwd。
@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("uname") .passwordParameter("pwd") .loginProcessingUrl("/sign") .defaultSuccessUrl("/success") .failureUrl("/failure"); }
<!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="@{/signin}" method="post"> <input th:name="uname" type="text"> <input th:name="pwd" type="password"> <input type="submit" value="login"> </form> </body> </html>
最後咱們看下數據庫:
熬,對啦,鏈接數據庫的地方須要寫在 application.properties 文件裏:
注意了,那個 url 數據庫(security)後面必定要寫上 ?serverTimezone=UTC&characterEncoding=utf-8 這樣的,否則會出錯的。
至此,入門項目就結束了,全部的源碼都在上面啦,以爲能夠的話點個贊啦!
連接:https://pan.baidu.com/s/1X1kTs6OpyidZv_627Xadiw&shfl=sharepset 提取碼:jexz